Skip to content

Commit

Permalink
[Reporting] fix dashboard "Copy Post URL" action (elastic#192530)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic#191673
Closes elastic#183566

Fixes the ability for the POST URL used to automate generation of
reports by adding a `generateExportUrl` function to the ShareMenuItemV2
interface. This function returns a dynamic export URL for PDF generation
by using the selected layout option.

Other changes: provides more strictness in type definitions by:
  * splitting the types that define `ShareMenuProvider`:
    * `ShareMenuProviderV2` provides the `getShareMenuItems` function
* `ShareMenuProviderLegacy` provides the `getShareMenuItemsLegacy`
function

### Release note
Fixed an issue with the export options for PNG/PDF reports in a
dashboard.

### Checklist

Delete any items that are not applicable to this PR.

- [x] Use the `generateExportUrl` function inputs to return a POST URL
that is aware of the layout mode (`print` or `preserve_layout`) and
screen dimensions
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Flaky test runner:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6986

(cherry picked from commit 38407ae)

# Conflicts:
#	src/plugins/share/public/components/share_tabs.test.tsx
#	src/plugins/share/public/services/share_menu_manager.tsx
  • Loading branch information
tsullivan committed Oct 18, 2024
1 parent eb30bae commit 37d6aee
Show file tree
Hide file tree
Showing 23 changed files with 323 additions and 254 deletions.
2 changes: 1 addition & 1 deletion examples/share_examples/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class ShareDemoPlugin implements Plugin<void, void, SetupDeps, StartDeps>
public setup(core: CoreSetup<StartDeps>, { share }: SetupDeps) {
share.register({
id: 'demo',
getShareMenuItems: (context) => [
getShareMenuItemsLegacy: (context) => [
{
panel: {
id: 'demo',
Expand Down
3 changes: 3 additions & 0 deletions packages/kbn-reporting/public/reporting_api_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ export class ReportingAPIClient implements IReportingAPI {
});
}

/**
* Adds the browserTimezone and kibana version to report job params
*/
public getDecoratedJobParams<T extends AppParams>(baseParams: T): BaseParams {
// If the TZ is set to the default "Browser", it will not be useful for
// server-side export. We need to derive the timezone and pass it as a param
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import * as Rx from 'rxjs';

import type { ApplicationStart, CoreStart } from '@kbn/core/public';
import { ILicense } from '@kbn/licensing-plugin/public';
import type { LayoutParams } from '@kbn/screenshotting-plugin/common';

import type { ReportingAPIClient } from '../../reporting_api_client';

Expand Down Expand Up @@ -46,13 +45,16 @@ export interface ExportPanelShareOpts {

export interface ReportingSharingData {
title: string;
layout: LayoutParams;
reportingDisabled?: boolean;
[key: string]: unknown;
locatorParams: {
id: string;
params: unknown;
};
}

export interface JobParamsProviderOptions {
sharingData: ReportingSharingData;
shareableUrl?: string;
objectType: string;
optimizedForPrinting?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CSV_JOB_TYPE, CSV_JOB_TYPE_V2 } from '@kbn/reporting-export-types-csv-c

import type { SearchSourceFields } from '@kbn/data-plugin/common';
import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react';
import { ShareContext, ShareMenuItem } from '@kbn/share-plugin/public';
import { ShareContext, ShareMenuItemV2 } from '@kbn/share-plugin/public';
import type { ExportModalShareOpts } from '.';
import { checkLicense } from '../..';

Expand Down Expand Up @@ -68,7 +68,7 @@ export const reportingCsvShareProvider = ({
};
};

const shareActions: ShareMenuItem[] = [];
const shareActions: ShareMenuItemV2[] = [];

const licenseCheck = checkLicense(license.check('reporting', 'basic'));
const licenseToolTipContent = licenseCheck.message;
Expand Down Expand Up @@ -176,8 +176,8 @@ export const reportingCsvShareProvider = ({
/>
),
generateExport: generateReportingJobCSV,
generateExportUrl: () => absoluteUrl,
generateCopyUrl: reportingUrl,
absoluteUrl,
renderCopyURLButton: true,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,28 @@
*/

import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { ShareContext, ShareMenuItem, ShareMenuProvider } from '@kbn/share-plugin/public';
import { ShareContext, ShareMenuItemV2, ShareMenuProvider } from '@kbn/share-plugin/public';
import React from 'react';
import { firstValueFrom } from 'rxjs';
import {
ExportModalShareOpts,
ExportPanelShareOpts,
JobParamsProviderOptions,
ReportingSharingData,
} from '.';
import { ScreenshotExportOpts } from '@kbn/share-plugin/public/types';
import { ExportModalShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
import { checkLicense } from '../../license_check';
import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';

const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printablePdfV2') => () => {
const {
objectType,
sharingData: { title, layout, locatorParams },
sharingData: { title, locatorParams },
optimizedForPrinting,
} = opts;

const baseParams = {
objectType,
layout,
title,
};
const el = document.querySelector('[data-shared-items-container]');
const { height, width } = el ? el.getBoundingClientRect() : { height: 768, width: 1024 };
const dimensions = { height, width };
const layoutId = optimizedForPrinting ? ('print' as const) : ('preserve_layout' as const);
const layout = { id: layoutId, dimensions };
const baseParams = { objectType, layout, title };

if (type === 'printablePdfV2') {
// multi locator for PDF V2
Expand All @@ -42,154 +39,8 @@ const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printable
};

/**
* This is used by Canvas
* This is used by Dashboard and Visualize apps (sharing modal)
*/
export const reportingScreenshotShareProvider = ({
apiClient,
license,
application,
usesUiCapabilities,
startServices$,
}: ExportPanelShareOpts): ShareMenuProvider => {
const getShareMenuItems = ({
objectType,
objectId,
isDirty,
onClose,
shareableUrl,
shareableUrlForSavedObject,
...shareOpts
}: ShareContext) => {
const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'gold'));
const licenseToolTipContent = message;
const licenseHasScreenshotReporting = showLinks;
const licenseDisabled = !enableLinks;

let capabilityHasDashboardScreenshotReporting = false;
let capabilityHasVisualizeScreenshotReporting = false;
if (usesUiCapabilities) {
capabilityHasDashboardScreenshotReporting =
application.capabilities.dashboard?.generateScreenshot === true;
capabilityHasVisualizeScreenshotReporting =
application.capabilities.visualize?.generateScreenshot === true;
} else {
// deprecated
capabilityHasDashboardScreenshotReporting = true;
capabilityHasVisualizeScreenshotReporting = true;
}

if (!licenseHasScreenshotReporting) {
return [];
}
const isSupportedType = ['dashboard', 'visualization', 'lens'].includes(objectType);

if (!isSupportedType) {
return [];
}

if (objectType === 'dashboard' && !capabilityHasDashboardScreenshotReporting) {
return [];
}

if (
isSupportedType &&
!capabilityHasVisualizeScreenshotReporting &&
!capabilityHasDashboardScreenshotReporting
) {
return [];
}

const { sharingData } = shareOpts as unknown as { sharingData: ReportingSharingData };
const shareActions: ShareMenuItem[] = [];

const pngPanelTitle = i18n.translate('reporting.share.contextMenu.pngReportsButtonLabel', {
defaultMessage: 'PNG Reports',
});

const jobProviderOptions: JobParamsProviderOptions = {
shareableUrl: isDirty ? shareableUrl : shareableUrlForSavedObject ?? shareableUrl,
objectType,
sharingData,
};
const isJobV2Params = ({
sharingData: _sharingData,
}: {
sharingData: Record<string, unknown>;
}) => _sharingData.locatorParams != null;

const isV2Job = isJobV2Params(jobProviderOptions);
const requiresSavedState = !isV2Job;

const panelPng = {
shareMenuItem: {
name: pngPanelTitle,
icon: 'document',
toolTipContent: licenseToolTipContent,
disabled: licenseDisabled || sharingData.reportingDisabled,
['data-test-subj']: 'PNGReports',
sortOrder: 10,
},
panel: {
id: 'reportingPngPanel',
title: pngPanelTitle,
content: (
<ScreenCapturePanelContent
apiClient={apiClient}
startServices$={startServices$}
reportType={'pngV2'}
objectId={objectId}
requiresSavedState={requiresSavedState}
getJobParams={getJobParams(jobProviderOptions, 'pngV2')}
isDirty={isDirty}
onClose={onClose}
/>
),
},
};

const pdfPanelTitle = i18n.translate('reporting.share.contextMenu.pdfReportsButtonLabel', {
defaultMessage: 'PDF Reports',
});

const panelPdf = {
shareMenuItem: {
name: pdfPanelTitle,
icon: 'document',
toolTipContent: licenseToolTipContent,
disabled: licenseDisabled || sharingData.reportingDisabled,
['data-test-subj']: 'PDFReports',
sortOrder: 10,
},
panel: {
id: 'reportingPdfPanel',
title: pdfPanelTitle,
content: (
<ScreenCapturePanelContent
apiClient={apiClient}
startServices$={startServices$}
reportType={'printablePdfV2'}
objectId={objectId}
requiresSavedState={requiresSavedState}
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
getJobParams={getJobParams(jobProviderOptions, 'printablePdfV2')}
isDirty={isDirty}
onClose={onClose}
/>
),
},
};

shareActions.push(panelPng);
shareActions.push(panelPdf);
return shareActions;
};

return {
id: 'screenCaptureReports',
getShareMenuItems,
};
};

export const reportingExportModalProvider = ({
apiClient,
license,
Expand Down Expand Up @@ -248,7 +99,7 @@ export const reportingExportModalProvider = ({
}

const { sharingData } = shareOpts as unknown as { sharingData: ReportingSharingData };
const shareActions: ShareMenuItem[] = [];
const shareActions: ShareMenuItemV2[] = [];

const jobProviderOptions: JobParamsProviderOptions = {
shareableUrl: isDirty ? shareableUrl : shareableUrlForSavedObject ?? shareableUrl,
Expand All @@ -258,32 +109,9 @@ export const reportingExportModalProvider = ({

const requiresSavedState = sharingData.locatorParams === null;

const relativePathPDF = apiClient.getReportingPublicJobPath(
'printablePdfV2',
apiClient.getDecoratedJobParams(getJobParams(jobProviderOptions, 'printablePdfV2')())
);

const relativePathPNG = apiClient.getReportingPublicJobPath(
'pngV2',
apiClient.getDecoratedJobParams(getJobParams(jobProviderOptions, 'pngV2')())
);

const generateReportPDF = ({
intl,
optimizedForPrinting = false,
}: {
intl: InjectedIntl;
optimizedForPrinting?: boolean;
}) => {
const el = document.querySelector('[data-shared-items-container]');
const { height, width } = el ? el.getBoundingClientRect() : { height: 768, width: 1024 };
const dimensions = { height, width };

const generateReportPDF = ({ intl, optimizedForPrinting = false }: ScreenshotExportOpts) => {
const decoratedJobParams = apiClient.getDecoratedJobParams({
...getJobParams(jobProviderOptions, 'printablePdfV2')(),
layout: { id: optimizedForPrinting ? 'print' : 'preserve_layout', dimensions },
objectType,
title: sharingData.title,
...getJobParams({ ...jobProviderOptions, optimizedForPrinting }, 'printablePdfV2')(),
});

return apiClient
Expand Down Expand Up @@ -329,19 +157,27 @@ export const reportingExportModalProvider = ({
});
};

const generateReportPNG = ({ intl }: { intl: InjectedIntl }) => {
const { layout: outerLayout } = getJobParams(jobProviderOptions, 'pngV2')();
let dimensions = outerLayout?.dimensions;
if (!dimensions) {
const el = document.querySelector('[data-shared-items-container]');
const { height, width } = el ? el.getBoundingClientRect() : { height: 768, width: 1024 };
dimensions = { height, width };
}
const generateExportUrlPDF = ({ optimizedForPrinting }: ScreenshotExportOpts) => {
const jobParams = apiClient.getDecoratedJobParams(
getJobParams({ ...jobProviderOptions, optimizedForPrinting }, 'printablePdfV2')()
);
const relativePathPDF = apiClient.getReportingPublicJobPath('printablePdfV2', jobParams);

return new URL(relativePathPDF, window.location.href).toString();
};

const generateExportUrlPNG = () => {
const jobParams = apiClient.getDecoratedJobParams(
getJobParams(jobProviderOptions, 'pngV2')()
);
const relativePathPNG = apiClient.getReportingPublicJobPath('pngV2', jobParams);

return new URL(relativePathPNG, window.location.href).toString();
};

const generateReportPNG = ({ intl }: ScreenshotExportOpts) => {
const decoratedJobParams = apiClient.getDecoratedJobParams({
...getJobParams(jobProviderOptions, 'pngV2')(),
layout: { id: 'preserve_layout', dimensions },
objectType,
title: sharingData.title,
});
return apiClient
.createReportingJob('pngV2', decoratedJobParams)
Expand Down Expand Up @@ -397,6 +233,7 @@ export const reportingExportModalProvider = ({
},
label: 'PDF' as const,
generateExport: generateReportPDF,
generateExportUrl: generateExportUrlPDF,
reportType: 'printablePdfV2',
requiresSavedState,
helpText: (
Expand All @@ -414,7 +251,6 @@ export const reportingExportModalProvider = ({
layoutOption: objectType === 'dashboard' ? ('print' as const) : undefined,
renderLayoutOptionSwitch: objectType === 'dashboard',
renderCopyURLButton: true,
absoluteUrl: new URL(relativePathPDF, window.location.href).toString(),
});

shareActions.push({
Expand All @@ -428,6 +264,7 @@ export const reportingExportModalProvider = ({
},
label: 'PNG' as const,
generateExport: generateReportPNG,
generateExportUrl: generateExportUrlPNG,
reportType: 'pngV2',
requiresSavedState,
helpText: (
Expand All @@ -441,7 +278,6 @@ export const reportingExportModalProvider = ({
),
layoutOption: objectType === 'dashboard' ? ('print' as const) : undefined,
renderCopyURLButton: true,
absoluteUrl: new URL(relativePathPNG, window.location.href).toString(),
});

return shareActions;
Expand Down
Loading

0 comments on commit 37d6aee

Please sign in to comment.