mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
backport (#22949)
This commit is contained in:
parent
28450485e2
commit
eadf7e847b
53 changed files with 831 additions and 685 deletions
|
@ -43,7 +43,7 @@ import { showCloneModal } from './top_nav/show_clone_modal';
|
|||
import { showSaveModal } from './top_nav/show_save_modal';
|
||||
import { showAddPanel } from './top_nav/show_add_panel';
|
||||
import { showOptionsPopover } from './top_nav/show_options_popover';
|
||||
import { showShareContextMenu } from 'ui/share';
|
||||
import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
||||
import * as filterActions from 'ui/doc_table/actions/filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
@ -86,6 +86,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider);
|
||||
const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider);
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
|
||||
|
||||
panelActionsStore.initializeFromRegistry(panelActionsRegistry);
|
||||
|
||||
|
@ -133,14 +134,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
dirty: !dash.id
|
||||
};
|
||||
|
||||
this.getSharingTitle = () => {
|
||||
return dash.title;
|
||||
};
|
||||
|
||||
this.getSharingType = () => {
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
dashboardStateManager.registerChangeListener(status => {
|
||||
this.appStatus.dirty = status.dirty || !dash.id;
|
||||
updateState();
|
||||
|
@ -409,6 +402,11 @@ app.directive('dashboardApp', function ($injector) {
|
|||
getUnhashableStates,
|
||||
objectId: dash.id,
|
||||
objectType: 'dashboard',
|
||||
shareContextMenuExtensions,
|
||||
sharingData: {
|
||||
title: dash.title,
|
||||
},
|
||||
isDirty: dashboardStateManager.getIsDirty(),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -470,10 +470,10 @@ export class DashboardStateManager {
|
|||
* @returns {boolean} True if the dashboard has changed since the last save (or, is new).
|
||||
*/
|
||||
getIsDirty(timeFilter) {
|
||||
return this.isDirty ||
|
||||
// Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker
|
||||
// changes are not tracked by the state monitor.
|
||||
this.getFiltersChanged(timeFilter);
|
||||
// Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker
|
||||
// changes are not tracked by the state monitor.
|
||||
const hasTimeFilterChanged = timeFilter ? this.getFiltersChanged(timeFilter) : false;
|
||||
return this.isDirty || hasTimeFilterChanged;
|
||||
}
|
||||
|
||||
getPanels() {
|
||||
|
|
|
@ -53,7 +53,7 @@ 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 { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
|
||||
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
|
||||
import { Inspector } from 'ui/inspector';
|
||||
import { RequestAdapter } from 'ui/inspector/adapters';
|
||||
|
@ -162,6 +162,7 @@ function discoverController(
|
|||
location: 'Discover'
|
||||
});
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
|
||||
const inspectorAdapters = {
|
||||
requests: new RequestAdapter()
|
||||
};
|
||||
|
@ -179,6 +180,10 @@ function discoverController(
|
|||
const savedSearch = $route.current.locals.savedSearch;
|
||||
$scope.$on('$destroy', savedSearch.destroy);
|
||||
|
||||
const $appStatus = $scope.appStatus = this.appStatus = {
|
||||
dirty: !savedSearch.id
|
||||
};
|
||||
|
||||
$scope.topNavMenu = [{
|
||||
key: 'new',
|
||||
description: 'New Search',
|
||||
|
@ -198,13 +203,20 @@ function discoverController(
|
|||
key: 'share',
|
||||
description: 'Share Search',
|
||||
testId: 'shareTopNavButton',
|
||||
run: (menuItem, navController, anchorElement) => {
|
||||
run: async (menuItem, navController, anchorElement) => {
|
||||
const sharingData = await this.getSharingData();
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: false,
|
||||
getUnhashableStates,
|
||||
objectId: savedSearch.id,
|
||||
objectType: 'search',
|
||||
shareContextMenuExtensions,
|
||||
sharingData: {
|
||||
...sharingData,
|
||||
title: savedSearch.title,
|
||||
},
|
||||
isDirty: $appStatus.dirty,
|
||||
});
|
||||
}
|
||||
}, {
|
||||
|
@ -239,9 +251,6 @@ function discoverController(
|
|||
docTitle.change(`Discover${pageTitleSuffix}`);
|
||||
|
||||
let stateMonitor;
|
||||
const $appStatus = $scope.appStatus = this.appStatus = {
|
||||
dirty: !savedSearch.id
|
||||
};
|
||||
|
||||
const $state = $scope.state = new AppState(getStateDefaults());
|
||||
|
||||
|
@ -306,14 +315,6 @@ function discoverController(
|
|||
};
|
||||
};
|
||||
|
||||
this.getSharingType = () => {
|
||||
return 'search';
|
||||
};
|
||||
|
||||
this.getSharingTitle = () => {
|
||||
return savedSearch.title;
|
||||
};
|
||||
|
||||
$scope.uiState = $state.makeStateful('uiState');
|
||||
|
||||
function getStateDefaults() {
|
||||
|
|
|
@ -43,6 +43,7 @@ import 'uiExports/embeddableFactories';
|
|||
import 'uiExports/inspectorViews';
|
||||
import 'uiExports/search';
|
||||
import 'uiExports/autocompleteProviders';
|
||||
import 'uiExports/shareContextMenuExtensions';
|
||||
|
||||
import 'ui/autoload/all';
|
||||
import './home';
|
||||
|
|
|
@ -42,7 +42,7 @@ 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 { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
|
||||
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
|
||||
|
||||
uiRoutes
|
||||
|
@ -117,6 +117,7 @@ function VisEditor(
|
|||
const docTitle = Private(DocTitleProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
|
||||
|
||||
// Retrieve the resolved SavedVis instance.
|
||||
const savedVis = $route.current.locals.savedVis;
|
||||
|
@ -138,6 +139,10 @@ function VisEditor(
|
|||
|
||||
$scope.vis = vis;
|
||||
|
||||
const $appStatus = this.appStatus = {
|
||||
dirty: !savedVis.id
|
||||
};
|
||||
|
||||
$scope.topNavMenu = [{
|
||||
key: 'save',
|
||||
description: 'Save Visualization',
|
||||
|
@ -156,12 +161,19 @@ function VisEditor(
|
|||
description: 'Share Visualization',
|
||||
testId: 'shareTopNavButton',
|
||||
run: (menuItem, navController, anchorElement) => {
|
||||
const hasUnappliedChanges = vis.dirty;
|
||||
const hasUnsavedChanges = $appStatus.dirty;
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: true,
|
||||
getUnhashableStates,
|
||||
objectId: savedVis.id,
|
||||
objectType: 'visualization',
|
||||
shareContextMenuExtensions,
|
||||
sharingData: {
|
||||
title: savedVis.title,
|
||||
},
|
||||
isDirty: hasUnappliedChanges || hasUnsavedChanges,
|
||||
});
|
||||
}
|
||||
}, {
|
||||
|
@ -190,18 +202,6 @@ function VisEditor(
|
|||
|
||||
let stateMonitor;
|
||||
|
||||
const $appStatus = this.appStatus = {
|
||||
dirty: !savedVis.id
|
||||
};
|
||||
|
||||
this.getSharingTitle = () => {
|
||||
return savedVis.title;
|
||||
};
|
||||
|
||||
this.getSharingType = () => {
|
||||
return 'visualization';
|
||||
};
|
||||
|
||||
if (savedVis.id) {
|
||||
docTitle.change(savedVis.title);
|
||||
}
|
||||
|
|
1
src/ui/public/chrome/index.d.ts
vendored
1
src/ui/public/chrome/index.d.ts
vendored
|
@ -27,6 +27,7 @@ declare class Chrome {
|
|||
public getBasePath(): string;
|
||||
public getXsrfToken(): string;
|
||||
public getKibanaVersion(): string;
|
||||
public getUiSettingsClient(): any;
|
||||
}
|
||||
|
||||
declare const chrome: Chrome;
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
exports[`should only render permalink panel when there are no other panels 1`] = `
|
||||
<EuiContextMenu
|
||||
data-test-subj="shareContextMenu"
|
||||
initialPanelId={1}
|
||||
panels={
|
||||
Array [
|
||||
Object {
|
||||
"content": <ShareUrlContent
|
||||
"content": <UrlPanelContent
|
||||
getUnhashableStates={[Function]}
|
||||
objectType="dashboard"
|
||||
/>,
|
||||
|
@ -20,11 +21,12 @@ exports[`should only render permalink panel when there are no other panels 1`] =
|
|||
|
||||
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": <ShareUrlContent
|
||||
"content": <UrlPanelContent
|
||||
getUnhashableStates={[Function]}
|
||||
objectType="dashboard"
|
||||
/>,
|
||||
|
@ -32,7 +34,7 @@ exports[`should render context menu panel when there are more than one panel 1`]
|
|||
"title": "Permalink",
|
||||
},
|
||||
Object {
|
||||
"content": <ShareUrlContent
|
||||
"content": <UrlPanelContent
|
||||
getUnhashableStates={[Function]}
|
||||
isEmbedded={true}
|
||||
objectType="dashboard"
|
||||
|
@ -44,11 +46,13 @@ exports[`should render context menu panel when there are more than one panel 1`]
|
|||
"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,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`render 1`] = `
|
||||
<EuiForm
|
||||
className="shareUrlContentForm"
|
||||
className="sharePanelContent"
|
||||
data-test-subj="shareUrlForm"
|
||||
>
|
||||
<EuiFormRow
|
||||
|
@ -128,6 +128,7 @@ exports[`render 1`] = `
|
|||
</EuiFormRow>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
anchorClassName="sharePanel__copyAnchor"
|
||||
textToCopy="about:blank"
|
||||
/>
|
||||
</EuiForm>
|
||||
|
@ -135,7 +136,7 @@ exports[`render 1`] = `
|
|||
|
||||
exports[`should enable saved object export option when objectId is provided 1`] = `
|
||||
<EuiForm
|
||||
className="shareUrlContentForm"
|
||||
className="sharePanelContent"
|
||||
data-test-subj="shareUrlForm"
|
||||
>
|
||||
<EuiFormRow
|
||||
|
@ -260,6 +261,7 @@ exports[`should enable saved object export option when objectId is provided 1`]
|
|||
</EuiFormRow>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
anchorClassName="sharePanel__copyAnchor"
|
||||
textToCopy="about:blank"
|
||||
/>
|
||||
</EuiForm>
|
|
@ -18,33 +18,46 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import './share_panel_content.less';
|
||||
|
||||
import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
|
||||
import { EuiContextMenu } from '@elastic/eui';
|
||||
|
||||
import { ShareUrlContent } from './share_url_content';
|
||||
import { ShareAction, ShareActionProvider } from 'ui/share/share_action';
|
||||
import { UrlPanelContent } from './url_panel_content';
|
||||
|
||||
interface Props {
|
||||
allowEmbed: boolean;
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
getUnhashableStates: () => object[];
|
||||
shareContextMenuExtensions?: ShareActionProvider[];
|
||||
sharingData: any;
|
||||
isDirty: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export class ShareContextMenu extends Component<Props> {
|
||||
public render() {
|
||||
const { panels, initialPanelId } = this.getPanels();
|
||||
return <EuiContextMenu initialPanelId={initialPanelId} panels={panels} />;
|
||||
return (
|
||||
<EuiContextMenu
|
||||
initialPanelId={initialPanelId}
|
||||
panels={panels}
|
||||
data-test-subj="shareContextMenu"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private getPanels = () => {
|
||||
const panels = [];
|
||||
const menuItems = [];
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [];
|
||||
const menuItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
|
||||
const permalinkPanel = {
|
||||
id: panels.length + 1,
|
||||
title: 'Permalink',
|
||||
content: (
|
||||
<ShareUrlContent
|
||||
<UrlPanelContent
|
||||
objectId={this.props.objectId}
|
||||
objectType={this.props.objectType}
|
||||
getUnhashableStates={this.props.getUnhashableStates}
|
||||
|
@ -63,7 +76,7 @@ export class ShareContextMenu extends Component<Props> {
|
|||
id: panels.length + 1,
|
||||
title: 'Embed Code',
|
||||
content: (
|
||||
<ShareUrlContent
|
||||
<UrlPanelContent
|
||||
isEmbedded
|
||||
objectId={this.props.objectId}
|
||||
objectType={this.props.objectType}
|
||||
|
@ -79,15 +92,51 @@ export class ShareContextMenu extends Component<Props> {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO add plugable panels here
|
||||
if (this.props.shareContextMenuExtensions) {
|
||||
const {
|
||||
objectType,
|
||||
objectId,
|
||||
getUnhashableStates,
|
||||
sharingData,
|
||||
isDirty,
|
||||
onClose,
|
||||
} = this.props;
|
||||
this.props.shareContextMenuExtensions.forEach((provider: ShareActionProvider) => {
|
||||
provider
|
||||
.getShareActions({
|
||||
objectType,
|
||||
objectId,
|
||||
getUnhashableStates,
|
||||
sharingData,
|
||||
isDirty,
|
||||
onClose,
|
||||
})
|
||||
.forEach(({ shareMenuItem, panel }: ShareAction) => {
|
||||
const panelId = panels.length + 1;
|
||||
panels.push({
|
||||
...panel,
|
||||
id: panelId,
|
||||
});
|
||||
menuItems.push({
|
||||
...shareMenuItem,
|
||||
panel: panelId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (menuItems.length > 1) {
|
||||
const topLevelMenuPanel = {
|
||||
id: panels.length + 1,
|
||||
title: `Share this ${this.props.objectType}`,
|
||||
items: menuItems.sort((a, b) => {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
}),
|
||||
items: menuItems
|
||||
.map(menuItem => {
|
||||
menuItem['data-test-subj'] = `sharePanel-${menuItem.name.replace(' ', '')}`;
|
||||
return menuItem;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
}),
|
||||
};
|
||||
panels.push(topLevelMenuPanel);
|
||||
}
|
||||
|
|
11
src/ui/public/share/components/share_panel_content.less
Normal file
11
src/ui/public/share/components/share_panel_content.less
Normal file
|
@ -0,0 +1,11 @@
|
|||
.sharePanelContent{
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sharePanel__copyAnchor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sharePanel__button {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.shareUrlContentForm{
|
||||
padding: 16px;
|
||||
}
|
|
@ -23,11 +23,11 @@ import React from 'react';
|
|||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
ShareUrlContent,
|
||||
} from './share_url_content';
|
||||
UrlPanelContent,
|
||||
} from './url_panel_content';
|
||||
|
||||
test('render', () => {
|
||||
const component = shallow(<ShareUrlContent
|
||||
const component = shallow(<UrlPanelContent
|
||||
objectType="dashboard"
|
||||
getUnhashableStates={() => {}}
|
||||
/>);
|
||||
|
@ -35,7 +35,7 @@ test('render', () => {
|
|||
});
|
||||
|
||||
test('should enable saved object export option when objectId is provided', () => {
|
||||
const component = shallow(<ShareUrlContent
|
||||
const component = shallow(<UrlPanelContent
|
||||
objectId="id1"
|
||||
objectType="dashboard"
|
||||
getUnhashableStates={() => {}}
|
|
@ -24,7 +24,6 @@ declare module '@elastic/eui' {
|
|||
}
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import './share_url_content.less';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -67,7 +66,7 @@ interface State {
|
|||
shortUrlErrorMsg?: string;
|
||||
}
|
||||
|
||||
export class ShareUrlContent extends Component<Props, State> {
|
||||
export class UrlPanelContent extends Component<Props, State> {
|
||||
private mounted?: boolean;
|
||||
private shortUrlCache?: string;
|
||||
|
||||
|
@ -99,22 +98,25 @@ export class ShareUrlContent extends Component<Props, State> {
|
|||
|
||||
public render() {
|
||||
return (
|
||||
<EuiForm className="shareUrlContentForm" data-test-subj="shareUrlForm">
|
||||
<EuiForm className="sharePanelContent" data-test-subj="shareUrlForm">
|
||||
{this.renderExportAsRadioGroup()}
|
||||
|
||||
{this.renderShortUrlSwitch()}
|
||||
|
||||
<EuiCopy textToCopy={this.state.url}>
|
||||
<EuiCopy textToCopy={this.state.url} anchorClassName="sharePanel__copyAnchor">
|
||||
{(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>
|
||||
<EuiFormRow>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={copy}
|
||||
disabled={this.state.isCreatingShortUrl || this.state.url === ''}
|
||||
data-share-url={this.state.url}
|
||||
data-test-subj="copyShareUrlButton"
|
||||
size="s"
|
||||
>
|
||||
Copy {this.props.isEmbedded ? 'iFrame code' : 'link'}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiForm>
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export { showShareContextMenu } from './show_share_context_menu';
|
||||
export { ShareContextMenuExtensionsRegistryProvider } from './share_action_registry';
|
||||
|
|
59
src/ui/public/share/share_action.ts
Normal file
59
src/ui/public/share/share_action.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 ShareAction {
|
||||
shareMenuItem: EuiContextMenuPanelItemDescriptor;
|
||||
panel: EuiContextMenuPanelDescriptor;
|
||||
}
|
||||
|
||||
export interface ShareActionProvider {
|
||||
readonly id: string;
|
||||
|
||||
getShareActions: (actionProps: ShareActionProps) => ShareAction[];
|
||||
}
|
26
src/ui/public/share/share_action_registry.ts
Normal file
26
src/ui/public/share/share_action_registry.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore: implicit any for JS file
|
||||
import { uiRegistry } from 'ui/registry/_registry';
|
||||
|
||||
export const ShareContextMenuExtensionsRegistryProvider = uiRegistry({
|
||||
name: 'shareContextMenuExtensions',
|
||||
index: ['id'],
|
||||
});
|
|
@ -26,6 +26,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { ShareContextMenu } from './components/share_context_menu';
|
||||
import { ShareActionProvider } from './share_action';
|
||||
|
||||
import { EuiWrappingPopover } from '@elastic/eui';
|
||||
|
||||
|
@ -44,6 +45,9 @@ interface ShowProps {
|
|||
getUnhashableStates: () => object[];
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
shareContextMenuExtensions?: ShareActionProvider[];
|
||||
sharingData: any;
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
export function showShareContextMenu({
|
||||
|
@ -52,6 +56,9 @@ export function showShareContextMenu({
|
|||
getUnhashableStates,
|
||||
objectId,
|
||||
objectType,
|
||||
shareContextMenuExtensions,
|
||||
sharingData,
|
||||
isDirty,
|
||||
}: ShowProps) {
|
||||
if (isOpen) {
|
||||
onClose();
|
||||
|
@ -76,6 +83,10 @@ export function showShareContextMenu({
|
|||
getUnhashableStates={getUnhashableStates}
|
||||
objectId={objectId}
|
||||
objectType={objectType}
|
||||
shareContextMenuExtensions={shareContextMenuExtensions}
|
||||
sharingData={sharingData}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</EuiWrappingPopover>
|
||||
);
|
||||
|
|
26
src/ui/public/utils/query_string.d.ts
vendored
Normal file
26
src/ui/public/utils/query_string.d.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare class QueryStringClass {
|
||||
public param(key: string, value: string): string;
|
||||
}
|
||||
|
||||
declare const QueryString: QueryStringClass;
|
||||
|
||||
export { QueryString };
|
|
@ -54,6 +54,7 @@ export {
|
|||
visualize,
|
||||
search,
|
||||
autocompleteProviders,
|
||||
shareContextMenuExtensions,
|
||||
} from './ui_app_extensions';
|
||||
|
||||
export {
|
||||
|
|
|
@ -52,6 +52,7 @@ export const hacks = appExtension;
|
|||
export const home = appExtension;
|
||||
export const inspectorViews = appExtension;
|
||||
export const search = appExtension;
|
||||
export const shareContextMenuExtensions = appExtension;
|
||||
// Add a visualize app extension that should be used for visualize specific stuff
|
||||
export const visualize = appExtension;
|
||||
|
||||
|
|
|
@ -19,13 +19,34 @@
|
|||
|
||||
export function SharePageProvider({ getService, getPageObjects }) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['visualize']);
|
||||
const PageObjects = getPageObjects(['visualize', 'common']);
|
||||
const log = getService('log');
|
||||
|
||||
class SharePage {
|
||||
async isShareMenuOpen() {
|
||||
return await testSubjects.exists('shareContextMenu');
|
||||
}
|
||||
|
||||
async clickShareTopNavButton() {
|
||||
return testSubjects.click('shareTopNavButton');
|
||||
}
|
||||
|
||||
async openShareMenuItem(itemTitle) {
|
||||
log.debug(`openShareMenuItem title:${itemTitle}`);
|
||||
const isShareMenuOpen = await this.isShareMenuOpen();
|
||||
if (!isShareMenuOpen) {
|
||||
await this.clickShareTopNavButton();
|
||||
} else {
|
||||
// there is no easy way to ensure the menu is at the top level
|
||||
// so just close the existing menu
|
||||
await this.clickShareTopNavButton();
|
||||
// and then re-open the menu
|
||||
await this.clickShareTopNavButton();
|
||||
}
|
||||
|
||||
return testSubjects.click(`sharePanel-${itemTitle.replace(' ', '')}`);
|
||||
}
|
||||
|
||||
async getSharedUrl() {
|
||||
return await testSubjects.getAttribute('copyShareUrlButton', 'data-share-url');
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
"@kbn/ui-framework": "link:../packages/kbn-ui-framework",
|
||||
"@samverschueren/stream-to-observable": "^0.3.0",
|
||||
"@slack/client": "^4.2.2",
|
||||
"@types/moment-timezone": "^0.5.8",
|
||||
"angular-paging": "2.2.1",
|
||||
"angular-resource": "1.4.9",
|
||||
"angular-sanitize": "1.4.9",
|
||||
|
|
|
@ -27,6 +27,7 @@ import 'uiExports/docViews';
|
|||
import 'uiExports/fieldFormats';
|
||||
import 'uiExports/search';
|
||||
import 'uiExports/autocompleteProviders';
|
||||
import 'uiExports/shareContextMenuExtensions';
|
||||
import _ from 'lodash';
|
||||
import 'ui/autoload/all';
|
||||
import 'plugins/kibana/dashboard';
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { JobParamsProvider } from './job_params_provider';
|
||||
import { metadata } from '../metadata';
|
||||
|
||||
export function register(registry) {
|
||||
registry.register({
|
||||
...metadata,
|
||||
JobParamsProvider
|
||||
});
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function JobParamsProvider() {
|
||||
return async function (controller) {
|
||||
const title = controller.getSharingTitle();
|
||||
const type = controller.getSharingType();
|
||||
const sharingData = await controller.getSharingData();
|
||||
|
||||
return {
|
||||
title,
|
||||
type,
|
||||
...sharingData
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './options';
|
||||
import { JobParamsProvider } from './job_params_provider';
|
||||
import { metadata } from '../metadata';
|
||||
|
||||
export function register(registry) {
|
||||
registry.register({
|
||||
...metadata,
|
||||
JobParamsProvider,
|
||||
optionsTemplate: `<pdf-options />`
|
||||
});
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import {
|
||||
getUnhashableStatesProvider,
|
||||
unhashUrl,
|
||||
} from 'ui/state_management/state_hashing';
|
||||
import moment from 'moment-timezone';
|
||||
import { getLayout } from './layouts';
|
||||
|
||||
|
||||
export function JobParamsProvider(Private, config) {
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
|
||||
function parseRelativeUrl(location) {
|
||||
// We need to convert the hashed states in the URL back into their original RISON values,
|
||||
// because this URL will be sent to the API.
|
||||
const unhashedUrl = unhashUrl(location.href, getUnhashableStates());
|
||||
|
||||
const relativeUrl = unhashedUrl.replace(location.origin + chrome.getBasePath(), '');
|
||||
return relativeUrl;
|
||||
}
|
||||
|
||||
return function jobParams(controller, options) {
|
||||
const layout = getLayout(options.layoutId);
|
||||
const browserTimezone = config.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() : config.get('dateFormat:tz');
|
||||
const relativeUrl = parseRelativeUrl(window.location);
|
||||
|
||||
return {
|
||||
title: controller.getSharingTitle(),
|
||||
objectType: controller.getSharingType(),
|
||||
browserTimezone: browserTimezone,
|
||||
relativeUrls: [ relativeUrl ],
|
||||
layout: layout.getJobParams(),
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { print } from './print';
|
||||
import { preserveLayout } from './preserve_layout';
|
||||
import { LayoutTypes } from '../../common/constants';
|
||||
|
||||
export function getLayout(name) {
|
||||
switch (name) {
|
||||
case LayoutTypes.PRINT:
|
||||
return print;
|
||||
case LayoutTypes.PRESERVE_LAYOUT:
|
||||
return preserveLayout;
|
||||
default:
|
||||
throw new Error(`Unexpected layout of ${name}`);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LayoutTypes } from '../../common/constants';
|
||||
|
||||
export const preserveLayout = {
|
||||
getJobParams() {
|
||||
const el = document.querySelector('[data-shared-items-container]');
|
||||
const bounds = el.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
id: LayoutTypes.PRESERVE_LAYOUT,
|
||||
dimensions: {
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
<div class="pdf-options">
|
||||
<label>
|
||||
<input type="radio" ng-model="options.layoutId" value="print">
|
||||
Optimize PDF for printing
|
||||
</label>
|
||||
<label data-test-subj="preserveLayoutOption">
|
||||
<input type="radio" ng-model="options.layoutId" value="preserve_layout">
|
||||
Preserve existing layout in PDF
|
||||
</label>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './options.html';
|
||||
|
||||
const module = uiModules.get('xpack/reporting');
|
||||
|
||||
module.directive('pdfOptions', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template,
|
||||
link: function ($scope) {
|
||||
if (!$scope.options.layoutId) {
|
||||
$scope.options.layoutId = 'print';
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -32,10 +32,9 @@ export const reporting = (kibana) => {
|
|||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
|
||||
uiExports: {
|
||||
navbarExtensions: [
|
||||
'plugins/reporting/controls/discover',
|
||||
'plugins/reporting/controls/visualize',
|
||||
'plugins/reporting/controls/dashboard',
|
||||
shareContextMenuExtensions: [
|
||||
'plugins/reporting/share_context_menu/register_csv_reporting',
|
||||
'plugins/reporting/share_context_menu/register_reporting',
|
||||
],
|
||||
hacks: ['plugins/reporting/hacks/job_completion_notifier'],
|
||||
home: ['plugins/reporting/register_feature'],
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic 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 { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import { KFetchError } from 'ui/kfetch/kfetch_error';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import url from 'url';
|
||||
import { reportingClient } from '../lib/reporting_client';
|
||||
|
||||
interface Props {
|
||||
reportType: string;
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
getJobParams: () => any;
|
||||
options?: ReactElement<any>;
|
||||
isDirty: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isStale: boolean;
|
||||
absoluteUrl: string;
|
||||
}
|
||||
|
||||
export class ReportingPanelContent extends Component<Props, State> {
|
||||
private mounted?: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isStale: false,
|
||||
absoluteUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.markAsStale);
|
||||
window.removeEventListener('resize', this.setAbsoluteReportGenerationUrl);
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.setAbsoluteReportGenerationUrl();
|
||||
|
||||
window.addEventListener('hashchange', this.markAsStale, false);
|
||||
window.addEventListener('resize', this.setAbsoluteReportGenerationUrl);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.isNotSaved() || this.props.isDirty || this.state.isStale) {
|
||||
return (
|
||||
<EuiForm className="sharePanelContent" data-test-subj="shareReportingForm">
|
||||
<EuiFormRow helpText={'Please save your work before generating a report.'}>
|
||||
{this.renderGenerateReportButton(true)}
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
const reportMsg = `${this.prettyPrintReportingType()}s can take a minute or two to generate based upon the size of your ${
|
||||
this.props.objectType
|
||||
}.`;
|
||||
|
||||
return (
|
||||
<EuiForm className="sharePanelContent" data-test-subj="shareReportingForm">
|
||||
<EuiText size="s">
|
||||
<p>{reportMsg}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{this.props.options}
|
||||
|
||||
{this.renderGenerateReportButton(false)}
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
Alternatively, copy this POST URL to call generation from outside Kibana or from
|
||||
Watcher.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiCopy textToCopy={this.state.absoluteUrl} anchorClassName="sharePanel__copyAnchor">
|
||||
{(copy: () => void) => (
|
||||
<EuiButton className="sharePanel__button" onClick={copy} size="s">
|
||||
Copy POST URL
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
private renderGenerateReportButton = (isDisabled: boolean) => {
|
||||
return (
|
||||
<EuiButton
|
||||
className="sharePanel__button"
|
||||
disabled={isDisabled}
|
||||
fill
|
||||
onClick={this.createReportingJob}
|
||||
data-test-subj="generateReportButton"
|
||||
size="s"
|
||||
>
|
||||
Generate {this.prettyPrintReportingType()}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
private prettyPrintReportingType = () => {
|
||||
switch (this.props.reportType) {
|
||||
case 'printablePdf':
|
||||
return 'PDF';
|
||||
case 'csv':
|
||||
return 'CSV';
|
||||
default:
|
||||
return this.props.reportType;
|
||||
}
|
||||
};
|
||||
|
||||
private markAsStale = () => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isStale: true });
|
||||
};
|
||||
|
||||
private isNotSaved = () => {
|
||||
return this.props.objectId === undefined || this.props.objectId === '';
|
||||
};
|
||||
|
||||
private setAbsoluteReportGenerationUrl = () => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = reportingClient.getReportingJobPath(
|
||||
this.props.reportType,
|
||||
this.props.getJobParams()
|
||||
);
|
||||
const absoluteUrl = url.resolve(window.location.href, relativePath);
|
||||
this.setState({ absoluteUrl });
|
||||
};
|
||||
|
||||
private createReportingJob = () => {
|
||||
return reportingClient
|
||||
.createReportingJob(this.props.reportType, this.props.getJobParams())
|
||||
.then(() => {
|
||||
toastNotifications.addSuccess({
|
||||
title: `Queued report for ${this.props.objectType}`,
|
||||
text: 'Track its progress in Management',
|
||||
'data-test-subj': 'queueReportSuccess',
|
||||
});
|
||||
this.props.onClose();
|
||||
})
|
||||
.catch((kfetchError: KFetchError) => {
|
||||
if (kfetchError.message === 'not exportable') {
|
||||
return toastNotifications.addWarning({
|
||||
title: `Only saved ${this.props.objectType} can be exported`,
|
||||
text: 'Please save your work first',
|
||||
});
|
||||
}
|
||||
|
||||
const defaultMessage =
|
||||
kfetchError.res.status === 403
|
||||
? `You don't have permission to generate this report.`
|
||||
: `Can't reach the server. Please try again.`;
|
||||
|
||||
toastNotifications.addDanger({
|
||||
title: 'Reporting error',
|
||||
text: kfetchError.message || defaultMessage,
|
||||
'data-test-subj': 'queueReportError',
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSpacer, EuiSwitch } from '@elastic/eui';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { ReportingPanelContent } from './reporting_panel_content';
|
||||
|
||||
interface Props {
|
||||
reportType: string;
|
||||
objectId?: string;
|
||||
objectType: string;
|
||||
getJobParams: () => any;
|
||||
isDirty: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
usePrintLayout: boolean;
|
||||
}
|
||||
|
||||
export class ScreenCapturePanelContent extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
usePrintLayout: false,
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ReportingPanelContent
|
||||
reportType={this.props.reportType}
|
||||
objectType={this.props.objectType}
|
||||
objectId={this.props.objectId}
|
||||
getJobParams={this.getJobParams}
|
||||
options={this.renderOptions()}
|
||||
isDirty={this.props.isDirty}
|
||||
onClose={this.props.onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderOptions = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSwitch
|
||||
label="Optimize for printing"
|
||||
checked={this.state.usePrintLayout}
|
||||
onChange={this.handlePrintLayoutChange}
|
||||
data-test-subj="usePrintLayout"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
private handlePrintLayoutChange = (evt: any) => {
|
||||
this.setState({ usePrintLayout: evt.target.checked });
|
||||
};
|
||||
|
||||
private getLayout = () => {
|
||||
if (this.state.usePrintLayout) {
|
||||
return { id: 'print' };
|
||||
}
|
||||
|
||||
const el = document.querySelector('[data-shared-items-container]');
|
||||
const bounds = el ? el.getBoundingClientRect() : { height: 768, width: 1024 };
|
||||
return {
|
||||
id: 'preserve_layout',
|
||||
dimensions: {
|
||||
height: bounds.height,
|
||||
width: bounds.width,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private getJobParams = () => {
|
||||
const jobParams = this.props.getJobParams();
|
||||
jobParams.layout = this.getLayout();
|
||||
return jobParams;
|
||||
};
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/directives/export_config';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { NavBarExtensionsRegistryProvider } from 'ui/registry/navbar_extensions';
|
||||
import { DashboardConstants } from 'plugins/kibana/dashboard/dashboard_constants';
|
||||
|
||||
function dashboardReportProvider(Private, $location, dashboardConfig) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
return {
|
||||
appName: 'dashboard',
|
||||
key: 'reporting-dashboard',
|
||||
label: 'Reporting',
|
||||
template: `<export-config object-type="Dashboard" enabled-export-type="printablePdf"></export-config>`,
|
||||
description: 'Dashboard Report',
|
||||
hideButton: () => (
|
||||
dashboardConfig.getHideWriteControls()
|
||||
|| $location.path() === DashboardConstants.LANDING_PAGE_PATH
|
||||
|| !xpackInfo.get('features.reporting.printablePdf.showLinks', false)
|
||||
),
|
||||
disableButton: () => !xpackInfo.get('features.reporting.printablePdf.enableLinks', false),
|
||||
tooltip: () => xpackInfo.get('features.reporting.printablePdf.message'),
|
||||
testId: 'topNavReportingLink',
|
||||
};
|
||||
}
|
||||
|
||||
NavBarExtensionsRegistryProvider.register(dashboardReportProvider);
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/directives/export_config';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { NavBarExtensionsRegistryProvider } from 'ui/registry/navbar_extensions';
|
||||
|
||||
function discoverReportProvider(Private) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
return {
|
||||
appName: 'discover',
|
||||
|
||||
key: 'reporting-discover',
|
||||
label: 'Reporting',
|
||||
template: '<export-config object-type="Search" enabled-export-type="csv"></export-config>',
|
||||
description: 'Search Report',
|
||||
hideButton: () => !xpackInfo.get('features.reporting.csv.showLinks', false),
|
||||
disableButton: () => !xpackInfo.get('features.reporting.csv.enableLinks', false),
|
||||
tooltip: () => xpackInfo.get('features.reporting.csv.message'),
|
||||
testId: 'topNavReportingLink',
|
||||
};
|
||||
}
|
||||
|
||||
NavBarExtensionsRegistryProvider.register(discoverReportProvider);
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/directives/export_config';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { NavBarExtensionsRegistryProvider } from 'ui/registry/navbar_extensions';
|
||||
import { VisualizeConstants } from 'plugins/kibana/visualize/visualize_constants';
|
||||
|
||||
function visualizeReportProvider(Private, $location) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
return {
|
||||
appName: 'visualize',
|
||||
|
||||
key: 'reporting-visualize',
|
||||
label: 'Reporting',
|
||||
template: `
|
||||
<export-config
|
||||
object-type="Visualization"
|
||||
enabled-export-type="printablePdf"
|
||||
options="{ layoutId: 'preserve_layout' }"
|
||||
></export-config>`,
|
||||
description: 'Visualization Report',
|
||||
hideButton: () => (
|
||||
$location.path() === VisualizeConstants.LANDING_PAGE_PATH
|
||||
|| $location.path() === VisualizeConstants.WIZARD_STEP_1_PAGE_PATH
|
||||
|| $location.path() === VisualizeConstants.WIZARD_STEP_2_PAGE_PATH
|
||||
|| !xpackInfo.get('features.reporting.printablePdf.showLinks', false)
|
||||
),
|
||||
disableButton: () => !xpackInfo.get('features.reporting.printablePdf.enableLinks', false),
|
||||
tooltip: () => xpackInfo.get('features.reporting.printablePdf.message'),
|
||||
testId: 'topNavReportingLink',
|
||||
};
|
||||
}
|
||||
|
||||
NavBarExtensionsRegistryProvider.register(visualizeReportProvider);
|
|
@ -1,55 +0,0 @@
|
|||
<div ng-show="!exportConfig.isDirty()">
|
||||
<div class="kuiLocalDropdownSection">
|
||||
<h2 class="kuiLocalDropdownTitle">
|
||||
Reporting
|
||||
</h2>
|
||||
|
||||
<div class="input-group generate-controls">
|
||||
<div class="options"></div>
|
||||
<button
|
||||
class="kuiButton kuiButton--primary"
|
||||
data-test-subj="generateReportButton"
|
||||
ng-click="exportConfig.export()"
|
||||
>
|
||||
Generate {{ exportConfig.exportType.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiLocalDropdownSection">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="kuiLocalDropdownHeader">
|
||||
<label
|
||||
class="kuiLocalDropdownHeader__label"
|
||||
for="reportGenerationUrl"
|
||||
>
|
||||
Generation URL
|
||||
</label>
|
||||
<div class="kuiLocalDropdownHeader__actions">
|
||||
<a
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-click="exportConfig.copyToClipboard('#reportGenerationUrl')"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Copy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
id="reportGenerationUrl"
|
||||
class="kuiLocalDropdownInput"
|
||||
type="text"
|
||||
readonly
|
||||
data-test-subj="reportGenerationUrl"
|
||||
value="{{ exportConfig.absoluteUrl || 'Loading...' }}"
|
||||
ng-click="updateUrl()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="exportConfig.isDirty()" data-test-subj="unsavedChangesReportingWarning">
|
||||
Please save your work before generating a report.
|
||||
</div>
|
|
@ -1,141 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import { debounce } from 'lodash';
|
||||
import 'plugins/reporting/services/document_control';
|
||||
import 'plugins/reporting/services/export_types';
|
||||
import './export_config.less';
|
||||
import template from 'plugins/reporting/directives/export_config/export_config.html';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
|
||||
import url from 'url';
|
||||
|
||||
const module = uiModules.get('xpack/reporting');
|
||||
|
||||
module.directive('exportConfig', ($rootScope, reportingDocumentControl, reportingExportTypes, $location, $compile) => {
|
||||
const createAbsoluteUrl = relativePath => {
|
||||
return url.resolve($location.absUrl(), relativePath);
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {},
|
||||
require: ['?^dashboardApp', '?^visualizeApp', '?^discoverApp'],
|
||||
controllerAs: 'exportConfig',
|
||||
template,
|
||||
transclude: true,
|
||||
async link($scope, $el, $attr, controllers) {
|
||||
const actualControllers = controllers.filter(c => c !== null);
|
||||
if (actualControllers.length !== 1) {
|
||||
throw new Error(`Expected there to be 1 controller, but there are ${actualControllers.length}`);
|
||||
}
|
||||
const controller = actualControllers[0];
|
||||
$scope.exportConfig.isDirty = () => controller.appStatus.dirty;
|
||||
if (controller.appStatus.dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exportTypeId = $attr.enabledExportType;
|
||||
$scope.exportConfig.exportType = reportingExportTypes.getById(exportTypeId);
|
||||
$scope.exportConfig.objectType = $attr.objectType;
|
||||
|
||||
$scope.options = $attr.options ? $scope.$eval($attr.options) : {};
|
||||
if ($scope.exportConfig.exportType.optionsTemplate) {
|
||||
$el.find('.options').append($compile($scope.exportConfig.exportType.optionsTemplate)($scope));
|
||||
}
|
||||
|
||||
$scope.getRelativePath = (options) => {
|
||||
return reportingDocumentControl.getPath($scope.exportConfig.exportType, controller, options || $scope.options);
|
||||
};
|
||||
|
||||
$scope.updateUrl = (options) => {
|
||||
return $scope.getRelativePath(options)
|
||||
.then(relativePath => {
|
||||
$scope.exportConfig.absoluteUrl = createAbsoluteUrl(relativePath);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('options', newOptions => $scope.updateUrl(newOptions), true);
|
||||
|
||||
await $scope.updateUrl();
|
||||
},
|
||||
controller($scope, $document, $window, $timeout, globalState) {
|
||||
const stateMonitor = stateMonitorFactory.create(globalState);
|
||||
stateMonitor.onChange(() => {
|
||||
if ($scope.exportConfig.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.updateUrl();
|
||||
});
|
||||
|
||||
const onResize = debounce(() => {
|
||||
$scope.updateUrl();
|
||||
}, 200);
|
||||
|
||||
angular.element($window).on('resize', onResize);
|
||||
$scope.$on('$destroy', () => {
|
||||
angular.element($window).off('resize', onResize);
|
||||
stateMonitor.destroy();
|
||||
});
|
||||
|
||||
this.export = () => {
|
||||
return $scope.getRelativePath()
|
||||
.then(relativePath => {
|
||||
return reportingDocumentControl.create(relativePath);
|
||||
})
|
||||
.then(() => {
|
||||
toastNotifications.addSuccess({
|
||||
title: `Queued report for ${this.objectType}`,
|
||||
text: 'Track its progress in Management',
|
||||
'data-test-subj': 'queueReportSuccess',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.message === 'not exportable') {
|
||||
return toastNotifications.addWarning({
|
||||
title: 'Only saved dashboards can be exported',
|
||||
text: 'Please save your work first',
|
||||
});
|
||||
}
|
||||
|
||||
toastNotifications.addDanger({
|
||||
title: 'Reporting error',
|
||||
text: err.message || `Can't reach the server. Please try again.`,
|
||||
'data-test-subj': 'queueReportError',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.copyToClipboard = selector => {
|
||||
// updating the URL in the input because it could have potentially changed and we missed the update
|
||||
$scope.updateUrl()
|
||||
.then(() => {
|
||||
|
||||
// we're using $timeout to make sure the URL has been updated in the HTML as this is where
|
||||
// we're copying the ext from
|
||||
$timeout(() => {
|
||||
const copyTextarea = $document.find(selector)[0];
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
const isCopied = document.execCommand('copy');
|
||||
if (isCopied) {
|
||||
toastNotifications.add('URL copied to clipboard');
|
||||
} else {
|
||||
toastNotifications.add('Press Ctrl+C to copy URL');
|
||||
}
|
||||
} catch (err) {
|
||||
toastNotifications.add('Press Ctrl+C to copy URL');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
export-config {
|
||||
.generate-controls {
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
|
||||
.clipboard-button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.form-control.url {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './export_config';
|
35
x-pack/plugins/reporting/public/lib/reporting_client.ts
Normal file
35
x-pack/plugins/reporting/public/lib/reporting_client.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
|
||||
// @ts-ignore
|
||||
import rison from 'rison-node';
|
||||
import chrome from 'ui/chrome';
|
||||
import { QueryString } from 'ui/utils/query_string';
|
||||
import { jobCompletionNotifications } from '../services/job_completion_notifications';
|
||||
|
||||
const API_BASE_URL = '/api/reporting/generate';
|
||||
|
||||
class ReportingClient {
|
||||
public getReportingJobPath = (exportType: string, jobParams: object) => {
|
||||
return `${chrome.addBasePath(API_BASE_URL)}/${exportType}?${QueryString.param(
|
||||
'jobParams',
|
||||
rison.encode(jobParams)
|
||||
)}`;
|
||||
};
|
||||
|
||||
public createReportingJob = async (exportType: string, jobParams: any) => {
|
||||
const query = {
|
||||
jobParams: rison.encode(jobParams),
|
||||
};
|
||||
const resp = await kfetch({ method: 'POST', pathname: `${API_BASE_URL}/${exportType}`, query });
|
||||
jobCompletionNotifications.add(resp.job.id);
|
||||
return resp;
|
||||
};
|
||||
}
|
||||
|
||||
export const reportingClient = new ReportingClient();
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/services/job_completion_notifications';
|
||||
import chrome from 'ui/chrome';
|
||||
import rison from 'rison-node';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { QueryString } from 'ui/utils/query_string';
|
||||
|
||||
uiModules.get('xpack/reporting')
|
||||
.service('reportingDocumentControl', function (Private, $http, reportingJobCompletionNotifications, $injector) {
|
||||
const $Promise = $injector.get('Promise');
|
||||
const mainEntry = '/api/reporting/generate';
|
||||
const reportPrefix = chrome.addBasePath(mainEntry);
|
||||
|
||||
const getJobParams = (exportType, controller, options) => {
|
||||
const jobParamsProvider = Private(exportType.JobParamsProvider);
|
||||
return $Promise.resolve(jobParamsProvider(controller, options));
|
||||
};
|
||||
|
||||
this.getPath = (exportType, controller, options) => {
|
||||
return getJobParams(exportType, controller, options)
|
||||
.then(jobParams => {
|
||||
return `${reportPrefix}/${exportType.id}?${QueryString.param('jobParams', rison.encode(jobParams))}`;
|
||||
});
|
||||
};
|
||||
|
||||
this.create = (relativePath) => {
|
||||
return $http.post(relativePath, {})
|
||||
.then(({ data }) => {
|
||||
reportingJobCompletionNotifications.add(data.job.id);
|
||||
return data;
|
||||
});
|
||||
};
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { ExportTypesRegistry } from '../../common/export_types_registry';
|
||||
|
||||
export const exportTypesRegistry = new ExportTypesRegistry();
|
||||
|
||||
const context = require.context('../../export_types', true, /public\/index.js/);
|
||||
context.keys().forEach(key => context(key).register(exportTypesRegistry));
|
||||
|
||||
uiModules.get('xpack/reporting')
|
||||
.service('reportingExportTypes', function () {
|
||||
this.getById = (exportTypeId) => {
|
||||
return exportTypesRegistry.getById(exportTypeId);
|
||||
};
|
||||
});
|
|
@ -4,12 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LayoutTypes } from '../../common/constants';
|
||||
declare class JobCompletionNotifications {
|
||||
public add(jobId: string): void;
|
||||
}
|
||||
|
||||
export const print = {
|
||||
getJobParams() {
|
||||
return {
|
||||
id: LayoutTypes.PRINT
|
||||
};
|
||||
}
|
||||
};
|
||||
declare const jobCompletionNotifications: JobCompletionNotifications;
|
||||
|
||||
export { jobCompletionNotifications };
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore: implicit any for JS file
|
||||
import { XPackInfoProvider } 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 { ReportingPanelContent } from '../components/reporting_panel_content';
|
||||
|
||||
function reportingProvider(Private: any) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const getShareActions = ({
|
||||
objectType,
|
||||
objectId,
|
||||
sharingData,
|
||||
isDirty,
|
||||
onClose,
|
||||
}: ShareActionProps) => {
|
||||
if ('search' !== objectType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const getJobParams = () => {
|
||||
return {
|
||||
...sharingData,
|
||||
type: objectType,
|
||||
};
|
||||
};
|
||||
|
||||
const shareActions = [];
|
||||
if (xpackInfo.get('features.reporting.csv.showLinks', false)) {
|
||||
const panelTitle = 'CSV Reports';
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: panelTitle,
|
||||
icon: 'document',
|
||||
toolTipContent: xpackInfo.get('features.reporting.csv.message'),
|
||||
disabled: !xpackInfo.get('features.reporting.csv.enableLinks', false) ? true : false,
|
||||
['data-test-subj']: 'csvReportMenuItem',
|
||||
},
|
||||
panel: {
|
||||
title: panelTitle,
|
||||
content: (
|
||||
<ReportingPanelContent
|
||||
reportType="csv"
|
||||
objectType={objectType}
|
||||
objectId={objectId}
|
||||
getJobParams={getJobParams}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return shareActions;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'csvReports',
|
||||
getShareActions,
|
||||
};
|
||||
}
|
||||
|
||||
ShareContextMenuExtensionsRegistryProvider.register(reportingProvider);
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
// @ts-ignore: implicit any for JS file
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
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';
|
||||
|
||||
function reportingProvider(Private: any, dashboardConfig: any) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const getShareActions = ({
|
||||
objectType,
|
||||
objectId,
|
||||
getUnhashableStates,
|
||||
sharingData,
|
||||
isDirty,
|
||||
onClose,
|
||||
}: ShareActionProps) => {
|
||||
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()) {
|
||||
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 browserTimezone =
|
||||
chrome.getUiSettingsClient().get('dateFormat:tz') === 'Browser'
|
||||
? moment.tz.guess()
|
||||
: chrome.getUiSettingsClient().get('dateFormat:tz');
|
||||
|
||||
return {
|
||||
...sharingData,
|
||||
objectType,
|
||||
browserTimezone,
|
||||
relativeUrls: [relativeUrl],
|
||||
};
|
||||
};
|
||||
|
||||
const shareActions = [];
|
||||
if (xpackInfo.get('features.reporting.printablePdf.showLinks', false)) {
|
||||
const panelTitle = 'PDF Reports';
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: panelTitle,
|
||||
icon: 'document',
|
||||
toolTipContent: xpackInfo.get('features.reporting.printablePdf.message'),
|
||||
disabled: !xpackInfo.get('features.reporting.printablePdf.enableLinks', false)
|
||||
? true
|
||||
: false,
|
||||
['data-test-subj']: 'pdfReportMenuItem',
|
||||
},
|
||||
panel: {
|
||||
title: panelTitle,
|
||||
content: (
|
||||
<ScreenCapturePanelContent
|
||||
reportType="printablePdf"
|
||||
objectType={objectType}
|
||||
objectId={objectId}
|
||||
getJobParams={getReportingJobParams}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO register PNG menu item once PNG is supported on server side
|
||||
|
||||
return shareActions;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'screenCaptureReports',
|
||||
getShareActions,
|
||||
};
|
||||
}
|
||||
|
||||
ShareContextMenuExtensionsRegistryProvider.register(reportingProvider);
|
|
@ -78,7 +78,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.discover.loadSavedSearch('A Saved Search');
|
||||
log.debug('click Reporting button');
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
await PageObjects.reporting.openCsvReportingPanel();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
const queueReportError = await PageObjects.reporting.getQueueReportError();
|
||||
expect(queueReportError).to.be(true);
|
||||
|
|
|
@ -15,7 +15,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
const remote = getService('remote');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'security', 'header', 'settings']);
|
||||
const PageObjects = getPageObjects(['common', 'security', 'header', 'settings', 'share']);
|
||||
|
||||
class ReportingPage {
|
||||
async initTests() {
|
||||
|
@ -31,18 +31,6 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
|
|||
await remote.setWindowSize(1600, 850);
|
||||
}
|
||||
|
||||
async clickTopNavReportingLink() {
|
||||
await retry.try(() => testSubjects.click('topNavReportingLink'));
|
||||
}
|
||||
|
||||
async isReportingPanelOpen() {
|
||||
const generateReportButtonExists = await this.getGenerateReportButtonExists();
|
||||
const unsavedChangesWarningExists = await this.getUnsavedChangesWarningExists();
|
||||
const isOpen = generateReportButtonExists || unsavedChangesWarningExists;
|
||||
log.debug('isReportingPanelOpen: ' + isOpen);
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
async getUrlOfTab(tabIndex) {
|
||||
return await retry.try(async () => {
|
||||
log.debug(`reportingPage.getUrlOfTab(${tabIndex}`);
|
||||
|
@ -118,20 +106,14 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
|
|||
});
|
||||
}
|
||||
|
||||
async openReportingPanel() {
|
||||
log.debug('openReportingPanel');
|
||||
await retry.try(async () => {
|
||||
const isOpen = await this.isReportingPanelOpen();
|
||||
async openCsvReportingPanel() {
|
||||
log.debug('openCsvReportingPanel');
|
||||
await PageObjects.share.openShareMenuItem('CSV Reports');
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
await this.clickTopNavReportingLink();
|
||||
}
|
||||
|
||||
const wasOpened = await this.isReportingPanelOpen();
|
||||
if (!wasOpened) {
|
||||
throw new Error('Reporting panel was not opened successfully');
|
||||
}
|
||||
});
|
||||
async openPdfReportingPanel() {
|
||||
log.debug('openPdfReportingPanel');
|
||||
await PageObjects.share.openShareMenuItem('PDF Reports');
|
||||
}
|
||||
|
||||
async clickDownloadReportButton(timeout) {
|
||||
|
@ -143,14 +125,6 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
|
|||
await Promise.all(toasts.map(t => t.click()));
|
||||
}
|
||||
|
||||
async getUnsavedChangesWarningExists() {
|
||||
return await testSubjects.exists('unsavedChangesReportingWarning');
|
||||
}
|
||||
|
||||
async getGenerateReportButtonExists() {
|
||||
return await testSubjects.exists('generateReportButton');
|
||||
}
|
||||
|
||||
async getQueueReportError() {
|
||||
return await testSubjects.exists('queueReportError');
|
||||
}
|
||||
|
@ -159,8 +133,8 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
|
|||
return await retry.try(() => testSubjects.find('generateReportButton'));
|
||||
}
|
||||
|
||||
async clickPreserveLayoutOption() {
|
||||
await retry.try(() => testSubjects.click('preserveLayoutOption'));
|
||||
async checkUsePrintLayout() {
|
||||
await retry.try(() => testSubjects.click('usePrintLayout'));
|
||||
}
|
||||
|
||||
async clickGenerateReportButton() {
|
||||
|
|
|
@ -29,19 +29,18 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.reporting.initTests();
|
||||
});
|
||||
|
||||
const expectUnsavedChangesWarning = async () => {
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
const warningExists = await PageObjects.reporting.getUnsavedChangesWarningExists();
|
||||
expect(warningExists).to.be(true);
|
||||
const buttonExists = await PageObjects.reporting.getGenerateReportButtonExists();
|
||||
expect(buttonExists).to.be(false);
|
||||
const expectDisabledGenerateReportButton = async () => {
|
||||
const generateReportButton = await PageObjects.reporting.getGenerateReportButton();
|
||||
await retry.try(async () => {
|
||||
const isDisabled = await generateReportButton.getProperty('disabled');
|
||||
expect(isDisabled).to.be(true);
|
||||
});
|
||||
};
|
||||
|
||||
const expectEnabledGenerateReportButton = async () => {
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
const printPdfButton = await PageObjects.reporting.getGenerateReportButton();
|
||||
const generateReportButton = await PageObjects.reporting.getGenerateReportButton();
|
||||
await retry.try(async () => {
|
||||
const isDisabled = await printPdfButton.getProperty('disabled');
|
||||
const isDisabled = await generateReportButton.getProperty('disabled');
|
||||
expect(isDisabled).to.be(false);
|
||||
});
|
||||
};
|
||||
|
@ -72,11 +71,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
it('is not available if new', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await expectUnsavedChangesWarning();
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await expectDisabledGenerateReportButton();
|
||||
});
|
||||
|
||||
it('becomes available when saved', async () => {
|
||||
await PageObjects.dashboard.saveDashboard('mydash');
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await expectEnabledGenerateReportButton();
|
||||
});
|
||||
});
|
||||
|
@ -101,7 +102,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
await PageObjects.dashboard.saveDashboard('report test');
|
||||
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.checkUsePrintLayout();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
await PageObjects.reporting.clickDownloadReportButton(60000);
|
||||
|
||||
|
@ -128,7 +130,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.dashboard.switchToEditMode();
|
||||
await PageObjects.dashboard.useMargins(true);
|
||||
await PageObjects.dashboard.saveDashboard('report test');
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.checkUsePrintLayout();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
await PageObjects.reporting.clickDownloadReportButton(60000);
|
||||
|
||||
|
@ -156,9 +159,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
// report than phantom.
|
||||
this.timeout(360000);
|
||||
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 });
|
||||
await PageObjects.reporting.clickPreserveLayoutOption();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
await PageObjects.reporting.removeForceSharedItemsContainerSize();
|
||||
|
||||
|
@ -190,23 +192,25 @@ export default function ({ getService, getPageObjects }) {
|
|||
describe('Generate CSV button', () => {
|
||||
it('is not available if new', async () => {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await expectUnsavedChangesWarning();
|
||||
await PageObjects.reporting.openCsvReportingPanel();
|
||||
await expectDisabledGenerateReportButton();
|
||||
});
|
||||
|
||||
it('becomes available when saved', async () => {
|
||||
await PageObjects.discover.saveSearch('my search');
|
||||
await PageObjects.reporting.openCsvReportingPanel();
|
||||
await expectEnabledGenerateReportButton();
|
||||
});
|
||||
|
||||
it('generates a report with data', async () => {
|
||||
await PageObjects.reporting.setTimepickerInDataRange();
|
||||
await PageObjects.reporting.clickTopNavReportingLink();
|
||||
await PageObjects.reporting.openCsvReportingPanel();
|
||||
await expectReportCanBeCreated();
|
||||
});
|
||||
|
||||
it('generates a report with no data', async () => {
|
||||
await PageObjects.reporting.setTimepickerInNoDataRange();
|
||||
await PageObjects.reporting.clickTopNavReportingLink();
|
||||
await PageObjects.reporting.openCsvReportingPanel();
|
||||
await expectReportCanBeCreated();
|
||||
});
|
||||
});
|
||||
|
@ -218,7 +222,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.common.navigateToUrl('visualize', 'new');
|
||||
await PageObjects.visualize.clickAreaChart();
|
||||
await PageObjects.visualize.clickNewSearch();
|
||||
await expectUnsavedChangesWarning();
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await expectDisabledGenerateReportButton();
|
||||
});
|
||||
|
||||
it('becomes available when saved', async () => {
|
||||
|
@ -227,6 +232,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.visualize.selectAggregation('Date Histogram');
|
||||
await PageObjects.visualize.clickGo();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess('my viz');
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await expectEnabledGenerateReportButton();
|
||||
});
|
||||
|
||||
|
@ -235,7 +241,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
// function is taking about 15 seconds per comparison in jenkins.
|
||||
this.timeout(180000);
|
||||
|
||||
await PageObjects.reporting.openReportingPanel();
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
await PageObjects.reporting.clickDownloadReportButton(60000);
|
||||
|
||||
|
|
Binary file not shown.
|
@ -158,6 +158,12 @@
|
|||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8"
|
||||
|
||||
"@types/moment-timezone@^0.5.8":
|
||||
version "0.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.8.tgz#92aba9bc238cabf69a27a1a4f52e0ebb8f10f896"
|
||||
dependencies:
|
||||
moment ">=2.14.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5"
|
||||
|
@ -5211,6 +5217,10 @@ moment@2.x.x, "moment@>= 2.9.0", moment@^2.13.0, moment@^2.20.1:
|
|||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
|
||||
|
||||
moment@>=2.14.0:
|
||||
version "2.22.2"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
|
||||
|
||||
ms@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -454,6 +454,12 @@
|
|||
version "2.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a"
|
||||
|
||||
"@types/moment-timezone@^0.5.8":
|
||||
version "0.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.8.tgz#92aba9bc238cabf69a27a1a4f52e0ebb8f10f896"
|
||||
dependencies:
|
||||
moment ">=2.14.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "9.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275"
|
||||
|
@ -8995,6 +9001,10 @@ moment@2.x.x, "moment@>= 2.9.0", moment@^2.10.6, moment@^2.13.0, moment@^2.20.1:
|
|||
version "2.21.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a"
|
||||
|
||||
moment@>=2.14.0:
|
||||
version "2.22.2"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue