Migrate share registry (#50137)

This commit is contained in:
Joe Reuter 2019-11-19 09:50:54 +01:00 committed by GitHub
parent c0d2b29e8b
commit 600862ef64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1584 additions and 1206 deletions

View file

@ -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",

View file

@ -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

View file

@ -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 |

View file

@ -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,
},

View file

@ -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,

View file

@ -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';

View file

@ -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,
},

View file

@ -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,

View file

@ -62,6 +62,9 @@ export const npSetup = {
}
},
},
share: {
register: () => {},
},
devTools: {
register: () => {},
},
@ -162,6 +165,9 @@ export const npStart = {
},
},
},
share: {
toggleShareContextMenu: () => {},
},
inspector: {
isAvailable: () => false,
open: () => ({

View file

@ -33,6 +33,7 @@ import {
FeatureCatalogueSetup,
FeatureCatalogueStart,
} from '../../../../plugins/feature_catalogue/public';
import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public';
export interface PluginsSetup {
data: ReturnType<DataPlugin['setup']>;
@ -41,6 +42,7 @@ export interface PluginsSetup {
feature_catalogue: FeatureCatalogueSetup;
inspector: InspectorSetup;
uiActions: IUiActionsSetup;
share: SharePluginSetup;
devTools: DevToolsSetup;
}
@ -52,6 +54,7 @@ export interface PluginsStart {
feature_catalogue: FeatureCatalogueStart;
inspector: InspectorStart;
uiActions: IUiActionsStart;
share: SharePluginStart;
devTools: DevToolsStart;
}

View file

@ -1 +1 @@
@import './components/index';
@import './share_context_menu';

View file

@ -0,0 +1,3 @@
.kbnShareContextMenu__finalPanel {
padding: $euiSize;
}

View file

@ -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`] = `
<EuiContextMenu
data-test-subj="shareContextMenu"
initialPanelId={4}
panels={
Array [
Object {
"content": <InjectIntl(UrlPanelContentUI)
getUnhashableStates={[Function]}
objectType="dashboard"
/>,
"id": 1,
"title": "Permalink",
},
Object {
"content": <div>
panel content
</div>,
"id": 2,
"title": "AAA panel",
},
Object {
"content": <div>
panel content
</div>,
"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`] = `
<EuiContextMenu
data-test-subj="shareContextMenu"
initialPanelId={1}
panels={
Array [
Object {
"content": <InjectIntl(UrlPanelContentUI)
getUnhashableStates={[Function]}
objectType="dashboard"
/>,
"id": 1,
"title": "Permalink",
},
]
}
/>
`;
exports[`should render context menu panel when there are more than one panel 1`] = `
<EuiContextMenu
data-test-subj="shareContextMenu"
initialPanelId={3}
panels={
Array [
Object {
"content": <InjectIntl(UrlPanelContentUI)
getUnhashableStates={[Function]}
objectType="dashboard"
/>,
"id": 1,
"title": "Permalink",
},
Object {
"content": <InjectIntl(UrlPanelContentUI)
getUnhashableStates={[Function]}
isEmbedded={true}
objectType="dashboard"
/>,
"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",
},
]
}
/>
`;

View file

@ -1,431 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText={
<FormattedMessage
defaultMessage="Can't share as saved object until the {objectType} has been saved."
id="common.ui.share.urlPanel.canNotShareAsSavedObjectHelpText"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="common.ui.share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Snapshot"
id="common.ui.share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="common.ui.share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": true,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Saved object"
id="common.ui.share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="common.ui.share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="common.ui.share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="common.ui.share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="kbnShareContextMenu__copyAnchor"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
`;
exports[`should enable saved object export option when objectId is provided 1`] = `
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="common.ui.share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Snapshot"
id="common.ui.share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="common.ui.share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": false,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Saved object"
id="common.ui.share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="common.ui.share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="common.ui.share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="common.ui.share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="kbnShareContextMenu__copyAnchor"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
`;
exports[`should hide short url section when allowShortUrl is false 1`] = `
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="common.ui.share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Snapshot"
id="common.ui.share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="common.ui.share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": false,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Saved object"
id="common.ui.share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="common.ui.share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="kbnShareContextMenu__copyAnchor"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
`;

View file

@ -1 +0,0 @@
@import './share_context_menu';

View file

@ -1,8 +0,0 @@
.kbnShareContextMenu__finalPanel {
padding: $euiSize;
}
.kbnShareContextMenu__copyAnchor,
.kbnShareContextMenu__copyButton {
width: 100%;
}

View file

@ -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(<ShareContextMenu.WrappedComponent
allowEmbed
objectType="dashboard"
getUnhashableStates={() => {}}
/>);
expect(component).toMatchSnapshot();
});
test('should only render permalink panel when there are no other panels', () => {
const component = shallowWithIntl(<ShareContextMenu.WrappedComponent
allowEmbed={false}
objectType="dashboard"
getUnhashableStates={() => {}}
/>);
expect(component).toMatchSnapshot();
});
describe('shareContextMenuExtensions', () => {
const shareContextMenuExtensions = [
{
getShareActions: () => {
return [
{
panel: {
title: 'AAA panel',
content: (<div>panel content</div>),
},
shareMenuItem: {
name: 'AAA panel',
sortOrder: 5,
}
}
];
}
},
{
getShareActions: () => {
return [
{
panel: {
title: 'ZZZ panel',
content: (<div>panel content</div>),
},
shareMenuItem: {
name: 'ZZZ panel',
sortOrder: 0,
}
}
];
}
}
];
test('should sort ascending on sort order first and then ascending on name', () => {
const component = shallowWithIntl(<ShareContextMenu.WrappedComponent
allowEmbed={false}
objectType="dashboard"
getUnhashableStates={() => {}}
shareContextMenuExtensions={shareContextMenuExtensions}
/>);
expect(component).toMatchSnapshot();
});
});

View file

@ -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}`);
});
});
});

View file

@ -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[];
}

View file

@ -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 = (
<I18nContext>
<EuiWrappingPopover
id="sharePopover"
button={anchorElement}
isOpen={true}
closePopover={onClose}
panelPaddingSize="none"
withTitle
anchorPosition="downLeft"
>
<ShareContextMenu
allowEmbed={allowEmbed}
allowShortUrl={allowShortUrl}
getUnhashableStates={getUnhashableStates}
objectId={objectId}
objectType={objectType}
shareContextMenuExtensions={shareContextMenuExtensions}
sharingData={sharingData}
isDirty={isDirty}
onClose={onClose}
/>
</EuiWrappingPopover>
</I18nContext>
);
ReactDOM.render(element, container);
}

View file

@ -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.

View file

@ -0,0 +1,6 @@
{
"id": "share",
"version": "kibana",
"server": false,
"ui": true
}

View file

@ -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`] = `
<I18nProvider>
<EuiContextMenu
data-test-subj="shareContextMenu"
initialPanelId={4}
panels={
Array [
Object {
"content": <UrlPanelContent
allowShortUrl={false}
basePath=""
objectType="dashboard"
post={[Function]}
/>,
"id": 1,
"title": "Permalink",
},
Object {
"content": <div>
panel content
</div>,
"id": 2,
"title": "AAA panel",
},
Object {
"content": <div>
panel content
</div>,
"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",
},
]
}
/>
</I18nProvider>
`;
exports[`should only render permalink panel when there are no other panels 1`] = `
<I18nProvider>
<EuiContextMenu
data-test-subj="shareContextMenu"
initialPanelId={1}
panels={
Array [
Object {
"content": <UrlPanelContent
allowShortUrl={false}
basePath=""
objectType="dashboard"
post={[Function]}
/>,
"id": 1,
"title": "Permalink",
},
]
}
/>
</I18nProvider>
`;
exports[`should render context menu panel when there are more than one panel 1`] = `
<I18nProvider>
<EuiContextMenu
data-test-subj="shareContextMenu"
initialPanelId={3}
panels={
Array [
Object {
"content": <UrlPanelContent
allowShortUrl={false}
basePath=""
objectType="dashboard"
post={[Function]}
/>,
"id": 1,
"title": "Permalink",
},
Object {
"content": <UrlPanelContent
allowShortUrl={false}
basePath=""
isEmbedded={true}
objectType="dashboard"
post={[Function]}
/>,
"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",
},
]
}
/>
</I18nProvider>
`;

View file

@ -0,0 +1,437 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<I18nProvider>
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText={
<FormattedMessage
defaultMessage="Can't share as saved object until the {objectType} has been saved."
id="share.urlPanel.canNotShareAsSavedObjectHelpText"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": true,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
</I18nProvider>
`;
exports[`should enable saved object export option when objectId is provided 1`] = `
<I18nProvider>
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": false,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
</I18nProvider>
`;
exports[`should hide short url section when allowShortUrl is false 1`] = `
<I18nProvider>
<EuiForm
className="kbnShareContextMenu__finalPanel"
data-test-subj="shareUrlForm"
>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Generate the link as"
id="share.urlPanel.generateLinkAsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiRadioGroup
idSelected="snapshot"
onChange={[Function]}
options={
Array [
Object {
"data-test-subj": "exportAsSnapshot",
"id": "snapshot",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Snapshot"
id="share.urlPanel.snapshotLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself. Edits to the saved {objectType} won't be visible via this URL."
id="share.urlPanel.snapshotDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
Object {
"data-test-subj": "exportAsSavedObject",
"disabled": false,
"id": "savedObject",
"label": <ForwardRef
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<FormattedMessage
defaultMessage="Saved object"
id="share.urlPanel.savedObjectLabel"
values={Object {}}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
id="share.urlPanel.savedObjectDescription"
values={
Object {
"objectType": "dashboard",
}
}
/>
}
position="bottom"
/>
</EuiFlexItem>
</ForwardRef>,
},
]
}
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiCopy
afterMessage="Copied"
anchorClassName="eui-displayBlock"
textToCopy="http://localhost/"
>
<Component />
</EuiCopy>
</EuiForm>
</I18nProvider>
`;

View file

@ -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(<ShareContextMenu {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should only render permalink panel when there are no other panels', () => {
const component = shallow(<ShareContextMenu {...defaultProps} allowEmbed={false} />);
expect(component).toMatchSnapshot();
});
describe('shareContextMenuExtensions', () => {
const shareContextMenuItems: ShareMenuItem[] = [
{
panel: {
id: '1',
title: 'AAA panel',
content: <div>panel content</div>,
},
shareMenuItem: {
name: 'AAA panel',
sortOrder: 5,
},
},
{
panel: {
id: '2',
title: 'ZZZ panel',
content: <div>panel content</div>,
},
shareMenuItem: {
name: 'ZZZ panel',
sortOrder: 0,
},
},
];
test('should sort ascending on sort order first and then ascending on name', () => {
const component = shallow(
<ShareContextMenu
{...defaultProps}
allowEmbed={false}
shareMenuItems={shareContextMenuItems}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -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<Props> {
export class ShareContextMenu extends Component<Props> {
public render() {
const { panels, initialPanelId } = this.getPanels();
return (
<EuiContextMenu
initialPanelId={initialPanelId}
panels={panels}
data-test-subj="shareContextMenu"
/>
<I18nProvider>
<EuiContextMenu
initialPanelId={initialPanelId}
panels={panels}
data-test-subj="shareContextMenu"
/>
</I18nProvider>
);
}
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<Props> {
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<Props> {
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<Props> {
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<Props> {
});
}
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<Props> {
return { panels, initialPanelId };
};
}
export const ShareContextMenu = injectI18n(ShareContextMenuUI);

View file

@ -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(<UrlPanelContent.WrappedComponent
allowShortUrl={true}
objectType="dashboard"
getUnhashableStates={() => {}}
/>);
const component = shallow(<UrlPanelContent {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should enable saved object export option when objectId is provided', () => {
const component = shallowWithIntl(<UrlPanelContent.WrappedComponent
allowShortUrl={true}
objectId="id1"
objectType="dashboard"
getUnhashableStates={() => {}}
/>);
const component = shallow(<UrlPanelContent {...defaultProps} objectId="id1" />);
expect(component).toMatchSnapshot();
});
test('should hide short url section when allowShortUrl is false', () => {
const component = shallowWithIntl(<UrlPanelContent.WrappedComponent
allowShortUrl={false}
objectId="id1"
objectType="dashboard"
getUnhashableStates={() => {}}
/>);
const component = shallow(
<UrlPanelContent {...defaultProps} allowShortUrl={false} objectId="id1" />
);
expect(component).toMatchSnapshot();
});

View file

@ -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<any>;
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<Props, State> {
export class UrlPanelContent extends Component<Props, State> {
private mounted?: boolean;
private shortUrlCache?: string;
@ -96,41 +97,41 @@ class UrlPanelContentUI extends Component<Props, State> {
public render() {
return (
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareUrlForm">
{this.renderExportAsRadioGroup()}
<I18nProvider>
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareUrlForm">
{this.renderExportAsRadioGroup()}
{this.renderShortUrlSwitch()}
{this.renderShortUrlSwitch()}
<EuiSpacer size="m" />
<EuiSpacer size="m" />
<EuiCopy
textToCopy={this.state.url || ''}
anchorClassName="kbnShareContextMenu__copyAnchor"
>
{(copy: () => void) => (
<EuiButton
fill
onClick={copy}
disabled={this.state.isCreatingShortUrl || this.state.url === ''}
data-share-url={this.state.url}
data-test-subj="copyShareUrlButton"
size="s"
>
{this.props.isEmbedded ? (
<FormattedMessage
id="common.ui.share.urlPanel.copyIframeCodeButtonLabel"
defaultMessage="Copy iFrame code"
/>
) : (
<FormattedMessage
id="common.ui.share.urlPanel.copyLinkButtonLabel"
defaultMessage="Copy link"
/>
)}
</EuiButton>
)}
</EuiCopy>
</EuiForm>
<EuiCopy textToCopy={this.state.url || ''} anchorClassName="eui-displayBlock">
{(copy: () => void) => (
<EuiButton
fill
fullWidth
onClick={copy}
disabled={this.state.isCreatingShortUrl || this.state.url === ''}
data-share-url={this.state.url}
data-test-subj="copyShareUrlButton"
size="s"
>
{this.props.isEmbedded ? (
<FormattedMessage
id="share.urlPanel.copyIframeCodeButtonLabel"
defaultMessage="Copy iFrame code"
/>
) : (
<FormattedMessage
id="share.urlPanel.copyLinkButtonLabel"
defaultMessage="Copy link"
/>
)}
</EuiButton>
)}
</EuiCopy>
</EuiForm>
</I18nProvider>
);
}
@ -155,11 +156,9 @@ class UrlPanelContentUI extends Component<Props, State> {
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<Props, State> {
};
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<Props, State> {
);
};
// TODO: switch evt type to ChangeEvent<HTMLInputElement> 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<Props, State> {
});
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<Props, State> {
{
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<Props, State> {
{
id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT,
label: this.renderWithIconTip(
<FormattedMessage id="share.urlPanel.snapshotLabel" defaultMessage="Snapshot" />,
<FormattedMessage
id="common.ui.share.urlPanel.snapshotLabel"
defaultMessage="Snapshot"
/>,
<FormattedMessage
id="common.ui.share.urlPanel.snapshotDescription"
id="share.urlPanel.snapshotDescription"
defaultMessage="Snapshot URLs encode the current state of the {objectType} in the URL itself.
Edits to the saved {objectType} won't be visible via this URL."
values={{ objectType: this.props.objectType }}
@ -305,12 +298,9 @@ class UrlPanelContentUI extends Component<Props, State> {
id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT,
disabled: this.isNotSaved(),
label: this.renderWithIconTip(
<FormattedMessage id="share.urlPanel.savedObjectLabel" defaultMessage="Saved object" />,
<FormattedMessage
id="common.ui.share.urlPanel.savedObjectLabel"
defaultMessage="Saved object"
/>,
<FormattedMessage
id="common.ui.share.urlPanel.savedObjectDescription"
id="share.urlPanel.savedObjectDescription"
defaultMessage="You can share this URL with people to let them load the most recent saved version of this {objectType}."
values={{ objectType: this.props.objectType }}
/>
@ -325,7 +315,7 @@ class UrlPanelContentUI extends Component<Props, State> {
<EuiFlexGroup gutterSize="none" responsive={false}>
<EuiFlexItem>{child}</EuiFlexItem>
<EuiFlexItem grow={false}>
<FixedEuiIconTip content={tipContent} position="bottom" />
<EuiIconTip content={tipContent} position="bottom" />
</EuiFlexItem>
</EuiFlexGroup>
);
@ -334,7 +324,7 @@ class UrlPanelContentUI extends Component<Props, State> {
private renderExportAsRadioGroup = () => {
const generateLinkAsHelp = this.isNotSaved() ? (
<FormattedMessage
id="common.ui.share.urlPanel.canNotShareAsSavedObjectHelpText"
id="share.urlPanel.canNotShareAsSavedObjectHelpText"
defaultMessage="Can't share as saved object until the {objectType} has been saved."
values={{ objectType: this.props.objectType }}
/>
@ -345,7 +335,7 @@ class UrlPanelContentUI extends Component<Props, State> {
<EuiFormRow
label={
<FormattedMessage
id="common.ui.share.urlPanel.generateLinkAsLabel"
id="share.urlPanel.generateLinkAsLabel"
defaultMessage="Generate the link as"
/>
}
@ -368,7 +358,7 @@ class UrlPanelContentUI extends Component<Props, State> {
return;
}
const shortUrlLabel = (
<FormattedMessage id="common.ui.share.urlPanel.shortUrlLabel" defaultMessage="Short URL" />
<FormattedMessage id="share.urlPanel.shortUrlLabel" defaultMessage="Short URL" />
);
const switchLabel = this.state.isCreatingShortUrl ? (
<span>
@ -387,7 +377,7 @@ class UrlPanelContentUI extends Component<Props, State> {
);
const tipContent = (
<FormattedMessage
id="common.ui.share.urlPanel.shortUrlHelpText"
id="share.urlPanel.shortUrlHelpText"
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility.
Internet Explorer has URL length restrictions,
and some wiki and markup parsers don't do well with the full-length version of the snapshot URL,
@ -402,5 +392,3 @@ class UrlPanelContentUI extends Component<Props, State> {
);
};
}
export const UrlPanelContent = injectI18n(UrlPanelContentUI);

View file

@ -17,11 +17,14 @@
* under the License.
*/
// @ts-ignore: implicit any for JS file
import { uiRegistry } from 'ui/registry/_registry';
import { ShareActionProvider } from './share_action';
export { SharePluginSetup, SharePluginStart } from './plugin';
export {
ShareContext,
ShareMenuProvider,
ShareMenuItem,
ShowShareMenuOptions,
ShareContextMenuPanelItem,
} from './types';
import { SharePlugin } from './plugin';
export const ShareContextMenuExtensionsRegistryProvider = uiRegistry<ShareActionProvider>({
name: 'shareContextMenuExtensions',
index: ['id'],
});
export const plugin = () => new SharePlugin();

View file

@ -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))"}',
});
});
});
});

View file

@ -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,

View file

@ -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),
}));

View file

@ -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();
});
});
});

View file

@ -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<SharePluginSetup, SharePluginStart> {
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;

View file

@ -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';

View file

@ -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<ShareMenuManagerStart> => {
const start = {
toggleShareContextMenu: jest.fn(),
};
return start;
};
const createMock = (): jest.Mocked<PublicMethodsOf<ShareMenuManager>> => {
const service = {
start: jest.fn(),
};
service.start.mockImplementation(createStartMock);
return service;
};
export const shareMenuManagerMock = {
createStart: createStartMock,
create: createMock,
};

View file

@ -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 = (
<I18nProvider>
<EuiWrappingPopover
id="sharePopover"
button={anchorElement}
isOpen={true}
closePopover={this.onClose}
panelPaddingSize="none"
withTitle
anchorPosition="downLeft"
>
<ShareContextMenu
allowEmbed={allowEmbed}
allowShortUrl={allowShortUrl}
objectId={objectId}
objectType={objectType}
shareMenuItems={menuItems}
sharingData={sharingData}
shareableUrl={shareableUrl}
onClose={this.onClose}
post={post}
basePath={basePath}
/>
</EuiWrappingPopover>
</I18nProvider>
);
ReactDOM.render(element, this.container);
}
}
export type ShareMenuManagerStart = ReturnType<ShareMenuManager['start']>;

View file

@ -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<ShareMenuRegistrySetup> => {
const setup = {
register: jest.fn(),
};
return setup;
};
const createStartMock = (): jest.Mocked<ShareMenuRegistryStart> => {
const start = {
getShareMenuItems: jest.fn((props: ShareContext) => [] as ShareMenuItem[]),
};
return start;
};
const createMock = (): jest.Mocked<PublicMethodsOf<ShareMenuRegistry>> => {
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,
};

View file

@ -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);
});
});
});
});

View file

@ -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<string, ShareMenuProvider>();
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<ShareMenuRegistry['setup']>;
export type ShareMenuRegistryStart = ReturnType<ShareMenuRegistry['start']>;

View file

@ -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<ShareContext, 'onClose'> {
anchorElement: HTMLElement;
allowEmbed: boolean;
allowShortUrl: boolean;
}

View file

@ -127,12 +127,9 @@ class ReportingPanelContentUi extends Component<Props, State> {
</EuiText>
<EuiSpacer size="s" />
<EuiCopy
textToCopy={this.state.absoluteUrl}
anchorClassName="kbnShareContextMenu__copyAnchor"
>
<EuiCopy textToCopy={this.state.absoluteUrl} anchorClassName="eui-displayBlock">
{copy => (
<EuiButton className="kbnShareContextMenu__copyButton" onClick={copy} size="s">
<EuiButton fullWidth onClick={copy} size="s">
<FormattedMessage
id="xpack.reporting.panelContent.copyUrlButtonLabel"
defaultMessage="Copy POST URL"
@ -147,8 +144,8 @@ class ReportingPanelContentUi extends Component<Props, State> {
private renderGenerateReportButton = (isDisabled: boolean) => {
return (
<EuiButton
className="kbnShareContextMenu__copyButton"
disabled={isDisabled}
fullWidth
fill
onClick={this.createReportingJob}
data-test-subj="generateReportButton"

View file

@ -8,18 +8,18 @@ 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 { ShareContext } from '../../../../../../src/plugins/share/public';
function reportingProvider() {
const getShareActions = ({
const getShareMenuItems = ({
objectType,
objectId,
sharingData,
isDirty,
onClose,
}: ShareActionProps) => {
}: 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: (
<ReportingPanelContent
@ -67,8 +69,8 @@ function reportingProvider() {
return {
id: 'csvReports',
getShareActions,
getShareMenuItems,
};
}
ShareContextMenuExtensionsRegistryProvider.register(reportingProvider);
npSetup.plugins.share.register(reportingProvider());

View file

@ -8,35 +8,34 @@ import { i18n } from '@kbn/i18n';
import moment from 'moment-timezone';
// @ts-ignore: implicit any for JS file
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 { ShareActionProps } from 'ui/share/share_action';
import { ShareContextMenuExtensionsRegistryProvider } from 'ui/share/share_action_registry';
import { unhashUrl } from 'ui/state_management/state_hashing';
import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content';
import { ShareContext } from '../../../../../../src/plugins/share/public';
function reportingProvider(dashboardConfig: any) {
const getShareActions = ({
async function reportingProvider() {
const injector = await chrome.dangerouslyGetActiveInjector();
const getShareMenuItems = ({
objectType,
objectId,
getUnhashableStates,
sharingData,
isDirty,
onClose,
}: ShareActionProps) => {
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<any>('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: (
<ScreenCapturePanelContent
@ -115,6 +114,7 @@ function reportingProvider(dashboardConfig: any) {
sortOrder: 10,
},
panel: {
id: 'reportingPngPanel',
title: panelTitle,
content: (
<ScreenCapturePanelContent
@ -135,8 +135,10 @@ function reportingProvider(dashboardConfig: any) {
return {
id: 'screenCaptureReports',
getShareActions,
getShareMenuItems,
};
}
ShareContextMenuExtensionsRegistryProvider.register(reportingProvider);
(async () => {
npSetup.plugins.share.register(await reportingProvider());
})();

View file

@ -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} で問題を報告してください。",
@ -2836,6 +2820,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": "ヒープ合計",
@ -12774,4 +12774,4 @@
"xpack.licensing.check.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。",
"xpack.licensing.check.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。"
}
}
}

View file

@ -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} 提交问题。",
@ -2837,6 +2821,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": "堆总计",
@ -12864,4 +12864,4 @@
"xpack.licensing.check.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。",
"xpack.licensing.check.errorUnsupportedMessage": "您的{licenseType}许可证不支持 {pluginName}。请升级您的许可证。"
}
}
}