Eui sharing top nav (#21997)
* just getting the popover to open and start laying out the context menu * pass getUnhashableStates to ShareMenu * generate original and snapshot ids * move state into ShareUrlContent * start working on form * use radio group * add input for creating short URL * display URL in alert until copy functionallity gets migrated to EUI * allowEmbed prop * replace share directive with showShareContextMenu * fix button styling * add jest test for share_context_menu * use EuiCopy to copy URL, add jest test for ShareUrlContent component * clean up * display short URL create error message in form instead of with toast * switch option order so disbaled option can not be first * fix discover share functional tests * add functions required by reporting * typescript * remove empty file * fix typescript compile error * move import so jest tests work * fix Failed prop type: The proptextToCopyis marked as required inEuiCopy, but its value isundefined * move shortUrl out of react state and into Component object * getUnhashableStates type from any[] to object[] * add comment about type change once EUI issue is solved * add functional test for saved object URL sharing * remove commit
This commit is contained in:
parent
11488d5055
commit
9d24c00a12
30 changed files with 1118 additions and 557 deletions
|
@ -43,6 +43,7 @@ import { showCloneModal } from './top_nav/show_clone_modal';
|
|||
import { showSaveModal } from './top_nav/show_save_modal';
|
||||
import { showAddPanel } from './top_nav/show_add_panel';
|
||||
import { showOptionsPopover } from './top_nav/show_options_popover';
|
||||
import { showShareContextMenu } from 'ui/share';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
||||
import * as filterActions from 'ui/doc_table/actions/filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
@ -50,6 +51,7 @@ import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_fa
|
|||
import { DashboardPanelActionsRegistryProvider } from 'ui/dashboard_panel_actions/dashboard_panel_actions_registry';
|
||||
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
|
||||
|
||||
import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider';
|
||||
|
||||
|
@ -83,6 +85,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
const docTitle = Private(DocTitleProvider);
|
||||
const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider);
|
||||
const panelActionsRegistry = Private(DashboardPanelActionsRegistryProvider);
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
|
||||
panelActionsStore.initializeFromRegistry(panelActionsRegistry);
|
||||
|
||||
|
@ -399,6 +402,16 @@ app.directive('dashboardApp', function ($injector) {
|
|||
},
|
||||
});
|
||||
};
|
||||
navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => {
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: true,
|
||||
getUnhashableStates,
|
||||
objectId: dash.id,
|
||||
objectType: 'dashboard',
|
||||
});
|
||||
};
|
||||
|
||||
updateViewMode(dashboardStateManager.getViewMode());
|
||||
|
||||
// update root source when filters update
|
||||
|
@ -438,11 +451,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
|
||||
kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM);
|
||||
}
|
||||
|
||||
// TODO remove opts once share has been converted to react
|
||||
$scope.opts = {
|
||||
dashboard: dash, // used in share.html
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
|
|||
]
|
||||
: [
|
||||
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
|
||||
getShareConfig(),
|
||||
getShareConfig(actions[TopNavIds.SHARE]),
|
||||
getCloneConfig(actions[TopNavIds.CLONE]),
|
||||
getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE])
|
||||
]
|
||||
|
@ -49,7 +49,7 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
|
|||
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
|
||||
getAddConfig(actions[TopNavIds.ADD]),
|
||||
getOptionsConfig(actions[TopNavIds.OPTIONS]),
|
||||
getShareConfig()];
|
||||
getShareConfig(actions[TopNavIds.SHARE])];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
@ -127,12 +127,12 @@ function getAddConfig(action) {
|
|||
/**
|
||||
* @returns {kbnTopNavConfig}
|
||||
*/
|
||||
function getShareConfig() {
|
||||
function getShareConfig(action) {
|
||||
return {
|
||||
key: TopNavIds.SHARE,
|
||||
description: 'Share Dashboard',
|
||||
testId: 'dashboardShareButton',
|
||||
template: require('plugins/kibana/dashboard/top_nav/share.html')
|
||||
testId: 'shareTopNavButton',
|
||||
run: action,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.dashOptionsPopover {
|
||||
height: 100%;
|
||||
|
||||
.euiPopover__anchor {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
<share
|
||||
object-type="dashboard"
|
||||
object-id="{{opts.dashboard.id}}">
|
||||
</share>
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './options_popover.less';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
|
@ -55,7 +54,7 @@ export function showOptionsPopover({
|
|||
document.body.appendChild(container);
|
||||
const element = (
|
||||
<EuiWrappingPopover
|
||||
className="dashOptionsPopover"
|
||||
className="navbar__popover"
|
||||
id="popover"
|
||||
button={anchorElement}
|
||||
isOpen={true}
|
||||
|
|
|
@ -31,7 +31,6 @@ import 'ui/filters/moment';
|
|||
import 'ui/index_patterns';
|
||||
import 'ui/state_management/app_state';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import 'ui/share';
|
||||
import 'ui/query_bar';
|
||||
import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
@ -54,6 +53,8 @@ import { recentlyAccessed } from 'ui/persisted_log';
|
|||
import { getDocLink } from 'ui/documentation_links';
|
||||
import '../components/fetch_error';
|
||||
import { getPainlessError } from './get_painless_error';
|
||||
import { showShareContextMenu } from 'ui/share';
|
||||
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
|
||||
|
||||
const app = uiModules.get('apps/discover', [
|
||||
'kibana/notify',
|
||||
|
@ -157,6 +158,7 @@ function discoverController(
|
|||
const notify = new Notifier({
|
||||
location: 'Discover'
|
||||
});
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
|
||||
$scope.getDocLink = getDocLink;
|
||||
$scope.intervalOptions = intervalOptions;
|
||||
|
@ -167,6 +169,10 @@ function discoverController(
|
|||
return interval.val !== 'custom';
|
||||
};
|
||||
|
||||
// the saved savedSearch
|
||||
const savedSearch = $route.current.locals.savedSearch;
|
||||
$scope.$on('$destroy', savedSearch.destroy);
|
||||
|
||||
$scope.topNavMenu = [{
|
||||
key: 'new',
|
||||
description: 'New Search',
|
||||
|
@ -185,14 +191,18 @@ function discoverController(
|
|||
}, {
|
||||
key: 'share',
|
||||
description: 'Share Search',
|
||||
template: require('plugins/kibana/discover/partials/share_search.html'),
|
||||
testId: 'discoverShareButton',
|
||||
testId: 'shareTopNavButton',
|
||||
run: (menuItem, navController, anchorElement) => {
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: false,
|
||||
getUnhashableStates,
|
||||
objectId: savedSearch.id,
|
||||
objectType: 'search',
|
||||
});
|
||||
}
|
||||
}];
|
||||
|
||||
// the saved savedSearch
|
||||
const savedSearch = $route.current.locals.savedSearch;
|
||||
$scope.$on('$destroy', savedSearch.destroy);
|
||||
|
||||
// the actual courier.SearchSource
|
||||
$scope.searchSource = savedSearch.searchSource;
|
||||
$scope.indexPattern = resolveIndexPatternLoading();
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<share
|
||||
object-type="search"
|
||||
object-id="{{opts.savedSearch.id}}"
|
||||
allow-embed="false">
|
||||
</share>
|
|
@ -23,7 +23,6 @@ import './visualization_editor';
|
|||
import 'ui/vis/editors/default/sidebar';
|
||||
import 'ui/visualize';
|
||||
import 'ui/collapsible_sidebar';
|
||||
import 'ui/share';
|
||||
import 'ui/query_bar';
|
||||
import chrome from 'ui/chrome';
|
||||
import angular from 'angular';
|
||||
|
@ -43,6 +42,8 @@ import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
|||
import { recentlyAccessed } from 'ui/persisted_log';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { getVisualizeLoader } from '../../../../../ui/public/visualize/loader';
|
||||
import { showShareContextMenu } from 'ui/share';
|
||||
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
|
||||
|
||||
uiRoutes
|
||||
.when(VisualizeConstants.CREATE_PATH, {
|
||||
|
@ -115,6 +116,7 @@ function VisEditor(
|
|||
) {
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
|
||||
const notify = new Notifier({
|
||||
location: 'Visualization Editor'
|
||||
|
@ -156,8 +158,16 @@ function VisEditor(
|
|||
}, {
|
||||
key: 'share',
|
||||
description: 'Share Visualization',
|
||||
template: require('plugins/kibana/visualize/editor/panels/share.html'),
|
||||
testId: 'visualizeShareButton',
|
||||
testId: 'shareTopNavButton',
|
||||
run: (menuItem, navController, anchorElement) => {
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: true,
|
||||
getUnhashableStates,
|
||||
objectId: savedVis.id,
|
||||
objectType: 'visualization',
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'inspect',
|
||||
description: 'Open Inspector for visualization',
|
||||
|
@ -251,7 +261,7 @@ function VisEditor(
|
|||
$scope.isAddToDashMode = () => addToDashMode;
|
||||
|
||||
$scope.timeRange = timefilter.getTime();
|
||||
$scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'isAddToDashMode');
|
||||
$scope.opts = _.pick($scope, 'doSave', 'savedVis', 'isAddToDashMode');
|
||||
|
||||
stateMonitor = stateMonitorFactory.create($state, stateDefaults);
|
||||
stateMonitor.ignoreProps([ 'vis.listeners' ]).onChange((status) => {
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<share
|
||||
object-type="visualization"
|
||||
object-id="{{opts.savedVis.id}}">
|
||||
</share>
|
|
@ -0,0 +1,65 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should only render permalink panel when there are no other panels 1`] = `
|
||||
<EuiContextMenu
|
||||
initialPanelId={1}
|
||||
panels={
|
||||
Array [
|
||||
Object {
|
||||
"content": <ShareUrlContent
|
||||
getUnhashableStates={[Function]}
|
||||
objectId={undefined}
|
||||
objectType="dashboard"
|
||||
/>,
|
||||
"id": 1,
|
||||
"title": "Permalink",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`should render context menu panel when there are more than one panel 1`] = `
|
||||
<EuiContextMenu
|
||||
initialPanelId={3}
|
||||
panels={
|
||||
Array [
|
||||
Object {
|
||||
"content": <ShareUrlContent
|
||||
getUnhashableStates={[Function]}
|
||||
objectId={undefined}
|
||||
objectType="dashboard"
|
||||
/>,
|
||||
"id": 1,
|
||||
"title": "Permalink",
|
||||
},
|
||||
Object {
|
||||
"content": <ShareUrlContent
|
||||
getUnhashableStates={[Function]}
|
||||
isEmbedded={true}
|
||||
objectId={undefined}
|
||||
objectType="dashboard"
|
||||
/>,
|
||||
"id": 2,
|
||||
"title": "Embed Code",
|
||||
},
|
||||
Object {
|
||||
"id": 3,
|
||||
"items": Array [
|
||||
Object {
|
||||
"icon": "console",
|
||||
"name": "Embed code",
|
||||
"panel": 2,
|
||||
},
|
||||
Object {
|
||||
"icon": "link",
|
||||
"name": "Permalinks",
|
||||
"panel": 1,
|
||||
},
|
||||
],
|
||||
"title": "Share this dashboard",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,266 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`render 1`] = `
|
||||
<EuiForm
|
||||
className="shareUrlContentForm"
|
||||
data-test-subj="shareUrlForm"
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
helpText="Can't share as saved object until the dashboard has been saved."
|
||||
label="Generate the link as"
|
||||
>
|
||||
<EuiRadioGroup
|
||||
idSelected="snapshot"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "exportAsSnapshot",
|
||||
"id": "snapshot",
|
||||
"label": <EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
Snapshot
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIconTip
|
||||
aria-label="Info"
|
||||
content="Snapshot URLs encode the current state of the dashboard in the URL itself.
|
||||
Edits to the saved dashboard won't be visible via this URL."
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "exportAsSavedObject",
|
||||
"disabled": true,
|
||||
"id": "savedObject",
|
||||
"label": <EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
Saved object
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIconTip
|
||||
aria-label="Info"
|
||||
content="You can share this URL with people to let them load the most recent saved version of this dashboard."
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={false}
|
||||
data-test-subj="useShortUrl"
|
||||
label="Short URL"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIconTip
|
||||
aria-label="Info"
|
||||
content="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."
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
textToCopy="about:blank"
|
||||
/>
|
||||
</EuiForm>
|
||||
`;
|
||||
|
||||
exports[`should enable saved object export option when objectId is provided 1`] = `
|
||||
<EuiForm
|
||||
className="shareUrlContentForm"
|
||||
data-test-subj="shareUrlForm"
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Generate the link as"
|
||||
>
|
||||
<EuiRadioGroup
|
||||
idSelected="snapshot"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "exportAsSnapshot",
|
||||
"id": "snapshot",
|
||||
"label": <EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
Snapshot
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIconTip
|
||||
aria-label="Info"
|
||||
content="Snapshot URLs encode the current state of the dashboard in the URL itself.
|
||||
Edits to the saved dashboard won't be visible via this URL."
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "exportAsSavedObject",
|
||||
"disabled": false,
|
||||
"id": "savedObject",
|
||||
"label": <EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
Saved object
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIconTip
|
||||
aria-label="Info"
|
||||
content="You can share this URL with people to let them load the most recent saved version of this dashboard."
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={false}
|
||||
data-test-subj="useShortUrl"
|
||||
label="Short URL"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIconTip
|
||||
aria-label="Info"
|
||||
content="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."
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
textToCopy="about:blank"
|
||||
/>
|
||||
</EuiForm>
|
||||
`;
|
45
src/ui/public/share/components/share_context_menu.test.js
Normal file
45
src/ui/public/share/components/share_context_menu.test.js
Normal 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.
|
||||
*/
|
||||
|
||||
jest.mock('../lib/url_shortener', () => ({}));
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
ShareContextMenu,
|
||||
} from './share_context_menu';
|
||||
|
||||
test('should render context menu panel when there are more than one panel', () => {
|
||||
const component = shallow(<ShareContextMenu
|
||||
allowEmbed
|
||||
objectType="dashboard"
|
||||
getUnhashableStates={() => {}}
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should only render permalink panel when there are no other panels', () => {
|
||||
const component = shallow(<ShareContextMenu
|
||||
allowEmbed={false}
|
||||
objectType="dashboard"
|
||||
getUnhashableStates={() => {}}
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
99
src/ui/public/share/components/share_context_menu.tsx
Normal file
99
src/ui/public/share/components/share_context_menu.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { EuiContextMenu } from '@elastic/eui';
|
||||
|
||||
import { ShareUrlContent } from './share_url_content';
|
||||
|
||||
interface Props {
|
||||
allowEmbed: boolean;
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
getUnhashableStates: () => object[];
|
||||
}
|
||||
|
||||
export class ShareContextMenu extends Component<Props> {
|
||||
public render() {
|
||||
const { panels, initialPanelId } = this.getPanels();
|
||||
return <EuiContextMenu initialPanelId={initialPanelId} panels={panels} />;
|
||||
}
|
||||
|
||||
private getPanels = () => {
|
||||
const panels = [];
|
||||
const menuItems = [];
|
||||
|
||||
const permalinkPanel = {
|
||||
id: panels.length + 1,
|
||||
title: 'Permalink',
|
||||
content: (
|
||||
<ShareUrlContent
|
||||
objectId={this.props.objectId}
|
||||
objectType={this.props.objectType}
|
||||
getUnhashableStates={this.props.getUnhashableStates}
|
||||
/>
|
||||
),
|
||||
};
|
||||
menuItems.push({
|
||||
name: 'Permalinks',
|
||||
icon: 'link',
|
||||
panel: permalinkPanel.id,
|
||||
});
|
||||
panels.push(permalinkPanel);
|
||||
|
||||
if (this.props.allowEmbed) {
|
||||
const embedPanel = {
|
||||
id: panels.length + 1,
|
||||
title: 'Embed Code',
|
||||
content: (
|
||||
<ShareUrlContent
|
||||
isEmbedded
|
||||
objectId={this.props.objectId}
|
||||
objectType={this.props.objectType}
|
||||
getUnhashableStates={this.props.getUnhashableStates}
|
||||
/>
|
||||
),
|
||||
};
|
||||
panels.push(embedPanel);
|
||||
menuItems.push({
|
||||
name: 'Embed code',
|
||||
icon: 'console',
|
||||
panel: embedPanel.id,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO add plugable panels here
|
||||
|
||||
if (menuItems.length > 1) {
|
||||
const topLevelMenuPanel = {
|
||||
id: panels.length + 1,
|
||||
title: `Share this ${this.props.objectType}`,
|
||||
items: menuItems.sort((a, b) => {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
}),
|
||||
};
|
||||
panels.push(topLevelMenuPanel);
|
||||
}
|
||||
|
||||
const lastPanelIndex = panels.length - 1;
|
||||
const initialPanelId = panels[lastPanelIndex].id;
|
||||
return { panels, initialPanelId };
|
||||
};
|
||||
}
|
3
src/ui/public/share/components/share_url_content.less
Normal file
3
src/ui/public/share/components/share_url_content.less
Normal file
|
@ -0,0 +1,3 @@
|
|||
.shareUrlContentForm{
|
||||
padding: 16px;
|
||||
}
|
44
src/ui/public/share/components/share_url_content.test.js
Normal file
44
src/ui/public/share/components/share_url_content.test.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('../lib/url_shortener', () => ({}));
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
ShareUrlContent,
|
||||
} from './share_url_content';
|
||||
|
||||
test('render', () => {
|
||||
const component = shallow(<ShareUrlContent
|
||||
objectType="dashboard"
|
||||
getUnhashableStates={() => {}}
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should enable saved object export option when objectId is provided', () => {
|
||||
const component = shallow(<ShareUrlContent
|
||||
objectId="id1"
|
||||
objectType="dashboard"
|
||||
getUnhashableStates={() => {}}
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
347
src/ui/public/share/components/share_url_content.tsx
Normal file
347
src/ui/public/share/components/share_url_content.tsx
Normal file
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO: Remove once typescript definitions are in EUI
|
||||
declare module '@elastic/eui' {
|
||||
export const EuiCopy: React.SFC<any>;
|
||||
export const EuiForm: React.SFC<any>;
|
||||
}
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import './share_url_content.less';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiLoadingSpinner,
|
||||
EuiRadioGroup,
|
||||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { format as formatUrl, parse as parseUrl } from 'url';
|
||||
|
||||
import { unhashUrl } from '../../state_management/state_hashing';
|
||||
import { shortenUrl } from '../lib/url_shortener';
|
||||
|
||||
// TODO: Remove once EuiIconTip supports "content" prop
|
||||
const FixedEuiIconTip = EuiIconTip as React.SFC<any>;
|
||||
|
||||
interface Props {
|
||||
isEmbedded?: boolean;
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
getUnhashableStates: () => object[];
|
||||
}
|
||||
|
||||
enum ExportUrlAsType {
|
||||
EXPORT_URL_AS_SAVED_OBJECT = 'savedObject',
|
||||
EXPORT_URL_AS_SNAPSHOT = 'snapshot',
|
||||
}
|
||||
|
||||
interface State {
|
||||
exportUrlAs: ExportUrlAsType;
|
||||
useShortUrl: boolean;
|
||||
isCreatingShortUrl: boolean;
|
||||
url?: string;
|
||||
shortUrlErrorMsg?: string;
|
||||
}
|
||||
|
||||
export class ShareUrlContent extends Component<Props, State> {
|
||||
private mounted?: boolean;
|
||||
private shortUrlCache?: string;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.shortUrlCache = undefined;
|
||||
|
||||
this.state = {
|
||||
exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT,
|
||||
useShortUrl: false,
|
||||
isCreatingShortUrl: false,
|
||||
url: '',
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.resetUrl);
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.setUrl();
|
||||
|
||||
window.addEventListener('hashchange', this.resetUrl, false);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EuiForm className="shareUrlContentForm" data-test-subj="shareUrlForm">
|
||||
{this.renderExportAsRadioGroup()}
|
||||
|
||||
{this.renderShortUrlSwitch()}
|
||||
|
||||
<EuiCopy textToCopy={this.state.url}>
|
||||
{(copy: () => void) => (
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={copy}
|
||||
disabled={this.state.isCreatingShortUrl || this.state.url === ''}
|
||||
data-share-url={this.state.url}
|
||||
data-test-subj="copyShareUrlButton"
|
||||
>
|
||||
Copy {this.props.isEmbedded ? 'iFrame code' : 'link'}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
private isNotSaved = () => {
|
||||
return this.props.objectId === undefined || this.props.objectId === '';
|
||||
};
|
||||
|
||||
private resetUrl = () => {
|
||||
if (this.mounted) {
|
||||
this.shortUrlCache = undefined;
|
||||
this.setState(
|
||||
{
|
||||
useShortUrl: false,
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private getSavedObjectUrl = () => {
|
||||
if (this.isNotSaved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = window.location.href;
|
||||
// Replace hashes with original RISON values.
|
||||
const unhashedUrl = unhashUrl(url, this.props.getUnhashableStates());
|
||||
|
||||
const parsedUrl = parseUrl(unhashedUrl);
|
||||
if (!parsedUrl || !parsedUrl.hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the application route, after the hash, and remove the #.
|
||||
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
|
||||
|
||||
return formatUrl({
|
||||
protocol: parsedUrl.protocol,
|
||||
auth: parsedUrl.auth,
|
||||
host: parsedUrl.host,
|
||||
pathname: parsedUrl.pathname,
|
||||
hash: formatUrl({
|
||||
pathname: parsedAppUrl.pathname,
|
||||
query: {
|
||||
// Add global state to the URL so that the iframe doesn't just show the time range
|
||||
// default.
|
||||
_g: parsedAppUrl.query._g,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
private getSnapshotUrl = () => {
|
||||
const url = window.location.href;
|
||||
// Replace hashes with original RISON values.
|
||||
return unhashUrl(url, this.props.getUnhashableStates());
|
||||
};
|
||||
|
||||
private makeUrlEmbeddable = (url: string) => {
|
||||
const embedQueryParam = '?embed=true';
|
||||
const urlHasQueryString = url.indexOf('?') !== -1;
|
||||
if (urlHasQueryString) {
|
||||
return url.replace('?', `${embedQueryParam}&`);
|
||||
}
|
||||
return `${url}${embedQueryParam}`;
|
||||
};
|
||||
|
||||
private makeIframeTag = (url?: string) => {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const embeddableUrl = this.makeUrlEmbeddable(url);
|
||||
return `<iframe src="${embeddableUrl}" height="600" width="800"></iframe>`;
|
||||
};
|
||||
|
||||
private setUrl = () => {
|
||||
let url;
|
||||
if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) {
|
||||
url = this.getSavedObjectUrl();
|
||||
} else if (this.state.useShortUrl) {
|
||||
url = this.shortUrlCache;
|
||||
} else {
|
||||
url = this.getSnapshotUrl();
|
||||
}
|
||||
|
||||
if (this.props.isEmbedded) {
|
||||
url = this.makeIframeTag(url);
|
||||
}
|
||||
|
||||
this.setState({ url });
|
||||
};
|
||||
|
||||
private handleExportUrlAs = (optionId: string) => {
|
||||
this.setState(
|
||||
{
|
||||
exportUrlAs: optionId as ExportUrlAsType,
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: switch evt type to ChangeEvent<HTMLInputElement> once https://github.com/elastic/eui/issues/1134 is resolved
|
||||
private handleShortUrlChange = async (evt: any) => {
|
||||
const isChecked = evt.target.checked;
|
||||
|
||||
if (!isChecked || this.shortUrlCache !== undefined) {
|
||||
this.setState({ useShortUrl: isChecked }, this.setUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// "Use short URL" is checked but shortUrl has not been generated yet so one needs to be created.
|
||||
this.setState({
|
||||
isCreatingShortUrl: true,
|
||||
shortUrlErrorMsg: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const shortUrl = await shortenUrl(this.getSnapshotUrl());
|
||||
if (this.mounted) {
|
||||
this.shortUrlCache = shortUrl;
|
||||
this.setState(
|
||||
{
|
||||
isCreatingShortUrl: false,
|
||||
useShortUrl: isChecked,
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
}
|
||||
} catch (fetchError) {
|
||||
if (this.mounted) {
|
||||
this.shortUrlCache = undefined;
|
||||
this.setState(
|
||||
{
|
||||
useShortUrl: false,
|
||||
isCreatingShortUrl: false,
|
||||
shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}`,
|
||||
},
|
||||
this.setUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private renderExportUrlAsOptions = () => {
|
||||
return [
|
||||
{
|
||||
id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT,
|
||||
label: this.renderWithIconTip(
|
||||
'Snapshot',
|
||||
`Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself.
|
||||
Edits to the saved ${this.props.objectType} won't be visible via this URL.`
|
||||
),
|
||||
['data-test-subj']: 'exportAsSnapshot',
|
||||
},
|
||||
{
|
||||
id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT,
|
||||
disabled: this.isNotSaved(),
|
||||
label: this.renderWithIconTip(
|
||||
'Saved object',
|
||||
`You can share this URL with people to let them load the most recent saved version of this ${
|
||||
this.props.objectType
|
||||
}.`
|
||||
),
|
||||
['data-test-subj']: 'exportAsSavedObject',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
private renderWithIconTip = (child: React.ReactNode, tipContent: React.ReactNode) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>{child}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FixedEuiIconTip content={tipContent} position="bottom" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
private renderExportAsRadioGroup = () => {
|
||||
const generateLinkAsHelp = this.isNotSaved()
|
||||
? `Can't share as saved object until the ${this.props.objectType} has been saved.`
|
||||
: undefined;
|
||||
return (
|
||||
<EuiFormRow label="Generate the link as" helpText={generateLinkAsHelp}>
|
||||
<EuiRadioGroup
|
||||
options={this.renderExportUrlAsOptions()}
|
||||
idSelected={this.state.exportUrlAs}
|
||||
onChange={this.handleExportUrlAs}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
private renderShortUrlSwitch = () => {
|
||||
if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const switchLabel = this.state.isCreatingShortUrl ? (
|
||||
<span>
|
||||
<EuiLoadingSpinner size="s" /> Short URL
|
||||
</span>
|
||||
) : (
|
||||
'Short URL'
|
||||
);
|
||||
const switchComponent = (
|
||||
<EuiSwitch
|
||||
label={switchLabel}
|
||||
checked={this.state.useShortUrl}
|
||||
onChange={this.handleShortUrlChange}
|
||||
data-test-subj="useShortUrl"
|
||||
/>
|
||||
);
|
||||
const tipContent = `We recommend sharing shortened snapshot URLs for maximum compatibility.
|
||||
Internet Explorer has URL length restrictions,
|
||||
and some wiki and markup parsers don't do well with the full-length version of the snapshot URL,
|
||||
but the short URL should work great.`;
|
||||
|
||||
return (
|
||||
<EuiFormRow helpText={this.state.shortUrlErrorMsg}>
|
||||
{this.renderWithIconTip(switchComponent, tipContent)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
parse as parseUrl,
|
||||
format as formatUrl,
|
||||
} from 'url';
|
||||
|
||||
import {
|
||||
getUnhashableStatesProvider,
|
||||
unhashUrl,
|
||||
} from '../../state_management/state_hashing';
|
||||
import { toastNotifications } from '../../notify';
|
||||
|
||||
import { shortenUrl } from '../lib/url_shortener';
|
||||
|
||||
import { uiModules } from '../../modules';
|
||||
import shareTemplate from '../views/share.html';
|
||||
const app = uiModules.get('kibana');
|
||||
|
||||
app.directive('share', function (Private) {
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
objectType: '@',
|
||||
objectId: '@',
|
||||
allowEmbed: '@',
|
||||
},
|
||||
template: shareTemplate,
|
||||
controllerAs: 'share',
|
||||
controller: function ($scope, $document, $location) {
|
||||
if ($scope.allowEmbed !== 'false' && $scope.allowEmbed !== undefined) {
|
||||
throw new Error('allowEmbed must be "false" or undefined');
|
||||
}
|
||||
|
||||
// Default to allowing an embedded IFRAME, unless it's explicitly set to false.
|
||||
this.allowEmbed = $scope.allowEmbed === 'false' ? false : true;
|
||||
this.objectType = $scope.objectType;
|
||||
|
||||
function getOriginalUrl() {
|
||||
// If there is no objectId, then it isn't saved, so it has no original URL.
|
||||
if ($scope.objectId === undefined || $scope.objectId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = $location.absUrl();
|
||||
// Replace hashes with original RISON values.
|
||||
const unhashedUrl = unhashUrl(url, getUnhashableStates());
|
||||
|
||||
const parsedUrl = parseUrl(unhashedUrl);
|
||||
// Get the Angular route, after the hash, and remove the #.
|
||||
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
|
||||
|
||||
return formatUrl({
|
||||
protocol: parsedUrl.protocol,
|
||||
auth: parsedUrl.auth,
|
||||
host: parsedUrl.host,
|
||||
pathname: parsedUrl.pathname,
|
||||
hash: formatUrl({
|
||||
pathname: parsedAppUrl.pathname,
|
||||
query: {
|
||||
// Add global state to the URL so that the iframe doesn't just show the time range
|
||||
// default.
|
||||
_g: parsedAppUrl.query._g,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function getSnapshotUrl() {
|
||||
const url = $location.absUrl();
|
||||
// Replace hashes with original RISON values.
|
||||
return unhashUrl(url, getUnhashableStates());
|
||||
}
|
||||
|
||||
this.makeUrlEmbeddable = url => {
|
||||
const embedQueryParam = '?embed=true';
|
||||
const urlHasQueryString = url.indexOf('?') !== -1;
|
||||
if (urlHasQueryString) {
|
||||
return url.replace('?', `${embedQueryParam}&`);
|
||||
}
|
||||
return `${url}${embedQueryParam}`;
|
||||
};
|
||||
|
||||
this.makeIframeTag = url => {
|
||||
if (!url) return;
|
||||
|
||||
const embeddableUrl = this.makeUrlEmbeddable(url);
|
||||
return `<iframe src="${embeddableUrl}" height="600" width="800"></iframe>`;
|
||||
};
|
||||
|
||||
this.urls = {
|
||||
original: undefined,
|
||||
snapshot: undefined,
|
||||
shortSnapshot: undefined,
|
||||
shortSnapshotIframe: undefined,
|
||||
};
|
||||
|
||||
this.urlFlags = {
|
||||
shortSnapshot: false,
|
||||
shortSnapshotIframe: false,
|
||||
};
|
||||
|
||||
const updateUrls = () => {
|
||||
this.urls = {
|
||||
original: getOriginalUrl(),
|
||||
snapshot: getSnapshotUrl(),
|
||||
shortSnapshot: undefined,
|
||||
shortSnapshotIframe: undefined,
|
||||
};
|
||||
|
||||
// Whenever the URL changes, reset the Short URLs to regular URLs.
|
||||
this.urlFlags = {
|
||||
shortSnapshot: false,
|
||||
shortSnapshotIframe: false,
|
||||
};
|
||||
};
|
||||
|
||||
// When the URL changes, update the links in the UI.
|
||||
$scope.$watch(() => $location.absUrl(), () => {
|
||||
updateUrls();
|
||||
});
|
||||
|
||||
this.toggleShortSnapshotUrl = () => {
|
||||
this.urlFlags.shortSnapshot = !this.urlFlags.shortSnapshot;
|
||||
|
||||
if (this.urlFlags.shortSnapshot) {
|
||||
shortenUrl(this.urls.snapshot)
|
||||
.then(shortUrl => {
|
||||
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
|
||||
$scope.$apply(() => {
|
||||
this.urls.shortSnapshot = shortUrl;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.toggleShortSnapshotIframeUrl = () => {
|
||||
this.urlFlags.shortSnapshotIframe = !this.urlFlags.shortSnapshotIframe;
|
||||
|
||||
if (this.urlFlags.shortSnapshotIframe) {
|
||||
const snapshotIframe = this.makeUrlEmbeddable(this.urls.snapshot);
|
||||
shortenUrl(snapshotIframe)
|
||||
.then(shortUrl => {
|
||||
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
|
||||
$scope.$apply(() => {
|
||||
this.urls.shortSnapshotIframe = shortUrl;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.copyToClipboard = selector => {
|
||||
// Select the text to be copied. If the copy fails, the user can easily copy it manually.
|
||||
const copyTextarea = $document.find(selector)[0];
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
const isCopied = document.execCommand('copy');
|
||||
if (isCopied) {
|
||||
toastNotifications.add({
|
||||
title: 'URL was copied to the clipboard',
|
||||
'data-test-subj': 'shareCopyToClipboardSuccess',
|
||||
});
|
||||
} else {
|
||||
toastNotifications.add({
|
||||
title: 'URL selected. Press Ctrl+C to copy.',
|
||||
'data-test-subj': 'shareCopyToClipboardSuccess',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
toastNotifications.add({
|
||||
title: 'URL selected. Press Ctrl+C to copy.',
|
||||
'data-test-subj': 'shareCopyToClipboardSuccess',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './directives/share';
|
||||
export { showShareContextMenu } from './show_share_context_menu';
|
||||
|
|
|
@ -20,13 +20,6 @@ jest.mock('ui/kfetch', () => ({}));
|
|||
|
||||
jest.mock('../../chrome', () => ({}));
|
||||
|
||||
jest.mock('ui/notify',
|
||||
() => ({
|
||||
toastNotifications: {
|
||||
addDanger: () => {},
|
||||
}
|
||||
}), { virtual: true });
|
||||
|
||||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { shortenUrl } from './url_shortener';
|
||||
|
|
|
@ -17,32 +17,27 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from '../../chrome';
|
||||
import url from 'url';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import url from 'url';
|
||||
import chrome from '../../chrome';
|
||||
|
||||
export async function shortenUrl(absoluteUrl) {
|
||||
export async function shortenUrl(absoluteUrl: string) {
|
||||
const basePath = chrome.getBasePath();
|
||||
|
||||
const parsedUrl = url.parse(absoluteUrl);
|
||||
if (!parsedUrl || !parsedUrl.path) {
|
||||
return;
|
||||
}
|
||||
const path = parsedUrl.path.replace(basePath, '');
|
||||
const hash = parsedUrl.hash ? parsedUrl.hash : '';
|
||||
const relativeUrl = path + hash;
|
||||
|
||||
const body = JSON.stringify({ url: relativeUrl });
|
||||
|
||||
try {
|
||||
const resp = await kfetch({ method: 'POST', 'pathname': '/api/shorten_url', body });
|
||||
return url.format({
|
||||
protocol: parsedUrl.protocol,
|
||||
host: parsedUrl.host,
|
||||
pathname: `${basePath}/goto/${resp.urlId}`
|
||||
});
|
||||
} catch (fetchError) {
|
||||
toastNotifications.addDanger({
|
||||
title: `Unable to create short URL. Error: ${fetchError.message}`,
|
||||
'data-test-subj': 'shortenUrlFailure',
|
||||
});
|
||||
}
|
||||
const resp = await kfetch({ method: 'POST', pathname: '/api/shorten_url', body });
|
||||
return url.format({
|
||||
protocol: parsedUrl.protocol,
|
||||
host: parsedUrl.host,
|
||||
pathname: `${basePath}/goto/${resp.urlId}`,
|
||||
});
|
||||
}
|
83
src/ui/public/share/show_share_context_menu.tsx
Normal file
83
src/ui/public/share/show_share_context_menu.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO: Remove once typescript definitions are in EUI
|
||||
declare module '@elastic/eui' {
|
||||
export const EuiWrappingPopover: React.SFC<any>;
|
||||
}
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { ShareContextMenu } from './components/share_context_menu';
|
||||
|
||||
import { EuiWrappingPopover } from '@elastic/eui';
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
const onClose = () => {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
isOpen = false;
|
||||
};
|
||||
|
||||
interface ShowProps {
|
||||
anchorElement: any;
|
||||
allowEmbed: boolean;
|
||||
getUnhashableStates: () => object[];
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
}
|
||||
|
||||
export function showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed,
|
||||
getUnhashableStates,
|
||||
objectId,
|
||||
objectType,
|
||||
}: ShowProps) {
|
||||
if (isOpen) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen = true;
|
||||
|
||||
document.body.appendChild(container);
|
||||
const element = (
|
||||
<EuiWrappingPopover
|
||||
className="navbar__popover"
|
||||
id="sharePopover"
|
||||
button={anchorElement}
|
||||
isOpen={true}
|
||||
closePopover={onClose}
|
||||
panelPaddingSize="none"
|
||||
withTitle
|
||||
>
|
||||
<ShareContextMenu
|
||||
allowEmbed={allowEmbed}
|
||||
getUnhashableStates={getUnhashableStates}
|
||||
objectId={objectId}
|
||||
objectType={objectType}
|
||||
/>
|
||||
</EuiWrappingPopover>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
<div class="kuiLocalDropdownPanels">
|
||||
<!-- Left panel -->
|
||||
<div class="kuiLocalDropdownPanel kuiLocalDropdownPanel--left">
|
||||
<!-- Title -->
|
||||
<h2
|
||||
data-test-subj="shareUiTitle"
|
||||
class="kuiLocalDropdownTitle"
|
||||
>
|
||||
Share saved {{share.objectType}}
|
||||
</h2>
|
||||
|
||||
<!-- Help text -->
|
||||
<div ng-if="share.urls.original" class="kuiLocalDropdownHelpText">
|
||||
You can share this URL with people to let them load the most recent saved version of this {{share.objectType}}.
|
||||
</div>
|
||||
|
||||
<div ng-if="!share.urls.original" class="kuiLocalDropdownWarning">
|
||||
Please save this {{share.objectType}} to enable this sharing option.
|
||||
</div>
|
||||
|
||||
<div ng-if="share.urls.original">
|
||||
<!-- iframe -->
|
||||
<div class="kuiLocalDropdownSection" ng-if="share.allowEmbed">
|
||||
<!-- Header -->
|
||||
<div class="kuiLocalDropdownHeader">
|
||||
<label
|
||||
id="originalIframeUrlLabel"
|
||||
class="kuiLocalDropdownHeader__label"
|
||||
for="originalIframeUrl"
|
||||
>
|
||||
Embedded iframe
|
||||
</label>
|
||||
<div class="kuiLocalDropdownHeader__actions">
|
||||
<a
|
||||
aria-describedby="originalIframeUrlLabel"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-click="share.copyToClipboard('#originalIframeUrl')"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Copy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
id="originalIframeUrl"
|
||||
aria-describedby="originalIframeUrlHelpText"
|
||||
class="kuiLocalDropdownInput"
|
||||
type="text"
|
||||
readonly
|
||||
value="{{share.makeIframeTag(share.urls.original)}}"
|
||||
/>
|
||||
|
||||
<!-- Notes -->
|
||||
<div
|
||||
id="originalIframeUrlHelpText"
|
||||
class="kuiLocalDropdownFormNote"
|
||||
>
|
||||
Add to your HTML source. Note that all clients must be able to access Kibana.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link -->
|
||||
<div class="kuiLocalDropdownSection">
|
||||
<!-- Header -->
|
||||
<div class="kuiLocalDropdownHeader">
|
||||
<label
|
||||
id="originalUrlLabel"
|
||||
class="kuiLocalDropdownHeader__label"
|
||||
for="originalUrl"
|
||||
>
|
||||
Link
|
||||
</label>
|
||||
<div class="kuiLocalDropdownHeader__actions">
|
||||
<a
|
||||
aria-describedby="originalUrlLabel"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-click="share.copyToClipboard('#originalUrl')"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Copy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
id="originalUrl"
|
||||
class="kuiLocalDropdownInput"
|
||||
type="text"
|
||||
readonly
|
||||
value="{{share.urls.original}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel -->
|
||||
<div class="kuiLocalDropdownPanel kuiLocalDropdownPanel--right">
|
||||
<!-- Title -->
|
||||
<h2 class="kuiLocalDropdownTitle">
|
||||
Share Snapshot
|
||||
</h2>
|
||||
|
||||
<!-- Help text -->
|
||||
<div class="kuiLocalDropdownHelpText">
|
||||
Snapshot URLs encode the current state of the {{share.objectType}} in the URL itself. Edits to the saved {{share.objectType}} won't be visible via this URL.
|
||||
</div>
|
||||
|
||||
<!-- iframe -->
|
||||
<div class="kuiLocalDropdownSection" ng-if="share.allowEmbed">
|
||||
<!-- Header -->
|
||||
<div class="kuiLocalDropdownHeader">
|
||||
<label
|
||||
id="snapshotIframeUrlLabel"
|
||||
class="kuiLocalDropdownHeader__label"
|
||||
for="snapshotIframeUrl"
|
||||
>
|
||||
Embedded iframe
|
||||
</label>
|
||||
<div class="kuiLocalDropdownHeader__actions">
|
||||
<a
|
||||
aria-describedby="snapshotIframeUrlLabel"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-if="!share.urlFlags.shortSnapshotIframe"
|
||||
ng-click="share.toggleShortSnapshotIframeUrl()"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Short URL
|
||||
</a>
|
||||
|
||||
<a
|
||||
aria-describedby="snapshotIframeUrlLabel"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-if="share.urlFlags.shortSnapshotIframe"
|
||||
ng-click="share.toggleShortSnapshotIframeUrl()"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Long URL
|
||||
</a>
|
||||
|
||||
<a
|
||||
aria-describedby="snapshotIframeUrlLabel"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-click="share.copyToClipboard('#snapshotIframeUrl')"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Copy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
id="snapshotIframeUrl"
|
||||
aria-describedby="snapshotIframeUrlHelpText"
|
||||
class="kuiLocalDropdownInput"
|
||||
type="text"
|
||||
readonly
|
||||
value="{{share.urlFlags.shortSnapshotIframe ? share.makeIframeTag(share.urls.shortSnapshotIframe) : share.makeIframeTag(share.urls.snapshot)}}"
|
||||
/>
|
||||
|
||||
<!-- Notes -->
|
||||
<div
|
||||
id="snapshotIframeUrlHelpText"
|
||||
class="kuiLocalDropdownFormNote"
|
||||
>
|
||||
Add to your HTML source. Note that all clients must be able to access Kibana.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link -->
|
||||
<div class="kuiLocalDropdownSection">
|
||||
<!-- Header -->
|
||||
<div class="kuiLocalDropdownHeader">
|
||||
<label
|
||||
id="snapshotUrlLabel"
|
||||
class="kuiLocalDropdownHeader__label"
|
||||
for="snapshotUrl"
|
||||
>
|
||||
Link
|
||||
</label>
|
||||
<div class="kuiLocalDropdownHeader__actions">
|
||||
<a
|
||||
aria-describedby="snapshotUrlLabel"
|
||||
data-test-subj="sharedSnapshotShortUrlButton"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-if="!share.urlFlags.shortSnapshot"
|
||||
ng-click="share.toggleShortSnapshotUrl()"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Short URL
|
||||
</a>
|
||||
|
||||
<a
|
||||
aria-describedby="snapshotUrlLabel"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-if="share.urlFlags.shortSnapshot"
|
||||
ng-click="share.toggleShortSnapshotUrl()"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Long URL
|
||||
</a>
|
||||
|
||||
<a
|
||||
aria-describedby="snapshotUrlLabel"
|
||||
data-test-subj="sharedSnapshotCopyButton"
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-click="share.copyToClipboard('#snapshotUrl')"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Copy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
data-test-subj="sharedSnapshotUrl"
|
||||
id="snapshotUrl"
|
||||
aria-describedby="snapshotUrlHelpText"
|
||||
class="kuiLocalDropdownInput"
|
||||
type="text"
|
||||
readonly
|
||||
value="{{share.urlFlags.shortSnapshot ? share.urls.shortSnapshot : share.urls.snapshot}}"
|
||||
/>
|
||||
|
||||
<!-- Notes -->
|
||||
<div
|
||||
id="snapshotUrlHelpText"
|
||||
class="kuiLocalDropdownFormNote"
|
||||
>
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
20
src/ui/public/state_management/state_hashing/index.d.ts
vendored
Normal file
20
src/ui/public/state_management/state_hashing/index.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function unhashUrl(url: string, kbnStates: any[]): any;
|
|
@ -111,3 +111,11 @@ navbar {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar__popover {
|
||||
height: 100%;
|
||||
|
||||
.euiPopover__anchor {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
const log = getService('log');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header']);
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'share']);
|
||||
|
||||
describe('shared links', function describeIndexTests() {
|
||||
let baseUrl;
|
||||
|
@ -59,20 +59,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
//After hiding the time picker, we need to wait for
|
||||
//the refresh button to hide before clicking the share button
|
||||
return PageObjects.common.sleep(1000);
|
||||
await PageObjects.common.sleep(1000);
|
||||
|
||||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
describe('shared link', function () {
|
||||
it('should show "Share a link" caption', async function () {
|
||||
const expectedCaption = 'Share saved';
|
||||
|
||||
await PageObjects.discover.clickShare();
|
||||
const actualCaption = await PageObjects.discover.getShareCaption();
|
||||
|
||||
expect(actualCaption).to.contain(expectedCaption);
|
||||
});
|
||||
|
||||
it('should show the correct formatted URL', async function () {
|
||||
describe('permalink', function () {
|
||||
it('should allow for copying the snapshot URL', async function () {
|
||||
const expectedUrl =
|
||||
baseUrl +
|
||||
'/app/kibana?_t=1453775307251#' +
|
||||
|
@ -81,32 +74,35 @@ export default function ({ getService, getPageObjects }) {
|
|||
'-23T18:31:44.000Z\'))&_a=(columns:!(_source),index:\'logstash-' +
|
||||
'*\',interval:auto,query:(language:lucene,query:\'\')' +
|
||||
',sort:!(\'@timestamp\',desc))';
|
||||
const actualUrl = await PageObjects.discover.getSharedUrl();
|
||||
const actualUrl = await PageObjects.share.getSharedUrl();
|
||||
// strip the timestamp out of each URL
|
||||
expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be(
|
||||
expectedUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')
|
||||
);
|
||||
});
|
||||
|
||||
it('gets copied to clipboard', async function () {
|
||||
const isCopiedToClipboard = await PageObjects.discover.clickCopyToClipboard();
|
||||
expect(isCopiedToClipboard).to.eql(true);
|
||||
});
|
||||
|
||||
// TODO: verify clipboard contents
|
||||
it('shorten URL button should produce a short URL', async function () {
|
||||
it('should allow for copying the snapshot URL as a short URL', async function () {
|
||||
const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$');
|
||||
await PageObjects.discover.clickShortenUrl();
|
||||
await retry.try(async function tryingForTime() {
|
||||
const actualUrl = await PageObjects.discover.getSharedUrl();
|
||||
await PageObjects.share.checkShortenUrl();
|
||||
await retry.try(async () => {
|
||||
const actualUrl = await PageObjects.share.getSharedUrl();
|
||||
expect(actualUrl).to.match(re);
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: This test has to run immediately after the test above
|
||||
it('copies short URL to clipboard', async function () {
|
||||
const isCopiedToClipboard = await PageObjects.discover.clickCopyToClipboard();
|
||||
expect(isCopiedToClipboard).to.eql(true);
|
||||
it('should allow for copying the saved object URL', async function () {
|
||||
const expectedUrl =
|
||||
baseUrl +
|
||||
'/app/kibana#' +
|
||||
'/discover/ab12e3c0-f231-11e6-9486-733b1ac9221a' +
|
||||
'?_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A0)' +
|
||||
'%2Ctime%3A(from%3A\'2015-09-19T06%3A31%3A44.000Z\'%2C' +
|
||||
'mode%3Aabsolute%2Cto%3A\'2015-09-23T18%3A31%3A44.000Z\'))';
|
||||
await PageObjects.discover.loadSavedSearch('A Saved Search');
|
||||
await PageObjects.share.clickShareTopNavButton();
|
||||
await PageObjects.share.exportAsSavedObject();
|
||||
const actualUrl = await PageObjects.share.getSharedUrl();
|
||||
expect(actualUrl).to.be(expectedUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
PointSeriesPageProvider,
|
||||
VisualBuilderPageProvider,
|
||||
TimelionPageProvider,
|
||||
SharePageProvider
|
||||
} from './page_objects';
|
||||
|
||||
import {
|
||||
|
@ -85,7 +86,8 @@ export default async function ({ readConfigFile }) {
|
|||
monitoring: MonitoringPageProvider,
|
||||
pointSeries: PointSeriesPageProvider,
|
||||
visualBuilder: VisualBuilderPageProvider,
|
||||
timelion: TimelionPageProvider
|
||||
timelion: TimelionPageProvider,
|
||||
share: SharePageProvider,
|
||||
},
|
||||
services: {
|
||||
es: commonConfig.get('services.es'),
|
||||
|
|
|
@ -226,29 +226,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
|
|||
.getVisibleText();
|
||||
}
|
||||
|
||||
clickShare() {
|
||||
return testSubjects.click('discoverShareButton');
|
||||
}
|
||||
|
||||
clickShortenUrl() {
|
||||
return testSubjects.click('sharedSnapshotShortUrlButton');
|
||||
}
|
||||
|
||||
async clickCopyToClipboard() {
|
||||
await testSubjects.click('sharedSnapshotCopyButton');
|
||||
|
||||
// Confirm that the content was copied to the clipboard.
|
||||
return await testSubjects.exists('shareCopyToClipboardSuccess');
|
||||
}
|
||||
|
||||
async getShareCaption() {
|
||||
return await testSubjects.getVisibleText('shareUiTitle');
|
||||
}
|
||||
|
||||
async getSharedUrl() {
|
||||
return await testSubjects.getProperty('sharedSnapshotUrl', 'value');
|
||||
}
|
||||
|
||||
async toggleSidebarCollapse() {
|
||||
return await testSubjects.click('collapseSideBarButton');
|
||||
}
|
||||
|
|
|
@ -31,3 +31,4 @@ export { MonitoringPageProvider } from './monitoring_page';
|
|||
export { PointSeriesPageProvider } from './point_series_page';
|
||||
export { VisualBuilderPageProvider } from './visual_builder_page';
|
||||
export { TimelionPageProvider } from './timelion_page';
|
||||
export { SharePageProvider } from './share_page';
|
||||
|
|
46
test/functional/page_objects/share_page.js
Normal file
46
test/functional/page_objects/share_page.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function SharePageProvider({ getService, getPageObjects }) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['visualize']);
|
||||
|
||||
class SharePage {
|
||||
async clickShareTopNavButton() {
|
||||
return testSubjects.click('shareTopNavButton');
|
||||
}
|
||||
|
||||
async getSharedUrl() {
|
||||
return await testSubjects.getAttribute('copyShareUrlButton', 'data-share-url');
|
||||
}
|
||||
|
||||
async checkShortenUrl() {
|
||||
const shareForm = await testSubjects.find('shareUrlForm');
|
||||
await PageObjects.visualize.checkCheckbox('useShortUrl');
|
||||
await shareForm.waitForDeletedByClassName('euiLoadingSpinner');
|
||||
}
|
||||
|
||||
async exportAsSavedObject() {
|
||||
return await testSubjects.click('exportAsSavedObject');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new SharePage();
|
||||
}
|
|
@ -158,7 +158,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
|
||||
it('does not show the sharing menu item', async () => {
|
||||
const shareMenuItemExists = await testSubjects.exists('dashboardShareButton');
|
||||
const shareMenuItemExists = await testSubjects.exists('shareTopNavButton');
|
||||
expect(shareMenuItemExists).to.be(false);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue