diff --git a/.i18nrc.json b/.i18nrc.json index d3ad522126e6e..d0d8beb6f5337 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": "src/plugins/expressions", "inputControl": "src/legacy/core_plugins/input_control_vis", diff --git a/docs/user/reporting/development/index.asciidoc b/docs/user/reporting/development/index.asciidoc index 2a9abae34f042..a64e540da0c70 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/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index d82b89339b0d0..457d8972876ae 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 { Query } from '../../../../../plugins/data/public'; import { start as data } from '../../../data/public/legacy'; @@ -131,7 +131,6 @@ export class DashboardAppController { }) { const queryFilter = Private(FilterBarQueryFilterProvider); const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); let lastReloadRequestTime = 0; @@ -758,14 +757,13 @@ export class DashboardAppController { }); }; navActions[TopNavIds.SHARE] = anchorElement => { - showShareContextMenu({ + npStart.plugins.share.toggleShareContextMenu({ 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/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index 8ee23bfb005a2..20a05e17d16d6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -50,7 +50,7 @@ import { migrateLegacyQuery, RequestAdapter, showSaveModal, - showShareContextMenu, + unhashUrl, stateMonitorFactory, subscribeWithScope, tabifyAggResponse, @@ -63,7 +63,7 @@ const { chrome, docTitle, FilterBarQueryFilterProvider, - ShareContextMenuExtensionsRegistryProvider, + share, StateProvider, timefilter, toastNotifications, @@ -190,7 +190,6 @@ function discoverController( ) { const responseHandler = vislibSeriesResponseHandlerProvider().handler; const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); const queryFilter = Private(FilterBarQueryFilterProvider); @@ -323,14 +322,13 @@ function discoverController( testId: 'shareTopNavButton', run: async (anchorElement) => { const sharingData = await this.getSharingData(); - showShareContextMenu({ + share.toggleShareContextMenu({ 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 d0eb115e32676..61d7933464e7f 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, @@ -99,7 +98,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 @@ -110,6 +108,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 { Vis } from 'ui/vis'; 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 58a0075e94b99..619903e93c127 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -42,10 +42,10 @@ import { KibanaParsedUrl, migrateLegacyQuery, SavedObjectSaveModal, - showShareContextMenu, showSaveModal, stateMonitorFactory, subscribeWithScope, + unhashUrl, } from '../kibana_services'; const { @@ -56,12 +56,12 @@ const { docTitle, FilterBarQueryFilterProvider, getBasePath, - ShareContextMenuExtensionsRegistryProvider, toastNotifications, timefilter, uiModules, uiRoutes, visualizations, + share, } = getServices(); const { savedQueryService } = data.search.services; @@ -159,7 +159,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; @@ -239,14 +238,13 @@ function VisEditor( run: (anchorElement) => { const hasUnappliedChanges = vis.dirty; const hasUnsavedChanges = $appStatus.dirty; - showShareContextMenu({ + share.toggleShareContextMenu({ 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 5c6d06b5eaeb6..3be49971cf4c9 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, @@ -99,13 +98,13 @@ 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'; export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; 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.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index e62ceeb2f3102..5c269c7b019aa 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: () => {}, + }, devTools: { register: () => {}, }, @@ -162,6 +165,9 @@ export const npStart = { }, }, }, + share: { + toggleShareContextMenu: () => {}, + }, inspector: { isAvailable: () => false, open: () => ({ diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 575ad4efa1ff9..1db360749c714 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -30,6 +30,7 @@ import { import { EuiUtilsStart } from '../../../../plugins/eui_utils/public'; import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/public'; import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public'; +import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; export interface PluginsSetup { data: ReturnType; @@ -38,6 +39,7 @@ export interface PluginsSetup { home: HomePublicPluginSetup; inspector: InspectorSetup; uiActions: IUiActionsSetup; + share: SharePluginSetup; devTools: DevToolsSetup; } @@ -49,6 +51,7 @@ export interface PluginsStart { home: HomePublicPluginStart; inspector: InspectorStart; uiActions: IUiActionsStart; + share: SharePluginStart; devTools: DevToolsStart; } diff --git a/src/legacy/ui/public/share/_index.scss b/src/legacy/ui/public/share/_index.scss index 192091fb04e3c..85168c9ea80f7 100644 --- a/src/legacy/ui/public/share/_index.scss +++ b/src/legacy/ui/public/share/_index.scss @@ -1 +1 @@ -@import './components/index'; +@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 0000000000000..a05164a6bb0d1 --- /dev/null +++ b/src/legacy/ui/public/share/_share_context_menu.scss @@ -0,0 +1,3 @@ +.kbnShareContextMenu__finalPanel { + padding: $euiSize; +} 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 df50f1d4a78ba..0000000000000 --- 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 645b8c662c417..0000000000000 --- 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 85168c9ea80f7..0000000000000 --- 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 d28e7846d813e..0000000000000 --- 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 5b583420f85e9..0000000000000 --- 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/lib/url_shortener.test.js b/src/legacy/ui/public/share/lib/url_shortener.test.js deleted file mode 100644 index 859873bd4989f..0000000000000 --- 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/share_action.ts b/src/legacy/ui/public/share/share_action.ts deleted file mode 100644 index 5e524944d2094..0000000000000 --- 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/show_share_context_menu.tsx b/src/legacy/ui/public/share/show_share_context_menu.tsx deleted file mode 100644 index 1b3da0c6dc060..0000000000000 --- 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/README.md b/src/plugins/share/README.md new file mode 100644 index 0000000000000..7ecf23134cf24 --- /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 0000000000000..bbe393a76c5da --- /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.tsx.snap b/src/plugins/share/public/components/__snapshots__/share_context_menu.test.tsx.snap new file mode 100644 index 0000000000000..fc3fa3e72b9c0 --- /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.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap new file mode 100644 index 0000000000000..c10ca55130880 --- /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.tsx b/src/plugins/share/public/components/share_context_menu.test.tsx new file mode 100644 index 0000000000000..7fb0449ead502 --- /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/legacy/ui/public/share/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx similarity index 63% rename from src/legacy/ui/public/share/components/share_context_menu.tsx rename to src/plugins/share/public/components/share_context_menu.tsx index 5d5c80f10e1d5..8676a07bc10c4 100644 --- a/src/legacy/ui/public/share/components/share_context_menu.tsx +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -19,47 +19,50 @@ 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 { ShareMenuItem, ShareContextMenuPanelItem } from '../types'; interface Props { allowEmbed: boolean; allowShortUrl: boolean; objectId?: string; objectType: string; - getUnhashableStates: () => object[]; - shareContextMenuExtensions?: ShareActionProvider[]; + shareableUrl?: string; + shareMenuItems: ShareMenuItem[]; 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('share.contextMenu.permalinkPanelTitle', { defaultMessage: 'Permalink', }), content: ( @@ -67,13 +70,14 @@ class ShareContextMenuUI extends Component { allowShortUrl={this.props.allowShortUrl} objectId={this.props.objectId} objectType={this.props.objectType} - getUnhashableStates={this.props.getUnhashableStates} + basePath={this.props.basePath} + post={this.props.post} + shareableUrl={this.props.shareableUrl} /> ), }; menuItems.push({ - name: intl.formatMessage({ - id: 'common.ui.share.contextMenu.permalinksLabel', + name: i18n.translate('share.contextMenu.permalinksLabel', { defaultMessage: 'Permalinks', }), icon: 'link', @@ -85,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('share.contextMenu.embedCodePanelTitle', { defaultMessage: 'Embed Code', }), content: ( @@ -95,14 +98,15 @@ class ShareContextMenuUI extends Component { isEmbedded objectId={this.props.objectId} objectType={this.props.objectType} - getUnhashableStates={this.props.getUnhashableStates} + basePath={this.props.basePath} + post={this.props.post} + shareableUrl={this.props.shareableUrl} /> ), }; panels.push(embedPanel); menuItems.push({ - name: intl.formatMessage({ - id: 'common.ui.share.contextMenu.embedCodeLabel', + name: i18n.translate('share.contextMenu.embedCodeLabel', { defaultMessage: 'Embed code', }), icon: 'console', @@ -111,51 +115,27 @@ class ShareContextMenuUI extends Component { }); } - 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, - }); - }); + this.props.shareMenuItems.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}', - }, - { + title: i18n.translate('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) => { @@ -186,5 +166,3 @@ class ShareContextMenuUI extends Component { return { panels, initialPanelId }; }; } - -export const ShareContextMenu = injectI18n(ShareContextMenuUI); diff --git a/src/legacy/ui/public/share/components/url_panel_content.test.js b/src/plugins/share/public/components/url_panel_content.test.tsx similarity index 64% rename from src/legacy/ui/public/share/components/url_panel_content.test.js rename to src/plugins/share/public/components/url_panel_content.test.tsx index 8dd94202fe726..9da1a23641ab8 100644 --- a/src/legacy/ui/public/share/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/legacy/ui/public/share/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx similarity index 72% rename from src/legacy/ui/public/share/components/url_panel_content.tsx rename to src/plugins/share/public/components/url_panel_content.tsx index 28d51e9826d40..d0d4ce55dc1ac 100644 --- a/src/legacy/ui/public/share/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -31,24 +31,25 @@ import { EuiLoadingSpinner, EuiRadioGroup, EuiSwitch, + EuiSwitchEvent, } 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'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { HttpStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; -// TODO: Remove once EuiIconTip supports "content" prop -const FixedEuiIconTip = EuiIconTip as React.SFC; +import { shortenUrl } from '../lib/url_shortener'; interface Props { allowShortUrl: boolean; isEmbedded?: boolean; objectId?: string; objectType: string; - getUnhashableStates: () => object[]; - intl: InjectedIntl; + shareableUrl?: string; + basePath: string; + post: HttpStart['post']; } enum ExportUrlAsType { @@ -64,7 +65,7 @@ interface State { shortUrlErrorMsg?: string; } -class UrlPanelContentUI extends Component { +export class UrlPanelContent extends Component { private mounted?: boolean; private shortUrlCache?: string; @@ -96,41 +97,41 @@ 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 ? ( + + ) : ( + + )} + + )} + + + ); } @@ -155,11 +156,9 @@ class UrlPanelContentUI extends Component { return; } - const url = window.location.href; - // Replace hashes with original RISON values. - const unhashedUrl = unhashUrl(url, this.props.getUnhashableStates()); + const url = this.getSnapshotUrl(); - const parsedUrl = parseUrl(unhashedUrl); + const parsedUrl = parseUrl(url); if (!parsedUrl || !parsedUrl.hash) { return; } @@ -184,9 +183,7 @@ class UrlPanelContentUI extends Component { }; private getSnapshotUrl = () => { - const url = window.location.href; - // Replace hashes with original RISON values. - return unhashUrl(url, this.props.getUnhashableStates()); + return this.props.shareableUrl || window.location.href; }; private makeUrlEmbeddable = (url: string) => { @@ -233,8 +230,7 @@ class UrlPanelContentUI extends Component { ); }; - // TODO: switch evt type to ChangeEvent once https://github.com/elastic/eui/issues/1134 is resolved - private handleShortUrlChange = async (evt: any) => { + private handleShortUrlChange = async (evt: EuiSwitchEvent) => { const isChecked = evt.target.checked; if (!isChecked || this.shortUrlCache !== undefined) { @@ -249,7 +245,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( @@ -267,15 +266,12 @@ class UrlPanelContentUI extends Component { { useShortUrl: false, isCreatingShortUrl: false, - shortUrlErrorMsg: this.props.intl.formatMessage( - { - id: 'common.ui.share.urlPanel.unableCreateShortUrlErrorMessage', - defaultMessage: 'Unable to create short URL. Error: {errorMessage}', - }, - { + shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', { + defaultMessage: 'Unable to create short URL. Error: {errorMessage}', + values: { errorMessage: fetchError.message, - } - ), + }, + }), }, this.setUrl ); @@ -288,12 +284,9 @@ class UrlPanelContentUI extends Component { { id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, label: this.renderWithIconTip( + , , - { id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, disabled: this.isNotSaved(), label: this.renderWithIconTip( + , , - @@ -325,7 +315,7 @@ class UrlPanelContentUI extends Component { {child} - + ); @@ -334,7 +324,7 @@ class UrlPanelContentUI extends Component { private renderExportAsRadioGroup = () => { const generateLinkAsHelp = this.isNotSaved() ? ( @@ -345,7 +335,7 @@ class UrlPanelContentUI extends Component { } @@ -368,7 +358,7 @@ class UrlPanelContentUI extends Component { return; } const shortUrlLabel = ( - + ); const switchLabel = this.state.isCreatingShortUrl ? ( @@ -387,7 +377,7 @@ class UrlPanelContentUI extends Component { ); const tipContent = ( { + 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/legacy/ui/public/share/lib/url_shortener.ts b/src/plugins/share/public/lib/url_shortener.ts similarity index 83% rename from src/legacy/ui/public/share/lib/url_shortener.ts rename to src/plugins/share/public/lib/url_shortener.ts index 037214bd9b450..29d91bdb1aae6 100644 --- a/src/legacy/ui/public/share/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.test.mocks.ts b/src/plugins/share/public/plugin.test.mocks.ts new file mode 100644 index 0000000000000..bd814ebc2500b --- /dev/null +++ b/src/plugins/share/public/plugin.test.mocks.ts @@ -0,0 +1,28 @@ +/* + * 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 { shareMenuRegistryMock } from './services/share_menu_registry.mock'; +import { shareMenuManagerMock } from './services/share_menu_manager.mock'; + +export const registryMock = shareMenuRegistryMock.create(); +export const managerMock = shareMenuManagerMock.create(); +jest.doMock('./services', () => ({ + 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 new file mode 100644 index 0000000000000..5610490be33b3 --- /dev/null +++ b/src/plugins/share/public/plugin.test.ts @@ -0,0 +1,52 @@ +/* + * 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, 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(); + }); + + 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 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(managerMock.start).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ getShareMenuItems: expect.any(Function) }) + ); + expect(start.toggleShareContextMenu).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts new file mode 100644 index 0000000000000..6d78211cf9954 --- /dev/null +++ b/src/plugins/share/public/plugin.ts @@ -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 { CoreStart, Plugin } from 'src/core/public'; +import { ShareMenuManager, ShareMenuManagerStart } from './services'; +import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; + +export class SharePlugin implements Plugin { + private readonly shareMenuRegistry = new ShareMenuRegistry(); + private readonly shareContextMenu = new ShareMenuManager(); + + public async setup() { + return { + ...this.shareMenuRegistry.setup(), + }; + } + + public async start(core: CoreStart) { + return { + ...this.shareContextMenu.start(core, this.shareMenuRegistry.start()), + }; + } +} + +/** @public */ +export type SharePluginSetup = ShareMenuRegistrySetup; + +/** @public */ +export type SharePluginStart = ShareMenuManagerStart; diff --git a/src/legacy/ui/public/share/index.ts b/src/plugins/share/public/services/index.ts similarity index 84% rename from src/legacy/ui/public/share/index.ts rename to src/plugins/share/public/services/index.ts index 3a1264541cdea..aebb81df9e968 100644 --- a/src/legacy/ui/public/share/index.ts +++ b/src/plugins/share/public/services/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { showShareContextMenu } from './show_share_context_menu'; -export { ShareContextMenuExtensionsRegistryProvider } from './share_action_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 0000000000000..7104abeb26090 --- /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 = { + toggleShareContextMenu: 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_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx new file mode 100644 index 0000000000000..35116efa85961 --- /dev/null +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -0,0 +1,112 @@ +/* + * 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 { ShareMenuItem, ShowShareMenuOptions } from '../types'; +import { ShareMenuRegistryStart } from './share_menu_registry'; + +export class ShareMenuManager { + private isOpen = false; + + private container = document.createElement('div'); + + start(core: CoreStart, shareRegistry: ShareMenuRegistryStart) { + return { + /** + * 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(), + }); + }, + }; + } + + private onClose = () => { + ReactDOM.unmountComponentAtNode(this.container); + this.isOpen = false; + }; + + private toggleShareContextMenu({ + anchorElement, + allowEmbed, + allowShortUrl, + objectId, + objectType, + sharingData, + menuItems, + shareableUrl, + post, + basePath, + }: ShowShareMenuOptions & { + menuItems: ShareMenuItem[]; + 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 ShareMenuManagerStart = ReturnType; diff --git a/src/plugins/share/public/services/share_menu_registry.mock.ts b/src/plugins/share/public/services/share_menu_registry.mock.ts new file mode 100644 index 0000000000000..b69032f0b3e09 --- /dev/null +++ b/src/plugins/share/public/services/share_menu_registry.mock.ts @@ -0,0 +1,55 @@ +/* + * 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 { + ShareMenuRegistry, + ShareMenuRegistrySetup, + ShareMenuRegistryStart, +} from './share_menu_registry'; +import { ShareMenuItem, ShareContext } from '../types'; + +const createSetupMock = (): jest.Mocked => { + const setup = { + register: jest.fn(), + }; + return setup; +}; + +const createStartMock = (): jest.Mocked => { + const start = { + getShareMenuItems: jest.fn((props: ShareContext) => [] as ShareMenuItem[]), + }; + 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 shareMenuRegistryMock = { + createSetup: createSetupMock, + createStart: createStartMock, + create: createMock, +}; diff --git a/src/plugins/share/public/services/share_menu_registry.test.ts b/src/plugins/share/public/services/share_menu_registry.test.ts new file mode 100644 index 0000000000000..b79f1858af051 --- /dev/null +++ b/src/plugins/share/public/services/share_menu_registry.test.ts @@ -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 { ShareMenuRegistry } from './share_menu_registry'; +import { ShareMenuItem, ShareContext } from '../types'; + +describe('ShareActionsRegistry', () => { + describe('setup', () => { + test('throws when registering duplicate id', () => { + const setup = new ShareMenuRegistry().setup(); + setup.register({ + id: 'myTest', + getShareMenuItems: () => [], + }); + expect(() => + setup.register({ + id: 'myTest', + getShareMenuItems: () => [], + }) + ).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 ShareMenuRegistry(); + const registerFunction = service.setup().register; + 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', + getShareMenuItems: provider1Callback, + }); + registerFunction({ + id: 'myTest2', + getShareMenuItems: provider2Callback, + }); + const context = {} as ShareContext; + expect(service.start().getShareMenuItems(context)).toEqual([ + shareAction1, + shareAction2, + shareAction3, + ]); + 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 new file mode 100644 index 0000000000000..1fec420a9a8f4 --- /dev/null +++ b/src/plugins/share/public/services/share_menu_registry.ts @@ -0,0 +1,57 @@ +/* + * 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 { 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( + `Share menu provider with id [${shareMenuProvider.id}] has already been registered. Use a unique id.` + ); + } + + this.shareMenuProviders.set(shareMenuProvider.id, shareMenuProvider); + }, + }; + } + + public start() { + return { + getShareMenuItems: (context: ShareContext) => + Array.from(this.shareMenuProviders.values()).flatMap(shareActionProvider => + shareActionProvider.getShareMenuItems(context) + ), + }; + } +} + +export type ShareMenuRegistrySetup = ReturnType; +export type ShareMenuRegistryStart = ReturnType; diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts new file mode 100644 index 0000000000000..2472129757133 --- /dev/null +++ b/src/plugins/share/public/types.ts @@ -0,0 +1,90 @@ +/* + * 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 + * 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 `toggleShareContextMenu` + * to render the menu as a popover. + * */ +export interface ShareContext { + 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: { [key: string]: 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 + * menu providers. + * */ +export interface ShareContextMenuPanelItem extends EuiContextMenuPanelItemDescriptor { + sortOrder: number; +} + +/** + * @public + * 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 ShareMenuItem { + shareMenuItem: ShareContextMenuPanelItem; + panel: EuiContextMenuPanelDescriptor; +} + +/** + * @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: (context: ShareContext) => ShareMenuItem[]; +} + +/** @public */ +export interface ShowShareMenuOptions extends Omit { + anchorElement: HTMLElement; + allowEmbed: boolean; + allowShortUrl: boolean; +} diff --git a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx index 41c83543750b3..ac966ceb99736 100644 --- a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx @@ -127,12 +127,9 @@ class ReportingPanelContentUi extends Component { - + {copy => ( - + { private renderGenerateReportButton = (isDisabled: boolean) => { return ( { + }: ShareContext) => { if ('search' !== objectType) { return []; } @@ -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: ( { + shareableUrl, + }: ShareContext) => { 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' @@ -53,8 +52,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' @@ -87,6 +85,7 @@ function reportingProvider(dashboardConfig: any) { sortOrder: 10, }, panel: { + id: 'reportingPdfPanel', title: panelTitle, content: ( { + npSetup.plugins.share.register(await reportingProvider()); +})(); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f7302157655b1..c4827190a0565 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -493,22 +493,6 @@ "common.ui.savedObjects.saveAsNewLabel": "新規 {savedObjectName} として保存", "common.ui.savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", "common.ui.scriptingLanguages.errorFetchingToastDescription": "Elasticsearch から利用可能なスクリプト言語の取得中にエラーが発生しました", - "common.ui.share.contextMenu.embedCodeLabel": "埋め込みコード", - "common.ui.share.contextMenu.embedCodePanelTitle": "埋め込みコード", - "common.ui.share.contextMenu.permalinkPanelTitle": "パーマリンク", - "common.ui.share.contextMenu.permalinksLabel": "パーマリンク", - "common.ui.share.contextMenuTitle": "この {objectType} を共有", - "common.ui.share.urlPanel.canNotShareAsSavedObjectHelpText": "{objectType} が保存されるまで保存されたオブジェクトを共有することはできません。", - "common.ui.share.urlPanel.copyIframeCodeButtonLabel": "iFrame コードをコピー", - "common.ui.share.urlPanel.copyLinkButtonLabel": "リンクをコピー", - "common.ui.share.urlPanel.generateLinkAsLabel": "名前を付けてリンクを生成", - "common.ui.share.urlPanel.savedObjectDescription": "この URL を共有することで、他のユーザーがこの {objectType} の最も最近保存されたバージョンを読み込めるようになります。", - "common.ui.share.urlPanel.savedObjectLabel": "保存されたオブジェクト", - "common.ui.share.urlPanel.shortUrlHelpText": "互換性が最も高くなるよう、短いスナップショット URL を共有することをお勧めします。Internet Explorer は URL の長さに制限があり、一部の wiki やマークアップパーサーは長い完全なスナップショット URL に対応していませんが、短い URL は正常に動作するはずです。", - "common.ui.share.urlPanel.shortUrlLabel": "短い URL", - "common.ui.share.urlPanel.snapshotDescription": "スナップショット URL には、{objectType} の現在の状態がエンコードされています。保存された {objectType} への編集内容はこの URL には反映されません。.", - "common.ui.share.urlPanel.snapshotLabel": "スナップショット", - "common.ui.share.urlPanel.unableCreateShortUrlErrorMessage": "短い URL を作成できません。エラー: {errorMessage}", "common.ui.stateManagement.unableToParseUrlErrorMessage": "URL をパースできません", "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", @@ -2841,6 +2825,22 @@ "server.status.redTitle": "赤", "server.status.uninitializedTitle": "アンインストールしました", "server.status.yellowTitle": "黄色", + "share.contextMenu.embedCodeLabel": "埋め込みコード", + "share.contextMenu.embedCodePanelTitle": "埋め込みコード", + "share.contextMenu.permalinkPanelTitle": "パーマリンク", + "share.contextMenu.permalinksLabel": "パーマリンク", + "share.contextMenuTitle": "この {objectType} を共有", + "share.urlPanel.canNotShareAsSavedObjectHelpText": "{objectType} が保存されるまで保存されたオブジェクトを共有することはできません。", + "share.urlPanel.copyIframeCodeButtonLabel": "iFrame コードをコピー", + "share.urlPanel.copyLinkButtonLabel": "リンクをコピー", + "share.urlPanel.generateLinkAsLabel": "名前を付けてリンクを生成", + "share.urlPanel.savedObjectDescription": "この URL を共有することで、他のユーザーがこの {objectType} の最も最近保存されたバージョンを読み込めるようになります。", + "share.urlPanel.savedObjectLabel": "保存されたオブジェクト", + "share.urlPanel.shortUrlHelpText": "互換性が最も高くなるよう、短いスナップショット URL を共有することをお勧めします。Internet Explorer は URL の長さに制限があり、一部の wiki やマークアップパーサーは長い完全なスナップショット URL に対応していませんが、短い URL は正常に動作するはずです。", + "share.urlPanel.shortUrlLabel": "短い URL", + "share.urlPanel.snapshotDescription": "スナップショット URL には、{objectType} の現在の状態がエンコードされています。保存された {objectType} への編集内容はこの URL には反映されません。.", + "share.urlPanel.snapshotLabel": "スナップショット", + "share.urlPanel.unableCreateShortUrlErrorMessage": "短い URL を作成できません。エラー: {errorMessage}", "statusPage.loadStatus.serverIsDownErrorMessage": "サーバーステータスのリクエストに失敗しました。サーバーがダウンしている可能性があります。", "statusPage.loadStatus.serverStatusCodeErrorMessage": "サーバーステータスのリクエストに失敗しました。ステータスコード: {responseStatus}", "statusPage.metricsTiles.columns.heapTotalHeader": "ヒープ合計", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 465ea33c48854..da63b7e9c3674 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -493,22 +493,6 @@ "common.ui.savedObjects.saveAsNewLabel": "另存为新的 {savedObjectName}", "common.ui.savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", "common.ui.scriptingLanguages.errorFetchingToastDescription": "从 Elasticsearch 获取可用的脚本语言时出错", - "common.ui.share.contextMenu.embedCodeLabel": "嵌入代码", - "common.ui.share.contextMenu.embedCodePanelTitle": "嵌入代码", - "common.ui.share.contextMenu.permalinkPanelTitle": "固定链接", - "common.ui.share.contextMenu.permalinksLabel": "固定链接", - "common.ui.share.contextMenuTitle": "共享此 {objectType}", - "common.ui.share.urlPanel.canNotShareAsSavedObjectHelpText": "只有保存 {objectType} 后,才能共享为已保存对象。", - "common.ui.share.urlPanel.copyIframeCodeButtonLabel": "复制 iFrame 代码", - "common.ui.share.urlPanel.copyLinkButtonLabel": "复制链接", - "common.ui.share.urlPanel.generateLinkAsLabel": "将链接生成为", - "common.ui.share.urlPanel.savedObjectDescription": "您可以将此 URL 共享给相关人员,以便他们可以加载此 {objectType} 最新的已保存版本。", - "common.ui.share.urlPanel.savedObjectLabel": "已保存对象", - "common.ui.share.urlPanel.shortUrlHelpText": "建议共享缩短的快照 URL,以实现最大的兼容性。Internet Explorer 有 URL 长度限制,某些 wiki 和标记分析器无法很好地处理全长版本的快照 URL,但应能很好地处理短 URL。", - "common.ui.share.urlPanel.shortUrlLabel": "短 URL", - "common.ui.share.urlPanel.snapshotDescription": "快照 URL 将{objectType}的当前状态编入 URL 自身之中。通过此 URL,将无法看到对已保存的{objectType}的编辑。", - "common.ui.share.urlPanel.snapshotLabel": "快照", - "common.ui.share.urlPanel.unableCreateShortUrlErrorMessage": "无法创建短 URL。错误:{errorMessage}", "common.ui.stateManagement.unableToParseUrlErrorMessage": "无法解析 URL", "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", @@ -2842,6 +2826,22 @@ "server.status.redTitle": "红", "server.status.uninitializedTitle": "未初始化", "server.status.yellowTitle": "黄", + "share.contextMenu.embedCodeLabel": "嵌入代码", + "share.contextMenu.embedCodePanelTitle": "嵌入代码", + "share.contextMenu.permalinkPanelTitle": "固定链接", + "share.contextMenu.permalinksLabel": "固定链接", + "share.contextMenuTitle": "共享此 {objectType}", + "share.urlPanel.canNotShareAsSavedObjectHelpText": "只有保存 {objectType} 后,才能共享为已保存对象。", + "share.urlPanel.copyIframeCodeButtonLabel": "复制 iFrame 代码", + "share.urlPanel.copyLinkButtonLabel": "复制链接", + "share.urlPanel.generateLinkAsLabel": "将链接生成为", + "share.urlPanel.savedObjectDescription": "您可以将此 URL 共享给相关人员,以便他们可以加载此 {objectType} 最新的已保存版本。", + "share.urlPanel.savedObjectLabel": "已保存对象", + "share.urlPanel.shortUrlHelpText": "建议共享缩短的快照 URL,以实现最大的兼容性。Internet Explorer 有 URL 长度限制,某些 wiki 和标记分析器无法很好地处理全长版本的快照 URL,但应能很好地处理短 URL。", + "share.urlPanel.shortUrlLabel": "短 URL", + "share.urlPanel.snapshotDescription": "快照 URL 将{objectType}的当前状态编入 URL 自身之中。通过此 URL,将无法看到对已保存的{objectType}的编辑。", + "share.urlPanel.snapshotLabel": "快照", + "share.urlPanel.unableCreateShortUrlErrorMessage": "无法创建短 URL。错误:{errorMessage}", "statusPage.loadStatus.serverIsDownErrorMessage": "无法请求服务器状态。也许您的服务器已关闭?", "statusPage.loadStatus.serverStatusCodeErrorMessage": "无法使用状态代码 {responseStatus} 请求服务器状态", "statusPage.metricsTiles.columns.heapTotalHeader": "堆总计",