Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add png report as panel action #26476

Closed
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const mergeProps = (
actions,
embeddable: ownProps.embeddable,
containerState,
closeContextMenu: closeMyContextMenuPanel,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class PanelActionsStore {
* @type {IndexedArray} panelActionsRegistry
*/
public initializeFromRegistry(panelActionsRegistry: ContextMenuAction[]) {
this.actions = [];
panelActionsRegistry.forEach(panelAction => {
this.actions.push(panelAction);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
* under the License.
*/

import url from 'url';
import angular from 'angular';
import { Embeddable } from 'ui/embeddable';
import searchTemplate from './search_template.html';
import * as columnActions from 'ui/doc_table/actions/columns';
import { getTime } from 'ui/timefilter/get_time';
import { RequestAdapter } from 'ui/inspector/adapters';
import rison from 'rison-node';

export class SearchEmbeddable extends Embeddable {
constructor({ onEmbeddableStateChanged, savedSearch, editUrl, loader, $rootScope, $compile }) {
Expand Down Expand Up @@ -58,6 +60,32 @@ export class SearchEmbeddable extends Embeddable {
};
}

/**
* Generates an access URL that will be used in creating a PNG report.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Generates an access URL that will be used in creating a PNG report.
* Generates an access URL that can be used to open up the saved search directly in the Discover App, with the settings applied from the given containerState.

Don't mention PNG report in here, this is on the base embeddable layer now, reporting just happens to use it, but it's not really here specifically for reporting (I think it will be useful in other scenarios as well).

*/
generateAccessLink(containerState) {

const id = this.savedSearch.id;

const appState = rison.encode({ filters: containerState.filters,
uiState: containerState.embeddableCustomization,
query: containerState.query,
});

const globalState = rison.encode({ time: containerState.timeRange });

// Use a url service to ensure everything is escaped properly
const myurl = `/app/kibana#` + url.format({
pathname: '/visualize/edit/' + id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't work, the url for saved searches I think is discover/edit .

Tests should probably be added as part of this PR too, so if the url format changes, the ci will fail.

query: {
'_g': globalState,
'_a': appState,
}
});

return myurl.toString();
}

pushContainerStateParamsToScope() {
// If there is column or sort data on the panel, that means the original columns or sort settings have
// been overridden in a dashboard.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import _ from 'lodash';
// @ts-ignore ts knows this shouldn't be possible, but just making sure
import rison from 'rison-node';
import { ContainerState, Embeddable } from 'ui/embeddable';
import { OnEmbeddableStateChanged } from 'ui/embeddable/embeddable_factory';
import { Filters, Query, TimeRange } from 'ui/embeddable/types';
Expand All @@ -29,6 +31,8 @@ import {
VisualizeLoaderParams,
VisualizeUpdateParams,
} from 'ui/visualize/loader/types';
import url from 'url';
import { VisualizeConstants } from '../visualize_constants';

export interface VisualizeEmbeddableConfiguration {
onEmbeddableStateChanged: OnEmbeddableStateChanged;
Expand Down Expand Up @@ -108,6 +112,34 @@ export class VisualizeEmbeddable extends Embeddable {
}
}

/**
* Generates an access URL that will be used in creating a PNG report.
*/
public generateAccessLink(containerState: ContainerState) {
const id = this.savedVisualization.id;

const appState = rison.encode({
filters: containerState.filters,
uiState: containerState.embeddableCustomization,
query: containerState.query,
});

const globalState = rison.encode({ time: containerState.timeRange });

// Use a url service to ensure everything is escaped properly
const myurl =
`/app/kibana#` +
url.format({
pathname: VisualizeConstants.EDIT_PATH + '/' + id,
query: {
_g: globalState,
_a: appState,
},
});

return myurl.toString();
}

public onContainerStateChanged(containerState: ContainerState) {
this.transferCustomizationsToUiState(containerState);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,19 @@ function buildEuiContextMenuPanelItemsAndChildPanels({
actions,
embeddable,
containerState,
closeContextMenu,
}: {
contextMenuPanelId: string;
actions: ContextMenuAction[];
embeddable?: Embeddable;
containerState: ContainerState;
closeContextMenu: any;
}) {
const items: EuiContextMenuPanelItemDescriptor[] = [];
const childPanels: EuiContextMenuPanelDescriptor[] = [];
const actionsForPanel = getActionsForPanel(contextMenuPanelId, actions);
actionsForPanel.forEach(action => {
const isVisible = action.isVisible({ embeddable, containerState });
const isVisible = action.isVisible({ embeddable, containerState, closeContextMenu });
if (!isVisible) {
return;
}
Expand All @@ -69,6 +71,7 @@ function buildEuiContextMenuPanelItemsAndChildPanels({
actions,
embeddable,
containerState,
closeContextMenu,
})
);
}
Expand All @@ -78,6 +81,7 @@ function buildEuiContextMenuPanelItemsAndChildPanels({
action,
containerState,
embeddable,
closeContextMenu,
})
);
});
Expand All @@ -99,17 +103,19 @@ export function buildEuiContextMenuPanels({
actions,
embeddable,
containerState,
closeContextMenu,
}: {
contextMenuPanel: ContextMenuPanel;
actions: ContextMenuAction[];
embeddable?: Embeddable;
containerState: ContainerState;
closeContextMenu: any;
}): EuiContextMenuPanelDescriptor[] {
const euiContextMenuPanel: EuiContextMenuPanelDescriptor = {
id: contextMenuPanel.id,
title: contextMenuPanel.title,
items: [],
content: contextMenuPanel.getContent({ embeddable, containerState }),
content: contextMenuPanel.getContent({ embeddable, containerState, closeContextMenu }),
};
const contextMenuPanels = [euiContextMenuPanel];

Expand All @@ -118,6 +124,7 @@ export function buildEuiContextMenuPanels({
actions,
embeddable,
containerState,
closeContextMenu,
});

euiContextMenuPanel.items = items;
Expand All @@ -135,29 +142,31 @@ function convertPanelActionToContextMenuItem({
action,
containerState,
embeddable,
closeContextMenu,
}: {
action: ContextMenuAction;
containerState: ContainerState;
embeddable?: Embeddable;
closeContextMenu: any;
}): EuiContextMenuPanelItemDescriptor {
const menuPanelItem: EuiContextMenuPanelItemDescriptor = {
name: action.displayName,
icon: action.icon,
panel: _.get(action, 'childContextMenuPanel.id'),
disabled: action.isDisabled({ embeddable, containerState }),
disabled: action.isDisabled({ embeddable, containerState, closeContextMenu }),
'data-test-subj': `dashboardPanelAction-${action.id}`,
};

if (action.onClick) {
menuPanelItem.onClick = () => {
if (action.onClick) {
action.onClick({ embeddable, containerState });
action.onClick({ embeddable, containerState, closeContextMenu });
}
};
}

if (action.getHref) {
menuPanelItem.href = action.getHref({ embeddable, containerState });
menuPanelItem.href = action.getHref({ embeddable, containerState, closeContextMenu });
}

return menuPanelItem;
Expand Down
2 changes: 2 additions & 0 deletions src/ui/public/embeddable/context_menu_actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ export interface PanelActionAPI {
* Information about the current state of the panel and dashboard.
*/
containerState: ContainerState;

closeContextMenu(): void;
}
1 change: 1 addition & 0 deletions src/ui/public/visualize/loader/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface Query {
}

export interface VisSavedObject {
id: string;
vis: Vis;
description?: string;
searchSource: SearchSource;
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/reporting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export const reporting = (kibana) => {
'plugins/reporting/share_context_menu/register_csv_reporting',
'plugins/reporting/share_context_menu/register_reporting',
],
contextMenuActions: [
'plugins/reporting/panel_actions/get_png_report_panel_action',
'plugins/reporting/panel_actions/get_csv_report_panel_action',
],
hacks: ['plugins/reporting/hacks/job_completion_notifier'],
home: ['plugins/reporting/register_feature'],
managementSections: ['plugins/reporting/views/management'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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 { i18n } from '@kbn/i18n';

import rison from 'rison-node';
import {
ContainerState,
ContextMenuAction,
ContextMenuActionsRegistryProvider,
Embeddable,
} from 'ui/embeddable';
import { kfetch } from 'ui/kfetch';
import { toastNotifications } from 'ui/notify';
import { SearchEmbeddable } from '../../../../../src/core_plugins/kibana/public/discover/embeddable/search_embeddable';
import { jobCompletionNotifications } from '../lib/job_completion_notifications';

class GetCsvReportPanelAction extends ContextMenuAction {
constructor() {
super(
{
displayName: i18n.translate('kbn.dashboard.panel.reportPanel.displayName', {
defaultMessage: 'CSV Report',
}),
id: 'openReport',
parentPanelId: 'mainMenu',
},
{
icon: 'document',
}
);
}
public isVisible({ embeddable }: { embeddable: Embeddable }): boolean {
if (!embeddable) {
return false;
}
if (!(embeddable instanceof SearchEmbeddable)) {
return false;
}
return true;
}

public async onClick({
embeddable,
containerState,
closeContextMenu,
}: {
embeddable: Embeddable;
closeContextMenu: any;
containerState: ContainerState;
}) {
if (!embeddable) {
return;
}
if (!(embeddable instanceof SearchEmbeddable)) {
return;
}
const searchEmbeddable = embeddable as SearchEmbeddable;

if (!searchEmbeddable.savedSearch.id) {
return;
}

closeContextMenu();

const searchURL = searchEmbeddable.generateAccessLink(containerState);

const reportconfig = {
browserTimezone: 'America/Phoenix',
layout: {
dimensions: {
width: 960,
height: 720,
},
},
objectType: 'search',
relativeUrl: searchURL,
title: 'searchEmbeddable.getPanelTitle(containerState)',
};

const query = {
jobParams: rison.encode(reportconfig),
};

const API_BASE_URL = '/api/reporting/generate';

toastNotifications.addSuccess({
title: `Queued report for ${reportconfig.objectType}`,
text: 'Track its progress in Management',
'data-test-subj': 'queueReportSuccess',
});

const resp = await kfetch({ method: 'POST', pathname: `${API_BASE_URL}/csv`, query });

jobCompletionNotifications.add(resp.job.id);
}
}

ContextMenuActionsRegistryProvider.register(() => new GetCsvReportPanelAction());
Loading