From 4624fa91ea81ed760e39a4e7c8785a27dbb109ba Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Sat, 9 Nov 2019 09:24:16 -0500 Subject: [PATCH 01/23] started migrating share --- src/plugins/share/README.md | 24 + src/plugins/share/kibana.json | 6 + .../share_context_menu.test.js.snap | 121 +++++ .../url_panel_content.test.js.snap | 431 ++++++++++++++++++ .../components/share_context_menu.test.js | 92 ++++ .../public/components/share_context_menu.tsx | 167 +++++++ .../components/url_panel_content.test.js | 56 +++ .../public/components/url_panel_content.tsx | 397 ++++++++++++++++ src/plugins/share/public/index.ts | 24 + .../public/lib/show_share_context_menu.tsx | 82 ++++ .../share/public/lib/url_shortener.test.js | 133 ++++++ src/plugins/share/public/lib/url_shortener.ts | 43 ++ src/plugins/share/public/plugin.test.mocks.ts | 25 + src/plugins/share/public/plugin.test.ts | 47 ++ src/plugins/share/public/plugin.ts | 47 ++ src/plugins/share/public/services/index.ts | 20 + .../services/share_actions_registry.mock.ts | 51 +++ .../services/share_actions_registry.test.ts | 68 +++ .../public/services/share_actions_registry.ts | 118 +++++ 19 files changed, 1952 insertions(+) create mode 100644 src/plugins/share/README.md create mode 100644 src/plugins/share/kibana.json create mode 100644 src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap create mode 100644 src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap create mode 100644 src/plugins/share/public/components/share_context_menu.test.js create mode 100644 src/plugins/share/public/components/share_context_menu.tsx create mode 100644 src/plugins/share/public/components/url_panel_content.test.js create mode 100644 src/plugins/share/public/components/url_panel_content.tsx create mode 100644 src/plugins/share/public/index.ts create mode 100644 src/plugins/share/public/lib/show_share_context_menu.tsx create mode 100644 src/plugins/share/public/lib/url_shortener.test.js create mode 100644 src/plugins/share/public/lib/url_shortener.ts create mode 100644 src/plugins/share/public/plugin.test.mocks.ts create mode 100644 src/plugins/share/public/plugin.test.ts create mode 100644 src/plugins/share/public/plugin.ts create mode 100644 src/plugins/share/public/services/index.ts create mode 100644 src/plugins/share/public/services/share_actions_registry.mock.ts create mode 100644 src/plugins/share/public/services/share_actions_registry.test.ts create mode 100644 src/plugins/share/public/services/share_actions_registry.ts diff --git a/src/plugins/share/README.md b/src/plugins/share/README.md new file mode 100644 index 000000000000..7ecf23134cf2 --- /dev/null +++ b/src/plugins/share/README.md @@ -0,0 +1,24 @@ +# Share plugin + +Replaces the legacy `ui/share` module for registering share context menus. + +## Example registration + +```ts +// For legacy plugins +import { npSetup } from 'ui/new_platform'; +npSetup.plugins.share.register(/* same details here */); + +// For new plugins: first add 'share' to the list of `optionalPlugins` +// in your kibana.json file. Then access the plugin directly in `setup`: + +class MyPlugin { + setup(core, plugins) { + if (plugins.share) { + plugins.share.register(/* same details here. */); + } + } +} +``` + +Note that the old module supported providing a Angular DI function to receive Angular dependencies. This is no longer supported as we migrate away from Angular and will be removed in 8.0. diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json new file mode 100644 index 000000000000..bbe393a76c5d --- /dev/null +++ b/src/plugins/share/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "share", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap b/src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap new file mode 100644 index 000000000000..df50f1d4a78b --- /dev/null +++ b/src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shareContextMenuExtensions should sort ascending on sort order first and then ascending on name 1`] = ` +, + "id": 1, + "title": "Permalink", + }, + Object { + "content":
+ panel content +
, + "id": 2, + "title": "AAA panel", + }, + Object { + "content":
+ panel content +
, + "id": 3, + "title": "ZZZ panel", + }, + Object { + "id": 4, + "items": Array [ + Object { + "data-test-subj": "sharePanel-Permalinks", + "icon": "link", + "name": "Permalinks", + "panel": 1, + }, + Object { + "data-test-subj": "sharePanel-ZZZpanel", + "name": "ZZZ panel", + "panel": 3, + }, + Object { + "data-test-subj": "sharePanel-AAApanel", + "name": "AAA panel", + "panel": 2, + }, + ], + "title": "Share this dashboard", + }, + ] + } +/> +`; + +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 { + "data-test-subj": "sharePanel-Embedcode", + "icon": "console", + "name": "Embed code", + "panel": 2, + }, + Object { + "data-test-subj": "sharePanel-Permalinks", + "icon": "link", + "name": "Permalinks", + "panel": 1, + }, + ], + "title": "Share this dashboard", + }, + ] + } +/> +`; diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap new file mode 100644 index 000000000000..645b8c662c41 --- /dev/null +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap @@ -0,0 +1,431 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render 1`] = ` + + + } + label={ + + } + labelType="label" + > + + + + + + + } + position="bottom" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": true, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + /> + + , + }, + ] + } + /> + + + + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + + + + + + +`; + +exports[`should enable saved object export option when objectId is provided 1`] = ` + + + } + labelType="label" + > + + + + + + + } + position="bottom" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": false, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + /> + + , + }, + ] + } + /> + + + + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + + + + + + +`; + +exports[`should hide short url section when allowShortUrl is false 1`] = ` + + + } + labelType="label" + > + + + + + + + } + position="bottom" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": false, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + /> + + , + }, + ] + } + /> + + + + + + +`; diff --git a/src/plugins/share/public/components/share_context_menu.test.js b/src/plugins/share/public/components/share_context_menu.test.js new file mode 100644 index 000000000000..5b583420f85e --- /dev/null +++ b/src/plugins/share/public/components/share_context_menu.test.js @@ -0,0 +1,92 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; + +import { + ShareContextMenu, +} from './share_context_menu'; + +test('should render context menu panel when there are more than one panel', () => { + const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should only render permalink panel when there are no other panels', () => { + const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); + +describe('shareContextMenuExtensions', () => { + const shareContextMenuExtensions = [ + { + getShareActions: () => { + return [ + { + panel: { + title: 'AAA panel', + content: (
panel content
), + }, + shareMenuItem: { + name: 'AAA panel', + sortOrder: 5, + } + } + ]; + } + }, + { + getShareActions: () => { + return [ + { + panel: { + title: 'ZZZ panel', + content: (
panel content
), + }, + shareMenuItem: { + name: 'ZZZ panel', + sortOrder: 0, + } + } + ]; + } + } + ]; + + test('should sort ascending on sort order first and then ascending on name', () => { + const component = shallowWithIntl( {}} + shareContextMenuExtensions={shareContextMenuExtensions} + />); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/share/public/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx new file mode 100644 index 000000000000..663a0392d9c1 --- /dev/null +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -0,0 +1,167 @@ +/* + * 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 { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EuiContextMenu } from '@elastic/eui'; + +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { ShareAction, ShareActionProvider, ShareContextMenuPanelItem } from 'ui/share/share_action'; +import { UrlPanelContent } from './url_panel_content'; + +interface Props { + allowEmbed: boolean; + allowShortUrl: boolean; + objectId?: string; + objectType: string; + shareableUrl?: string; + shareActions: ShareAction[]; + sharingData: any; + isDirty: boolean; + onClose: () => void; + intl: InjectedIntl; +} + +class ShareContextMenuUI extends Component { + public render() { + const { panels, initialPanelId } = this.getPanels(); + return ( + + ); + } + + private getPanels = () => { + const panels: EuiContextMenuPanelDescriptor[] = []; + const menuItems: ShareContextMenuPanelItem[] = []; + const { intl } = this.props; + + const permalinkPanel = { + id: panels.length + 1, + title: intl.formatMessage({ + id: 'common.ui.share.contextMenu.permalinkPanelTitle', + defaultMessage: 'Permalink', + }), + content: ( + + ), + }; + menuItems.push({ + name: intl.formatMessage({ + id: 'common.ui.share.contextMenu.permalinksLabel', + defaultMessage: 'Permalinks', + }), + icon: 'link', + panel: permalinkPanel.id, + sortOrder: 0, + }); + panels.push(permalinkPanel); + + if (this.props.allowEmbed) { + const embedPanel = { + id: panels.length + 1, + title: intl.formatMessage({ + id: 'common.ui.share.contextMenu.embedCodePanelTitle', + defaultMessage: 'Embed Code', + }), + content: ( + + ), + }; + panels.push(embedPanel); + menuItems.push({ + name: intl.formatMessage({ + id: 'common.ui.share.contextMenu.embedCodeLabel', + defaultMessage: 'Embed code', + }), + icon: 'console', + panel: embedPanel.id, + sortOrder: 0, + }); + } + + this.props.shareActions.forEach(({ shareMenuItem, panel }) => { + const panelId = panels.length + 1; + panels.push({ + ...panel, + id: panelId, + }); + menuItems.push({ + ...shareMenuItem, + panel: panelId, + }); + }); + + if (menuItems.length > 1) { + const topLevelMenuPanel = { + id: panels.length + 1, + title: intl.formatMessage( + { + id: 'common.ui.share.contextMenuTitle', + defaultMessage: 'Share this {objectType}', + }, + { + objectType: this.props.objectType, + } + ), + items: menuItems + // Sorts ascending on sort order first and then ascending on name + .sort((a, b) => { + const aSortOrder = a.sortOrder || 0; + const bSortOrder = b.sortOrder || 0; + if (aSortOrder > bSortOrder) { + return 1; + } + if (aSortOrder < bSortOrder) { + return -1; + } + if (a.name.toLowerCase().localeCompare(b.name.toLowerCase()) > 0) { + return 1; + } + return -1; + }) + .map(menuItem => { + menuItem['data-test-subj'] = `sharePanel-${menuItem.name.replace(' ', '')}`; + delete menuItem.sortOrder; + return menuItem; + }), + }; + panels.push(topLevelMenuPanel); + } + + const lastPanelIndex = panels.length - 1; + const initialPanelId = panels[lastPanelIndex].id; + return { panels, initialPanelId }; + }; +} + +export const ShareContextMenu = injectI18n(ShareContextMenuUI); diff --git a/src/plugins/share/public/components/url_panel_content.test.js b/src/plugins/share/public/components/url_panel_content.test.js new file mode 100644 index 000000000000..8dd94202fe72 --- /dev/null +++ b/src/plugins/share/public/components/url_panel_content.test.js @@ -0,0 +1,56 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; + +import { + UrlPanelContent, +} from './url_panel_content'; + +test('render', () => { + const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should enable saved object export option when objectId is provided', () => { + const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should hide short url section when allowShortUrl is false', () => { + const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx new file mode 100644 index 000000000000..7d4a7e91920a --- /dev/null +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -0,0 +1,397 @@ +/* + * 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, { ChangeEvent, Component } from 'react'; + +import { + EuiButton, + EuiCopy, + EuiFlexGroup, + EuiSpacer, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiIconTip, + EuiLoadingSpinner, + EuiRadioGroup, + EuiSwitch, +} from '@elastic/eui'; + +import { format as formatUrl, parse as parseUrl } from 'url'; + +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { shortenUrl } from '../lib/url_shortener'; + +interface Props { + allowShortUrl: boolean; + isEmbedded?: boolean; + objectId?: string; + objectType: string; + shareableUrl?: string; + intl: InjectedIntl; +} + +enum ExportUrlAsType { + EXPORT_URL_AS_SAVED_OBJECT = 'savedObject', + EXPORT_URL_AS_SNAPSHOT = 'snapshot', +} + +interface State { + exportUrlAs: ExportUrlAsType; + useShortUrl: boolean; + isCreatingShortUrl: boolean; + url?: string; + shortUrlErrorMsg?: string; +} + +class UrlPanelContentUI 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, + isCreatingShortUrl: false, + url: '', + }; + } + + public componentWillUnmount() { + window.removeEventListener('hashchange', this.resetUrl); + + this.mounted = false; + } + + public componentDidMount() { + this.mounted = true; + this.setUrl(); + + window.addEventListener('hashchange', this.resetUrl, false); + } + + public render() { + return ( + + {this.renderExportAsRadioGroup()} + + {this.renderShortUrlSwitch()} + + + + + {(copy: () => void) => ( + + {this.props.isEmbedded ? ( + + ) : ( + + )} + + )} + + + ); + } + + private isNotSaved = () => { + return this.props.objectId === undefined || this.props.objectId === ''; + }; + + private resetUrl = () => { + if (this.mounted) { + this.shortUrlCache = undefined; + this.setState( + { + useShortUrl: false, + }, + this.setUrl + ); + } + }; + + private getSavedObjectUrl = () => { + if (this.isNotSaved()) { + return; + } + + const url = this.getSnapshotUrl(); + + const parsedUrl = parseUrl(url); + if (!parsedUrl || !parsedUrl.hash) { + return; + } + + // 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, + }, + }), + }); + }; + + private getSnapshotUrl = () => { + return this.props.shareableUrl || window.location.href; + }; + + private makeUrlEmbeddable = (url: string) => { + const embedQueryParam = '?embed=true'; + const urlHasQueryString = url.indexOf('?') !== -1; + if (urlHasQueryString) { + return url.replace('?', `${embedQueryParam}&`); + } + return `${url}${embedQueryParam}`; + }; + + private makeIframeTag = (url?: string) => { + if (!url) { + return; + } + + const embeddableUrl = this.makeUrlEmbeddable(url); + return ``; + }; + + private setUrl = () => { + let url; + if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { + url = this.getSavedObjectUrl(); + } else if (this.state.useShortUrl) { + url = this.shortUrlCache; + } else { + url = this.getSnapshotUrl(); + } + + if (this.props.isEmbedded) { + url = this.makeIframeTag(url); + } + + this.setState({ url }); + }; + + private handleExportUrlAs = (optionId: string) => { + this.setState( + { + exportUrlAs: optionId as ExportUrlAsType, + }, + this.setUrl + ); + }; + + private handleShortUrlChange = async (evt: ChangeEvent) => { + const isChecked = evt.target.checked; + + if (!isChecked || this.shortUrlCache !== 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.shortUrlCache = shortUrl; + this.setState( + { + isCreatingShortUrl: false, + useShortUrl: isChecked, + }, + this.setUrl + ); + } + } catch (fetchError) { + if (this.mounted) { + this.shortUrlCache = undefined; + this.setState( + { + useShortUrl: false, + isCreatingShortUrl: false, + shortUrlErrorMsg: this.props.intl.formatMessage( + { + id: 'common.ui.share.urlPanel.unableCreateShortUrlErrorMessage', + defaultMessage: 'Unable to create short URL. Error: {errorMessage}', + }, + { + errorMessage: fetchError.message, + } + ), + }, + this.setUrl + ); + } + } + }; + + private renderExportUrlAsOptions = () => { + return [ + { + id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, + label: this.renderWithIconTip( + , + + ), + ['data-test-subj']: 'exportAsSnapshot', + }, + { + id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, + disabled: this.isNotSaved(), + label: this.renderWithIconTip( + , + + ), + ['data-test-subj']: 'exportAsSavedObject', + }, + ]; + }; + + private renderWithIconTip = (child: React.ReactNode, tipContent: React.ReactNode) => { + return ( + + {child} + + + + + ); + }; + + private renderExportAsRadioGroup = () => { + const generateLinkAsHelp = this.isNotSaved() ? ( + + ) : ( + undefined + ); + return ( + + } + helpText={generateLinkAsHelp} + > + + + ); + }; + + private renderShortUrlSwitch = () => { + if ( + this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT || + !this.props.allowShortUrl + ) { + return; + } + const shortUrlLabel = ( + + ); + const switchLabel = this.state.isCreatingShortUrl ? ( + + {shortUrlLabel} + + ) : ( + shortUrlLabel + ); + const switchComponent = ( + + ); + const tipContent = ( + + ); + + return ( + + {this.renderWithIconTip(switchComponent, tipContent)} + + ); + }; +} + +export const UrlPanelContent = injectI18n(UrlPanelContentUI); diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts new file mode 100644 index 000000000000..fe3694b886ac --- /dev/null +++ b/src/plugins/share/public/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { SharePluginSetup, SharePluginStart } from './plugin'; +export { ShareActionProps, ShareActionsProvider, ShareAction } from './services'; +import { SharePlugin } from './plugin'; + +export const plugin = () => new SharePlugin(); diff --git a/src/plugins/share/public/lib/show_share_context_menu.tsx b/src/plugins/share/public/lib/show_share_context_menu.tsx new file mode 100644 index 000000000000..8aedc5428c97 --- /dev/null +++ b/src/plugins/share/public/lib/show_share_context_menu.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiWrappingPopover } from '@elastic/eui'; +import { I18nContext } from 'ui/i18n'; +import { ShareContextMenu } from '../components/share_context_menu'; +import { ShowProps } from '../services'; + +let isOpen = false; + +const container = document.createElement('div'); + +const onClose = () => { + ReactDOM.unmountComponentAtNode(container); + isOpen = false; +}; + +export function showShareContextMenu({ + anchorElement, + allowEmbed, + allowShortUrl, + objectId, + objectType, + sharingData, + isDirty, + shareActions, + shareableUrl, +}: ShowProps) { + if (isOpen) { + onClose(); + return; + } + + isOpen = true; + + document.body.appendChild(container); + const element = ( + + + + + + ); + ReactDOM.render(element, container); +} diff --git a/src/plugins/share/public/lib/url_shortener.test.js b/src/plugins/share/public/lib/url_shortener.test.js new file mode 100644 index 000000000000..859873bd4989 --- /dev/null +++ b/src/plugins/share/public/lib/url_shortener.test.js @@ -0,0 +1,133 @@ +/* + * 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('ui/kfetch', () => ({})); + +jest.mock('../../chrome', () => ({})); + +import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { shortenUrl } from './url_shortener'; + +describe('Url shortener', () => { + const shareId = 'id123'; + + let kfetchStub; + beforeEach(() => { + kfetchStub = sinon.stub(); + require('ui/kfetch').kfetch = async (...args) => { + return kfetchStub(...args); + }; + }); + + describe('Shorten without base path', () => { + beforeAll(() => { + require('../../chrome').getBasePath = () => { + return ''; + }; + }); + + it('should shorten urls with a port', async () => { + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana#123"}' + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl('http://localhost:5601/app/kibana#123'); + expect(shortUrl).to.be(`http://localhost:5601/goto/${shareId}`); + }); + + it('should shorten urls without a port', async () => { + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana#123"}' + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl('http://localhost/app/kibana#123'); + expect(shortUrl).to.be(`http://localhost/goto/${shareId}`); + }); + }); + + describe('Shorten with base path', () => { + const basePath = '/foo'; + + beforeAll(() => { + require('../../chrome').getBasePath = () => { + return basePath; + }; + }); + + it('should shorten urls with a port', async () => { + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana#123"}' + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl(`http://localhost:5601${basePath}/app/kibana#123`); + expect(shortUrl).to.be(`http://localhost:5601${basePath}/goto/${shareId}`); + }); + + it('should shorten urls without a port', async () => { + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana#123"}' + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana#123`); + expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); + }); + + it('should shorten urls with a query string', async () => { + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana?foo#123"}' + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana?foo#123`); + expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); + }); + + it('should shorten urls without a hash', async () => { + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana"}' + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana`); + expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); + }); + + it('should shorten urls with a query string in the hash', async () => { + const relativeUrl = "/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"; //eslint-disable-line max-len, quotes + kfetchStub.withArgs({ + method: 'POST', + pathname: `/api/shorten_url`, + body: '{"url":"/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"}' //eslint-disable-line max-len, quotes + }).returns(Promise.resolve({ urlId: shareId })); + + const shortUrl = await shortenUrl(`http://localhost${basePath}${relativeUrl}`); + expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); + }); + }); +}); diff --git a/src/plugins/share/public/lib/url_shortener.ts b/src/plugins/share/public/lib/url_shortener.ts new file mode 100644 index 000000000000..037214bd9b45 --- /dev/null +++ b/src/plugins/share/public/lib/url_shortener.ts @@ -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. + */ + +import { kfetch } from 'ui/kfetch'; +import url from 'url'; +import chrome from '../../chrome'; + +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 }); + return url.format({ + protocol: parsedUrl.protocol, + host: parsedUrl.host, + pathname: `${basePath}/goto/${resp.urlId}`, + }); +} diff --git a/src/plugins/share/public/plugin.test.mocks.ts b/src/plugins/share/public/plugin.test.mocks.ts new file mode 100644 index 000000000000..bc07f5a4c200 --- /dev/null +++ b/src/plugins/share/public/plugin.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * 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 { shareActionsRegistryMock } from './services/share_actions_registry.mock'; + +export const registryMock = shareActionsRegistryMock.create(); +jest.doMock('./services', () => ({ + ShareActionsRegistry: jest.fn(() => registryMock), +})); diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts new file mode 100644 index 000000000000..91869eb2edb1 --- /dev/null +++ b/src/plugins/share/public/plugin.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { registryMock } from './plugin.test.mocks'; +import { SharePlugin } from './plugin'; +import { CoreStart } from 'kibana/public'; + +describe('SharePlugin', () => { + beforeEach(() => { + registryMock.setup.mockClear(); + registryMock.start.mockClear(); + }); + + describe('setup', () => { + test('wires up and returns registry', async () => { + const setup = await new SharePlugin().setup(); + expect(registryMock.setup).toHaveBeenCalledWith(); + expect(setup.register).toBeDefined(); + }); + }); + + describe('start', () => { + test('wires up and returns registry', async () => { + const service = new SharePlugin(); + await service.setup(); + const start = await service.start({} as CoreStart); + expect(registryMock.start).toHaveBeenCalled(); + expect(start.getActions).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts new file mode 100644 index 000000000000..dd26820e9015 --- /dev/null +++ b/src/plugins/share/public/plugin.ts @@ -0,0 +1,47 @@ +/* + * 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 { CoreStart, Plugin } from 'src/core/public'; +import { + ShareActionsRegistry, + ShareActionsRegistrySetup, + ShareActionsRegistryStart, +} from './services'; + +export class SharePlugin implements Plugin { + private readonly shareActionsRegistry = new ShareActionsRegistry(); + + public async setup() { + return { + ...this.shareActionsRegistry.setup(), + }; + } + + public async start(core: CoreStart) { + return { + ...this.shareActionsRegistry.start(), + }; + } +} + +/** @public */ +export type SharePluginSetup = ShareActionsRegistrySetup; + +/** @public */ +export type SharePluginStart = ShareActionsRegistryStart; diff --git a/src/plugins/share/public/services/index.ts b/src/plugins/share/public/services/index.ts new file mode 100644 index 000000000000..094c139f71e6 --- /dev/null +++ b/src/plugins/share/public/services/index.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 * from './share_actions_registry'; diff --git a/src/plugins/share/public/services/share_actions_registry.mock.ts b/src/plugins/share/public/services/share_actions_registry.mock.ts new file mode 100644 index 000000000000..a810e9bced7e --- /dev/null +++ b/src/plugins/share/public/services/share_actions_registry.mock.ts @@ -0,0 +1,51 @@ +/* + * 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 { SharePluginSetup, SharePluginStart } from '../plugin'; +import { ShareActionsRegistry } from './share_actions_registry'; + +const createSetupMock = (): jest.Mocked => { + const setup = { + register: jest.fn(), + }; + return setup; +}; + +const createStartMock = (): jest.Mocked => { + const start = { + getActions: jest.fn(), + }; + return start; +}; + +const createMock = (): jest.Mocked> => { + const service = { + setup: jest.fn(), + start: jest.fn(), + }; + service.setup.mockImplementation(createSetupMock); + service.start.mockImplementation(createStartMock); + return service; +}; + +export const shareActionsRegistryMock = { + createSetup: createSetupMock, + createStart: createStartMock, + create: createMock, +}; diff --git a/src/plugins/share/public/services/share_actions_registry.test.ts b/src/plugins/share/public/services/share_actions_registry.test.ts new file mode 100644 index 000000000000..9318ca0f05cb --- /dev/null +++ b/src/plugins/share/public/services/share_actions_registry.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { ShareAction, ShareActionProps, ShareActionsRegistry } from './share_actions_registry'; + +describe('ShareActionsRegistry', () => { + describe('setup', () => { + test('throws when registering duplicate id', () => { + const setup = new ShareActionsRegistry().setup(); + setup.register({ + id: 'myTest', + getShareActions: () => [], + }); + expect(() => + setup.register({ + id: 'myTest', + getShareActions: () => [], + }) + ).toThrowErrorMatchingInlineSnapshot(); + }); + }); + + describe('start', () => { + describe('getActions', () => { + test('returns a flat list of actions returned by all providers', () => { + const service = new ShareActionsRegistry(); + const registerFunction = service.setup().register; + const shareAction1 = {} as ShareAction; + const shareAction2 = {} as ShareAction; + const shareAction3 = {} as ShareAction; + const provider1Callback = jest.fn(() => [shareAction1]); + const provider2Callback = jest.fn(() => [shareAction2, shareAction3]); + registerFunction({ + id: 'myTest', + getShareActions: provider1Callback, + }); + registerFunction({ + id: 'myTest2', + getShareActions: provider2Callback, + }); + const actionProps = {} as ShareActionProps; + expect(service.start().getActions(actionProps)).toEqual([ + shareAction1, + shareAction2, + shareAction3, + ]); + expect(provider1Callback).toHaveBeenCalledWith(actionProps); + expect(provider2Callback).toHaveBeenCalledWith(actionProps); + }); + }); + }); +}); diff --git a/src/plugins/share/public/services/share_actions_registry.ts b/src/plugins/share/public/services/share_actions_registry.ts new file mode 100644 index 000000000000..e1a57126be38 --- /dev/null +++ b/src/plugins/share/public/services/share_actions_registry.ts @@ -0,0 +1,118 @@ +/* + * 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 { + EuiContextMenuPanelDescriptor, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui/src/components/context_menu/context_menu'; + +/** + * @public + * Properties of the current object to share. Registered share + * actions provider will provide suitable actions which have to + * be rendered in an appropriate place by the caller. + * + * It is possible to use the static function `showShareContextMenu` + * to render the menu as a popover. + * */ +export interface ShareActionProps { + objectType: string; + objectId?: string; + /** + * Current url for sharing. This can be set in cases where `window.location.href` + * does not contain a shareable URL (e.g. if using session storage to store the current + * app state is enabled). In these cases the property should contain the URL in a + * format which makes it possible to use it without having access to any other state + * like the current session. + * + * If not set it will default to `window.location.href` + */ + shareableUrl?: string; + getUnhashableStates: () => object[]; + sharingData: any; + isDirty: boolean; + onClose: () => void; +} + +/** + * @public + * Eui context menu entry shown directly in the context menu. `sortOrder` is + * used to order the individual items in a flat list returned by all registered + * action providers. + * */ +export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescriptor { + sortOrder: number; +} + +/** + * @public + * Definition of an action item rendered in the share menu. `shareMenuItem` is shown + * directly in the context menu. If the item is clicked, the `panel` is shown. + * */ +export interface ShareAction { + shareMenuItem: ShareContextMenuPanelItem; + panel: EuiContextMenuPanelDescriptor; +} + +/** @public */ +export interface ShareActionsProvider { + readonly id: string; + + getShareActions: (actionProps: ShareActionProps) => ShareAction[]; +} + +/** @public */ +export interface ShowProps extends Omit { + anchorElement: HTMLElement; + allowEmbed: boolean; + allowShortUrl: boolean; + shareActions: ShareAction[]; +} + +export class ShareActionsRegistry { + private readonly shareActionsProviders = new Map(); + + public setup() { + return { + register: (shareActionsProvider: ShareActionsProvider) => { + if (this.shareActionsProviders.has(shareActionsProvider.id)) { + throw new Error( + `Share action provider with id [${shareActionsProvider.id}] has already been registered. Use a unique id.` + ); + } + + this.shareActionsProviders.set(shareActionsProvider.id, shareActionsProvider); + }, + }; + } + + public start() { + return { + getActions: (props: ShareActionProps) => + Array.from(this.shareActionsProviders.values()) + .flatMap(shareActionProvider => shareActionProvider.getShareActions(props)) + showShareContextMenu: (showProps: ShowProps) => { + + } + }; + } +} + +export type ShareActionsRegistrySetup = ReturnType; +export type ShareActionsRegistryStart = ReturnType; From 0043259c446653bd91912d7fe571f65b0b244407 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Sun, 10 Nov 2019 09:32:51 +0100 Subject: [PATCH 02/23] continued migrating share --- .../dashboard/dashboard_app_controller.tsx | 8 +- .../kibana/public/visualize/editor/editor.js | 10 +- .../public/visualize/kibana_services.ts | 4 +- .../ui/public/new_platform/new_platform.ts | 3 + .../public/components/share_context_menu.tsx | 56 +++++----- .../public/components/url_panel_content.tsx | 99 +++++++++-------- src/plugins/share/public/index.ts | 2 +- .../public/lib/show_share_context_menu.tsx | 82 -------------- src/plugins/share/public/lib/url_shortener.ts | 12 +- src/plugins/share/public/plugin.ts | 13 ++- .../public/services/share_actions_registry.ts | 71 +----------- .../public/services/share_context_menu.tsx | 105 ++++++++++++++++++ src/plugins/share/public/types.ts | 84 ++++++++++++++ .../register_csv_reporting.tsx | 8 +- .../share_context_menu/register_reporting.tsx | 21 ++-- 15 files changed, 314 insertions(+), 264 deletions(-) delete mode 100644 src/plugins/share/public/lib/show_share_context_menu.tsx create mode 100644 src/plugins/share/public/services/share_context_menu.tsx create mode 100644 src/plugins/share/public/types.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 64c756094768..2872a8967507 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -35,7 +35,6 @@ import { docTitle } from 'ui/doc_title/doc_title'; import { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; -import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; import { timefilter } from 'ui/timefilter'; @@ -55,6 +54,7 @@ import { SaveOptions } from 'ui/saved_objects/saved_object'; import { capabilities } from 'ui/capabilities'; import { Subscription } from 'rxjs'; import { npStart } from 'ui/new_platform'; +import { unhashUrl } from 'ui/state_management/state_hashing'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; import { extractTimeFilter, changeTimeFilter } from '../../../data/public'; import { start as data } from '../../../data/public/legacy'; @@ -132,7 +132,6 @@ export class DashboardAppController { }) { const queryFilter = Private(FilterBarQueryFilterProvider); const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); let lastReloadRequestTime = 0; @@ -790,14 +789,13 @@ export class DashboardAppController { }); }; navActions[TopNavIds.SHARE] = anchorElement => { - showShareContextMenu({ + npStart.plugins.share.showShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: !dashboardConfig.getHideWriteControls(), - getUnhashableStates, + shareableUrl: unhashUrl(window.location.href, getUnhashableStates()), objectId: dash.id, objectType: 'dashboard', - shareContextMenuExtensions: shareContextMenuExtensions.raw, sharingData: { title: dash.title, }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index c0a7615f207e..5a8545b99fe9 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -43,10 +43,10 @@ import { KibanaParsedUrl, migrateLegacyQuery, SavedObjectSaveModal, - showShareContextMenu, showSaveModal, stateMonitorFactory, subscribeWithScope, + unhashUrl, } from '../kibana_services'; const { @@ -57,12 +57,12 @@ const { docTitle, FilterBarQueryFilterProvider, getBasePath, - ShareContextMenuExtensionsRegistryProvider, toastNotifications, timefilter, uiModules, uiRoutes, visualizations, + share, } = getServices(); const { savedQueryService } = data.search.services; @@ -160,7 +160,6 @@ function VisEditor( ) { const queryFilter = Private(FilterBarQueryFilterProvider); const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; @@ -240,14 +239,13 @@ function VisEditor( run: (anchorElement) => { const hasUnappliedChanges = vis.dirty; const hasUnsavedChanges = $appStatus.dirty; - showShareContextMenu({ + share.showShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: capabilities.visualize.createShortUrl, - getUnhashableStates, + shareableUrl: unhashUrl(window.location.href, getUnhashableStates()), objectId: savedVis.id, objectType: 'visualization', - shareContextMenuExtensions, sharingData: { title: savedVis.title, }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 7e8435bbdc65..25d3473d9d1e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,7 +36,6 @@ import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore import { uiModules } from 'ui/modules'; import { FeatureCatalogueRegistryProvider } from 'ui/registry/feature_catalogue'; -import { ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; import { timefilter } from 'ui/timefilter'; // Saved objects @@ -62,6 +61,7 @@ const services = { toastNotifications: npStart.core.notifications.toasts, uiSettings: npStart.core.uiSettings, + share: npStart.plugins.share, data, embeddables, visualizations, @@ -77,7 +77,6 @@ const services = { SavedObjectProvider, SavedObjectRegistryProvider, SavedObjectsClientProvider, - ShareContextMenuExtensionsRegistryProvider, timefilter, uiModules, uiRoutes, @@ -107,6 +106,7 @@ export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { getVisualizeLoader } from 'ui/visualize/loader'; export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; +export { unhashUrl } from 'ui/state_management/state_hashing'; export { Container, Embeddable, diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 0c7b28e7da3d..537cafd00359 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -32,6 +32,7 @@ import { FeatureCatalogueSetup, FeatureCatalogueStart, } from '../../../../plugins/feature_catalogue/public'; +import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; export interface PluginsSetup { data: ReturnType; @@ -40,6 +41,7 @@ export interface PluginsSetup { feature_catalogue: FeatureCatalogueSetup; inspector: InspectorSetup; uiActions: IUiActionsSetup; + share: SharePluginSetup; } export interface PluginsStart { @@ -50,6 +52,7 @@ export interface PluginsStart { feature_catalogue: FeatureCatalogueStart; inspector: InspectorStart; uiActions: IUiActionsStart; + share: SharePluginStart; } export const npSetup = { diff --git a/src/plugins/share/public/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx index 663a0392d9c1..9931a2eddc8d 100644 --- a/src/plugins/share/public/components/share_context_menu.tsx +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -19,12 +19,15 @@ import React, { Component } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiContextMenu } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { ShareAction, ShareActionProvider, ShareContextMenuPanelItem } from 'ui/share/share_action'; +import { HttpStart } from 'kibana/public'; + import { UrlPanelContent } from './url_panel_content'; +import { ShareAction, ShareContextMenuPanelItem } from '../types'; interface Props { allowEmbed: boolean; @@ -36,30 +39,31 @@ interface Props { sharingData: any; isDirty: boolean; onClose: () => void; - intl: InjectedIntl; + basePath: string; + post: HttpStart['post']; } -class ShareContextMenuUI extends Component { +export class ShareContextMenu extends Component { public render() { const { panels, initialPanelId } = this.getPanels(); return ( - + + + ); } private getPanels = () => { const panels: EuiContextMenuPanelDescriptor[] = []; const menuItems: ShareContextMenuPanelItem[] = []; - const { intl } = this.props; const permalinkPanel = { id: panels.length + 1, - title: intl.formatMessage({ - id: 'common.ui.share.contextMenu.permalinkPanelTitle', + title: i18n.translate('common.ui.share.contextMenu.permalinkPanelTitle', { defaultMessage: 'Permalink', }), content: ( @@ -67,12 +71,13 @@ class ShareContextMenuUI extends Component { allowShortUrl={this.props.allowShortUrl} objectId={this.props.objectId} objectType={this.props.objectType} + basePath={this.props.basePath} + post={this.props.post} /> ), }; menuItems.push({ - name: intl.formatMessage({ - id: 'common.ui.share.contextMenu.permalinksLabel', + name: i18n.translate('common.ui.share.contextMenu.permalinksLabel', { defaultMessage: 'Permalinks', }), icon: 'link', @@ -84,8 +89,7 @@ class ShareContextMenuUI extends Component { if (this.props.allowEmbed) { const embedPanel = { id: panels.length + 1, - title: intl.formatMessage({ - id: 'common.ui.share.contextMenu.embedCodePanelTitle', + title: i18n.translate('common.ui.share.contextMenu.embedCodePanelTitle', { defaultMessage: 'Embed Code', }), content: ( @@ -94,13 +98,14 @@ class ShareContextMenuUI extends Component { isEmbedded objectId={this.props.objectId} objectType={this.props.objectType} + basePath={this.props.basePath} + post={this.props.post} /> ), }; panels.push(embedPanel); menuItems.push({ - name: intl.formatMessage({ - id: 'common.ui.share.contextMenu.embedCodeLabel', + name: i18n.translate('common.ui.share.contextMenu.embedCodeLabel', { defaultMessage: 'Embed code', }), icon: 'console', @@ -124,15 +129,12 @@ class ShareContextMenuUI extends Component { if (menuItems.length > 1) { const topLevelMenuPanel = { id: panels.length + 1, - title: intl.formatMessage( - { - id: 'common.ui.share.contextMenuTitle', - defaultMessage: 'Share this {objectType}', - }, - { + title: i18n.translate('common.ui.share.contextMenuTitle', { + defaultMessage: 'Share this {objectType}', + values: { objectType: this.props.objectType, - } - ), + }, + }), items: menuItems // Sorts ascending on sort order first and then ascending on name .sort((a, b) => { @@ -163,5 +165,3 @@ class ShareContextMenuUI extends Component { return { panels, initialPanelId }; }; } - -export const ShareContextMenu = injectI18n(ShareContextMenuUI); diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index 7d4a7e91920a..30a62bbeeebc 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -35,7 +35,10 @@ import { import { format as formatUrl, parse as parseUrl } from 'url'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { HttpStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; + import { shortenUrl } from '../lib/url_shortener'; interface Props { @@ -44,7 +47,8 @@ interface Props { objectId?: string; objectType: string; shareableUrl?: string; - intl: InjectedIntl; + basePath: string; + post: HttpStart['post']; } enum ExportUrlAsType { @@ -60,7 +64,7 @@ interface State { shortUrlErrorMsg?: string; } -class UrlPanelContentUI extends Component { +export class UrlPanelContent extends Component { private mounted?: boolean; private shortUrlCache?: string; @@ -92,41 +96,43 @@ class UrlPanelContentUI extends Component { public render() { return ( - - {this.renderExportAsRadioGroup()} - - {this.renderShortUrlSwitch()} - - - - - {(copy: () => void) => ( - - {this.props.isEmbedded ? ( - - ) : ( - - )} - - )} - - + + + {this.renderExportAsRadioGroup()} + + {this.renderShortUrlSwitch()} + + + + + {(copy: () => void) => ( + + {this.props.isEmbedded ? ( + + ) : ( + + )} + + )} + + + ); } @@ -240,7 +246,10 @@ class UrlPanelContentUI extends Component { }); try { - const shortUrl = await shortenUrl(this.getSnapshotUrl()); + const shortUrl = await shortenUrl(this.getSnapshotUrl(), { + basePath: this.props.basePath, + post: this.props.post, + }); if (this.mounted) { this.shortUrlCache = shortUrl; this.setState( @@ -258,13 +267,13 @@ class UrlPanelContentUI extends Component { { useShortUrl: false, isCreatingShortUrl: false, - shortUrlErrorMsg: this.props.intl.formatMessage( + shortUrlErrorMsg: i18n.translate( + 'common.ui.share.urlPanel.unableCreateShortUrlErrorMessage', { - id: 'common.ui.share.urlPanel.unableCreateShortUrlErrorMessage', defaultMessage: 'Unable to create short URL. Error: {errorMessage}', - }, - { - errorMessage: fetchError.message, + values: { + errorMessage: fetchError.message, + }, } ), }, @@ -393,5 +402,3 @@ class UrlPanelContentUI extends Component { ); }; } - -export const UrlPanelContent = injectI18n(UrlPanelContentUI); diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index fe3694b886ac..36baeff77e69 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -18,7 +18,7 @@ */ export { SharePluginSetup, SharePluginStart } from './plugin'; -export { ShareActionProps, ShareActionsProvider, ShareAction } from './services'; +export { ShareActionProps, ShareActionsProvider, ShareAction } from './types'; import { SharePlugin } from './plugin'; export const plugin = () => new SharePlugin(); diff --git a/src/plugins/share/public/lib/show_share_context_menu.tsx b/src/plugins/share/public/lib/show_share_context_menu.tsx deleted file mode 100644 index 8aedc5428c97..000000000000 --- a/src/plugins/share/public/lib/show_share_context_menu.tsx +++ /dev/null @@ -1,82 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -import { EuiWrappingPopover } from '@elastic/eui'; -import { I18nContext } from 'ui/i18n'; -import { ShareContextMenu } from '../components/share_context_menu'; -import { ShowProps } from '../services'; - -let isOpen = false; - -const container = document.createElement('div'); - -const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - isOpen = false; -}; - -export function showShareContextMenu({ - anchorElement, - allowEmbed, - allowShortUrl, - objectId, - objectType, - sharingData, - isDirty, - shareActions, - shareableUrl, -}: ShowProps) { - if (isOpen) { - onClose(); - return; - } - - isOpen = true; - - document.body.appendChild(container); - const element = ( - - - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/plugins/share/public/lib/url_shortener.ts b/src/plugins/share/public/lib/url_shortener.ts index 037214bd9b45..29d91bdb1aae 100644 --- a/src/plugins/share/public/lib/url_shortener.ts +++ b/src/plugins/share/public/lib/url_shortener.ts @@ -17,13 +17,13 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; import url from 'url'; -import chrome from '../../chrome'; - -export async function shortenUrl(absoluteUrl: string) { - const basePath = chrome.getBasePath(); +import { HttpStart } from 'kibana/public'; +export async function shortenUrl( + absoluteUrl: string, + { basePath, post }: { basePath: string; post: HttpStart['post'] } +) { const parsedUrl = url.parse(absoluteUrl); if (!parsedUrl || !parsedUrl.path) { return; @@ -34,7 +34,7 @@ export async function shortenUrl(absoluteUrl: string) { const body = JSON.stringify({ url: relativeUrl }); - const resp = await kfetch({ method: 'POST', pathname: '/api/shorten_url', body }); + const resp = await post('/api/shorten_url', { body }); return url.format({ protocol: parsedUrl.protocol, host: parsedUrl.host, diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index dd26820e9015..53777d1c13f3 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -18,14 +18,15 @@ */ import { CoreStart, Plugin } from 'src/core/public'; +import { ShareActionsRegistry, ShareActionsRegistrySetup } from './services'; import { - ShareActionsRegistry, - ShareActionsRegistrySetup, - ShareActionsRegistryStart, -} from './services'; + ShareActionsContextMenu, + ShareActionsContextMenuStart, +} from './services/share_context_menu'; export class SharePlugin implements Plugin { private readonly shareActionsRegistry = new ShareActionsRegistry(); + private readonly shareActionsContextMenu = new ShareActionsContextMenu(); public async setup() { return { @@ -35,7 +36,7 @@ export class SharePlugin implements Plugin { public async start(core: CoreStart) { return { - ...this.shareActionsRegistry.start(), + ...this.shareActionsContextMenu.start(core, this.shareActionsRegistry.start()), }; } } @@ -44,4 +45,4 @@ export class SharePlugin implements Plugin { export type SharePluginSetup = ShareActionsRegistrySetup; /** @public */ -export type SharePluginStart = ShareActionsRegistryStart; +export type SharePluginStart = ShareActionsContextMenuStart; diff --git a/src/plugins/share/public/services/share_actions_registry.ts b/src/plugins/share/public/services/share_actions_registry.ts index e1a57126be38..4140f2eb62f5 100644 --- a/src/plugins/share/public/services/share_actions_registry.ts +++ b/src/plugins/share/public/services/share_actions_registry.ts @@ -17,73 +17,7 @@ * under the License. */ -import { - EuiContextMenuPanelDescriptor, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui/src/components/context_menu/context_menu'; - -/** - * @public - * Properties of the current object to share. Registered share - * actions provider will provide suitable actions which have to - * be rendered in an appropriate place by the caller. - * - * It is possible to use the static function `showShareContextMenu` - * to render the menu as a popover. - * */ -export interface ShareActionProps { - objectType: string; - objectId?: string; - /** - * Current url for sharing. This can be set in cases where `window.location.href` - * does not contain a shareable URL (e.g. if using session storage to store the current - * app state is enabled). In these cases the property should contain the URL in a - * format which makes it possible to use it without having access to any other state - * like the current session. - * - * If not set it will default to `window.location.href` - */ - shareableUrl?: string; - getUnhashableStates: () => object[]; - sharingData: any; - isDirty: boolean; - onClose: () => void; -} - -/** - * @public - * Eui context menu entry shown directly in the context menu. `sortOrder` is - * used to order the individual items in a flat list returned by all registered - * action providers. - * */ -export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescriptor { - sortOrder: number; -} - -/** - * @public - * Definition of an action item rendered in the share menu. `shareMenuItem` is shown - * directly in the context menu. If the item is clicked, the `panel` is shown. - * */ -export interface ShareAction { - shareMenuItem: ShareContextMenuPanelItem; - panel: EuiContextMenuPanelDescriptor; -} - -/** @public */ -export interface ShareActionsProvider { - readonly id: string; - - getShareActions: (actionProps: ShareActionProps) => ShareAction[]; -} - -/** @public */ -export interface ShowProps extends Omit { - anchorElement: HTMLElement; - allowEmbed: boolean; - allowShortUrl: boolean; - shareActions: ShareAction[]; -} +import { ShareActionProps, ShareActionsProvider } from '../types'; export class ShareActionsRegistry { private readonly shareActionsProviders = new Map(); @@ -107,9 +41,6 @@ export class ShareActionsRegistry { getActions: (props: ShareActionProps) => Array.from(this.shareActionsProviders.values()) .flatMap(shareActionProvider => shareActionProvider.getShareActions(props)) - showShareContextMenu: (showProps: ShowProps) => { - - } }; } } diff --git a/src/plugins/share/public/services/share_context_menu.tsx b/src/plugins/share/public/services/share_context_menu.tsx new file mode 100644 index 000000000000..698c855759bd --- /dev/null +++ b/src/plugins/share/public/services/share_context_menu.tsx @@ -0,0 +1,105 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n/react'; +import { EuiWrappingPopover } from '@elastic/eui'; + +import { CoreStart, HttpStart } from 'kibana/public'; +import { ShareContextMenu } from '../components/share_context_menu'; +import { ShareAction, ShowProps } from '../types'; +import { ShareActionsRegistryStart } from './share_actions_registry'; + +export class ShareActionsContextMenu { + private isOpen = false; + + private container = document.createElement('div'); + + start(core: CoreStart, shareRegistry: ShareActionsRegistryStart) { + return { + showShareContextMenu: (props: ShowProps) => { + const shareActions = shareRegistry.getActions({ ...props, onClose: this.onClose }); + this.showShareContextMenu({ + ...props, + shareActions, + post: core.http.post, + basePath: core.http.basePath.get(), + }); + }, + }; + } + + private onClose() { + ReactDOM.unmountComponentAtNode(this.container); + this.isOpen = false; + } + + private showShareContextMenu({ + anchorElement, + allowEmbed, + allowShortUrl, + objectId, + objectType, + sharingData, + isDirty, + shareActions, + shareableUrl, + post, + basePath, + }: ShowProps & { shareActions: ShareAction[]; post: HttpStart['post']; basePath: string }) { + if (this.isOpen) { + this.onClose(); + return; + } + + this.isOpen = true; + + document.body.appendChild(this.container); + const element = ( + + + + + + ); + ReactDOM.render(element, this.container); + } +} +export type ShareActionsContextMenuStart = ReturnType; diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts new file mode 100644 index 000000000000..aa68f9a1577a --- /dev/null +++ b/src/plugins/share/public/types.ts @@ -0,0 +1,84 @@ +/* + * 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 { + EuiContextMenuPanelDescriptor, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui/src/components/context_menu/context_menu'; + +/** + * @public + * Properties of the current object to share. Registered share + * actions provider will provide suitable actions which have to + * be rendered in an appropriate place by the caller. + * + * It is possible to use the static function `showShareContextMenu` + * to render the menu as a popover. + * */ +export interface ShareActionProps { + objectType: string; + objectId?: string; + /** + * Current url for sharing. This can be set in cases where `window.location.href` + * does not contain a shareable URL (e.g. if using session storage to store the current + * app state is enabled). In these cases the property should contain the URL in a + * format which makes it possible to use it without having access to any other state + * like the current session. + * + * If not set it will default to `window.location.href` + */ + shareableUrl: string; + sharingData: unknown; + isDirty: boolean; + onClose: () => void; +} + +/** + * @public + * Eui context menu entry shown directly in the context menu. `sortOrder` is + * used to order the individual items in a flat list returned by all registered + * action providers. + * */ +export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescriptor { + sortOrder: number; +} + +/** + * @public + * Definition of an action item rendered in the share menu. `shareMenuItem` is shown + * directly in the context menu. If the item is clicked, the `panel` is shown. + * */ +export interface ShareAction { + shareMenuItem: ShareContextMenuPanelItem; + panel: EuiContextMenuPanelDescriptor; +} + +/** @public */ +export interface ShareActionsProvider { + readonly id: string; + + getShareActions: (actionProps: ShareActionProps) => ShareAction[]; +} + +/** @public */ +export interface ShowProps extends Omit { + anchorElement: HTMLElement; + allowEmbed: boolean; + allowShortUrl: boolean; +} diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index d1a764d7e386..da1bfc50372a 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -8,9 +8,9 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore: implicit any for JS file import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import React from 'react'; -import { ShareActionProps } from 'ui/share/share_action'; -import { ShareContextMenuExtensionsRegistryProvider } from 'ui/share/share_action_registry'; +import { npSetup } from 'ui/new_platform'; import { ReportingPanelContent } from '../components/reporting_panel_content'; +import { ShareActionProps } from '../../../../../../src/plugins/share/public'; function reportingProvider() { const getShareActions = ({ @@ -44,8 +44,10 @@ function reportingProvider() { toolTipContent: xpackInfo.get('features.reporting.csv.message'), disabled: !xpackInfo.get('features.reporting.csv.enableLinks', false) ? true : false, ['data-test-subj']: 'csvReportMenuItem', + sortOrder: 1, }, panel: { + id: 'csvReportingPanel', title: panelTitle, content: ( { if (!['dashboard', 'visualization'].includes(objectType)) { return []; } // Dashboard only mode does not currently support reporting // https://github.com/elastic/kibana/issues/18286 - if (objectType === 'dashboard' && dashboardConfig.getHideWriteControls()) { + if (objectType === 'dashboard' && injector.get('dashboardConfig').getHideWriteControls()) { return []; } const getReportingJobParams = () => { // Replace hashes with original RISON values. - const unhashedUrl = unhashUrl(window.location.href, getUnhashableStates()); - const relativeUrl = unhashedUrl.replace(window.location.origin + chrome.getBasePath(), ''); + const relativeUrl = shareableUrl.replace(window.location.origin + chrome.getBasePath(), ''); const browserTimezone = chrome.getUiSettingsClient().get('dateFormat:tz') === 'Browser' @@ -54,7 +54,7 @@ function reportingProvider(dashboardConfig: any) { const getPngJobParams = () => { // Replace hashes with original RISON values. const unhashedUrl = unhashUrl(window.location.href, getUnhashableStates()); - const relativeUrl = unhashedUrl.replace(window.location.origin + chrome.getBasePath(), ''); + const relativeUrl = shareableUrl.replace(window.location.origin + chrome.getBasePath(), ''); const browserTimezone = chrome.getUiSettingsClient().get('dateFormat:tz') === 'Browser' @@ -115,6 +115,7 @@ function reportingProvider(dashboardConfig: any) { sortOrder: 10, }, panel: { + id: 'reportingPanel', title: panelTitle, content: ( { + npSetup.plugins.share.register(await reportingProvider()); +})(); From 3627faddb802492907172975284ab9adedf65a0f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Sun, 10 Nov 2019 10:21:23 +0100 Subject: [PATCH 03/23] continued migrating share --- .../user/reporting/development/index.asciidoc | 4 +- .../public/discover/angular/discover.js | 10 +- .../kibana/public/discover/kibana_services.ts | 5 +- .../public/visualize/kibana_services.ts | 1 - src/legacy/ui/public/share/_index.scss | 1 - .../share_context_menu.test.js.snap | 121 ----- .../url_panel_content.test.js.snap | 431 ------------------ .../ui/public/share/components/_index.scss | 1 - .../share/components/_share_context_menu.scss | 8 - .../components/share_context_menu.test.js | 92 ---- .../share/components/share_context_menu.tsx | 190 -------- .../components/url_panel_content.test.js | 56 --- .../share/components/url_panel_content.tsx | 406 ----------------- src/legacy/ui/public/share/index.ts | 21 - .../ui/public/share/lib/url_shortener.test.js | 133 ------ .../ui/public/share/lib/url_shortener.ts | 43 -- src/legacy/ui/public/share/share_action.ts | 63 --- .../ui/public/share/share_action_registry.ts | 27 -- .../public/share/show_share_context_menu.tsx | 94 ---- .../public/services/share_context_menu.tsx | 4 +- 20 files changed, 9 insertions(+), 1702 deletions(-) delete mode 100644 src/legacy/ui/public/share/_index.scss delete mode 100644 src/legacy/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap delete mode 100644 src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap delete mode 100644 src/legacy/ui/public/share/components/_index.scss delete mode 100644 src/legacy/ui/public/share/components/_share_context_menu.scss delete mode 100644 src/legacy/ui/public/share/components/share_context_menu.test.js delete mode 100644 src/legacy/ui/public/share/components/share_context_menu.tsx delete mode 100644 src/legacy/ui/public/share/components/url_panel_content.test.js delete mode 100644 src/legacy/ui/public/share/components/url_panel_content.tsx delete mode 100644 src/legacy/ui/public/share/index.ts delete mode 100644 src/legacy/ui/public/share/lib/url_shortener.test.js delete mode 100644 src/legacy/ui/public/share/lib/url_shortener.ts delete mode 100644 src/legacy/ui/public/share/share_action.ts delete mode 100644 src/legacy/ui/public/share/share_action_registry.ts delete mode 100644 src/legacy/ui/public/share/show_share_context_menu.tsx diff --git a/docs/user/reporting/development/index.asciidoc b/docs/user/reporting/development/index.asciidoc index 2a9abae34f04..a64e540da0c7 100644 --- a/docs/user/reporting/development/index.asciidoc +++ b/docs/user/reporting/development/index.asciidoc @@ -14,9 +14,7 @@ However, these docs will be kept up-to-date to reflect the current implementatio [float] [[reporting-nav-bar-extensions]] === Share menu extensions -X-Pack uses the `ShareContextMenuExtensionsRegistryProvider` to register actions in the share menu. - -This integration will likely be changing in the near future as we move towards a unified actions abstraction across {kib}. +X-Pack uses the `share` plugin of the Kibana platform to register actions in the share menu. [float] === Generate job URL diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index ed5049aa912e..65c61f49f73c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -52,7 +52,7 @@ import { migrateLegacyQuery, RequestAdapter, showSaveModal, - showShareContextMenu, + unhashUrl, stateMonitorFactory, subscribeWithScope, tabifyAggResponse, @@ -65,7 +65,7 @@ const { chrome, docTitle, FilterBarQueryFilterProvider, - ShareContextMenuExtensionsRegistryProvider, + share, StateProvider, timefilter, toastNotifications, @@ -193,7 +193,6 @@ function discoverController( const Vis = Private(VisProvider); const responseHandler = vislibSeriesResponseHandlerProvider().handler; const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); const queryFilter = Private(FilterBarQueryFilterProvider); const filterGen = getFilterGenerator(queryFilter); @@ -327,14 +326,13 @@ function discoverController( testId: 'shareTopNavButton', run: async (anchorElement) => { const sharingData = await this.getSharingData(); - showShareContextMenu({ + share.showShareContextMenu({ anchorElement, allowEmbed: false, allowShortUrl: uiCapabilities.discover.createShortUrl, - getUnhashableStates, + shareableUrl: unhashUrl(window.location.href, getUnhashableStates()), objectId: savedSearch.id, objectType: 'search', - shareContextMenuExtensions, sharingData: { ...sharingData, title: savedSearch.title, diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index b78d05e68aca..e89bdf17b80a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -36,7 +36,6 @@ import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { timefilter } from 'ui/timefilter'; -import { ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; // @ts-ignore import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; import { wrapInI18nContext } from 'ui/i18n'; @@ -58,6 +57,7 @@ const services = { uiSettings: npStart.core.uiSettings, uiActions: npStart.plugins.uiActions, embeddable: npStart.plugins.embeddable, + share: npStart.plugins.share, // legacy docTitle, docViewsRegistry, @@ -68,7 +68,6 @@ const services = { SavedObjectRegistryProvider, SavedObjectProvider, SearchSource, - ShareContextMenuExtensionsRegistryProvider, StateProvider, timefilter, uiModules, @@ -101,7 +100,6 @@ export { RequestAdapter } from 'ui/inspector/adapters'; export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; export { FieldList } from 'ui/index_patterns'; export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -export { showShareContextMenu } from 'ui/share'; export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore @@ -112,6 +110,7 @@ export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; export { tabifyAggResponse } from 'ui/agg_response/tabify'; // @ts-ignore export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; +export { unhashUrl } from 'ui/state_management/state_hashing'; // EXPORT types export { VisProvider } from 'ui/vis'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 25d3473d9d1e..c60ad7216949 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -98,7 +98,6 @@ export { VisEditorTypesRegistryProvider } from 'ui/registry/vis_editor_types'; // @ts-ignore export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -export { showShareContextMenu } from 'ui/share'; export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; diff --git a/src/legacy/ui/public/share/_index.scss b/src/legacy/ui/public/share/_index.scss deleted file mode 100644 index 192091fb04e3..000000000000 --- a/src/legacy/ui/public/share/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/src/legacy/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap b/src/legacy/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap deleted file mode 100644 index df50f1d4a78b..000000000000 --- a/src/legacy/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap +++ /dev/null @@ -1,121 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`shareContextMenuExtensions should sort ascending on sort order first and then ascending on name 1`] = ` -, - "id": 1, - "title": "Permalink", - }, - Object { - "content":
- panel content -
, - "id": 2, - "title": "AAA panel", - }, - Object { - "content":
- panel content -
, - "id": 3, - "title": "ZZZ panel", - }, - Object { - "id": 4, - "items": Array [ - Object { - "data-test-subj": "sharePanel-Permalinks", - "icon": "link", - "name": "Permalinks", - "panel": 1, - }, - Object { - "data-test-subj": "sharePanel-ZZZpanel", - "name": "ZZZ panel", - "panel": 3, - }, - Object { - "data-test-subj": "sharePanel-AAApanel", - "name": "AAA panel", - "panel": 2, - }, - ], - "title": "Share this dashboard", - }, - ] - } -/> -`; - -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 { - "data-test-subj": "sharePanel-Embedcode", - "icon": "console", - "name": "Embed code", - "panel": 2, - }, - Object { - "data-test-subj": "sharePanel-Permalinks", - "icon": "link", - "name": "Permalinks", - "panel": 1, - }, - ], - "title": "Share this dashboard", - }, - ] - } -/> -`; diff --git a/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap b/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap deleted file mode 100644 index 645b8c662c41..000000000000 --- a/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap +++ /dev/null @@ -1,431 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render 1`] = ` - - - } - label={ - - } - labelType="label" - > - - - - - - - } - position="bottom" - /> - - , - }, - Object { - "data-test-subj": "exportAsSavedObject", - "disabled": true, - "id": "savedObject", - "label": - - - - - - } - position="bottom" - /> - - , - }, - ] - } - /> - - - - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - - - - - - - -`; - -exports[`should enable saved object export option when objectId is provided 1`] = ` - - - } - labelType="label" - > - - - - - - - } - position="bottom" - /> - - , - }, - Object { - "data-test-subj": "exportAsSavedObject", - "disabled": false, - "id": "savedObject", - "label": - - - - - - } - position="bottom" - /> - - , - }, - ] - } - /> - - - - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - - - - - - - -`; - -exports[`should hide short url section when allowShortUrl is false 1`] = ` - - - } - labelType="label" - > - - - - - - - } - position="bottom" - /> - - , - }, - Object { - "data-test-subj": "exportAsSavedObject", - "disabled": false, - "id": "savedObject", - "label": - - - - - - } - position="bottom" - /> - - , - }, - ] - } - /> - - - - - - -`; diff --git a/src/legacy/ui/public/share/components/_index.scss b/src/legacy/ui/public/share/components/_index.scss deleted file mode 100644 index 85168c9ea80f..000000000000 --- a/src/legacy/ui/public/share/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './share_context_menu'; diff --git a/src/legacy/ui/public/share/components/_share_context_menu.scss b/src/legacy/ui/public/share/components/_share_context_menu.scss deleted file mode 100644 index d28e7846d813..000000000000 --- a/src/legacy/ui/public/share/components/_share_context_menu.scss +++ /dev/null @@ -1,8 +0,0 @@ -.kbnShareContextMenu__finalPanel { - padding: $euiSize; -} - -.kbnShareContextMenu__copyAnchor, -.kbnShareContextMenu__copyButton { - width: 100%; -} diff --git a/src/legacy/ui/public/share/components/share_context_menu.test.js b/src/legacy/ui/public/share/components/share_context_menu.test.js deleted file mode 100644 index 5b583420f85e..000000000000 --- a/src/legacy/ui/public/share/components/share_context_menu.test.js +++ /dev/null @@ -1,92 +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. - */ - -jest.mock('../lib/url_shortener', () => ({})); - -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; - -import { - ShareContextMenu, -} from './share_context_menu'; - -test('should render context menu panel when there are more than one panel', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); - -test('should only render permalink panel when there are no other panels', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); - -describe('shareContextMenuExtensions', () => { - const shareContextMenuExtensions = [ - { - getShareActions: () => { - return [ - { - panel: { - title: 'AAA panel', - content: (
panel content
), - }, - shareMenuItem: { - name: 'AAA panel', - sortOrder: 5, - } - } - ]; - } - }, - { - getShareActions: () => { - return [ - { - panel: { - title: 'ZZZ panel', - content: (
panel content
), - }, - shareMenuItem: { - name: 'ZZZ panel', - sortOrder: 0, - } - } - ]; - } - } - ]; - - test('should sort ascending on sort order first and then ascending on name', () => { - const component = shallowWithIntl( {}} - shareContextMenuExtensions={shareContextMenuExtensions} - />); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/share/components/share_context_menu.tsx b/src/legacy/ui/public/share/components/share_context_menu.tsx deleted file mode 100644 index 5d5c80f10e1d..000000000000 --- a/src/legacy/ui/public/share/components/share_context_menu.tsx +++ /dev/null @@ -1,190 +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 React, { Component } from 'react'; - -import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; -import { EuiContextMenu } from '@elastic/eui'; - -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { ShareAction, ShareActionProvider, ShareContextMenuPanelItem } from 'ui/share/share_action'; -import { UrlPanelContent } from './url_panel_content'; - -interface Props { - allowEmbed: boolean; - allowShortUrl: boolean; - objectId?: string; - objectType: string; - getUnhashableStates: () => object[]; - shareContextMenuExtensions?: ShareActionProvider[]; - sharingData: any; - isDirty: boolean; - onClose: () => void; - intl: InjectedIntl; -} - -class ShareContextMenuUI extends Component { - public render() { - const { panels, initialPanelId } = this.getPanels(); - return ( - - ); - } - - private getPanels = () => { - const panels: EuiContextMenuPanelDescriptor[] = []; - const menuItems: ShareContextMenuPanelItem[] = []; - const { intl } = this.props; - - const permalinkPanel = { - id: panels.length + 1, - title: intl.formatMessage({ - id: 'common.ui.share.contextMenu.permalinkPanelTitle', - defaultMessage: 'Permalink', - }), - content: ( - - ), - }; - menuItems.push({ - name: intl.formatMessage({ - id: 'common.ui.share.contextMenu.permalinksLabel', - defaultMessage: 'Permalinks', - }), - icon: 'link', - panel: permalinkPanel.id, - sortOrder: 0, - }); - panels.push(permalinkPanel); - - if (this.props.allowEmbed) { - const embedPanel = { - id: panels.length + 1, - title: intl.formatMessage({ - id: 'common.ui.share.contextMenu.embedCodePanelTitle', - defaultMessage: 'Embed Code', - }), - content: ( - - ), - }; - panels.push(embedPanel); - menuItems.push({ - name: intl.formatMessage({ - id: 'common.ui.share.contextMenu.embedCodeLabel', - defaultMessage: 'Embed code', - }), - icon: 'console', - panel: embedPanel.id, - sortOrder: 0, - }); - } - - if (this.props.shareContextMenuExtensions) { - const { - objectType, - objectId, - getUnhashableStates, - sharingData, - isDirty, - onClose, - } = this.props; - this.props.shareContextMenuExtensions.forEach((provider: ShareActionProvider) => { - provider - .getShareActions({ - objectType, - objectId, - getUnhashableStates, - sharingData, - isDirty, - onClose, - }) - .forEach(({ shareMenuItem, panel }: ShareAction) => { - const panelId = panels.length + 1; - panels.push({ - ...panel, - id: panelId, - }); - menuItems.push({ - ...shareMenuItem, - panel: panelId, - }); - }); - }); - } - - if (menuItems.length > 1) { - const topLevelMenuPanel = { - id: panels.length + 1, - title: intl.formatMessage( - { - id: 'common.ui.share.contextMenuTitle', - defaultMessage: 'Share this {objectType}', - }, - { - objectType: this.props.objectType, - } - ), - items: menuItems - // Sorts ascending on sort order first and then ascending on name - .sort((a, b) => { - const aSortOrder = a.sortOrder || 0; - const bSortOrder = b.sortOrder || 0; - if (aSortOrder > bSortOrder) { - return 1; - } - if (aSortOrder < bSortOrder) { - return -1; - } - if (a.name.toLowerCase().localeCompare(b.name.toLowerCase()) > 0) { - return 1; - } - return -1; - }) - .map(menuItem => { - menuItem['data-test-subj'] = `sharePanel-${menuItem.name.replace(' ', '')}`; - delete menuItem.sortOrder; - return menuItem; - }), - }; - panels.push(topLevelMenuPanel); - } - - const lastPanelIndex = panels.length - 1; - const initialPanelId = panels[lastPanelIndex].id; - return { panels, initialPanelId }; - }; -} - -export const ShareContextMenu = injectI18n(ShareContextMenuUI); diff --git a/src/legacy/ui/public/share/components/url_panel_content.test.js b/src/legacy/ui/public/share/components/url_panel_content.test.js deleted file mode 100644 index 8dd94202fe72..000000000000 --- a/src/legacy/ui/public/share/components/url_panel_content.test.js +++ /dev/null @@ -1,56 +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. - */ - -jest.mock('../lib/url_shortener', () => ({})); - -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; - -import { - UrlPanelContent, -} from './url_panel_content'; - -test('render', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); - -test('should enable saved object export option when objectId is provided', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); - -test('should hide short url section when allowShortUrl is false', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); diff --git a/src/legacy/ui/public/share/components/url_panel_content.tsx b/src/legacy/ui/public/share/components/url_panel_content.tsx deleted file mode 100644 index 28d51e9826d4..000000000000 --- a/src/legacy/ui/public/share/components/url_panel_content.tsx +++ /dev/null @@ -1,406 +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 React, { Component } from 'react'; - -import { - EuiButton, - EuiCopy, - EuiFlexGroup, - EuiSpacer, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiIconTip, - EuiLoadingSpinner, - EuiRadioGroup, - EuiSwitch, -} from '@elastic/eui'; - -import { format as formatUrl, parse as parseUrl } from 'url'; - -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -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; - -interface Props { - allowShortUrl: boolean; - isEmbedded?: boolean; - objectId?: string; - objectType: string; - getUnhashableStates: () => object[]; - intl: InjectedIntl; -} - -enum ExportUrlAsType { - EXPORT_URL_AS_SAVED_OBJECT = 'savedObject', - EXPORT_URL_AS_SNAPSHOT = 'snapshot', -} - -interface State { - exportUrlAs: ExportUrlAsType; - useShortUrl: boolean; - isCreatingShortUrl: boolean; - url?: string; - shortUrlErrorMsg?: string; -} - -class UrlPanelContentUI 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, - isCreatingShortUrl: false, - url: '', - }; - } - - public componentWillUnmount() { - window.removeEventListener('hashchange', this.resetUrl); - - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - this.setUrl(); - - window.addEventListener('hashchange', this.resetUrl, false); - } - - public render() { - return ( - - {this.renderExportAsRadioGroup()} - - {this.renderShortUrlSwitch()} - - - - - {(copy: () => void) => ( - - {this.props.isEmbedded ? ( - - ) : ( - - )} - - )} - - - ); - } - - private isNotSaved = () => { - return this.props.objectId === undefined || this.props.objectId === ''; - }; - - private resetUrl = () => { - if (this.mounted) { - this.shortUrlCache = undefined; - this.setState( - { - useShortUrl: false, - }, - this.setUrl - ); - } - }; - - private getSavedObjectUrl = () => { - if (this.isNotSaved()) { - return; - } - - const url = window.location.href; - // Replace hashes with original RISON values. - 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); - - 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, - }, - }), - }); - }; - - private getSnapshotUrl = () => { - const url = window.location.href; - // Replace hashes with original RISON values. - return unhashUrl(url, this.props.getUnhashableStates()); - }; - - private makeUrlEmbeddable = (url: string) => { - const embedQueryParam = '?embed=true'; - const urlHasQueryString = url.indexOf('?') !== -1; - if (urlHasQueryString) { - return url.replace('?', `${embedQueryParam}&`); - } - return `${url}${embedQueryParam}`; - }; - - private makeIframeTag = (url?: string) => { - if (!url) { - return; - } - - const embeddableUrl = this.makeUrlEmbeddable(url); - return ``; - }; - - private setUrl = () => { - let url; - if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { - url = this.getSavedObjectUrl(); - } else if (this.state.useShortUrl) { - url = this.shortUrlCache; - } else { - url = this.getSnapshotUrl(); - } - - if (this.props.isEmbedded) { - url = this.makeIframeTag(url); - } - - this.setState({ url }); - }; - - private handleExportUrlAs = (optionId: string) => { - this.setState( - { - exportUrlAs: optionId as ExportUrlAsType, - }, - this.setUrl - ); - }; - - // 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; - - if (!isChecked || this.shortUrlCache !== 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.shortUrlCache = shortUrl; - this.setState( - { - isCreatingShortUrl: false, - useShortUrl: isChecked, - }, - this.setUrl - ); - } - } catch (fetchError) { - if (this.mounted) { - this.shortUrlCache = undefined; - this.setState( - { - useShortUrl: false, - isCreatingShortUrl: false, - shortUrlErrorMsg: this.props.intl.formatMessage( - { - id: 'common.ui.share.urlPanel.unableCreateShortUrlErrorMessage', - defaultMessage: 'Unable to create short URL. Error: {errorMessage}', - }, - { - errorMessage: fetchError.message, - } - ), - }, - this.setUrl - ); - } - } - }; - - private renderExportUrlAsOptions = () => { - return [ - { - id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, - label: this.renderWithIconTip( - , - - ), - ['data-test-subj']: 'exportAsSnapshot', - }, - { - id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, - disabled: this.isNotSaved(), - label: this.renderWithIconTip( - , - - ), - ['data-test-subj']: 'exportAsSavedObject', - }, - ]; - }; - - private renderWithIconTip = (child: React.ReactNode, tipContent: React.ReactNode) => { - return ( - - {child} - - - - - ); - }; - - private renderExportAsRadioGroup = () => { - const generateLinkAsHelp = this.isNotSaved() ? ( - - ) : ( - undefined - ); - return ( - - } - helpText={generateLinkAsHelp} - > - - - ); - }; - - private renderShortUrlSwitch = () => { - if ( - this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT || - !this.props.allowShortUrl - ) { - return; - } - const shortUrlLabel = ( - - ); - const switchLabel = this.state.isCreatingShortUrl ? ( - - {shortUrlLabel} - - ) : ( - shortUrlLabel - ); - const switchComponent = ( - - ); - const tipContent = ( - - ); - - return ( - - {this.renderWithIconTip(switchComponent, tipContent)} - - ); - }; -} - -export const UrlPanelContent = injectI18n(UrlPanelContentUI); diff --git a/src/legacy/ui/public/share/index.ts b/src/legacy/ui/public/share/index.ts deleted file mode 100644 index 3a1264541cde..000000000000 --- a/src/legacy/ui/public/share/index.ts +++ /dev/null @@ -1,21 +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. - */ - -export { showShareContextMenu } from './show_share_context_menu'; -export { ShareContextMenuExtensionsRegistryProvider } from './share_action_registry'; diff --git a/src/legacy/ui/public/share/lib/url_shortener.test.js b/src/legacy/ui/public/share/lib/url_shortener.test.js deleted file mode 100644 index 859873bd4989..000000000000 --- a/src/legacy/ui/public/share/lib/url_shortener.test.js +++ /dev/null @@ -1,133 +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. - */ -jest.mock('ui/kfetch', () => ({})); - -jest.mock('../../chrome', () => ({})); - -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { shortenUrl } from './url_shortener'; - -describe('Url shortener', () => { - const shareId = 'id123'; - - let kfetchStub; - beforeEach(() => { - kfetchStub = sinon.stub(); - require('ui/kfetch').kfetch = async (...args) => { - return kfetchStub(...args); - }; - }); - - describe('Shorten without base path', () => { - beforeAll(() => { - require('../../chrome').getBasePath = () => { - return ''; - }; - }); - - it('should shorten urls with a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl('http://localhost:5601/app/kibana#123'); - expect(shortUrl).to.be(`http://localhost:5601/goto/${shareId}`); - }); - - it('should shorten urls without a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl('http://localhost/app/kibana#123'); - expect(shortUrl).to.be(`http://localhost/goto/${shareId}`); - }); - }); - - describe('Shorten with base path', () => { - const basePath = '/foo'; - - beforeAll(() => { - require('../../chrome').getBasePath = () => { - return basePath; - }; - }); - - it('should shorten urls with a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost:5601${basePath}/app/kibana#123`); - expect(shortUrl).to.be(`http://localhost:5601${basePath}/goto/${shareId}`); - }); - - it('should shorten urls without a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana#123`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - - it('should shorten urls with a query string', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana?foo#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana?foo#123`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - - it('should shorten urls without a hash', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - - it('should shorten urls with a query string in the hash', async () => { - const relativeUrl = "/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"; //eslint-disable-line max-len, quotes - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"}' //eslint-disable-line max-len, quotes - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}${relativeUrl}`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - }); -}); diff --git a/src/legacy/ui/public/share/lib/url_shortener.ts b/src/legacy/ui/public/share/lib/url_shortener.ts deleted file mode 100644 index 037214bd9b45..000000000000 --- a/src/legacy/ui/public/share/lib/url_shortener.ts +++ /dev/null @@ -1,43 +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 { kfetch } from 'ui/kfetch'; -import url from 'url'; -import chrome from '../../chrome'; - -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 }); - return url.format({ - protocol: parsedUrl.protocol, - host: parsedUrl.host, - pathname: `${basePath}/goto/${resp.urlId}`, - }); -} diff --git a/src/legacy/ui/public/share/share_action.ts b/src/legacy/ui/public/share/share_action.ts deleted file mode 100644 index 5e524944d209..000000000000 --- a/src/legacy/ui/public/share/share_action.ts +++ /dev/null @@ -1,63 +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. - */ - -/* - * 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 mayexport - * 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 { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; - -export interface ShareActionProps { - objectType: string; - objectId?: string; - getUnhashableStates: () => object[]; - sharingData: any; - isDirty: boolean; - onClose: () => void; -} - -export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescriptor { - sortOrder: number; -} - -export interface ShareAction { - shareMenuItem: ShareContextMenuPanelItem; - panel: EuiContextMenuPanelDescriptor; -} - -export interface ShareActionProvider { - readonly id: string; - - getShareActions: (actionProps: ShareActionProps) => ShareAction[]; -} diff --git a/src/legacy/ui/public/share/share_action_registry.ts b/src/legacy/ui/public/share/share_action_registry.ts deleted file mode 100644 index eec743d783a3..000000000000 --- a/src/legacy/ui/public/share/share_action_registry.ts +++ /dev/null @@ -1,27 +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. - */ - -// @ts-ignore: implicit any for JS file -import { uiRegistry } from 'ui/registry/_registry'; -import { ShareActionProvider } from './share_action'; - -export const ShareContextMenuExtensionsRegistryProvider = uiRegistry({ - name: 'shareContextMenuExtensions', - index: ['id'], -}); diff --git a/src/legacy/ui/public/share/show_share_context_menu.tsx b/src/legacy/ui/public/share/show_share_context_menu.tsx deleted file mode 100644 index 1b3da0c6dc06..000000000000 --- a/src/legacy/ui/public/share/show_share_context_menu.tsx +++ /dev/null @@ -1,94 +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 React from 'react'; -import ReactDOM from 'react-dom'; - -import { EuiWrappingPopover } from '@elastic/eui'; -import { I18nContext } from 'ui/i18n'; -import { ShareContextMenu } from './components/share_context_menu'; -import { ShareActionProvider } from './share_action'; - -let isOpen = false; - -const container = document.createElement('div'); - -const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - isOpen = false; -}; - -interface ShowProps { - anchorElement: any; - allowEmbed: boolean; - allowShortUrl: boolean; - getUnhashableStates: () => object[]; - objectId?: string; - objectType: string; - shareContextMenuExtensions?: ShareActionProvider[]; - sharingData: any; - isDirty: boolean; -} - -export function showShareContextMenu({ - anchorElement, - allowEmbed, - allowShortUrl, - getUnhashableStates, - objectId, - objectType, - shareContextMenuExtensions, - sharingData, - isDirty, -}: ShowProps) { - if (isOpen) { - onClose(); - return; - } - - isOpen = true; - - document.body.appendChild(container); - const element = ( - - - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/plugins/share/public/services/share_context_menu.tsx b/src/plugins/share/public/services/share_context_menu.tsx index 698c855759bd..5da8b97b4bdc 100644 --- a/src/plugins/share/public/services/share_context_menu.tsx +++ b/src/plugins/share/public/services/share_context_menu.tsx @@ -46,10 +46,10 @@ export class ShareActionsContextMenu { }; } - private onClose() { + private onClose = () => { ReactDOM.unmountComponentAtNode(this.container); this.isOpen = false; - } + }; private showShareContextMenu({ anchorElement, From 0a93622fd8f497099c032b8f6d0cedd75108f01e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 11 Nov 2019 10:42:29 +0100 Subject: [PATCH 04/23] clean up and test --- .../share_context_menu.test.js.snap | 121 ----- .../share_context_menu.test.tsx.snap | 135 ++++++ .../url_panel_content.test.js.snap | 431 ----------------- .../url_panel_content.test.tsx.snap | 437 ++++++++++++++++++ .../components/share_context_menu.test.js | 92 ---- .../components/share_context_menu.test.tsx | 87 ++++ .../public/components/share_context_menu.tsx | 8 +- ...ent.test.js => url_panel_content.test.tsx} | 35 +- src/plugins/share/public/index.ts | 2 +- .../share/public/lib/url_shortener.test.js | 133 ------ .../share/public/lib/url_shortener.test.ts | 115 +++++ src/plugins/share/public/plugin.test.mocks.ts | 9 +- src/plugins/share/public/plugin.test.ts | 11 +- src/plugins/share/public/plugin.ts | 19 +- src/plugins/share/public/services/index.ts | 3 +- .../services/share_menu_manager.mock.ts | 40 ++ ...ontext_menu.tsx => share_menu_manager.tsx} | 20 +- ...ry.mock.ts => share_menu_registry.mock.ts} | 18 +- ...ry.test.ts => share_menu_registry.test.ts} | 29 +- ...ons_registry.ts => share_menu_registry.ts} | 25 +- src/plugins/share/public/types.ts | 16 +- .../register_csv_reporting.tsx | 8 +- .../share_context_menu/register_reporting.tsx | 13 +- 23 files changed, 926 insertions(+), 881 deletions(-) delete mode 100644 src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap create mode 100644 src/plugins/share/public/components/__snapshots__/share_context_menu.test.tsx.snap delete mode 100644 src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap create mode 100644 src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap delete mode 100644 src/plugins/share/public/components/share_context_menu.test.js create mode 100644 src/plugins/share/public/components/share_context_menu.test.tsx rename src/plugins/share/public/components/{url_panel_content.test.js => url_panel_content.test.tsx} (64%) delete mode 100644 src/plugins/share/public/lib/url_shortener.test.js create mode 100644 src/plugins/share/public/lib/url_shortener.test.ts create mode 100644 src/plugins/share/public/services/share_menu_manager.mock.ts rename src/plugins/share/public/services/{share_context_menu.tsx => share_menu_manager.tsx} (81%) rename src/plugins/share/public/services/{share_actions_registry.mock.ts => share_menu_registry.mock.ts} (69%) rename src/plugins/share/public/services/{share_actions_registry.test.ts => share_menu_registry.test.ts} (68%) rename src/plugins/share/public/services/{share_actions_registry.ts => share_menu_registry.ts} (51%) diff --git a/src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap b/src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap deleted file mode 100644 index df50f1d4a78b..000000000000 --- a/src/plugins/share/public/components/__snapshots__/share_context_menu.test.js.snap +++ /dev/null @@ -1,121 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`shareContextMenuExtensions should sort ascending on sort order first and then ascending on name 1`] = ` -, - "id": 1, - "title": "Permalink", - }, - Object { - "content":
- panel content -
, - "id": 2, - "title": "AAA panel", - }, - Object { - "content":
- panel content -
, - "id": 3, - "title": "ZZZ panel", - }, - Object { - "id": 4, - "items": Array [ - Object { - "data-test-subj": "sharePanel-Permalinks", - "icon": "link", - "name": "Permalinks", - "panel": 1, - }, - Object { - "data-test-subj": "sharePanel-ZZZpanel", - "name": "ZZZ panel", - "panel": 3, - }, - Object { - "data-test-subj": "sharePanel-AAApanel", - "name": "AAA panel", - "panel": 2, - }, - ], - "title": "Share this dashboard", - }, - ] - } -/> -`; - -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 { - "data-test-subj": "sharePanel-Embedcode", - "icon": "console", - "name": "Embed code", - "panel": 2, - }, - Object { - "data-test-subj": "sharePanel-Permalinks", - "icon": "link", - "name": "Permalinks", - "panel": 1, - }, - ], - "title": "Share this dashboard", - }, - ] - } -/> -`; diff --git a/src/plugins/share/public/components/__snapshots__/share_context_menu.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/share_context_menu.test.tsx.snap new file mode 100644 index 000000000000..fc3fa3e72b9c --- /dev/null +++ b/src/plugins/share/public/components/__snapshots__/share_context_menu.test.tsx.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shareContextMenuExtensions should sort ascending on sort order first and then ascending on name 1`] = ` + + , + "id": 1, + "title": "Permalink", + }, + Object { + "content":
+ panel content +
, + "id": 2, + "title": "AAA panel", + }, + Object { + "content":
+ panel content +
, + "id": 3, + "title": "ZZZ panel", + }, + Object { + "id": 4, + "items": Array [ + Object { + "data-test-subj": "sharePanel-Permalinks", + "icon": "link", + "name": "Permalinks", + "panel": 1, + }, + Object { + "data-test-subj": "sharePanel-ZZZpanel", + "name": "ZZZ panel", + "panel": 3, + }, + Object { + "data-test-subj": "sharePanel-AAApanel", + "name": "AAA panel", + "panel": 2, + }, + ], + "title": "Share this dashboard", + }, + ] + } + /> +
+`; + +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 { + "data-test-subj": "sharePanel-Embedcode", + "icon": "console", + "name": "Embed code", + "panel": 2, + }, + Object { + "data-test-subj": "sharePanel-Permalinks", + "icon": "link", + "name": "Permalinks", + "panel": 1, + }, + ], + "title": "Share this dashboard", + }, + ] + } + /> + +`; diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap deleted file mode 100644 index 645b8c662c41..000000000000 --- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.js.snap +++ /dev/null @@ -1,431 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render 1`] = ` - - - } - label={ - - } - labelType="label" - > - - - - - - - } - position="bottom" - /> - - , - }, - Object { - "data-test-subj": "exportAsSavedObject", - "disabled": true, - "id": "savedObject", - "label": - - - - - - } - position="bottom" - /> - - , - }, - ] - } - /> - - - - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - - - - - - - -`; - -exports[`should enable saved object export option when objectId is provided 1`] = ` - - - } - labelType="label" - > - - - - - - - } - position="bottom" - /> - - , - }, - Object { - "data-test-subj": "exportAsSavedObject", - "disabled": false, - "id": "savedObject", - "label": - - - - - - } - position="bottom" - /> - - , - }, - ] - } - /> - - - - - - } - onChange={[Function]} - /> - - - - } - position="bottom" - /> - - - - - - - - -`; - -exports[`should hide short url section when allowShortUrl is false 1`] = ` - - - } - labelType="label" - > - - - - - - - } - position="bottom" - /> - - , - }, - Object { - "data-test-subj": "exportAsSavedObject", - "disabled": false, - "id": "savedObject", - "label": - - - - - - } - position="bottom" - /> - - , - }, - ] - } - /> - - - - - - -`; diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap new file mode 100644 index 000000000000..980779a02e69 --- /dev/null +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap @@ -0,0 +1,437 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render 1`] = ` + + + + } + label={ + + } + labelType="label" + > + + + + + + + } + position="bottom" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": true, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + /> + + , + }, + ] + } + /> + + + + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + + + + + + + +`; + +exports[`should enable saved object export option when objectId is provided 1`] = ` + + + + } + labelType="label" + > + + + + + + + } + position="bottom" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": false, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + /> + + , + }, + ] + } + /> + + + + + + } + onChange={[Function]} + /> + + + + } + position="bottom" + /> + + + + + + + + + +`; + +exports[`should hide short url section when allowShortUrl is false 1`] = ` + + + + } + labelType="label" + > + + + + + + + } + position="bottom" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": false, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + /> + + , + }, + ] + } + /> + + + + + + + +`; diff --git a/src/plugins/share/public/components/share_context_menu.test.js b/src/plugins/share/public/components/share_context_menu.test.js deleted file mode 100644 index 5b583420f85e..000000000000 --- a/src/plugins/share/public/components/share_context_menu.test.js +++ /dev/null @@ -1,92 +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. - */ - -jest.mock('../lib/url_shortener', () => ({})); - -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; - -import { - ShareContextMenu, -} from './share_context_menu'; - -test('should render context menu panel when there are more than one panel', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); - -test('should only render permalink panel when there are no other panels', () => { - const component = shallowWithIntl( {}} - />); - expect(component).toMatchSnapshot(); -}); - -describe('shareContextMenuExtensions', () => { - const shareContextMenuExtensions = [ - { - getShareActions: () => { - return [ - { - panel: { - title: 'AAA panel', - content: (
panel content
), - }, - shareMenuItem: { - name: 'AAA panel', - sortOrder: 5, - } - } - ]; - } - }, - { - getShareActions: () => { - return [ - { - panel: { - title: 'ZZZ panel', - content: (
panel content
), - }, - shareMenuItem: { - name: 'ZZZ panel', - sortOrder: 0, - } - } - ]; - } - } - ]; - - test('should sort ascending on sort order first and then ascending on name', () => { - const component = shallowWithIntl( {}} - shareContextMenuExtensions={shareContextMenuExtensions} - />); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/share/public/components/share_context_menu.test.tsx b/src/plugins/share/public/components/share_context_menu.test.tsx new file mode 100644 index 000000000000..7fb0449ead50 --- /dev/null +++ b/src/plugins/share/public/components/share_context_menu.test.tsx @@ -0,0 +1,87 @@ +/* + * 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 { ShareMenuItem } from '../types'; + +jest.mock('../lib/url_shortener', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ShareContextMenu } from './share_context_menu'; + +const defaultProps = { + allowEmbed: true, + allowShortUrl: false, + shareMenuItems: [], + sharingData: null, + isDirty: false, + onClose: () => {}, + basePath: '', + post: () => Promise.resolve(), + objectType: 'dashboard', +}; + +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(); +}); + +describe('shareContextMenuExtensions', () => { + const shareContextMenuItems: ShareMenuItem[] = [ + { + panel: { + id: '1', + title: 'AAA panel', + content:
panel content
, + }, + shareMenuItem: { + name: 'AAA panel', + sortOrder: 5, + }, + }, + { + panel: { + id: '2', + title: 'ZZZ panel', + content:
panel content
, + }, + shareMenuItem: { + name: 'ZZZ panel', + sortOrder: 0, + }, + }, + ]; + + test('should sort ascending on sort order first and then ascending on name', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/share/public/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx index 9931a2eddc8d..4aca5cf621d9 100644 --- a/src/plugins/share/public/components/share_context_menu.tsx +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -27,7 +27,7 @@ import { EuiContextMenu } from '@elastic/eui'; import { HttpStart } from 'kibana/public'; import { UrlPanelContent } from './url_panel_content'; -import { ShareAction, ShareContextMenuPanelItem } from '../types'; +import { ShareMenuItem, ShareContextMenuPanelItem } from '../types'; interface Props { allowEmbed: boolean; @@ -35,7 +35,7 @@ interface Props { objectId?: string; objectType: string; shareableUrl?: string; - shareActions: ShareAction[]; + shareMenuItems: ShareMenuItem[]; sharingData: any; isDirty: boolean; onClose: () => void; @@ -73,6 +73,7 @@ export class ShareContextMenu extends Component { objectType={this.props.objectType} basePath={this.props.basePath} post={this.props.post} + shareableUrl={this.props.shareableUrl} /> ), }; @@ -100,6 +101,7 @@ export class ShareContextMenu extends Component { objectType={this.props.objectType} basePath={this.props.basePath} post={this.props.post} + shareableUrl={this.props.shareableUrl} /> ), }; @@ -114,7 +116,7 @@ export class ShareContextMenu extends Component { }); } - this.props.shareActions.forEach(({ shareMenuItem, panel }) => { + this.props.shareMenuItems.forEach(({ shareMenuItem, panel }) => { const panelId = panels.length + 1; panels.push({ ...panel, diff --git a/src/plugins/share/public/components/url_panel_content.test.js b/src/plugins/share/public/components/url_panel_content.test.tsx similarity index 64% rename from src/plugins/share/public/components/url_panel_content.test.js rename to src/plugins/share/public/components/url_panel_content.test.tsx index 8dd94202fe72..9da1a23641ab 100644 --- a/src/plugins/share/public/components/url_panel_content.test.js +++ b/src/plugins/share/public/components/url_panel_content.test.tsx @@ -20,37 +20,30 @@ jest.mock('../lib/url_shortener', () => ({})); import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; -import { - UrlPanelContent, -} from './url_panel_content'; +import { UrlPanelContent } from './url_panel_content'; + +const defaultProps = { + allowShortUrl: true, + objectType: 'dashboard', + basePath: '', + post: () => Promise.resolve(), +}; test('render', () => { - const component = shallowWithIntl( {}} - />); + const component = shallow(); expect(component).toMatchSnapshot(); }); test('should enable saved object export option when objectId is provided', () => { - const component = shallowWithIntl( {}} - />); + const component = shallow(); expect(component).toMatchSnapshot(); }); test('should hide short url section when allowShortUrl is false', () => { - const component = shallowWithIntl( {}} - />); + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 36baeff77e69..31fe66369ddd 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -18,7 +18,7 @@ */ export { SharePluginSetup, SharePluginStart } from './plugin'; -export { ShareActionProps, ShareActionsProvider, ShareAction } from './types'; +export { ShareMenuItemProps, ShareMenuProvider, ShareMenuItem } from './types'; import { SharePlugin } from './plugin'; export const plugin = () => new SharePlugin(); diff --git a/src/plugins/share/public/lib/url_shortener.test.js b/src/plugins/share/public/lib/url_shortener.test.js deleted file mode 100644 index 859873bd4989..000000000000 --- a/src/plugins/share/public/lib/url_shortener.test.js +++ /dev/null @@ -1,133 +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. - */ -jest.mock('ui/kfetch', () => ({})); - -jest.mock('../../chrome', () => ({})); - -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { shortenUrl } from './url_shortener'; - -describe('Url shortener', () => { - const shareId = 'id123'; - - let kfetchStub; - beforeEach(() => { - kfetchStub = sinon.stub(); - require('ui/kfetch').kfetch = async (...args) => { - return kfetchStub(...args); - }; - }); - - describe('Shorten without base path', () => { - beforeAll(() => { - require('../../chrome').getBasePath = () => { - return ''; - }; - }); - - it('should shorten urls with a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl('http://localhost:5601/app/kibana#123'); - expect(shortUrl).to.be(`http://localhost:5601/goto/${shareId}`); - }); - - it('should shorten urls without a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl('http://localhost/app/kibana#123'); - expect(shortUrl).to.be(`http://localhost/goto/${shareId}`); - }); - }); - - describe('Shorten with base path', () => { - const basePath = '/foo'; - - beforeAll(() => { - require('../../chrome').getBasePath = () => { - return basePath; - }; - }); - - it('should shorten urls with a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost:5601${basePath}/app/kibana#123`); - expect(shortUrl).to.be(`http://localhost:5601${basePath}/goto/${shareId}`); - }); - - it('should shorten urls without a port', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana#123`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - - it('should shorten urls with a query string', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana?foo#123"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana?foo#123`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - - it('should shorten urls without a hash', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana"}' - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - - it('should shorten urls with a query string in the hash', async () => { - const relativeUrl = "/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"; //eslint-disable-line max-len, quotes - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/shorten_url`, - body: '{"url":"/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"}' //eslint-disable-line max-len, quotes - }).returns(Promise.resolve({ urlId: shareId })); - - const shortUrl = await shortenUrl(`http://localhost${basePath}${relativeUrl}`); - expect(shortUrl).to.be(`http://localhost${basePath}/goto/${shareId}`); - }); - }); -}); diff --git a/src/plugins/share/public/lib/url_shortener.test.ts b/src/plugins/share/public/lib/url_shortener.test.ts new file mode 100644 index 000000000000..1c7bf08fa878 --- /dev/null +++ b/src/plugins/share/public/lib/url_shortener.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { shortenUrl } from './url_shortener'; + +describe('Url shortener', () => { + const shareId = 'id123'; + + let postStub: jest.Mock; + beforeEach(() => { + postStub = jest.fn(() => Promise.resolve({ urlId: shareId })); + }); + + describe('Shorten without base path', () => { + it('should shorten urls with a port', async () => { + const shortUrl = await shortenUrl('http://localhost:5601/app/kibana#123', { + basePath: '', + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost:5601/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: '{"url":"/app/kibana#123"}', + }); + }); + + it('should shorten urls without a port', async () => { + const shortUrl = await shortenUrl('http://localhost/app/kibana#123', { + basePath: '', + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: '{"url":"/app/kibana#123"}', + }); + }); + }); + + describe('Shorten with base path', () => { + const basePath = '/foo'; + + it('should shorten urls with a port', async () => { + const shortUrl = await shortenUrl(`http://localhost:5601${basePath}/app/kibana#123`, { + basePath, + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost:5601${basePath}/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: '{"url":"/app/kibana#123"}', + }); + }); + + it('should shorten urls without a port', async () => { + const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana#123`, { + basePath, + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost${basePath}/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: '{"url":"/app/kibana#123"}', + }); + }); + + it('should shorten urls with a query string', async () => { + const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana?foo#123`, { + basePath, + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost${basePath}/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: '{"url":"/app/kibana?foo#123"}', + }); + }); + + it('should shorten urls without a hash', async () => { + const shortUrl = await shortenUrl(`http://localhost${basePath}/app/kibana`, { + basePath, + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost${basePath}/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: '{"url":"/app/kibana"}', + }); + }); + + it('should shorten urls with a query string in the hash', async () => { + const relativeUrl = + '/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))'; + const shortUrl = await shortenUrl(`http://localhost${basePath}${relativeUrl}`, { + basePath, + post: postStub, + }); + expect(shortUrl).toBe(`http://localhost${basePath}/goto/${shareId}`); + expect(postStub).toHaveBeenCalledWith(`/api/shorten_url`, { + body: + '{"url":"/app/kibana#/discover?_g=(refreshInterval:(pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"}', + }); + }); + }); +}); diff --git a/src/plugins/share/public/plugin.test.mocks.ts b/src/plugins/share/public/plugin.test.mocks.ts index bc07f5a4c200..bd814ebc2500 100644 --- a/src/plugins/share/public/plugin.test.mocks.ts +++ b/src/plugins/share/public/plugin.test.mocks.ts @@ -17,9 +17,12 @@ * under the License. */ -import { shareActionsRegistryMock } from './services/share_actions_registry.mock'; +import { shareMenuRegistryMock } from './services/share_menu_registry.mock'; +import { shareMenuManagerMock } from './services/share_menu_manager.mock'; -export const registryMock = shareActionsRegistryMock.create(); +export const registryMock = shareMenuRegistryMock.create(); +export const managerMock = shareMenuManagerMock.create(); jest.doMock('./services', () => ({ - ShareActionsRegistry: jest.fn(() => registryMock), + ShareMenuRegistry: jest.fn(() => registryMock), + ShareMenuManager: jest.fn(() => managerMock), })); diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts index 91869eb2edb1..7b0d86316e94 100644 --- a/src/plugins/share/public/plugin.test.ts +++ b/src/plugins/share/public/plugin.test.ts @@ -17,12 +17,13 @@ * under the License. */ -import { registryMock } from './plugin.test.mocks'; +import { registryMock, managerMock } from './plugin.test.mocks'; import { SharePlugin } from './plugin'; import { CoreStart } from 'kibana/public'; describe('SharePlugin', () => { beforeEach(() => { + managerMock.start.mockClear(); registryMock.setup.mockClear(); registryMock.start.mockClear(); }); @@ -36,12 +37,16 @@ describe('SharePlugin', () => { }); describe('start', () => { - test('wires up and returns registry', async () => { + test('wires up and returns show function, but not registry', async () => { const service = new SharePlugin(); await service.setup(); const start = await service.start({} as CoreStart); expect(registryMock.start).toHaveBeenCalled(); - expect(start.getActions).toBeDefined(); + expect(managerMock.start).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ getShareMenuItems: expect.any(Function) }) + ); + expect(start.showShareContextMenu).toBeDefined(); }); }); }); diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 53777d1c13f3..6d78211cf995 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -18,31 +18,28 @@ */ import { CoreStart, Plugin } from 'src/core/public'; -import { ShareActionsRegistry, ShareActionsRegistrySetup } from './services'; -import { - ShareActionsContextMenu, - ShareActionsContextMenuStart, -} from './services/share_context_menu'; +import { ShareMenuManager, ShareMenuManagerStart } from './services'; +import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; export class SharePlugin implements Plugin { - private readonly shareActionsRegistry = new ShareActionsRegistry(); - private readonly shareActionsContextMenu = new ShareActionsContextMenu(); + private readonly shareMenuRegistry = new ShareMenuRegistry(); + private readonly shareContextMenu = new ShareMenuManager(); public async setup() { return { - ...this.shareActionsRegistry.setup(), + ...this.shareMenuRegistry.setup(), }; } public async start(core: CoreStart) { return { - ...this.shareActionsContextMenu.start(core, this.shareActionsRegistry.start()), + ...this.shareContextMenu.start(core, this.shareMenuRegistry.start()), }; } } /** @public */ -export type SharePluginSetup = ShareActionsRegistrySetup; +export type SharePluginSetup = ShareMenuRegistrySetup; /** @public */ -export type SharePluginStart = ShareActionsContextMenuStart; +export type SharePluginStart = ShareMenuManagerStart; diff --git a/src/plugins/share/public/services/index.ts b/src/plugins/share/public/services/index.ts index 094c139f71e6..aebb81df9e96 100644 --- a/src/plugins/share/public/services/index.ts +++ b/src/plugins/share/public/services/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export * from './share_actions_registry'; +export * from './share_menu_registry'; +export * from './share_menu_manager'; diff --git a/src/plugins/share/public/services/share_menu_manager.mock.ts b/src/plugins/share/public/services/share_menu_manager.mock.ts new file mode 100644 index 000000000000..23cf03e7ec75 --- /dev/null +++ b/src/plugins/share/public/services/share_menu_manager.mock.ts @@ -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 { ShareMenuManager, ShareMenuManagerStart } from './share_menu_manager'; + +const createStartMock = (): jest.Mocked => { + const start = { + showShareContextMenu: jest.fn(), + }; + return start; +}; + +const createMock = (): jest.Mocked> => { + const service = { + start: jest.fn(), + }; + service.start.mockImplementation(createStartMock); + return service; +}; + +export const shareMenuManagerMock = { + createStart: createStartMock, + create: createMock, +}; diff --git a/src/plugins/share/public/services/share_context_menu.tsx b/src/plugins/share/public/services/share_menu_manager.tsx similarity index 81% rename from src/plugins/share/public/services/share_context_menu.tsx rename to src/plugins/share/public/services/share_menu_manager.tsx index 5da8b97b4bdc..441d7d69a8eb 100644 --- a/src/plugins/share/public/services/share_context_menu.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -24,21 +24,21 @@ import { EuiWrappingPopover } from '@elastic/eui'; import { CoreStart, HttpStart } from 'kibana/public'; import { ShareContextMenu } from '../components/share_context_menu'; -import { ShareAction, ShowProps } from '../types'; -import { ShareActionsRegistryStart } from './share_actions_registry'; +import { ShareMenuItem, ShowProps } from '../types'; +import { ShareMenuRegistryStart } from './share_menu_registry'; -export class ShareActionsContextMenu { +export class ShareMenuManager { private isOpen = false; private container = document.createElement('div'); - start(core: CoreStart, shareRegistry: ShareActionsRegistryStart) { + start(core: CoreStart, shareRegistry: ShareMenuRegistryStart) { return { showShareContextMenu: (props: ShowProps) => { - const shareActions = shareRegistry.getActions({ ...props, onClose: this.onClose }); + const menuItems = shareRegistry.getShareMenuItems({ ...props, onClose: this.onClose }); this.showShareContextMenu({ ...props, - shareActions, + menuItems, post: core.http.post, basePath: core.http.basePath.get(), }); @@ -59,11 +59,11 @@ export class ShareActionsContextMenu { objectType, sharingData, isDirty, - shareActions, + menuItems, shareableUrl, post, basePath, - }: ShowProps & { shareActions: ShareAction[]; post: HttpStart['post']; basePath: string }) { + }: ShowProps & { menuItems: ShareMenuItem[]; post: HttpStart['post']; basePath: string }) { if (this.isOpen) { this.onClose(); return; @@ -88,7 +88,7 @@ export class ShareActionsContextMenu { allowShortUrl={allowShortUrl} objectId={objectId} objectType={objectType} - shareActions={shareActions} + shareMenuItems={menuItems} sharingData={sharingData} shareableUrl={shareableUrl} isDirty={isDirty} @@ -102,4 +102,4 @@ export class ShareActionsContextMenu { ReactDOM.render(element, this.container); } } -export type ShareActionsContextMenuStart = ReturnType; +export type ShareMenuManagerStart = ReturnType; diff --git a/src/plugins/share/public/services/share_actions_registry.mock.ts b/src/plugins/share/public/services/share_menu_registry.mock.ts similarity index 69% rename from src/plugins/share/public/services/share_actions_registry.mock.ts rename to src/plugins/share/public/services/share_menu_registry.mock.ts index a810e9bced7e..511875d19d63 100644 --- a/src/plugins/share/public/services/share_actions_registry.mock.ts +++ b/src/plugins/share/public/services/share_menu_registry.mock.ts @@ -17,24 +17,28 @@ * under the License. */ -import { SharePluginSetup, SharePluginStart } from '../plugin'; -import { ShareActionsRegistry } from './share_actions_registry'; +import { + ShareMenuRegistry, + ShareMenuRegistrySetup, + ShareMenuRegistryStart, +} from './share_menu_registry'; +import { ShareMenuItem, ShareMenuItemProps } from '../types'; -const createSetupMock = (): jest.Mocked => { +const createSetupMock = (): jest.Mocked => { const setup = { register: jest.fn(), }; return setup; }; -const createStartMock = (): jest.Mocked => { +const createStartMock = (): jest.Mocked => { const start = { - getActions: jest.fn(), + getShareMenuItems: jest.fn((props: ShareMenuItemProps) => [] as ShareMenuItem[]), }; return start; }; -const createMock = (): jest.Mocked> => { +const createMock = (): jest.Mocked> => { const service = { setup: jest.fn(), start: jest.fn(), @@ -44,7 +48,7 @@ const createMock = (): jest.Mocked> => { return service; }; -export const shareActionsRegistryMock = { +export const shareMenuRegistryMock = { createSetup: createSetupMock, createStart: createStartMock, create: createMock, diff --git a/src/plugins/share/public/services/share_actions_registry.test.ts b/src/plugins/share/public/services/share_menu_registry.test.ts similarity index 68% rename from src/plugins/share/public/services/share_actions_registry.test.ts rename to src/plugins/share/public/services/share_menu_registry.test.ts index 9318ca0f05cb..f9f8ff20c200 100644 --- a/src/plugins/share/public/services/share_actions_registry.test.ts +++ b/src/plugins/share/public/services/share_menu_registry.test.ts @@ -17,45 +17,48 @@ * under the License. */ -import { ShareAction, ShareActionProps, ShareActionsRegistry } from './share_actions_registry'; +import { ShareMenuRegistry } from './share_menu_registry'; +import { ShareMenuItem, ShareMenuItemProps } from '../types'; describe('ShareActionsRegistry', () => { describe('setup', () => { test('throws when registering duplicate id', () => { - const setup = new ShareActionsRegistry().setup(); + const setup = new ShareMenuRegistry().setup(); setup.register({ id: 'myTest', - getShareActions: () => [], + getShareMenuItems: () => [], }); expect(() => setup.register({ id: 'myTest', - getShareActions: () => [], + getShareMenuItems: () => [], }) - ).toThrowErrorMatchingInlineSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"Share menu provider with id [myTest] has already been registered. Use a unique id."` + ); }); }); describe('start', () => { describe('getActions', () => { test('returns a flat list of actions returned by all providers', () => { - const service = new ShareActionsRegistry(); + const service = new ShareMenuRegistry(); const registerFunction = service.setup().register; - const shareAction1 = {} as ShareAction; - const shareAction2 = {} as ShareAction; - const shareAction3 = {} as ShareAction; + const shareAction1 = {} as ShareMenuItem; + const shareAction2 = {} as ShareMenuItem; + const shareAction3 = {} as ShareMenuItem; const provider1Callback = jest.fn(() => [shareAction1]); const provider2Callback = jest.fn(() => [shareAction2, shareAction3]); registerFunction({ id: 'myTest', - getShareActions: provider1Callback, + getShareMenuItems: provider1Callback, }); registerFunction({ id: 'myTest2', - getShareActions: provider2Callback, + getShareMenuItems: provider2Callback, }); - const actionProps = {} as ShareActionProps; - expect(service.start().getActions(actionProps)).toEqual([ + const actionProps = {} as ShareMenuItemProps; + expect(service.start().getShareMenuItems(actionProps)).toEqual([ shareAction1, shareAction2, shareAction3, diff --git a/src/plugins/share/public/services/share_actions_registry.ts b/src/plugins/share/public/services/share_menu_registry.ts similarity index 51% rename from src/plugins/share/public/services/share_actions_registry.ts rename to src/plugins/share/public/services/share_menu_registry.ts index 4140f2eb62f5..016048407cad 100644 --- a/src/plugins/share/public/services/share_actions_registry.ts +++ b/src/plugins/share/public/services/share_menu_registry.ts @@ -17,33 +17,34 @@ * under the License. */ -import { ShareActionProps, ShareActionsProvider } from '../types'; +import { ShareMenuItemProps, ShareMenuProvider } from '../types'; -export class ShareActionsRegistry { - private readonly shareActionsProviders = new Map(); +export class ShareMenuRegistry { + private readonly shareMenuProviders = new Map(); public setup() { return { - register: (shareActionsProvider: ShareActionsProvider) => { - if (this.shareActionsProviders.has(shareActionsProvider.id)) { + register: (shareMenuProvider: ShareMenuProvider) => { + if (this.shareMenuProviders.has(shareMenuProvider.id)) { throw new Error( - `Share action provider with id [${shareActionsProvider.id}] has already been registered. Use a unique id.` + `Share menu provider with id [${shareMenuProvider.id}] has already been registered. Use a unique id.` ); } - this.shareActionsProviders.set(shareActionsProvider.id, shareActionsProvider); + this.shareMenuProviders.set(shareMenuProvider.id, shareMenuProvider); }, }; } public start() { return { - getActions: (props: ShareActionProps) => - Array.from(this.shareActionsProviders.values()) - .flatMap(shareActionProvider => shareActionProvider.getShareActions(props)) + getShareMenuItems: (props: ShareMenuItemProps) => + Array.from(this.shareMenuProviders.values()).flatMap(shareActionProvider => + shareActionProvider.getShareMenuItems(props) + ), }; } } -export type ShareActionsRegistrySetup = ReturnType; -export type ShareActionsRegistryStart = ReturnType; +export type ShareMenuRegistrySetup = ReturnType; +export type ShareMenuRegistryStart = ReturnType; diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index aa68f9a1577a..c6b171580dc0 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -25,13 +25,13 @@ import { /** * @public * Properties of the current object to share. Registered share - * actions provider will provide suitable actions which have to + * menu providers will provide suitable items which have to * be rendered in an appropriate place by the caller. * * It is possible to use the static function `showShareContextMenu` * to render the menu as a popover. * */ -export interface ShareActionProps { +export interface ShareMenuItemProps { objectType: string; objectId?: string; /** @@ -53,7 +53,7 @@ export interface ShareActionProps { * @public * Eui context menu entry shown directly in the context menu. `sortOrder` is * used to order the individual items in a flat list returned by all registered - * action providers. + * menu providers. * */ export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescriptor { sortOrder: number; @@ -61,23 +61,23 @@ export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescri /** * @public - * Definition of an action item rendered in the share menu. `shareMenuItem` is shown + * Definition of a menu item rendered in the share menu. `shareMenuItem` is shown * directly in the context menu. If the item is clicked, the `panel` is shown. * */ -export interface ShareAction { +export interface ShareMenuItem { shareMenuItem: ShareContextMenuPanelItem; panel: EuiContextMenuPanelDescriptor; } /** @public */ -export interface ShareActionsProvider { +export interface ShareMenuProvider { readonly id: string; - getShareActions: (actionProps: ShareActionProps) => ShareAction[]; + getShareMenuItems: (actionProps: ShareMenuItemProps) => ShareMenuItem[]; } /** @public */ -export interface ShowProps extends Omit { +export interface ShowProps extends Omit { anchorElement: HTMLElement; allowEmbed: boolean; allowShortUrl: boolean; diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index da1bfc50372a..41f6d7beeac4 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -10,16 +10,16 @@ import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import React from 'react'; import { npSetup } from 'ui/new_platform'; import { ReportingPanelContent } from '../components/reporting_panel_content'; -import { ShareActionProps } from '../../../../../../src/plugins/share/public'; +import { ShareMenuItemProps } from '../../../../../../src/plugins/share/public'; function reportingProvider() { - const getShareActions = ({ + const getShareMenuItems = ({ objectType, objectId, sharingData, isDirty, onClose, - }: ShareActionProps) => { + }: ShareMenuItemProps) => { if ('search' !== objectType) { return []; } @@ -69,7 +69,7 @@ function reportingProvider() { return { id: 'csvReports', - getShareActions, + getShareMenuItems, }; } diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx index b6651efb2329..10830c59715e 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx +++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx @@ -11,20 +11,19 @@ import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { npSetup } from 'ui/new_platform'; import React from 'react'; import chrome from 'ui/chrome'; -import { unhashUrl } from 'ui/state_management/state_hashing'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; -import { ShareActionProps } from '../../../../../../src/plugins/share/public'; +import { ShareMenuItemProps } from '../../../../../../src/plugins/share/public'; async function reportingProvider() { const injector = await chrome.dangerouslyGetActiveInjector(); - const getShareActions = ({ + const getShareMenuItems = ({ objectType, objectId, sharingData, isDirty, onClose, shareableUrl, - }: ShareActionProps) => { + }: ShareMenuItemProps) => { if (!['dashboard', 'visualization'].includes(objectType)) { return []; } @@ -53,7 +52,6 @@ async function reportingProvider() { const getPngJobParams = () => { // Replace hashes with original RISON values. - const unhashedUrl = unhashUrl(window.location.href, getUnhashableStates()); const relativeUrl = shareableUrl.replace(window.location.origin + chrome.getBasePath(), ''); const browserTimezone = @@ -87,6 +85,7 @@ async function reportingProvider() { sortOrder: 10, }, panel: { + id: 'reportingPdfPanel', title: panelTitle, content: ( Date: Wed, 13 Nov 2019 13:43:36 +0100 Subject: [PATCH 05/23] review fixes --- .../share/public/components/share_context_menu.tsx | 1 - src/plugins/share/public/index.ts | 8 +++++++- src/plugins/share/public/services/share_menu_manager.tsx | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/plugins/share/public/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx index 4aca5cf621d9..88eac6f7e1dd 100644 --- a/src/plugins/share/public/components/share_context_menu.tsx +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -37,7 +37,6 @@ interface Props { shareableUrl?: string; shareMenuItems: ShareMenuItem[]; sharingData: any; - isDirty: boolean; onClose: () => void; basePath: string; post: HttpStart['post']; diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 31fe66369ddd..bbc461d5452a 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -18,7 +18,13 @@ */ export { SharePluginSetup, SharePluginStart } from './plugin'; -export { ShareMenuItemProps, ShareMenuProvider, ShareMenuItem } from './types'; +export { + ShareMenuItemProps, + ShareMenuProvider, + ShareMenuItem, + ShowProps, + ShareContextMenuPanelItem, +} from './types'; import { SharePlugin } from './plugin'; export const plugin = () => new SharePlugin(); diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx index 441d7d69a8eb..00402312bcd4 100644 --- a/src/plugins/share/public/services/share_menu_manager.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -58,7 +58,6 @@ export class ShareMenuManager { objectId, objectType, sharingData, - isDirty, menuItems, shareableUrl, post, @@ -91,7 +90,6 @@ export class ShareMenuManager { shareMenuItems={menuItems} sharingData={sharingData} shareableUrl={shareableUrl} - isDirty={isDirty} onClose={this.onClose} post={post} basePath={basePath} From f48be82dd61b224d92fc4813eba7690d94397223 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 13 Nov 2019 14:42:59 +0100 Subject: [PATCH 06/23] fix i18ns --- .i18nrc.json | 1 + .../url_panel_content.test.tsx.snap | 40 +++++++++---------- .../public/components/share_context_menu.tsx | 10 ++--- .../public/components/url_panel_content.tsx | 26 +++++------- .../translations/translations/ja-JP.json | 34 ++++++++-------- .../translations/translations/zh-CN.json | 34 ++++++++-------- 6 files changed, 70 insertions(+), 75 deletions(-) diff --git a/.i18nrc.json b/.i18nrc.json index 01065201b9d6..134f2afde6f8 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -6,6 +6,7 @@ "dashboardEmbeddableContainer": "src/plugins/dashboard_embeddable_container", "data": ["src/legacy/core_plugins/data", "src/plugins/data"], "embeddableApi": "src/plugins/embeddable", + "share": "src/plugins/share", "esUi": "src/plugins/es_ui_shared", "expressions_np": "src/plugins/expressions", "expressions": "src/legacy/core_plugins/expressions", diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap index 980779a02e69..a8ba5f8edf98 100644 --- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap @@ -15,7 +15,7 @@ exports[`render 1`] = ` helpText={ } @@ -47,7 +47,7 @@ exports[`render 1`] = ` @@ -58,7 +58,7 @@ exports[`render 1`] = ` content={ @@ -93,7 +93,7 @@ exports[`render 1`] = ` content={ } @@ -144,7 +144,7 @@ exports[`render 1`] = ` content={ } @@ -182,7 +182,7 @@ exports[`should enable saved object export option when objectId is provided 1`] label={ } @@ -203,7 +203,7 @@ exports[`should enable saved object export option when objectId is provided 1`] @@ -214,7 +214,7 @@ exports[`should enable saved object export option when objectId is provided 1`] content={ @@ -249,7 +249,7 @@ exports[`should enable saved object export option when objectId is provided 1`] content={ } @@ -300,7 +300,7 @@ exports[`should enable saved object export option when objectId is provided 1`] content={ } @@ -338,7 +338,7 @@ exports[`should hide short url section when allowShortUrl is false 1`] = ` label={ } @@ -359,7 +359,7 @@ exports[`should hide short url section when allowShortUrl is false 1`] = ` @@ -370,7 +370,7 @@ exports[`should hide short url section when allowShortUrl is false 1`] = ` content={ @@ -405,7 +405,7 @@ exports[`should hide short url section when allowShortUrl is false 1`] = ` content={ { const permalinkPanel = { id: panels.length + 1, - title: i18n.translate('common.ui.share.contextMenu.permalinkPanelTitle', { + title: i18n.translate('share.contextMenu.permalinkPanelTitle', { defaultMessage: 'Permalink', }), content: ( @@ -77,7 +77,7 @@ export class ShareContextMenu extends Component { ), }; menuItems.push({ - name: i18n.translate('common.ui.share.contextMenu.permalinksLabel', { + name: i18n.translate('share.contextMenu.permalinksLabel', { defaultMessage: 'Permalinks', }), icon: 'link', @@ -89,7 +89,7 @@ export class ShareContextMenu extends Component { if (this.props.allowEmbed) { const embedPanel = { id: panels.length + 1, - title: i18n.translate('common.ui.share.contextMenu.embedCodePanelTitle', { + title: i18n.translate('share.contextMenu.embedCodePanelTitle', { defaultMessage: 'Embed Code', }), content: ( @@ -106,7 +106,7 @@ export class ShareContextMenu extends Component { }; panels.push(embedPanel); menuItems.push({ - name: i18n.translate('common.ui.share.contextMenu.embedCodeLabel', { + name: i18n.translate('share.contextMenu.embedCodeLabel', { defaultMessage: 'Embed code', }), icon: 'console', @@ -130,7 +130,7 @@ export class ShareContextMenu extends Component { if (menuItems.length > 1) { const topLevelMenuPanel = { id: panels.length + 1, - title: i18n.translate('common.ui.share.contextMenuTitle', { + title: i18n.translate('share.contextMenuTitle', { defaultMessage: 'Share this {objectType}', values: { objectType: this.props.objectType, diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index 30a62bbeeebc..c5ce12ed3c02 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -119,12 +119,12 @@ export class UrlPanelContent extends Component { > {this.props.isEmbedded ? ( ) : ( )} @@ -288,12 +288,9 @@ export class UrlPanelContent extends Component { { id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, label: this.renderWithIconTip( + , , - { id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, disabled: this.isNotSaved(), label: this.renderWithIconTip( + , , - @@ -334,7 +328,7 @@ export class UrlPanelContent extends Component { private renderExportAsRadioGroup = () => { const generateLinkAsHelp = this.isNotSaved() ? ( @@ -345,7 +339,7 @@ export class UrlPanelContent extends Component { } @@ -368,7 +362,7 @@ export class UrlPanelContent extends Component { return; } const shortUrlLabel = ( - + ); const switchLabel = this.state.isCreatingShortUrl ? ( @@ -387,7 +381,7 @@ export class UrlPanelContent extends Component { ); const tipContent = ( Date: Wed, 13 Nov 2019 17:02:26 +0100 Subject: [PATCH 07/23] fix missing i18n --- .../share/public/components/url_panel_content.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index c5ce12ed3c02..d14fddbe2614 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -267,15 +267,12 @@ export class UrlPanelContent extends Component { { useShortUrl: false, isCreatingShortUrl: false, - shortUrlErrorMsg: i18n.translate( - 'common.ui.share.urlPanel.unableCreateShortUrlErrorMessage', - { - defaultMessage: 'Unable to create short URL. Error: {errorMessage}', - values: { - errorMessage: fetchError.message, - }, - } - ), + shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', { + defaultMessage: 'Unable to create short URL. Error: {errorMessage}', + values: { + errorMessage: fetchError.message, + }, + }), }, this.setUrl ); From e2e9015ae9bec79a755dac3012b83eec7dc1f1c2 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 14 Nov 2019 10:16:19 +0100 Subject: [PATCH 08/23] rename things --- .../dashboard/dashboard_app_controller.tsx | 2 +- .../public/discover/angular/discover.js | 2 +- .../kibana/public/visualize/editor/editor.js | 2 +- .../public/components/url_panel_content.tsx | 5 ++-- src/plugins/share/public/index.ts | 4 ++-- src/plugins/share/public/plugin.test.ts | 2 +- .../services/share_menu_manager.mock.ts | 2 +- .../public/services/share_menu_manager.tsx | 23 +++++++++++++------ .../services/share_menu_registry.mock.ts | 4 ++-- .../services/share_menu_registry.test.ts | 10 ++++---- .../public/services/share_menu_registry.ts | 13 ++++++++--- src/plugins/share/public/types.ts | 16 +++++++++---- .../register_csv_reporting.tsx | 4 ++-- .../share_context_menu/register_reporting.tsx | 4 ++-- 14 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index e46c06d8c951..4d871f466bce 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -756,7 +756,7 @@ export class DashboardAppController { }); }; navActions[TopNavIds.SHARE] = anchorElement => { - npStart.plugins.share.showShareContextMenu({ + npStart.plugins.share.toggleShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: !dashboardConfig.getHideWriteControls(), diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index 65c61f49f73c..234f102fb392 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -326,7 +326,7 @@ function discoverController( testId: 'shareTopNavButton', run: async (anchorElement) => { const sharingData = await this.getSharingData(); - share.showShareContextMenu({ + share.toggleShareContextMenu({ anchorElement, allowEmbed: false, allowShortUrl: uiCapabilities.discover.createShortUrl, diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index 8b05e0d5ba9e..619903e93c12 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -238,7 +238,7 @@ function VisEditor( run: (anchorElement) => { const hasUnappliedChanges = vis.dirty; const hasUnsavedChanges = $appStatus.dirty; - share.showShareContextMenu({ + share.toggleShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: capabilities.visualize.createShortUrl, diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index d14fddbe2614..f411e11b245d 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { ChangeEvent, Component } from 'react'; +import React, { Component } from 'react'; import { EuiButton, @@ -31,6 +31,7 @@ import { EuiLoadingSpinner, EuiRadioGroup, EuiSwitch, + EuiSwitchEvent, } from '@elastic/eui'; import { format as formatUrl, parse as parseUrl } from 'url'; @@ -231,7 +232,7 @@ export class UrlPanelContent extends Component { ); }; - private handleShortUrlChange = async (evt: ChangeEvent) => { + private handleShortUrlChange = async (evt: EuiSwitchEvent) => { const isChecked = evt.target.checked; if (!isChecked || this.shortUrlCache !== undefined) { diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index bbc461d5452a..fe5822c79366 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -19,10 +19,10 @@ export { SharePluginSetup, SharePluginStart } from './plugin'; export { - ShareMenuItemProps, + ShareContext, ShareMenuProvider, ShareMenuItem, - ShowProps, + ShowShareMenuOptions, ShareContextMenuPanelItem, } from './types'; import { SharePlugin } from './plugin'; diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts index 7b0d86316e94..5610490be33b 100644 --- a/src/plugins/share/public/plugin.test.ts +++ b/src/plugins/share/public/plugin.test.ts @@ -46,7 +46,7 @@ describe('SharePlugin', () => { expect.anything(), expect.objectContaining({ getShareMenuItems: expect.any(Function) }) ); - expect(start.showShareContextMenu).toBeDefined(); + expect(start.toggleShareContextMenu).toBeDefined(); }); }); }); diff --git a/src/plugins/share/public/services/share_menu_manager.mock.ts b/src/plugins/share/public/services/share_menu_manager.mock.ts index 23cf03e7ec75..7104abeb2609 100644 --- a/src/plugins/share/public/services/share_menu_manager.mock.ts +++ b/src/plugins/share/public/services/share_menu_manager.mock.ts @@ -21,7 +21,7 @@ import { ShareMenuManager, ShareMenuManagerStart } from './share_menu_manager'; const createStartMock = (): jest.Mocked => { const start = { - showShareContextMenu: jest.fn(), + toggleShareContextMenu: jest.fn(), }; return start; }; diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx index 00402312bcd4..35116efa8596 100644 --- a/src/plugins/share/public/services/share_menu_manager.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -24,7 +24,7 @@ import { EuiWrappingPopover } from '@elastic/eui'; import { CoreStart, HttpStart } from 'kibana/public'; import { ShareContextMenu } from '../components/share_context_menu'; -import { ShareMenuItem, ShowProps } from '../types'; +import { ShareMenuItem, ShowShareMenuOptions } from '../types'; import { ShareMenuRegistryStart } from './share_menu_registry'; export class ShareMenuManager { @@ -34,10 +34,15 @@ export class ShareMenuManager { start(core: CoreStart, shareRegistry: ShareMenuRegistryStart) { return { - showShareContextMenu: (props: ShowProps) => { - const menuItems = shareRegistry.getShareMenuItems({ ...props, onClose: this.onClose }); - this.showShareContextMenu({ - ...props, + /** + * Collects share menu items from registered providers and mounts the share context menu under + * the given `anchorElement`. If the context menu is already opened, a call to this method closes it. + * @param options + */ + toggleShareContextMenu: (options: ShowShareMenuOptions) => { + const menuItems = shareRegistry.getShareMenuItems({ ...options, onClose: this.onClose }); + this.toggleShareContextMenu({ + ...options, menuItems, post: core.http.post, basePath: core.http.basePath.get(), @@ -51,7 +56,7 @@ export class ShareMenuManager { this.isOpen = false; }; - private showShareContextMenu({ + private toggleShareContextMenu({ anchorElement, allowEmbed, allowShortUrl, @@ -62,7 +67,11 @@ export class ShareMenuManager { shareableUrl, post, basePath, - }: ShowProps & { menuItems: ShareMenuItem[]; post: HttpStart['post']; basePath: string }) { + }: ShowShareMenuOptions & { + menuItems: ShareMenuItem[]; + post: HttpStart['post']; + basePath: string; + }) { if (this.isOpen) { this.onClose(); return; diff --git a/src/plugins/share/public/services/share_menu_registry.mock.ts b/src/plugins/share/public/services/share_menu_registry.mock.ts index 511875d19d63..b69032f0b3e0 100644 --- a/src/plugins/share/public/services/share_menu_registry.mock.ts +++ b/src/plugins/share/public/services/share_menu_registry.mock.ts @@ -22,7 +22,7 @@ import { ShareMenuRegistrySetup, ShareMenuRegistryStart, } from './share_menu_registry'; -import { ShareMenuItem, ShareMenuItemProps } from '../types'; +import { ShareMenuItem, ShareContext } from '../types'; const createSetupMock = (): jest.Mocked => { const setup = { @@ -33,7 +33,7 @@ const createSetupMock = (): jest.Mocked => { const createStartMock = (): jest.Mocked => { const start = { - getShareMenuItems: jest.fn((props: ShareMenuItemProps) => [] as ShareMenuItem[]), + getShareMenuItems: jest.fn((props: ShareContext) => [] as ShareMenuItem[]), }; return start; }; diff --git a/src/plugins/share/public/services/share_menu_registry.test.ts b/src/plugins/share/public/services/share_menu_registry.test.ts index f9f8ff20c200..b79f1858af05 100644 --- a/src/plugins/share/public/services/share_menu_registry.test.ts +++ b/src/plugins/share/public/services/share_menu_registry.test.ts @@ -18,7 +18,7 @@ */ import { ShareMenuRegistry } from './share_menu_registry'; -import { ShareMenuItem, ShareMenuItemProps } from '../types'; +import { ShareMenuItem, ShareContext } from '../types'; describe('ShareActionsRegistry', () => { describe('setup', () => { @@ -57,14 +57,14 @@ describe('ShareActionsRegistry', () => { id: 'myTest2', getShareMenuItems: provider2Callback, }); - const actionProps = {} as ShareMenuItemProps; - expect(service.start().getShareMenuItems(actionProps)).toEqual([ + const context = {} as ShareContext; + expect(service.start().getShareMenuItems(context)).toEqual([ shareAction1, shareAction2, shareAction3, ]); - expect(provider1Callback).toHaveBeenCalledWith(actionProps); - expect(provider2Callback).toHaveBeenCalledWith(actionProps); + expect(provider1Callback).toHaveBeenCalledWith(context); + expect(provider2Callback).toHaveBeenCalledWith(context); }); }); }); diff --git a/src/plugins/share/public/services/share_menu_registry.ts b/src/plugins/share/public/services/share_menu_registry.ts index 016048407cad..1fec420a9a8f 100644 --- a/src/plugins/share/public/services/share_menu_registry.ts +++ b/src/plugins/share/public/services/share_menu_registry.ts @@ -17,13 +17,20 @@ * under the License. */ -import { ShareMenuItemProps, ShareMenuProvider } from '../types'; +import { ShareContext, ShareMenuProvider } from '../types'; export class ShareMenuRegistry { private readonly shareMenuProviders = new Map(); public setup() { return { + /** + * Register an additional source of items for share context menu items. All registered providers + * will be called if a consumer displays the context menu. Returned `ShareMenuItem`s will be shown + * in the context menu together with the default built-in share options. + * Each share provider needs a globally unique id. + * @param shareMenuProvider + */ register: (shareMenuProvider: ShareMenuProvider) => { if (this.shareMenuProviders.has(shareMenuProvider.id)) { throw new Error( @@ -38,9 +45,9 @@ export class ShareMenuRegistry { public start() { return { - getShareMenuItems: (props: ShareMenuItemProps) => + getShareMenuItems: (context: ShareContext) => Array.from(this.shareMenuProviders.values()).flatMap(shareActionProvider => - shareActionProvider.getShareMenuItems(props) + shareActionProvider.getShareMenuItems(context) ), }; } diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index c6b171580dc0..7f93ba2c3aec 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -28,10 +28,10 @@ import { * menu providers will provide suitable items which have to * be rendered in an appropriate place by the caller. * - * It is possible to use the static function `showShareContextMenu` + * It is possible to use the static function `toggleShareContextMenu` * to render the menu as a popover. * */ -export interface ShareMenuItemProps { +export interface ShareContext { objectType: string; objectId?: string; /** @@ -69,15 +69,21 @@ export interface ShareMenuItem { panel: EuiContextMenuPanelDescriptor; } -/** @public */ +/** + * @public + * A source for additional menu items shown in the share context menu. Any provider + * registered via `share.register()` will be called if a consumer displays the context + * menu. Returned `ShareMenuItem`s will be shown in the context menu together with the + * default built-in share options. Each share provider needs a globally unique id. + * */ export interface ShareMenuProvider { readonly id: string; - getShareMenuItems: (actionProps: ShareMenuItemProps) => ShareMenuItem[]; + getShareMenuItems: (context: ShareContext) => ShareMenuItem[]; } /** @public */ -export interface ShowProps extends Omit { +export interface ShowShareMenuOptions extends Omit { anchorElement: HTMLElement; allowEmbed: boolean; allowShortUrl: boolean; diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 41f6d7beeac4..3c9d1d726258 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -10,7 +10,7 @@ import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import React from 'react'; import { npSetup } from 'ui/new_platform'; import { ReportingPanelContent } from '../components/reporting_panel_content'; -import { ShareMenuItemProps } from '../../../../../../src/plugins/share/public'; +import { ShareContext } from '../../../../../../src/plugins/share/public'; function reportingProvider() { const getShareMenuItems = ({ @@ -19,7 +19,7 @@ function reportingProvider() { sharingData, isDirty, onClose, - }: ShareMenuItemProps) => { + }: ShareContext) => { if ('search' !== objectType) { return []; } diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx index 10830c59715e..fb5e74664e6c 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx +++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx @@ -12,7 +12,7 @@ import { npSetup } from 'ui/new_platform'; import React from 'react'; import chrome from 'ui/chrome'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; -import { ShareMenuItemProps } from '../../../../../../src/plugins/share/public'; +import { ShareContext } from '../../../../../../src/plugins/share/public'; async function reportingProvider() { const injector = await chrome.dangerouslyGetActiveInjector(); @@ -23,7 +23,7 @@ async function reportingProvider() { isDirty, onClose, shareableUrl, - }: ShareMenuItemProps) => { + }: ShareContext) => { if (!['dashboard', 'visualization'].includes(objectType)) { return []; } From 540c00b86b9d72b2defe4fefd11ed248a83acd4d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 14 Nov 2019 13:48:17 +0100 Subject: [PATCH 09/23] add back stylings --- src/legacy/ui/public/share/_index.scss | 1 + src/legacy/ui/public/share/_share_context_menu.scss | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 src/legacy/ui/public/share/_index.scss create mode 100644 src/legacy/ui/public/share/_share_context_menu.scss diff --git a/src/legacy/ui/public/share/_index.scss b/src/legacy/ui/public/share/_index.scss new file mode 100644 index 000000000000..85168c9ea80f --- /dev/null +++ b/src/legacy/ui/public/share/_index.scss @@ -0,0 +1 @@ +@import './share_context_menu'; diff --git a/src/legacy/ui/public/share/_share_context_menu.scss b/src/legacy/ui/public/share/_share_context_menu.scss new file mode 100644 index 000000000000..d28e7846d813 --- /dev/null +++ b/src/legacy/ui/public/share/_share_context_menu.scss @@ -0,0 +1,8 @@ +.kbnShareContextMenu__finalPanel { + padding: $euiSize; +} + +.kbnShareContextMenu__copyAnchor, +.kbnShareContextMenu__copyButton { + width: 100%; +} From 6f3f8023977faeeecda2f7f609458b4a582e34d5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 14 Nov 2019 14:01:56 +0100 Subject: [PATCH 10/23] add karma mocks --- .../ui/public/new_platform/new_platform.karma_mock.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index bb055d6ce1e3..cc0d7e24df42 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -62,6 +62,9 @@ export const npSetup = { } }, }, + share: { + register: () => {}, + }, inspector: { registerView: () => undefined, __LEGACY: { @@ -156,6 +159,9 @@ export const npStart = { }, }, }, + share: { + toggleShareContextMenu: () => {}, + }, inspector: { isAvailable: () => false, open: () => ({ From 1a6997812f087ce3fe24e8336ed0cf89e54c1c9a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 15 Nov 2019 14:20:41 +0100 Subject: [PATCH 11/23] fix css and add to migration doc --- src/core/MIGRATION.md | 3 ++- src/legacy/ui/public/share/_share_context_menu.scss | 5 ----- .../share/public/components/url_panel_content.tsx | 6 ++---- .../public/components/reporting_panel_content.tsx | 9 +++------ 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 366a5b65fbb9..6989c2159dce 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1130,7 +1130,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | -| `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | +| `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | | `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | @@ -1142,6 +1142,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `ui/registry/feature_catalogue | `feature_catalogue.register` | Must add `feature_catalogue` as a dependency in your kibana.json. | | `ui/registry/vis_types` | `visualizations.types` | -- | | `ui/vis` | `visualizations.types` | -- | +| `ui/share` | `share` | `showShareContextMenu` is now called `toggleShareContextMenu`, `ShareContextMenuExtensionsRegistryProvider` is now called `register` | | `ui/vis/vis_factory` | `visualizations.types` | -- | | `ui/vis/vis_filters` | `visualizations.filters` | -- | | `ui/utils/parse_es_interval` | `import { parseEsInterval } from '../data/public'` | `parseEsInterval`, `ParsedInterval`, `InvalidEsCalendarIntervalError`, `InvalidEsIntervalFormatError` items were moved to the `Data Plugin` as a static code | diff --git a/src/legacy/ui/public/share/_share_context_menu.scss b/src/legacy/ui/public/share/_share_context_menu.scss index d28e7846d813..a05164a6bb0d 100644 --- a/src/legacy/ui/public/share/_share_context_menu.scss +++ b/src/legacy/ui/public/share/_share_context_menu.scss @@ -1,8 +1,3 @@ .kbnShareContextMenu__finalPanel { padding: $euiSize; } - -.kbnShareContextMenu__copyAnchor, -.kbnShareContextMenu__copyButton { - width: 100%; -} diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index f411e11b245d..d0d4ce55dc1a 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -105,13 +105,11 @@ export class UrlPanelContent extends Component { - + {(copy: () => void) => ( { - + {copy => ( - + { private renderGenerateReportButton = (isDisabled: boolean) => { return ( Date: Fri, 15 Nov 2019 14:43:04 +0100 Subject: [PATCH 12/23] started moving url shortener to the new platform --- src/plugins/share/kibana.json | 2 +- src/plugins/share/server/index.ts | 25 ++++ src/plugins/share/server/plugin.ts | 36 ++++++ .../share/server/routes/create_routes.ts | 34 +++++ src/plugins/share/server/routes/goto.js | 45 +++++++ .../routes/lib/short_url_assert_valid.js | 39 ++++++ .../routes/lib/short_url_assert_valid.test.js | 65 ++++++++++ .../server/routes/lib/short_url_error.js | 26 ++++ .../server/routes/lib/short_url_error.test.js | 71 ++++++++++ .../server/routes/lib/short_url_lookup.js | 67 ++++++++++ .../routes/lib/short_url_lookup.test.js | 121 ++++++++++++++++++ .../share/server/routes/shorten_url.js | 35 +++++ 12 files changed, 565 insertions(+), 1 deletion(-) create mode 100644 src/plugins/share/server/index.ts create mode 100644 src/plugins/share/server/plugin.ts create mode 100644 src/plugins/share/server/routes/create_routes.ts create mode 100644 src/plugins/share/server/routes/goto.js create mode 100644 src/plugins/share/server/routes/lib/short_url_assert_valid.js create mode 100644 src/plugins/share/server/routes/lib/short_url_assert_valid.test.js create mode 100644 src/plugins/share/server/routes/lib/short_url_error.js create mode 100644 src/plugins/share/server/routes/lib/short_url_error.test.js create mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.js create mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.test.js create mode 100644 src/plugins/share/server/routes/shorten_url.js diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index bbe393a76c5d..dce2ac9281ab 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -1,6 +1,6 @@ { "id": "share", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/share/server/index.ts b/src/plugins/share/server/index.ts new file mode 100644 index 000000000000..9e574314f800 --- /dev/null +++ b/src/plugins/share/server/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/server'; +import { SharePlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new SharePlugin(initializerContext); +} diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts new file mode 100644 index 000000000000..1493393f9c3b --- /dev/null +++ b/src/plugins/share/server/plugin.ts @@ -0,0 +1,36 @@ +/* + * 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 { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; + +export class SharePlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup({ http }: CoreSetup) { + + } + + public start() { + this.initializerContext.logger.get().debug('Starting plugin'); + } + + public stop() { + this.initializerContext.logger.get().debug('Stopping plugin'); + } +} diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts new file mode 100644 index 000000000000..8a833b51f5f5 --- /dev/null +++ b/src/plugins/share/server/routes/create_routes.ts @@ -0,0 +1,34 @@ +/* + * 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 { shortUrlLookupProvider } from './lib/short_url_lookup'; +import { createGotoRoute } from './goto'; +import { createShortenUrlRoute } from './shorten_url'; +import { CoreSetup } from 'kibana/server'; + + +export function createRoutes({ http }: CoreSetup) { + const shortUrlLookup = shortUrlLookupProvider(server); + const router = http.createRouter(); + + createGotoRoute({ router, shortUrlLookup }); + + server.route(createGotoRoute({ server, shortUrlLookup })); + server.route(createShortenUrlRoute({ shortUrlLookup })); +} diff --git a/src/plugins/share/server/routes/goto.js b/src/plugins/share/server/routes/goto.js new file mode 100644 index 000000000000..628e48a63d9d --- /dev/null +++ b/src/plugins/share/server/routes/goto.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. + */ + +import { handleShortUrlError } from './lib/short_url_error'; +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; + +export const createGotoRoute = ({ router, shortUrlLookup }) => ({ + method: 'GET', + path: '/goto/{urlId}', + handler: async function (request, h) { + try { + const url = await shortUrlLookup.getUrl(request.params.urlId, request); + shortUrlAssertValid(url); + + const uiSettings = request.getUiSettingsService(); + const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); + if (!stateStoreInSessionStorage) { + return h.redirect(request.getBasePath() + url); + } + + const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); + return h.renderApp(app, { + redirectUrl: url, + }); + } catch (err) { + throw handleShortUrlError(err); + } + } +}); diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.js b/src/plugins/share/server/routes/lib/short_url_assert_valid.js new file mode 100644 index 000000000000..6004e6929eea --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.js @@ -0,0 +1,39 @@ +/* + * 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 } from 'url'; +import { trim } from 'lodash'; +import Boom from 'boom'; + +export function shortUrlAssertValid(url) { + const { protocol, hostname, pathname } = parse(url); + + if (protocol) { + throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); + } + + if (hostname) { + throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); + } + + const pathnameParts = trim(pathname, '/').split('/'); + if (pathnameParts.length !== 2) { + throw Boom.notAcceptable(`Short url target path must be in the format "/app/{{appId}}", found "${pathname}"`); + } +} diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.js b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.js new file mode 100644 index 000000000000..190762e2e87c --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.js @@ -0,0 +1,65 @@ +/* + * 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 { shortUrlAssertValid } from './short_url_assert_valid'; + + +describe('shortUrlAssertValid()', () => { + const invalid = [ + ['protocol', 'http://localhost:5601/app/kibana'], + ['protocol', 'https://localhost:5601/app/kibana'], + ['protocol', 'mailto:foo@bar.net'], + ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url + ['hostname', 'localhost/app/kibana'], + ['hostname and port', 'local.host:5601/app/kibana'], + ['hostname and auth', 'user:pass@localhost.net/app/kibana'], + ['path traversal', '/app/../../not-kibana'], + ['deep path', '/app/kibana/foo'], + ['deep path', '/app/kibana/foo/bar'], + ['base path', '/base/app/kibana'], + ]; + + invalid.forEach(([desc, url]) => { + it(`fails when url has ${desc}`, () => { + try { + shortUrlAssertValid(url); + throw new Error(`expected assertion to throw`); + } catch (err) { + if (!err || !err.isBoom) { + throw err; + } + } + }); + }); + + const valid = [ + '/app/kibana', + '/app/monitoring#angular/route', + '/app/text#document-id', + '/app/some?with=query', + '/app/some?with=query#and-a-hash', + ]; + + valid.forEach(url => { + it(`allows ${url}`, () => { + shortUrlAssertValid(url); + }); + }); + +}); diff --git a/src/plugins/share/server/routes/lib/short_url_error.js b/src/plugins/share/server/routes/lib/short_url_error.js new file mode 100644 index 000000000000..cf12cef79b47 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_error.js @@ -0,0 +1,26 @@ +/* + * 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 Boom from 'boom'; + +export function handleShortUrlError(error) { + return Boom.boomify(error, { + statusCode: error.statusCode || error.status || 500 + }); +} diff --git a/src/plugins/share/server/routes/lib/short_url_error.test.js b/src/plugins/share/server/routes/lib/short_url_error.test.js new file mode 100644 index 000000000000..43e71541aa19 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_error.test.js @@ -0,0 +1,71 @@ +/* + * 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 _ from 'lodash'; +import { handleShortUrlError } from './short_url_error'; + +function createErrorWithStatus(status) { + const error = new Error(); + error.status = status; + return error; +} + +function createErrorWithStatusCode(statusCode) { + const error = new Error(); + error.statusCode = statusCode; + return error; +} + +describe('handleShortUrlError()', () => { + const caughtErrorsWithStatus = [ + createErrorWithStatus(401), + createErrorWithStatus(403), + createErrorWithStatus(404), + ]; + + const caughtErrorsWithStatusCode = [ + createErrorWithStatusCode(401), + createErrorWithStatusCode(403), + createErrorWithStatusCode(404), + ]; + + const uncaughtErrors = [ + new Error(), + createErrorWithStatus(500), + createErrorWithStatusCode(500) + ]; + + caughtErrorsWithStatus.forEach((err) => { + it(`should handle errors with status of ${err.status}`, function () { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.status); + }); + }); + + caughtErrorsWithStatusCode.forEach((err) => { + it(`should handle errors with statusCode of ${err.statusCode}`, function () { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.statusCode); + }); + }); + + uncaughtErrors.forEach((err) => { + it(`should not handle unknown errors`, function () { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); + }); + }); +}); diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.js b/src/plugins/share/server/routes/lib/short_url_lookup.js new file mode 100644 index 000000000000..c4f6af03d7d9 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.js @@ -0,0 +1,67 @@ +/* + * 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 crypto from 'crypto'; +import { get } from 'lodash'; + +export function shortUrlLookupProvider(server) { + async function updateMetadata(doc, req) { + try { + await req.getSavedObjectsClient().update('url', doc.id, { + accessDate: new Date(), + accessCount: get(doc, 'attributes.accessCount', 0) + 1 + }); + } catch (err) { + server.log('Warning: Error updating url metadata', err); + //swallow errors. It isn't critical if there is no update. + } + } + + return { + async generateUrlId(url, req) { + const id = crypto.createHash('md5').update(url).digest('hex'); + const savedObjectsClient = req.getSavedObjectsClient(); + const { isConflictError } = savedObjectsClient.errors; + + try { + const doc = await savedObjectsClient.create('url', { + url, + accessCount: 0, + createDate: new Date(), + accessDate: new Date() + }, { id }); + + return doc.id; + } catch (error) { + if (isConflictError(error)) { + return id; + } + + throw error; + } + }, + + async getUrl(id, req) { + const doc = await req.getSavedObjectsClient().get('url', id); + updateMetadata(doc, req); + + return doc.attributes.url; + } + }; +} diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.test.js b/src/plugins/share/server/routes/lib/short_url_lookup.test.js new file mode 100644 index 000000000000..033aeb92926a --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.test.js @@ -0,0 +1,121 @@ +/* + * 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 sinon from 'sinon'; +import { shortUrlLookupProvider } from './short_url_lookup'; +import { SavedObjectsClient } from '../../../../../core/server'; + +describe('shortUrlLookupProvider', () => { + const ID = 'bf00ad16941fc51420f91a93428b27a0'; + const TYPE = 'url'; + const URL = 'http://elastic.co'; + const server = { log: sinon.stub() }; + const sandbox = sinon.createSandbox(); + + let savedObjectsClient; + let req; + let shortUrl; + + beforeEach(() => { + savedObjectsClient = { + get: sandbox.stub(), + create: sandbox.stub().returns(Promise.resolve({ id: ID })), + update: sandbox.stub(), + errors: SavedObjectsClient.errors + }; + + req = { getSavedObjectsClient: () => savedObjectsClient }; + shortUrl = shortUrlLookupProvider(server); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('generateUrlId', () => { + it('returns the document id', async () => { + const id = await shortUrl.generateUrlId(URL, req); + expect(id).toEqual(ID); + }); + + it('provides correct arguments to savedObjectsClient', async () => { + await shortUrl.generateUrlId(URL, req); + + sinon.assert.calledOnce(savedObjectsClient.create); + const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); + expect(attributes.url).toEqual(URL); + expect(options.id).toEqual(ID); + }); + + it('passes persists attributes', async () => { + await shortUrl.generateUrlId(URL, req); + + sinon.assert.calledOnce(savedObjectsClient.create); + const [type, attributes] = savedObjectsClient.create.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); + expect(attributes.url).toEqual(URL); + }); + + it('gracefully handles version conflict', async () => { + const error = savedObjectsClient.errors.decorateConflictError(new Error()); + savedObjectsClient.create.throws(error); + const id = await shortUrl.generateUrlId(URL, req); + expect(id).toEqual(ID); + }); + }); + + describe('getUrl', () => { + beforeEach(() => { + const attributes = { accessCount: 2, url: URL }; + savedObjectsClient.get.returns({ id: ID, attributes }); + }); + + it('provides the ID to savedObjectsClient', async () => { + await shortUrl.getUrl(ID, req); + + sinon.assert.calledOnce(savedObjectsClient.get); + const [type, id] = savedObjectsClient.get.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + }); + + it('returns the url', async () => { + const response = await shortUrl.getUrl(ID, req); + expect(response).toEqual(URL); + }); + + it('increments accessCount', async () => { + await shortUrl.getUrl(ID, req); + + sinon.assert.calledOnce(savedObjectsClient.update); + const [type, id, attributes] = savedObjectsClient.update.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); + expect(attributes.accessCount).toEqual(3); + }); + }); +}); diff --git a/src/plugins/share/server/routes/shorten_url.js b/src/plugins/share/server/routes/shorten_url.js new file mode 100644 index 000000000000..0203e9373384 --- /dev/null +++ b/src/plugins/share/server/routes/shorten_url.js @@ -0,0 +1,35 @@ +/* + * 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 { handleShortUrlError } from './lib/short_url_error'; +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; + +export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ + method: 'POST', + path: '/api/shorten_url', + handler: async function (request) { + try { + shortUrlAssertValid(request.payload.url); + const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); + return { urlId }; + } catch (err) { + throw handleShortUrlError(err); + } + } +}); From bbad895540df7e839d44aaff9392251192c82003 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 15 Nov 2019 16:44:51 +0100 Subject: [PATCH 13/23] fix types for typescript 3.7 --- src/plugins/share/public/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index 7f93ba2c3aec..247212975713 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -44,7 +44,7 @@ export interface ShareContext { * If not set it will default to `window.location.href` */ shareableUrl: string; - sharingData: unknown; + sharingData: { [key: string]: unknown }; isDirty: boolean; onClose: () => void; } From f1b0ecf809c09d471bf5ac327d2d91090127a698 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 15 Nov 2019 17:14:50 +0100 Subject: [PATCH 14/23] continued working on shimming shorten url service --- src/legacy/core_plugins/share/server/index.ts | 25 ++++ .../core_plugins/share/server/plugin.ts | 37 ++++++ .../share/server/routes/create_routes.ts | 33 +++++ .../core_plugins/share/server/routes/goto.ts | 63 +++++++++ .../routes/lib/short_url_assert_valid.js | 39 ++++++ .../routes/lib/short_url_assert_valid.test.js | 65 ++++++++++ .../server/routes/lib/short_url_error.js | 26 ++++ .../server/routes/lib/short_url_error.test.js | 71 ++++++++++ .../server/routes/lib/short_url_lookup.js | 67 ++++++++++ .../routes/lib/short_url_lookup.test.js | 121 ++++++++++++++++++ .../share/server/routes/shorten_url.ts | 35 +++++ src/plugins/share/kibana.json | 2 +- 12 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 src/legacy/core_plugins/share/server/index.ts create mode 100644 src/legacy/core_plugins/share/server/plugin.ts create mode 100644 src/legacy/core_plugins/share/server/routes/create_routes.ts create mode 100644 src/legacy/core_plugins/share/server/routes/goto.ts create mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js create mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js create mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_error.js create mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js create mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js create mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js create mode 100644 src/legacy/core_plugins/share/server/routes/shorten_url.ts diff --git a/src/legacy/core_plugins/share/server/index.ts b/src/legacy/core_plugins/share/server/index.ts new file mode 100644 index 000000000000..9e574314f800 --- /dev/null +++ b/src/legacy/core_plugins/share/server/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/server'; +import { SharePlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new SharePlugin(initializerContext); +} diff --git a/src/legacy/core_plugins/share/server/plugin.ts b/src/legacy/core_plugins/share/server/plugin.ts new file mode 100644 index 000000000000..06e76fb20f8f --- /dev/null +++ b/src/legacy/core_plugins/share/server/plugin.ts @@ -0,0 +1,37 @@ +/* + * 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 { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { createRoutes } from './routes/create_routes'; + +export class SharePlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup) { + createRoutes(core); + } + + public start() { + this.initializerContext.logger.get().debug('Starting plugin'); + } + + public stop() { + this.initializerContext.logger.get().debug('Stopping plugin'); + } +} diff --git a/src/legacy/core_plugins/share/server/routes/create_routes.ts b/src/legacy/core_plugins/share/server/routes/create_routes.ts new file mode 100644 index 000000000000..e32152ae5c37 --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/create_routes.ts @@ -0,0 +1,33 @@ +/* + * 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 { shortUrlLookupProvider } from './lib/short_url_lookup'; +import { createGotoRoute } from './goto'; +import { createShortenUrlRoute } from './shorten_url'; +import { CoreSetup } from 'kibana/server'; + +export function createRoutes({ http }: CoreSetup) { + const shortUrlLookup = shortUrlLookupProvider(server); + const router = http.createRouter(); + + createGotoRoute({ router, shortUrlLookup }); + + server.route(createGotoRoute({ server, shortUrlLookup })); + server.route(createShortenUrlRoute({ shortUrlLookup })); +} diff --git a/src/legacy/core_plugins/share/server/routes/goto.ts b/src/legacy/core_plugins/share/server/routes/goto.ts new file mode 100644 index 000000000000..8ee5d6037c02 --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/goto.ts @@ -0,0 +1,63 @@ +/* + * 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 { CoreSetup, IRouter } from 'kibana/server'; +import { handleShortUrlError } from './lib/short_url_error'; +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; + +export const createGotoRoute = ({ + router, + shortUrlLookup, + core, + getHiddenUiAppById, +}: { + router: IRouter; + shortUrlLookup: any; + core: CoreSetup; + getHiddenUiAppById: any; +}) => { + router.get( + { + path: '/goto/{urlId}', + // TODO add validation + validate: false, + }, + async function(context, request, response) { + try { + const url = await shortUrlLookup.getUrl(request.params.urlId, request); + shortUrlAssertValid(url); + + const uiSettings = context.core.uiSettings.client; + const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); + if (!stateStoreInSessionStorage) { + return response.redirected({ headers: { + location: core.http.basePath.serverBasePath + url + }}); + } + + const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); + return h.renderApp(app, { + redirectUrl: url, + }); + } catch (err) { + throw handleShortUrlError(err); + } + } + ); +}; diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js new file mode 100644 index 000000000000..6004e6929eea --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js @@ -0,0 +1,39 @@ +/* + * 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 } from 'url'; +import { trim } from 'lodash'; +import Boom from 'boom'; + +export function shortUrlAssertValid(url) { + const { protocol, hostname, pathname } = parse(url); + + if (protocol) { + throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); + } + + if (hostname) { + throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); + } + + const pathnameParts = trim(pathname, '/').split('/'); + if (pathnameParts.length !== 2) { + throw Boom.notAcceptable(`Short url target path must be in the format "/app/{{appId}}", found "${pathname}"`); + } +} diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js new file mode 100644 index 000000000000..190762e2e87c --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js @@ -0,0 +1,65 @@ +/* + * 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 { shortUrlAssertValid } from './short_url_assert_valid'; + + +describe('shortUrlAssertValid()', () => { + const invalid = [ + ['protocol', 'http://localhost:5601/app/kibana'], + ['protocol', 'https://localhost:5601/app/kibana'], + ['protocol', 'mailto:foo@bar.net'], + ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url + ['hostname', 'localhost/app/kibana'], + ['hostname and port', 'local.host:5601/app/kibana'], + ['hostname and auth', 'user:pass@localhost.net/app/kibana'], + ['path traversal', '/app/../../not-kibana'], + ['deep path', '/app/kibana/foo'], + ['deep path', '/app/kibana/foo/bar'], + ['base path', '/base/app/kibana'], + ]; + + invalid.forEach(([desc, url]) => { + it(`fails when url has ${desc}`, () => { + try { + shortUrlAssertValid(url); + throw new Error(`expected assertion to throw`); + } catch (err) { + if (!err || !err.isBoom) { + throw err; + } + } + }); + }); + + const valid = [ + '/app/kibana', + '/app/monitoring#angular/route', + '/app/text#document-id', + '/app/some?with=query', + '/app/some?with=query#and-a-hash', + ]; + + valid.forEach(url => { + it(`allows ${url}`, () => { + shortUrlAssertValid(url); + }); + }); + +}); diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_error.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_error.js new file mode 100644 index 000000000000..cf12cef79b47 --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/lib/short_url_error.js @@ -0,0 +1,26 @@ +/* + * 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 Boom from 'boom'; + +export function handleShortUrlError(error) { + return Boom.boomify(error, { + statusCode: error.statusCode || error.status || 500 + }); +} diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js new file mode 100644 index 000000000000..43e71541aa19 --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js @@ -0,0 +1,71 @@ +/* + * 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 _ from 'lodash'; +import { handleShortUrlError } from './short_url_error'; + +function createErrorWithStatus(status) { + const error = new Error(); + error.status = status; + return error; +} + +function createErrorWithStatusCode(statusCode) { + const error = new Error(); + error.statusCode = statusCode; + return error; +} + +describe('handleShortUrlError()', () => { + const caughtErrorsWithStatus = [ + createErrorWithStatus(401), + createErrorWithStatus(403), + createErrorWithStatus(404), + ]; + + const caughtErrorsWithStatusCode = [ + createErrorWithStatusCode(401), + createErrorWithStatusCode(403), + createErrorWithStatusCode(404), + ]; + + const uncaughtErrors = [ + new Error(), + createErrorWithStatus(500), + createErrorWithStatusCode(500) + ]; + + caughtErrorsWithStatus.forEach((err) => { + it(`should handle errors with status of ${err.status}`, function () { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.status); + }); + }); + + caughtErrorsWithStatusCode.forEach((err) => { + it(`should handle errors with statusCode of ${err.statusCode}`, function () { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.statusCode); + }); + }); + + uncaughtErrors.forEach((err) => { + it(`should not handle unknown errors`, function () { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); + }); + }); +}); diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js new file mode 100644 index 000000000000..c4f6af03d7d9 --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js @@ -0,0 +1,67 @@ +/* + * 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 crypto from 'crypto'; +import { get } from 'lodash'; + +export function shortUrlLookupProvider(server) { + async function updateMetadata(doc, req) { + try { + await req.getSavedObjectsClient().update('url', doc.id, { + accessDate: new Date(), + accessCount: get(doc, 'attributes.accessCount', 0) + 1 + }); + } catch (err) { + server.log('Warning: Error updating url metadata', err); + //swallow errors. It isn't critical if there is no update. + } + } + + return { + async generateUrlId(url, req) { + const id = crypto.createHash('md5').update(url).digest('hex'); + const savedObjectsClient = req.getSavedObjectsClient(); + const { isConflictError } = savedObjectsClient.errors; + + try { + const doc = await savedObjectsClient.create('url', { + url, + accessCount: 0, + createDate: new Date(), + accessDate: new Date() + }, { id }); + + return doc.id; + } catch (error) { + if (isConflictError(error)) { + return id; + } + + throw error; + } + }, + + async getUrl(id, req) { + const doc = await req.getSavedObjectsClient().get('url', id); + updateMetadata(doc, req); + + return doc.attributes.url; + } + }; +} diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js new file mode 100644 index 000000000000..033aeb92926a --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js @@ -0,0 +1,121 @@ +/* + * 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 sinon from 'sinon'; +import { shortUrlLookupProvider } from './short_url_lookup'; +import { SavedObjectsClient } from '../../../../../core/server'; + +describe('shortUrlLookupProvider', () => { + const ID = 'bf00ad16941fc51420f91a93428b27a0'; + const TYPE = 'url'; + const URL = 'http://elastic.co'; + const server = { log: sinon.stub() }; + const sandbox = sinon.createSandbox(); + + let savedObjectsClient; + let req; + let shortUrl; + + beforeEach(() => { + savedObjectsClient = { + get: sandbox.stub(), + create: sandbox.stub().returns(Promise.resolve({ id: ID })), + update: sandbox.stub(), + errors: SavedObjectsClient.errors + }; + + req = { getSavedObjectsClient: () => savedObjectsClient }; + shortUrl = shortUrlLookupProvider(server); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('generateUrlId', () => { + it('returns the document id', async () => { + const id = await shortUrl.generateUrlId(URL, req); + expect(id).toEqual(ID); + }); + + it('provides correct arguments to savedObjectsClient', async () => { + await shortUrl.generateUrlId(URL, req); + + sinon.assert.calledOnce(savedObjectsClient.create); + const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); + expect(attributes.url).toEqual(URL); + expect(options.id).toEqual(ID); + }); + + it('passes persists attributes', async () => { + await shortUrl.generateUrlId(URL, req); + + sinon.assert.calledOnce(savedObjectsClient.create); + const [type, attributes] = savedObjectsClient.create.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); + expect(attributes.url).toEqual(URL); + }); + + it('gracefully handles version conflict', async () => { + const error = savedObjectsClient.errors.decorateConflictError(new Error()); + savedObjectsClient.create.throws(error); + const id = await shortUrl.generateUrlId(URL, req); + expect(id).toEqual(ID); + }); + }); + + describe('getUrl', () => { + beforeEach(() => { + const attributes = { accessCount: 2, url: URL }; + savedObjectsClient.get.returns({ id: ID, attributes }); + }); + + it('provides the ID to savedObjectsClient', async () => { + await shortUrl.getUrl(ID, req); + + sinon.assert.calledOnce(savedObjectsClient.get); + const [type, id] = savedObjectsClient.get.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + }); + + it('returns the url', async () => { + const response = await shortUrl.getUrl(ID, req); + expect(response).toEqual(URL); + }); + + it('increments accessCount', async () => { + await shortUrl.getUrl(ID, req); + + sinon.assert.calledOnce(savedObjectsClient.update); + const [type, id, attributes] = savedObjectsClient.update.getCall(0).args; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); + expect(attributes.accessCount).toEqual(3); + }); + }); +}); diff --git a/src/legacy/core_plugins/share/server/routes/shorten_url.ts b/src/legacy/core_plugins/share/server/routes/shorten_url.ts new file mode 100644 index 000000000000..0203e9373384 --- /dev/null +++ b/src/legacy/core_plugins/share/server/routes/shorten_url.ts @@ -0,0 +1,35 @@ +/* + * 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 { handleShortUrlError } from './lib/short_url_error'; +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; + +export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ + method: 'POST', + path: '/api/shorten_url', + handler: async function (request) { + try { + shortUrlAssertValid(request.payload.url); + const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); + return { urlId }; + } catch (err) { + throw handleShortUrlError(err); + } + } +}); diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index dce2ac9281ab..bbe393a76c5d 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -1,6 +1,6 @@ { "id": "share", "version": "kibana", - "server": true, + "server": false, "ui": true } From 5f5358cbd4349617170be903914cc85ca4702d53 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 15 Nov 2019 20:42:58 +0100 Subject: [PATCH 15/23] fix snapshots --- .../__snapshots__/url_panel_content.test.tsx.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap index a8ba5f8edf98..c10ca5513088 100644 --- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap @@ -158,7 +158,7 @@ exports[`render 1`] = ` /> @@ -314,7 +314,7 @@ exports[`should enable saved object export option when objectId is provided 1`] /> @@ -427,7 +427,7 @@ exports[`should hide short url section when allowShortUrl is false 1`] = ` /> From 46a06103a0339ff18f046518c468ccf414cd94cd Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 18 Nov 2019 12:42:07 +0100 Subject: [PATCH 16/23] move over most of the share plugin --- .../url_shortening/routes/create_routes.js | 2 - .../server/url_shortening/routes/goto.js | 8 +- .../routes/lib/short_url_lookup.js | 24 ---- .../routes/lib/short_url_lookup.test.js | 37 ------ .../url_shortening/routes/shorten_url.js | 35 ----- src/plugins/share/kibana.json | 2 +- .../url_panel_content.test.tsx.snap | 6 +- src/plugins/share/server/plugin.ts | 5 +- .../share/server/routes/create_routes.ts | 14 +- src/plugins/share/server/routes/goto.ts | 70 ++++++++++ ...test.js => short_url_assert_valid.test.ts} | 2 - ...ert_valid.js => short_url_assert_valid.ts} | 6 +- ..._error.test.js => short_url_error.test.ts} | 42 ++---- ...{short_url_error.js => short_url_error.ts} | 4 +- .../server/routes/lib/short_url_lookup.js | 67 ---------- .../routes/lib/short_url_lookup.test.js | 121 ----------------- .../routes/lib/short_url_lookup.test.ts | 125 ++++++++++++++++++ .../server/routes/lib/short_url_lookup.ts | 84 ++++++++++++ .../share/server/routes/shorten_url.js | 35 ----- .../server/routes/{goto.js => shorten_url.ts} | 50 ++++--- 20 files changed, 338 insertions(+), 401 deletions(-) delete mode 100644 src/legacy/server/url_shortening/routes/shorten_url.js create mode 100644 src/plugins/share/server/routes/goto.ts rename src/plugins/share/server/routes/lib/{short_url_assert_valid.test.js => short_url_assert_valid.test.ts} (99%) rename src/plugins/share/server/routes/lib/{short_url_assert_valid.js => short_url_assert_valid.ts} (87%) rename src/plugins/share/server/routes/lib/{short_url_error.test.js => short_url_error.test.ts} (56%) rename src/plugins/share/server/routes/lib/{short_url_error.js => short_url_error.ts} (87%) delete mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.js delete mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.test.js create mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.test.ts create mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.ts delete mode 100644 src/plugins/share/server/routes/shorten_url.js rename src/plugins/share/server/routes/{goto.js => shorten_url.ts} (53%) diff --git a/src/legacy/server/url_shortening/routes/create_routes.js b/src/legacy/server/url_shortening/routes/create_routes.js index 091eabcf47c1..c6347ace873f 100644 --- a/src/legacy/server/url_shortening/routes/create_routes.js +++ b/src/legacy/server/url_shortening/routes/create_routes.js @@ -19,12 +19,10 @@ import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; -import { createShortenUrlRoute } from './shorten_url'; export function createRoutes(server) { const shortUrlLookup = shortUrlLookupProvider(server); server.route(createGotoRoute({ server, shortUrlLookup })); - server.route(createShortenUrlRoute({ shortUrlLookup })); } diff --git a/src/legacy/server/url_shortening/routes/goto.js b/src/legacy/server/url_shortening/routes/goto.js index 675bc5df5067..60a34499dd2d 100644 --- a/src/legacy/server/url_shortening/routes/goto.js +++ b/src/legacy/server/url_shortening/routes/goto.js @@ -22,18 +22,12 @@ import { shortUrlAssertValid } from './lib/short_url_assert_valid'; export const createGotoRoute = ({ server, shortUrlLookup }) => ({ method: 'GET', - path: '/goto/{urlId}', + path: '/goto_LP/{urlId}', handler: async function (request, h) { try { const url = await shortUrlLookup.getUrl(request.params.urlId, request); shortUrlAssertValid(url); - const uiSettings = request.getUiSettingsService(); - const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); - if (!stateStoreInSessionStorage) { - return h.redirect(request.getBasePath() + url); - } - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); return h.renderApp(app, { redirectUrl: url, diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js index c4f6af03d7d9..3a4b96c802c5 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js @@ -17,7 +17,6 @@ * under the License. */ -import crypto from 'crypto'; import { get } from 'lodash'; export function shortUrlLookupProvider(server) { @@ -34,29 +33,6 @@ export function shortUrlLookupProvider(server) { } return { - async generateUrlId(url, req) { - const id = crypto.createHash('md5').update(url).digest('hex'); - const savedObjectsClient = req.getSavedObjectsClient(); - const { isConflictError } = savedObjectsClient.errors; - - try { - const doc = await savedObjectsClient.create('url', { - url, - accessCount: 0, - createDate: new Date(), - accessDate: new Date() - }, { id }); - - return doc.id; - } catch (error) { - if (isConflictError(error)) { - return id; - } - - throw error; - } - }, - async getUrl(id, req) { const doc = await req.getSavedObjectsClient().get('url', id); updateMetadata(doc, req); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js index 033aeb92926a..7303682c63e0 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js @@ -48,43 +48,6 @@ describe('shortUrlLookupProvider', () => { sandbox.restore(); }); - describe('generateUrlId', () => { - it('returns the document id', async () => { - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - - it('provides correct arguments to savedObjectsClient', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - expect(options.id).toEqual(ID); - }); - - it('passes persists attributes', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - }); - - it('gracefully handles version conflict', async () => { - const error = savedObjectsClient.errors.decorateConflictError(new Error()); - savedObjectsClient.create.throws(error); - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - }); - describe('getUrl', () => { beforeEach(() => { const attributes = { accessCount: 2, url: URL }; diff --git a/src/legacy/server/url_shortening/routes/shorten_url.js b/src/legacy/server/url_shortening/routes/shorten_url.js deleted file mode 100644 index 0203e9373384..000000000000 --- a/src/legacy/server/url_shortening/routes/shorten_url.js +++ /dev/null @@ -1,35 +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 { handleShortUrlError } from './lib/short_url_error'; -import { shortUrlAssertValid } from './lib/short_url_assert_valid'; - -export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ - method: 'POST', - path: '/api/shorten_url', - handler: async function (request) { - try { - shortUrlAssertValid(request.payload.url); - const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); - return { urlId }; - } catch (err) { - throw handleShortUrlError(err); - } - } -}); diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index bbe393a76c5d..dce2ac9281ab 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -1,6 +1,6 @@ { "id": "share", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap index a8ba5f8edf98..c10ca5513088 100644 --- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap @@ -158,7 +158,7 @@ exports[`render 1`] = ` /> @@ -314,7 +314,7 @@ exports[`should enable saved object export option when objectId is provided 1`] /> @@ -427,7 +427,7 @@ exports[`should hide short url section when allowShortUrl is false 1`] = ` /> diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 1493393f9c3b..bcb681a50652 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -18,12 +18,13 @@ */ import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { createRoutes } from './routes/create_routes'; export class SharePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup({ http }: CoreSetup) { - + public async setup(core: CoreSetup) { + createRoutes(core, this.initializerContext.logger.get()); } public start() { diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts index 8a833b51f5f5..bd4b6fdb0879 100644 --- a/src/plugins/share/server/routes/create_routes.ts +++ b/src/plugins/share/server/routes/create_routes.ts @@ -17,18 +17,16 @@ * under the License. */ +import { CoreSetup, Logger } from 'kibana/server'; + import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; import { createShortenUrlRoute } from './shorten_url'; -import { CoreSetup } from 'kibana/server'; - -export function createRoutes({ http }: CoreSetup) { - const shortUrlLookup = shortUrlLookupProvider(server); +export function createRoutes({ http }: CoreSetup, logger: Logger) { + const shortUrlLookup = shortUrlLookupProvider({ logger }); const router = http.createRouter(); - createGotoRoute({ router, shortUrlLookup }); - - server.route(createGotoRoute({ server, shortUrlLookup })); - server.route(createShortenUrlRoute({ shortUrlLookup })); + createGotoRoute({ router, shortUrlLookup, http }); + createShortenUrlRoute({ router, shortUrlLookup }); } diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts new file mode 100644 index 000000000000..1aae0b830f25 --- /dev/null +++ b/src/plugins/share/server/routes/goto.ts @@ -0,0 +1,70 @@ +/* + * 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 { CoreSetup, IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +import { handleShortUrlError } from './lib/short_url_error'; +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; + +export const createGotoRoute = ({ + router, + shortUrlLookup, + http, +}: { + router: IRouter; + shortUrlLookup: ShortUrlLookupService; + http: CoreSetup['http']; +}) => { + router.get( + { + path: '/goto/{urlId}', + validate: { + params: schema.object({ urlId: schema.string() }), + }, + }, + async function(context, request, response) { + try { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); + shortUrlAssertValid(url); + + const uiSettings = context.core.uiSettings.client; + const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); + if (!stateStoreInSessionStorage) { + return response.redirected({ + headers: { + location: http.basePath.prepend(url), + }, + }); + } else { + return response.redirected({ + headers: { + location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + }, + }); + } + } catch (err) { + throw handleShortUrlError(err); + } + } + ); +}; diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.js b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts similarity index 99% rename from src/plugins/share/server/routes/lib/short_url_assert_valid.test.js rename to src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts index 190762e2e87c..f83073e6aefe 100644 --- a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.js +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts @@ -19,7 +19,6 @@ import { shortUrlAssertValid } from './short_url_assert_valid'; - describe('shortUrlAssertValid()', () => { const invalid = [ ['protocol', 'http://localhost:5601/app/kibana'], @@ -61,5 +60,4 @@ describe('shortUrlAssertValid()', () => { shortUrlAssertValid(url); }); }); - }); diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.js b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts similarity index 87% rename from src/plugins/share/server/routes/lib/short_url_assert_valid.js rename to src/plugins/share/server/routes/lib/short_url_assert_valid.ts index 6004e6929eea..2f120bbc03cd 100644 --- a/src/plugins/share/server/routes/lib/short_url_assert_valid.js +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts @@ -21,7 +21,7 @@ import { parse } from 'url'; import { trim } from 'lodash'; import Boom from 'boom'; -export function shortUrlAssertValid(url) { +export function shortUrlAssertValid(url: string) { const { protocol, hostname, pathname } = parse(url); if (protocol) { @@ -34,6 +34,8 @@ export function shortUrlAssertValid(url) { const pathnameParts = trim(pathname, '/').split('/'); if (pathnameParts.length !== 2) { - throw Boom.notAcceptable(`Short url target path must be in the format "/app/{{appId}}", found "${pathname}"`); + throw Boom.notAcceptable( + `Short url target path must be in the format "/app/{{appId}}", found "${pathname}"` + ); } } diff --git a/src/plugins/share/server/routes/lib/short_url_error.test.js b/src/plugins/share/server/routes/lib/short_url_error.test.ts similarity index 56% rename from src/plugins/share/server/routes/lib/short_url_error.test.js rename to src/plugins/share/server/routes/lib/short_url_error.test.ts index 43e71541aa19..d0176db94f19 100644 --- a/src/plugins/share/server/routes/lib/short_url_error.test.js +++ b/src/plugins/share/server/routes/lib/short_url_error.test.ts @@ -19,52 +19,30 @@ import _ from 'lodash'; import { handleShortUrlError } from './short_url_error'; +import Boom from 'boom'; -function createErrorWithStatus(status) { - const error = new Error(); - error.status = status; - return error; -} - -function createErrorWithStatusCode(statusCode) { - const error = new Error(); - error.statusCode = statusCode; - return error; +function createErrorWithStatusCode(statusCode: number) { + return new Boom('', { statusCode }); } describe('handleShortUrlError()', () => { - const caughtErrorsWithStatus = [ - createErrorWithStatus(401), - createErrorWithStatus(403), - createErrorWithStatus(404), - ]; - const caughtErrorsWithStatusCode = [ createErrorWithStatusCode(401), createErrorWithStatusCode(403), createErrorWithStatusCode(404), ]; - const uncaughtErrors = [ - new Error(), - createErrorWithStatus(500), - createErrorWithStatusCode(500) - ]; - - caughtErrorsWithStatus.forEach((err) => { - it(`should handle errors with status of ${err.status}`, function () { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.status); - }); - }); + const uncaughtErrors = [new Error(), createErrorWithStatusCode(500)]; - caughtErrorsWithStatusCode.forEach((err) => { - it(`should handle errors with statusCode of ${err.statusCode}`, function () { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.statusCode); + caughtErrorsWithStatusCode.forEach(err => { + const statusCode = (err as Boom).output.statusCode; + it(`should handle errors with statusCode of ${statusCode}`, function() { + expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(statusCode); }); }); - uncaughtErrors.forEach((err) => { - it(`should not handle unknown errors`, function () { + uncaughtErrors.forEach(err => { + it(`should not handle unknown errors`, function() { expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); }); }); diff --git a/src/plugins/share/server/routes/lib/short_url_error.js b/src/plugins/share/server/routes/lib/short_url_error.ts similarity index 87% rename from src/plugins/share/server/routes/lib/short_url_error.js rename to src/plugins/share/server/routes/lib/short_url_error.ts index cf12cef79b47..3730bea65a4b 100644 --- a/src/plugins/share/server/routes/lib/short_url_error.js +++ b/src/plugins/share/server/routes/lib/short_url_error.ts @@ -19,8 +19,8 @@ import Boom from 'boom'; -export function handleShortUrlError(error) { +export function handleShortUrlError(error: Error) { return Boom.boomify(error, { - statusCode: error.statusCode || error.status || 500 + statusCode: Boom.isBoom(error) ? error.output.statusCode : 500, }); } diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.js b/src/plugins/share/server/routes/lib/short_url_lookup.js deleted file mode 100644 index c4f6af03d7d9..000000000000 --- a/src/plugins/share/server/routes/lib/short_url_lookup.js +++ /dev/null @@ -1,67 +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 crypto from 'crypto'; -import { get } from 'lodash'; - -export function shortUrlLookupProvider(server) { - async function updateMetadata(doc, req) { - try { - await req.getSavedObjectsClient().update('url', doc.id, { - accessDate: new Date(), - accessCount: get(doc, 'attributes.accessCount', 0) + 1 - }); - } catch (err) { - server.log('Warning: Error updating url metadata', err); - //swallow errors. It isn't critical if there is no update. - } - } - - return { - async generateUrlId(url, req) { - const id = crypto.createHash('md5').update(url).digest('hex'); - const savedObjectsClient = req.getSavedObjectsClient(); - const { isConflictError } = savedObjectsClient.errors; - - try { - const doc = await savedObjectsClient.create('url', { - url, - accessCount: 0, - createDate: new Date(), - accessDate: new Date() - }, { id }); - - return doc.id; - } catch (error) { - if (isConflictError(error)) { - return id; - } - - throw error; - } - }, - - async getUrl(id, req) { - const doc = await req.getSavedObjectsClient().get('url', id); - updateMetadata(doc, req); - - return doc.attributes.url; - } - }; -} diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.test.js b/src/plugins/share/server/routes/lib/short_url_lookup.test.js deleted file mode 100644 index 033aeb92926a..000000000000 --- a/src/plugins/share/server/routes/lib/short_url_lookup.test.js +++ /dev/null @@ -1,121 +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 sinon from 'sinon'; -import { shortUrlLookupProvider } from './short_url_lookup'; -import { SavedObjectsClient } from '../../../../../core/server'; - -describe('shortUrlLookupProvider', () => { - const ID = 'bf00ad16941fc51420f91a93428b27a0'; - const TYPE = 'url'; - const URL = 'http://elastic.co'; - const server = { log: sinon.stub() }; - const sandbox = sinon.createSandbox(); - - let savedObjectsClient; - let req; - let shortUrl; - - beforeEach(() => { - savedObjectsClient = { - get: sandbox.stub(), - create: sandbox.stub().returns(Promise.resolve({ id: ID })), - update: sandbox.stub(), - errors: SavedObjectsClient.errors - }; - - req = { getSavedObjectsClient: () => savedObjectsClient }; - shortUrl = shortUrlLookupProvider(server); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('generateUrlId', () => { - it('returns the document id', async () => { - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - - it('provides correct arguments to savedObjectsClient', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - expect(options.id).toEqual(ID); - }); - - it('passes persists attributes', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - }); - - it('gracefully handles version conflict', async () => { - const error = savedObjectsClient.errors.decorateConflictError(new Error()); - savedObjectsClient.create.throws(error); - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - }); - - describe('getUrl', () => { - beforeEach(() => { - const attributes = { accessCount: 2, url: URL }; - savedObjectsClient.get.returns({ id: ID, attributes }); - }); - - it('provides the ID to savedObjectsClient', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.get); - const [type, id] = savedObjectsClient.get.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - }); - - it('returns the url', async () => { - const response = await shortUrl.getUrl(ID, req); - expect(response).toEqual(URL); - }); - - it('increments accessCount', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.update); - const [type, id, attributes] = savedObjectsClient.update.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); - expect(attributes.accessCount).toEqual(3); - }); - }); -}); diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.test.ts b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts new file mode 100644 index 000000000000..87e2b7b726e5 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { shortUrlLookupProvider, ShortUrlLookupService } from './short_url_lookup'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../../core/server'; + +describe('shortUrlLookupProvider', () => { + const ID = 'bf00ad16941fc51420f91a93428b27a0'; + const TYPE = 'url'; + const URL = 'http://elastic.co'; + + let savedObjects: jest.Mocked; + let deps: { savedObjects: SavedObjectsClientContract }; + let shortUrl: ShortUrlLookupService; + + beforeEach(() => { + savedObjects = ({ + get: jest.fn(), + create: jest.fn(() => Promise.resolve({ id: ID })), + update: jest.fn(), + errors: SavedObjectsClient.errors, + } as unknown) as jest.Mocked; + + deps = { savedObjects }; + shortUrl = shortUrlLookupProvider({ logger: ({ warn: () => {} } as unknown) as Logger }); + }); + + describe('generateUrlId', () => { + it('returns the document id', async () => { + const id = await shortUrl.generateUrlId(URL, deps); + expect(id).toEqual(ID); + }); + + it('provides correct arguments to savedObjectsClient', async () => { + await shortUrl.generateUrlId(URL, { savedObjects }); + + expect(savedObjects.create).toHaveBeenCalledTimes(1); + const [type, attributes, options] = savedObjects.create.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual([ + 'accessCount', + 'accessDate', + 'createDate', + 'url', + ]); + expect(attributes.url).toEqual(URL); + expect(options!.id).toEqual(ID); + }); + + it('passes persists attributes', async () => { + await shortUrl.generateUrlId(URL, deps); + + expect(savedObjects.create).toHaveBeenCalledTimes(1); + const [type, attributes] = savedObjects.create.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual([ + 'accessCount', + 'accessDate', + 'createDate', + 'url', + ]); + expect(attributes.url).toEqual(URL); + }); + + it('gracefully handles version conflict', async () => { + const error = savedObjects.errors.decorateConflictError(new Error()); + savedObjects.create.mockImplementation(() => { + throw error; + }); + const id = await shortUrl.generateUrlId(URL, deps); + expect(id).toEqual(ID); + }); + }); + + describe('getUrl', () => { + beforeEach(() => { + const attributes = { accessCount: 2, url: URL }; + savedObjects.get.mockResolvedValue({ id: ID, attributes, type: 'url', references: [] }); + }); + + it('provides the ID to savedObjectsClient', async () => { + await shortUrl.getUrl(ID, { savedObjects }); + + expect(savedObjects.get).toHaveBeenCalledTimes(1); + expect(savedObjects.get).toHaveBeenCalledWith(TYPE, ID); + }); + + it('returns the url', async () => { + const response = await shortUrl.getUrl(ID, deps); + expect(response).toEqual(URL); + }); + + it('increments accessCount', async () => { + await shortUrl.getUrl(ID, { savedObjects }); + + expect(savedObjects.update).toHaveBeenCalledTimes(1); + + const [type, id, attributes] = savedObjects.update.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); + expect(attributes.accessCount).toEqual(3); + }); + }); +}); diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.ts b/src/plugins/share/server/routes/lib/short_url_lookup.ts new file mode 100644 index 000000000000..0d8a9c86621d --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.ts @@ -0,0 +1,84 @@ +/* + * 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 crypto from 'crypto'; +import { get } from 'lodash'; + +import { Logger, SavedObject, SavedObjectsClientContract } from 'kibana/server'; + +export interface ShortUrlLookupService { + generateUrlId(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; + getUrl(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; +} + +export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrlLookupService { + async function updateMetadata( + doc: SavedObject, + { savedObjects }: { savedObjects: SavedObjectsClientContract } + ) { + try { + await savedObjects.update('url', doc.id, { + accessDate: new Date().valueOf(), + accessCount: get(doc, 'attributes.accessCount', 0) + 1, + }); + } catch (error) { + logger.warn('Warning: Error updating url metadata'); + logger.warn(error); + // swallow errors. It isn't critical if there is no update. + } + } + + return { + async generateUrlId(url, { savedObjects }) { + const id = crypto + .createHash('md5') + .update(url) + .digest('hex'); + const { isConflictError } = savedObjects.errors; + + try { + const doc = await savedObjects.create( + 'url', + { + url, + accessCount: 0, + createDate: new Date().valueOf(), + accessDate: new Date().valueOf(), + }, + { id } + ); + + return doc.id; + } catch (error) { + if (isConflictError(error)) { + return id; + } + + throw error; + } + }, + + async getUrl(id, { savedObjects }) { + const doc = await savedObjects.get('url', id); + updateMetadata(doc, { savedObjects }); + + return doc.attributes.url; + }, + }; +} diff --git a/src/plugins/share/server/routes/shorten_url.js b/src/plugins/share/server/routes/shorten_url.js deleted file mode 100644 index 0203e9373384..000000000000 --- a/src/plugins/share/server/routes/shorten_url.js +++ /dev/null @@ -1,35 +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 { handleShortUrlError } from './lib/short_url_error'; -import { shortUrlAssertValid } from './lib/short_url_assert_valid'; - -export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ - method: 'POST', - path: '/api/shorten_url', - handler: async function (request) { - try { - shortUrlAssertValid(request.payload.url); - const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); - return { urlId }; - } catch (err) { - throw handleShortUrlError(err); - } - } -}); diff --git a/src/plugins/share/server/routes/goto.js b/src/plugins/share/server/routes/shorten_url.ts similarity index 53% rename from src/plugins/share/server/routes/goto.js rename to src/plugins/share/server/routes/shorten_url.ts index 628e48a63d9d..4f72e10d4d3d 100644 --- a/src/plugins/share/server/routes/goto.js +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -17,29 +17,37 @@ * under the License. */ +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + import { handleShortUrlError } from './lib/short_url_error'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; -export const createGotoRoute = ({ router, shortUrlLookup }) => ({ - method: 'GET', - path: '/goto/{urlId}', - handler: async function (request, h) { - try { - const url = await shortUrlLookup.getUrl(request.params.urlId, request); - shortUrlAssertValid(url); - - const uiSettings = request.getUiSettingsService(); - const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); - if (!stateStoreInSessionStorage) { - return h.redirect(request.getBasePath() + url); +export const createShortenUrlRoute = ({ + shortUrlLookup, + router, +}: { + shortUrlLookup: ShortUrlLookupService; + router: IRouter; +}) => { + router.post( + { + path: '/api/shorten_url', + validate: { + body: schema.object({ url: schema.string() }, { allowUnknowns: false }), + }, + }, + async function(context, request, response) { + try { + shortUrlAssertValid(request.body.url); + const urlId = await shortUrlLookup.generateUrlId(request.body.url, { + savedObjects: context.core.savedObjects.client, + }); + return response.ok({ body: { urlId } }); + } catch (err) { + throw handleShortUrlError(err); } - - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); - return h.renderApp(app, { - redirectUrl: url, - }); - } catch (err) { - throw handleShortUrlError(err); } - } -}); + ); +}; From b622b64dc64b8aaf032bb0bc67fd6c744acc3224 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 18 Nov 2019 12:43:24 +0100 Subject: [PATCH 17/23] remove core plugin share --- src/legacy/core_plugins/share/server/index.ts | 25 ---- .../core_plugins/share/server/plugin.ts | 37 ------ .../share/server/routes/create_routes.ts | 33 ----- .../core_plugins/share/server/routes/goto.ts | 63 --------- .../routes/lib/short_url_assert_valid.js | 39 ------ .../routes/lib/short_url_assert_valid.test.js | 65 ---------- .../server/routes/lib/short_url_error.js | 26 ---- .../server/routes/lib/short_url_error.test.js | 71 ---------- .../server/routes/lib/short_url_lookup.js | 67 ---------- .../routes/lib/short_url_lookup.test.js | 121 ------------------ .../share/server/routes/shorten_url.ts | 35 ----- 11 files changed, 582 deletions(-) delete mode 100644 src/legacy/core_plugins/share/server/index.ts delete mode 100644 src/legacy/core_plugins/share/server/plugin.ts delete mode 100644 src/legacy/core_plugins/share/server/routes/create_routes.ts delete mode 100644 src/legacy/core_plugins/share/server/routes/goto.ts delete mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js delete mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js delete mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_error.js delete mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js delete mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js delete mode 100644 src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js delete mode 100644 src/legacy/core_plugins/share/server/routes/shorten_url.ts diff --git a/src/legacy/core_plugins/share/server/index.ts b/src/legacy/core_plugins/share/server/index.ts deleted file mode 100644 index 9e574314f800..000000000000 --- a/src/legacy/core_plugins/share/server/index.ts +++ /dev/null @@ -1,25 +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 { PluginInitializerContext } from '../../../core/server'; -import { SharePlugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new SharePlugin(initializerContext); -} diff --git a/src/legacy/core_plugins/share/server/plugin.ts b/src/legacy/core_plugins/share/server/plugin.ts deleted file mode 100644 index 06e76fb20f8f..000000000000 --- a/src/legacy/core_plugins/share/server/plugin.ts +++ /dev/null @@ -1,37 +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 { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; -import { createRoutes } from './routes/create_routes'; - -export class SharePlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup(core: CoreSetup) { - createRoutes(core); - } - - public start() { - this.initializerContext.logger.get().debug('Starting plugin'); - } - - public stop() { - this.initializerContext.logger.get().debug('Stopping plugin'); - } -} diff --git a/src/legacy/core_plugins/share/server/routes/create_routes.ts b/src/legacy/core_plugins/share/server/routes/create_routes.ts deleted file mode 100644 index e32152ae5c37..000000000000 --- a/src/legacy/core_plugins/share/server/routes/create_routes.ts +++ /dev/null @@ -1,33 +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 { shortUrlLookupProvider } from './lib/short_url_lookup'; -import { createGotoRoute } from './goto'; -import { createShortenUrlRoute } from './shorten_url'; -import { CoreSetup } from 'kibana/server'; - -export function createRoutes({ http }: CoreSetup) { - const shortUrlLookup = shortUrlLookupProvider(server); - const router = http.createRouter(); - - createGotoRoute({ router, shortUrlLookup }); - - server.route(createGotoRoute({ server, shortUrlLookup })); - server.route(createShortenUrlRoute({ shortUrlLookup })); -} diff --git a/src/legacy/core_plugins/share/server/routes/goto.ts b/src/legacy/core_plugins/share/server/routes/goto.ts deleted file mode 100644 index 8ee5d6037c02..000000000000 --- a/src/legacy/core_plugins/share/server/routes/goto.ts +++ /dev/null @@ -1,63 +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 { CoreSetup, IRouter } from 'kibana/server'; -import { handleShortUrlError } from './lib/short_url_error'; -import { shortUrlAssertValid } from './lib/short_url_assert_valid'; - -export const createGotoRoute = ({ - router, - shortUrlLookup, - core, - getHiddenUiAppById, -}: { - router: IRouter; - shortUrlLookup: any; - core: CoreSetup; - getHiddenUiAppById: any; -}) => { - router.get( - { - path: '/goto/{urlId}', - // TODO add validation - validate: false, - }, - async function(context, request, response) { - try { - const url = await shortUrlLookup.getUrl(request.params.urlId, request); - shortUrlAssertValid(url); - - const uiSettings = context.core.uiSettings.client; - const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); - if (!stateStoreInSessionStorage) { - return response.redirected({ headers: { - location: core.http.basePath.serverBasePath + url - }}); - } - - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); - return h.renderApp(app, { - redirectUrl: url, - }); - } catch (err) { - throw handleShortUrlError(err); - } - } - ); -}; diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js deleted file mode 100644 index 6004e6929eea..000000000000 --- a/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.js +++ /dev/null @@ -1,39 +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 } from 'url'; -import { trim } from 'lodash'; -import Boom from 'boom'; - -export function shortUrlAssertValid(url) { - const { protocol, hostname, pathname } = parse(url); - - if (protocol) { - throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); - } - - if (hostname) { - throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); - } - - const pathnameParts = trim(pathname, '/').split('/'); - if (pathnameParts.length !== 2) { - throw Boom.notAcceptable(`Short url target path must be in the format "/app/{{appId}}", found "${pathname}"`); - } -} diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js deleted file mode 100644 index 190762e2e87c..000000000000 --- a/src/legacy/core_plugins/share/server/routes/lib/short_url_assert_valid.test.js +++ /dev/null @@ -1,65 +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 { shortUrlAssertValid } from './short_url_assert_valid'; - - -describe('shortUrlAssertValid()', () => { - const invalid = [ - ['protocol', 'http://localhost:5601/app/kibana'], - ['protocol', 'https://localhost:5601/app/kibana'], - ['protocol', 'mailto:foo@bar.net'], - ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url - ['hostname', 'localhost/app/kibana'], - ['hostname and port', 'local.host:5601/app/kibana'], - ['hostname and auth', 'user:pass@localhost.net/app/kibana'], - ['path traversal', '/app/../../not-kibana'], - ['deep path', '/app/kibana/foo'], - ['deep path', '/app/kibana/foo/bar'], - ['base path', '/base/app/kibana'], - ]; - - invalid.forEach(([desc, url]) => { - it(`fails when url has ${desc}`, () => { - try { - shortUrlAssertValid(url); - throw new Error(`expected assertion to throw`); - } catch (err) { - if (!err || !err.isBoom) { - throw err; - } - } - }); - }); - - const valid = [ - '/app/kibana', - '/app/monitoring#angular/route', - '/app/text#document-id', - '/app/some?with=query', - '/app/some?with=query#and-a-hash', - ]; - - valid.forEach(url => { - it(`allows ${url}`, () => { - shortUrlAssertValid(url); - }); - }); - -}); diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_error.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_error.js deleted file mode 100644 index cf12cef79b47..000000000000 --- a/src/legacy/core_plugins/share/server/routes/lib/short_url_error.js +++ /dev/null @@ -1,26 +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 Boom from 'boom'; - -export function handleShortUrlError(error) { - return Boom.boomify(error, { - statusCode: error.statusCode || error.status || 500 - }); -} diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js deleted file mode 100644 index 43e71541aa19..000000000000 --- a/src/legacy/core_plugins/share/server/routes/lib/short_url_error.test.js +++ /dev/null @@ -1,71 +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 _ from 'lodash'; -import { handleShortUrlError } from './short_url_error'; - -function createErrorWithStatus(status) { - const error = new Error(); - error.status = status; - return error; -} - -function createErrorWithStatusCode(statusCode) { - const error = new Error(); - error.statusCode = statusCode; - return error; -} - -describe('handleShortUrlError()', () => { - const caughtErrorsWithStatus = [ - createErrorWithStatus(401), - createErrorWithStatus(403), - createErrorWithStatus(404), - ]; - - const caughtErrorsWithStatusCode = [ - createErrorWithStatusCode(401), - createErrorWithStatusCode(403), - createErrorWithStatusCode(404), - ]; - - const uncaughtErrors = [ - new Error(), - createErrorWithStatus(500), - createErrorWithStatusCode(500) - ]; - - caughtErrorsWithStatus.forEach((err) => { - it(`should handle errors with status of ${err.status}`, function () { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.status); - }); - }); - - caughtErrorsWithStatusCode.forEach((err) => { - it(`should handle errors with statusCode of ${err.statusCode}`, function () { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.statusCode); - }); - }); - - uncaughtErrors.forEach((err) => { - it(`should not handle unknown errors`, function () { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); - }); - }); -}); diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js deleted file mode 100644 index c4f6af03d7d9..000000000000 --- a/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.js +++ /dev/null @@ -1,67 +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 crypto from 'crypto'; -import { get } from 'lodash'; - -export function shortUrlLookupProvider(server) { - async function updateMetadata(doc, req) { - try { - await req.getSavedObjectsClient().update('url', doc.id, { - accessDate: new Date(), - accessCount: get(doc, 'attributes.accessCount', 0) + 1 - }); - } catch (err) { - server.log('Warning: Error updating url metadata', err); - //swallow errors. It isn't critical if there is no update. - } - } - - return { - async generateUrlId(url, req) { - const id = crypto.createHash('md5').update(url).digest('hex'); - const savedObjectsClient = req.getSavedObjectsClient(); - const { isConflictError } = savedObjectsClient.errors; - - try { - const doc = await savedObjectsClient.create('url', { - url, - accessCount: 0, - createDate: new Date(), - accessDate: new Date() - }, { id }); - - return doc.id; - } catch (error) { - if (isConflictError(error)) { - return id; - } - - throw error; - } - }, - - async getUrl(id, req) { - const doc = await req.getSavedObjectsClient().get('url', id); - updateMetadata(doc, req); - - return doc.attributes.url; - } - }; -} diff --git a/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js b/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js deleted file mode 100644 index 033aeb92926a..000000000000 --- a/src/legacy/core_plugins/share/server/routes/lib/short_url_lookup.test.js +++ /dev/null @@ -1,121 +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 sinon from 'sinon'; -import { shortUrlLookupProvider } from './short_url_lookup'; -import { SavedObjectsClient } from '../../../../../core/server'; - -describe('shortUrlLookupProvider', () => { - const ID = 'bf00ad16941fc51420f91a93428b27a0'; - const TYPE = 'url'; - const URL = 'http://elastic.co'; - const server = { log: sinon.stub() }; - const sandbox = sinon.createSandbox(); - - let savedObjectsClient; - let req; - let shortUrl; - - beforeEach(() => { - savedObjectsClient = { - get: sandbox.stub(), - create: sandbox.stub().returns(Promise.resolve({ id: ID })), - update: sandbox.stub(), - errors: SavedObjectsClient.errors - }; - - req = { getSavedObjectsClient: () => savedObjectsClient }; - shortUrl = shortUrlLookupProvider(server); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('generateUrlId', () => { - it('returns the document id', async () => { - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - - it('provides correct arguments to savedObjectsClient', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - expect(options.id).toEqual(ID); - }); - - it('passes persists attributes', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - }); - - it('gracefully handles version conflict', async () => { - const error = savedObjectsClient.errors.decorateConflictError(new Error()); - savedObjectsClient.create.throws(error); - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - }); - - describe('getUrl', () => { - beforeEach(() => { - const attributes = { accessCount: 2, url: URL }; - savedObjectsClient.get.returns({ id: ID, attributes }); - }); - - it('provides the ID to savedObjectsClient', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.get); - const [type, id] = savedObjectsClient.get.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - }); - - it('returns the url', async () => { - const response = await shortUrl.getUrl(ID, req); - expect(response).toEqual(URL); - }); - - it('increments accessCount', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.update); - const [type, id, attributes] = savedObjectsClient.update.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); - expect(attributes.accessCount).toEqual(3); - }); - }); -}); diff --git a/src/legacy/core_plugins/share/server/routes/shorten_url.ts b/src/legacy/core_plugins/share/server/routes/shorten_url.ts deleted file mode 100644 index 0203e9373384..000000000000 --- a/src/legacy/core_plugins/share/server/routes/shorten_url.ts +++ /dev/null @@ -1,35 +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 { handleShortUrlError } from './lib/short_url_error'; -import { shortUrlAssertValid } from './lib/short_url_assert_valid'; - -export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ - method: 'POST', - path: '/api/shorten_url', - handler: async function (request) { - try { - shortUrlAssertValid(request.payload.url); - const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); - return { urlId }; - } catch (err) { - throw handleShortUrlError(err); - } - } -}); From 44c4c9c7e77bf1aef031e6e5353bc2d41768dea8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 3 Dec 2019 14:10:46 +0100 Subject: [PATCH 18/23] fix error handling --- src/plugins/share/server/routes/goto.ts | 2 +- .../server/routes/lib/short_url_error.test.ts | 17 +++++++++++++++-- .../share/server/routes/lib/short_url_error.ts | 6 ++++-- src/plugins/share/server/routes/shorten_url.ts | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 1aae0b830f25..a8f9d887a064 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -63,7 +63,7 @@ export const createGotoRoute = ({ }); } } catch (err) { - throw handleShortUrlError(err); + return handleShortUrlError(response, err); } } ); diff --git a/src/plugins/share/server/routes/lib/short_url_error.test.ts b/src/plugins/share/server/routes/lib/short_url_error.test.ts index d0176db94f19..dabcfb18d588 100644 --- a/src/plugins/share/server/routes/lib/short_url_error.test.ts +++ b/src/plugins/share/server/routes/lib/short_url_error.test.ts @@ -20,11 +20,18 @@ import _ from 'lodash'; import { handleShortUrlError } from './short_url_error'; import Boom from 'boom'; +import { KibanaResponseFactory } from 'kibana/server'; function createErrorWithStatusCode(statusCode: number) { return new Boom('', { statusCode }); } +function createResponseStub() { + return ({ + customError: jest.fn(), + } as unknown) as jest.Mocked; +} + describe('handleShortUrlError()', () => { const caughtErrorsWithStatusCode = [ createErrorWithStatusCode(401), @@ -36,14 +43,20 @@ describe('handleShortUrlError()', () => { caughtErrorsWithStatusCode.forEach(err => { const statusCode = (err as Boom).output.statusCode; + const response = createResponseStub(); it(`should handle errors with statusCode of ${statusCode}`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(statusCode); + handleShortUrlError(response, err); + expect(response.customError).toHaveBeenCalledWith(expect.objectContaining({ statusCode })); }); }); uncaughtErrors.forEach(err => { it(`should not handle unknown errors`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); + const response = createResponseStub(); + handleShortUrlError(response, err); + expect(response.customError).toHaveBeenCalledWith( + expect.objectContaining({ statusCode: 500 }) + ); }); }); }); diff --git a/src/plugins/share/server/routes/lib/short_url_error.ts b/src/plugins/share/server/routes/lib/short_url_error.ts index 3730bea65a4b..feb916fe8019 100644 --- a/src/plugins/share/server/routes/lib/short_url_error.ts +++ b/src/plugins/share/server/routes/lib/short_url_error.ts @@ -18,9 +18,11 @@ */ import Boom from 'boom'; +import { KibanaResponseFactory } from 'kibana/server'; -export function handleShortUrlError(error: Error) { - return Boom.boomify(error, { +export function handleShortUrlError(response: KibanaResponseFactory, error: Error) { + return response.customError({ statusCode: Boom.isBoom(error) ? error.output.statusCode : 500, + body: error.message, }); } diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index 4f72e10d4d3d..fe206179b4e1 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -46,7 +46,7 @@ export const createShortenUrlRoute = ({ }); return response.ok({ body: { urlId } }); } catch (err) { - throw handleShortUrlError(err); + return handleShortUrlError(response, err); } } ); From cd2afab043f2e8f931f3a6d9250b3ed427b695b9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 3 Dec 2019 14:24:03 +0100 Subject: [PATCH 19/23] switch to boom wrapper --- src/plugins/share/server/routes/goto.ts | 45 ++++++-------- .../server/routes/lib/short_url_error.test.ts | 62 ------------------- .../server/routes/lib/short_url_error.ts | 28 --------- .../share/server/routes/shorten_url.ts | 20 +++--- 4 files changed, 28 insertions(+), 127 deletions(-) delete mode 100644 src/plugins/share/server/routes/lib/short_url_error.test.ts delete mode 100644 src/plugins/share/server/routes/lib/short_url_error.ts diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index a8f9d887a064..454beddd19c9 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -20,7 +20,6 @@ import { CoreSetup, IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; -import { handleShortUrlError } from './lib/short_url_error'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; @@ -40,31 +39,27 @@ export const createGotoRoute = ({ params: schema.object({ urlId: schema.string() }), }, }, - async function(context, request, response) { - try { - const url = await shortUrlLookup.getUrl(request.params.urlId, { - savedObjects: context.core.savedObjects.client, - }); - shortUrlAssertValid(url); + router.handleLegacyErrors(async function(context, request, response) { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); + shortUrlAssertValid(url); - const uiSettings = context.core.uiSettings.client; - const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); - if (!stateStoreInSessionStorage) { - return response.redirected({ - headers: { - location: http.basePath.prepend(url), - }, - }); - } else { - return response.redirected({ - headers: { - location: http.basePath.prepend('/goto_LP/' + request.params.urlId), - }, - }); - } - } catch (err) { - return handleShortUrlError(response, err); + const uiSettings = context.core.uiSettings.client; + const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); + if (!stateStoreInSessionStorage) { + return response.redirected({ + headers: { + location: http.basePath.prepend(url), + }, + }); + } else { + return response.redirected({ + headers: { + location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + }, + }); } - } + }) ); }; diff --git a/src/plugins/share/server/routes/lib/short_url_error.test.ts b/src/plugins/share/server/routes/lib/short_url_error.test.ts deleted file mode 100644 index dabcfb18d588..000000000000 --- a/src/plugins/share/server/routes/lib/short_url_error.test.ts +++ /dev/null @@ -1,62 +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 _ from 'lodash'; -import { handleShortUrlError } from './short_url_error'; -import Boom from 'boom'; -import { KibanaResponseFactory } from 'kibana/server'; - -function createErrorWithStatusCode(statusCode: number) { - return new Boom('', { statusCode }); -} - -function createResponseStub() { - return ({ - customError: jest.fn(), - } as unknown) as jest.Mocked; -} - -describe('handleShortUrlError()', () => { - const caughtErrorsWithStatusCode = [ - createErrorWithStatusCode(401), - createErrorWithStatusCode(403), - createErrorWithStatusCode(404), - ]; - - const uncaughtErrors = [new Error(), createErrorWithStatusCode(500)]; - - caughtErrorsWithStatusCode.forEach(err => { - const statusCode = (err as Boom).output.statusCode; - const response = createResponseStub(); - it(`should handle errors with statusCode of ${statusCode}`, function() { - handleShortUrlError(response, err); - expect(response.customError).toHaveBeenCalledWith(expect.objectContaining({ statusCode })); - }); - }); - - uncaughtErrors.forEach(err => { - it(`should not handle unknown errors`, function() { - const response = createResponseStub(); - handleShortUrlError(response, err); - expect(response.customError).toHaveBeenCalledWith( - expect.objectContaining({ statusCode: 500 }) - ); - }); - }); -}); diff --git a/src/plugins/share/server/routes/lib/short_url_error.ts b/src/plugins/share/server/routes/lib/short_url_error.ts deleted file mode 100644 index feb916fe8019..000000000000 --- a/src/plugins/share/server/routes/lib/short_url_error.ts +++ /dev/null @@ -1,28 +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 Boom from 'boom'; -import { KibanaResponseFactory } from 'kibana/server'; - -export function handleShortUrlError(response: KibanaResponseFactory, error: Error) { - return response.customError({ - statusCode: Boom.isBoom(error) ? error.output.statusCode : 500, - body: error.message, - }); -} diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index fe206179b4e1..1acb187c932b 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -20,7 +20,6 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; -import { handleShortUrlError } from './lib/short_url_error'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; @@ -38,16 +37,13 @@ export const createShortenUrlRoute = ({ body: schema.object({ url: schema.string() }, { allowUnknowns: false }), }, }, - async function(context, request, response) { - try { - shortUrlAssertValid(request.body.url); - const urlId = await shortUrlLookup.generateUrlId(request.body.url, { - savedObjects: context.core.savedObjects.client, - }); - return response.ok({ body: { urlId } }); - } catch (err) { - return handleShortUrlError(response, err); - } - } + router.handleLegacyErrors(async function(context, request, response) { + shortUrlAssertValid(request.body.url); + const urlId = await shortUrlLookup.generateUrlId(request.body.url, { + savedObjects: context.core.savedObjects.client, + }); + throw new Error(); + return response.ok({ body: { urlId } }); + }) ); }; From 8e045a93e725a2fc2235fdeaabcdc2bfa235097f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 3 Dec 2019 15:31:38 +0100 Subject: [PATCH 20/23] remove debug statement and fix codeowners --- .github/CODEOWNERS | 4 ++-- src/plugins/share/server/routes/shorten_url.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 610681e83798..4f4da3a3ac2a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,8 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app +/src/plugins/share/ @elastic/kibana-app +/src/legacy/server/url_shortening/ @elastic/kibana-app # App Architecture /src/plugins/data/ @elastic/kibana-app-arch @@ -13,7 +15,6 @@ /src/plugins/kibana_react/ @elastic/kibana-app-arch /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch @@ -27,7 +28,6 @@ /src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch -/src/legacy/server/url_shortening/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index 1acb187c932b..8ef74d5a6e7c 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -42,7 +42,6 @@ export const createShortenUrlRoute = ({ const urlId = await shortUrlLookup.generateUrlId(request.body.url, { savedObjects: context.core.savedObjects.client, }); - throw new Error(); return response.ok({ body: { urlId } }); }) ); From cf2b6a9c95b762dd515acf6e316ee416a880910d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 3 Dec 2019 18:30:28 +0100 Subject: [PATCH 21/23] remove optional setting --- src/plugins/share/server/routes/shorten_url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index 8ef74d5a6e7c..116b90c6971c 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -34,7 +34,7 @@ export const createShortenUrlRoute = ({ { path: '/api/shorten_url', validate: { - body: schema.object({ url: schema.string() }, { allowUnknowns: false }), + body: schema.object({ url: schema.string() }), }, }, router.handleLegacyErrors(async function(context, request, response) { From 7cd578bd30fb695b3cf70fc90257e23a5ab08359 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 4 Dec 2019 09:51:20 +0100 Subject: [PATCH 22/23] fix functional test --- x-pack/test/api_integration/apis/short_urls/feature_controls.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index 7e1b9e7bd971..8975c826655a 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -108,7 +108,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expect(resp.status).to.eql(302); expect(resp.headers.location).to.eql('/app/kibana#foo/bar/baz'); } else { - expect(resp.status).to.eql(500); + expect(resp.status).to.eql(403); expect(resp.headers.location).to.eql(undefined); } }); From 0a129087842715a5f0f463c3015694b2eee40c4b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 9 Dec 2019 15:04:21 +0100 Subject: [PATCH 23/23] remove unnecessary goto --- src/plugins/share/server/routes/goto.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 454beddd19c9..7343dc1bd34a 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -53,13 +53,12 @@ export const createGotoRoute = ({ location: http.basePath.prepend(url), }, }); - } else { - return response.redirected({ - headers: { - location: http.basePath.prepend('/goto_LP/' + request.params.urlId), - }, - }); } + return response.redirected({ + headers: { + location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + }, + }); }) ); };