[Reporting] fix dashboard "Copy Post URL" action (#192530)

## Summary

Closes https://github.com/elastic/kibana/issues/191673
Closes https://github.com/elastic/kibana/issues/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
This commit is contained in:
Tim Sullivan 2024-10-07 16:54:21 -07:00 committed by GitHub
parent 28d6a22263
commit 38407ae6b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 220 additions and 257 deletions

View file

@ -22,7 +22,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',

View file

@ -227,6 +227,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

View file

@ -11,7 +11,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';
@ -47,13 +46,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;
}

View file

@ -16,7 +16,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 '../..';
@ -69,7 +69,7 @@ export const reportingCsvShareProvider = ({
};
};
const shareActions: ShareMenuItem[] = [];
const shareActions: ShareMenuItemV2[] = [];
const licenseCheck = checkLicense(license.check('reporting', 'basic'));
const licenseToolTipContent = licenseCheck.message;
@ -177,8 +177,8 @@ export const reportingCsvShareProvider = ({
/>
),
generateExport: generateReportingJobCSV,
generateExportUrl: () => absoluteUrl,
generateCopyUrl: reportingUrl,
absoluteUrl,
renderCopyURLButton: true,
});
}

View file

@ -8,31 +8,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
@ -43,154 +40,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,
@ -249,7 +100,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,
@ -259,32 +110,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
@ -330,19 +158,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)
@ -398,6 +234,7 @@ export const reportingExportModalProvider = ({
},
label: 'PDF' as const,
generateExport: generateReportPDF,
generateExportUrl: generateExportUrlPDF,
reportType: 'printablePdfV2',
requiresSavedState,
helpText: (
@ -415,7 +252,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({
@ -429,6 +265,7 @@ export const reportingExportModalProvider = ({
},
label: 'PNG' as const,
generateExport: generateReportPNG,
generateExportUrl: generateExportUrlPNG,
reportType: 'pngV2',
requiresSavedState,
helpText: (
@ -442,7 +279,6 @@ export const reportingExportModalProvider = ({
),
layoutOption: objectType === 'dashboard' ? ('print' as const) : undefined,
renderCopyURLButton: true,
absoluteUrl: new URL(relativePathPNG, window.location.href).toString(),
});
return shareActions;

View file

@ -13,7 +13,7 @@ import { createContext, useContext } from 'react';
import { AnonymousAccessServiceContract } from '../../../common';
import type {
ShareMenuItem,
ShareMenuItemV2,
UrlParamExtension,
BrowserUrlService,
ShareContext,
@ -24,7 +24,7 @@ export type { ShareMenuItemV2 } from '../../types';
export interface IShareContext extends ShareContext {
allowEmbed: boolean;
allowShortUrl: boolean;
shareMenuItems: ShareMenuItem[];
shareMenuItems: ShareMenuItemV2[];
embedUrlParamExtensions?: UrlParamExtension[];
anonymousAccess?: AnonymousAccessServiceContract;
urlService: BrowserUrlService;

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ShareMenuItem } from '../types';
import { ShareMenuItemLegacy } from '../types';
import React from 'react';
import { shallow } from 'enzyme';
@ -40,7 +40,7 @@ test('should disable the share URL when set', () => {
});
describe('shareContextMenuExtensions', () => {
const shareContextMenuItems: ShareMenuItem[] = [
const shareContextMenuItems: ShareMenuItemLegacy[] = [
{
panel: {
id: '1',

View file

@ -17,7 +17,7 @@ import type { Capabilities } from '@kbn/core/public';
import type { LocatorPublic } from '../../common';
import { UrlPanelContent } from './url_panel_content';
import { ShareMenuItem, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
import { ShareMenuItemLegacy, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
import { AnonymousAccessServiceContract } from '../../common/anonymous_access';
import type { BrowserUrlService } from '../types';
@ -32,7 +32,7 @@ export interface ShareContextMenuProps {
locator: LocatorPublic<any>;
params: any;
};
shareMenuItems: ShareMenuItem[];
shareMenuItems: ShareMenuItemLegacy[];
sharingData: any;
onClose: () => void;
embedUrlParamExtensions?: UrlParamExtension[];

View file

@ -62,12 +62,19 @@ const mockShareContext = {
toasts: toastsServiceMock.createStartContract(),
i18n: i18nServiceMock.createStartContract(),
};
const mockGenerateExport = jest.fn();
const mockGenerateExportUrl = jest.fn().mockImplementation(() => 'generated-export-url');
const CSV = 'CSV' as const;
const PNG = 'PNG' as const;
describe('Share modal tabs', () => {
it('should render export tab when there are share menu items that are not disabled', async () => {
const testItem = [
{ shareMenuItem: { name: 'test', disabled: false }, label: CSV, generateExport: jest.fn() },
{
shareMenuItem: { name: 'test', disabled: false },
label: CSV,
generateExport: mockGenerateExport,
generateExportUrl: mockGenerateExportUrl,
},
];
const wrapper = mountWithIntl(
<ShareTabsContext.Provider value={{ ...mockShareContext, shareMenuItems: testItem }}>
@ -78,7 +85,12 @@ describe('Share modal tabs', () => {
});
it('should not render export tab when the license is disabled', async () => {
const testItems = [
{ shareMenuItem: { name: 'test', disabled: true }, label: CSV, generateExport: jest.fn() },
{
shareMenuItem: { name: 'test', disabled: true },
label: CSV,
generateExport: mockGenerateExport,
generateExportUrl: mockGenerateExportUrl,
},
];
const wrapper = mountWithIntl(
<ShareTabsContext.Provider value={{ ...mockShareContext, shareMenuItems: testItems }}>
@ -90,8 +102,18 @@ describe('Share modal tabs', () => {
it('should render export tab is at least one is not disabled', async () => {
const testItem = [
{ shareMenuItem: { name: 'test', disabled: false }, label: CSV, generateExport: jest.fn() },
{ shareMenuItem: { name: 'test', disabled: true }, label: PNG, generateExport: jest.fn() },
{
shareMenuItem: { name: 'test', disabled: false },
label: CSV,
generateExport: mockGenerateExport,
generateExportUrl: mockGenerateExportUrl,
},
{
shareMenuItem: { name: 'test', disabled: true },
label: PNG,
generateExport: mockGenerateExport,
generateExportUrl: mockGenerateExportUrl,
},
];
const wrapper = mountWithIntl(
<ShareTabsContext.Provider value={{ ...mockShareContext, shareMenuItems: testItem }}>

View file

@ -64,7 +64,7 @@ const ExportContentUi = ({
helpText,
renderCopyURLButton,
generateExport,
absoluteUrl,
generateExportUrl,
renderLayoutOptionSwitch,
} = useMemo(() => {
return aggregateReportTypes?.find(({ reportType }) => reportType === selectedRadio)!;
@ -124,7 +124,8 @@ const ExportContentUi = ({
}, [usePrintLayout, renderLayoutOptionSwitch, handlePrintLayoutChange]);
const showCopyURLButton = useCallback(() => {
if (renderCopyURLButton && publicAPIEnabled)
if (renderCopyURLButton && publicAPIEnabled) {
const absoluteUrl = generateExportUrl?.({ intl, optimizedForPrinting: usePrintLayout });
return (
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false} css={{ flexGrow: 0 }}>
<EuiFlexItem grow={false}>
@ -160,7 +161,8 @@ const ExportContentUi = ({
</EuiFlexItem>
</EuiFlexGroup>
);
}, [absoluteUrl, renderCopyURLButton, publicAPIEnabled]);
}
}, [renderCopyURLButton, publicAPIEnabled, usePrintLayout, generateExportUrl, intl]);
const renderGenerateReportButton = useCallback(() => {
return (

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal';
import { ExportContent } from './export_content';
import { useShareTabsContext, type ShareMenuItemV2 } from '../../context';
import { useShareTabsContext } from '../../context';
type IExportTab = IModalTabDeclaration;
@ -23,8 +23,7 @@ const ExportTabContent = () => {
objectType={objectType}
isDirty={isDirty}
onClose={onClose}
// we are guaranteed that shareMenuItems will be a ShareMenuItem V2 variant
aggregateReportTypes={shareMenuItems as unknown as ShareMenuItemV2[]}
aggregateReportTypes={shareMenuItems}
publicAPIEnabled={publicAPIEnabled ?? true}
/>
);

View file

@ -23,7 +23,8 @@ export type {
export type {
ShareContext,
ShareMenuProvider,
ShareMenuItem,
ShareMenuItemLegacy,
ShareMenuItemV2,
ShowShareMenuOptions,
ShareContextMenuPanelItem,
BrowserUrlService,

View file

@ -11,10 +11,10 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { CoreStart, ThemeServiceStart, ToastsSetup } from '@kbn/core/public';
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
import { ShowShareMenuOptions } from '../types';
import { ShareMenuRegistryStart } from './share_menu_registry';
import { AnonymousAccessServiceContract } from '../../common/anonymous_access';
import type { BrowserUrlService } from '../types';
import type { BrowserUrlService, ShareMenuItemV2 } from '../types';
import { ShareMenu } from '../components/share_tabs';
export class ShareMenuManager {
@ -89,7 +89,7 @@ export class ShareMenuManager {
publicAPIEnabled,
}: ShowShareMenuOptions & {
anchorElement: HTMLElement;
menuItems: ShareMenuItem[];
menuItems: ShareMenuItemV2[];
urlService: BrowserUrlService;
anonymousAccess: AnonymousAccessServiceContract | undefined;
theme: ThemeServiceStart;

View file

@ -13,7 +13,7 @@ import {
ShareMenuRegistrySetup,
ShareMenuRegistryStart,
} from './share_menu_registry';
import { ShareMenuItem, ShareContext } from '../types';
import { ShareMenuItemV2, ShareContext } from '../types';
const createSetupMock = (): jest.Mocked<ShareMenuRegistrySetup> => {
const setup = {
@ -24,7 +24,7 @@ const createSetupMock = (): jest.Mocked<ShareMenuRegistrySetup> => {
const createStartMock = (): jest.Mocked<ShareMenuRegistryStart> => {
const start = {
getShareMenuItems: jest.fn((props: ShareContext) => [] as ShareMenuItem[]),
getShareMenuItems: jest.fn((_props: ShareContext) => [] as ShareMenuItemV2[]),
};
return start;
};

View file

@ -8,7 +8,7 @@
*/
import { ShareMenuRegistry } from './share_menu_registry';
import { ShareMenuItem, ShareContext } from '../types';
import { ShareMenuItemV2, ShareContext } from '../types';
describe('ShareActionsRegistry', () => {
describe('setup', () => {
@ -34,9 +34,9 @@ describe('ShareActionsRegistry', () => {
test('returns a flat list of actions returned by all providers', () => {
const service = new ShareMenuRegistry();
const registerFunction = service.setup().register;
const shareAction1 = {} as ShareMenuItem;
const shareAction2 = {} as ShareMenuItem;
const shareAction3 = {} as ShareMenuItem;
const shareAction1 = {} as ShareMenuItemV2;
const shareAction2 = {} as ShareMenuItemV2;
const shareAction3 = {} as ShareMenuItemV2;
const provider1Callback = jest.fn(() => [shareAction1]);
const provider2Callback = jest.fn(() => [shareAction2, shareAction3]);
registerFunction({

View file

@ -7,7 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ShareContext, ShareMenuProvider } from '../types';
import {
ShareContext,
ShareMenuProvider,
ShareMenuProviderV2,
ShareMenuProviderLegacy,
} from '../types';
export class ShareMenuRegistry {
private readonly shareMenuProviders = new Map<string, ShareMenuProvider>();
@ -36,7 +41,10 @@ export class ShareMenuRegistry {
return {
getShareMenuItems: (context: ShareContext) =>
Array.from(this.shareMenuProviders.values()).flatMap((shareActionProvider) =>
shareActionProvider.getShareMenuItems(context)
(
(shareActionProvider as ShareMenuProviderV2).getShareMenuItems ??
(shareActionProvider as ShareMenuProviderLegacy).getShareMenuItemsLegacy
).call(shareActionProvider, context)
),
};
}

View file

@ -100,10 +100,16 @@ export type SupportedExportTypes =
interface ShareMenuItemBase {
shareMenuItem?: ShareContextMenuPanelItem;
}
interface ShareMenuItemLegacy extends ShareMenuItemBase {
export interface ShareMenuItemLegacy extends ShareMenuItemBase {
panel?: EuiContextMenuPanelDescriptor;
}
export interface ScreenshotExportOpts {
optimizedForPrinting?: boolean;
intl: InjectedIntl;
}
export interface ShareMenuItemV2 extends ShareMenuItemBase {
// extended props to support share modal
label: 'PDF' | 'CSV' | 'PNG';
@ -112,21 +118,31 @@ export interface ShareMenuItemV2 extends ShareMenuItemBase {
helpText?: ReactElement;
copyURLButton?: { id: string; dataTestSubj: string; label: string };
generateExportButton?: ReactElement;
generateExport: (args: {
intl: InjectedIntl;
optimizedForPrinting?: boolean;
}) => Promise<unknown>;
/**
* Function to trigger an export
*/
generateExport: (args: ScreenshotExportOpts) => Promise<unknown>;
/**
* Function to generate a URL to be used for automating export
* Not applicable for exports that do not call a remote API (i.e Lens CSV export)
*/
generateExportUrl?: (args: ScreenshotExportOpts) => string | undefined;
theme?: ThemeServiceSetup;
renderLayoutOptionSwitch?: boolean;
layoutOption?: 'print';
absoluteUrl?: string;
generateCopyUrl?: URL;
renderCopyURLButton?: boolean;
}
export type ShareMenuItem = ShareMenuItemLegacy | ShareMenuItemV2;
export interface ShareMenuProviderV2 {
readonly id: string;
getShareMenuItems: (context: ShareContext) => Array<Omit<ShareMenuItemV2, 'intl'>>;
}
export interface ShareMenuProviderLegacy {
readonly id: string;
getShareMenuItemsLegacy: (context: ShareContext) => ShareMenuItemLegacy[];
}
type ShareMenuItemType = Omit<ShareMenuItem, 'intl'>;
/**
* @public
* A source for additional menu items shown in the share context menu. Any provider
@ -134,10 +150,7 @@ type ShareMenuItemType = Omit<ShareMenuItem, 'intl'>;
* menu. Returned `ShareMenuItem`s will be shown in the context menu together with the
* default built-in share options. Each share provider needs a globally unique id.
* */
export interface ShareMenuProvider {
readonly id: string;
getShareMenuItems: (context: ShareContext) => ShareMenuItemType[];
}
export type ShareMenuProvider = ShareMenuProviderV2 | ShareMenuProviderLegacy;
interface UrlParamExtensionProps {
setParamValue: (values: {}) => void;

View file

@ -144,8 +144,8 @@ export const downloadCsvShareProvider = ({
return [
{
...menuItemMetadata,
label: 'CSV',
reportType: 'lens_csv',
label: 'CSV' as const,
reportType: 'lens_csv' as const,
generateExport: downloadCSVHandler,
...(atLeastGold()
? {

View file

@ -6122,8 +6122,6 @@
"reporting.printablePdfV2.generateButtonLabel": "Exporter un fichier",
"reporting.printablePdfV2.helpText": "Sélectionnez le type de fichier que vous souhaitez exporter pour cette visualisation.",
"reporting.share.contextMenu.export.csvReportsButtonLabel": "Exporter",
"reporting.share.contextMenu.pdfReportsButtonLabel": "Rapports PDF",
"reporting.share.contextMenu.pngReportsButtonLabel": "Rapports PNG",
"reporting.share.csv.reporting.helpTextCSV": "Exporter un fichier CSV à partir de ce {objectType}.",
"reporting.share.generateButtonLabelCSV": "Générer un CSV",
"reporting.share.modalContent.notification.reportingErrorTitle": "Impossible de créer le rapport",

View file

@ -5876,8 +5876,6 @@
"reporting.printablePdfV2.generateButtonLabel": "ファイルのエクスポート",
"reporting.printablePdfV2.helpText": "このビジュアライゼーションでエクスポートするファイルタイプを選択します。",
"reporting.share.contextMenu.export.csvReportsButtonLabel": "エクスポート",
"reporting.share.contextMenu.pdfReportsButtonLabel": "PDF レポート",
"reporting.share.contextMenu.pngReportsButtonLabel": "PNG レポート",
"reporting.share.csv.reporting.helpTextCSV": "この{objectType}のCSVをエクスポートします。",
"reporting.share.generateButtonLabelCSV": "CSVを生成",
"reporting.share.modalContent.notification.reportingErrorTitle": "レポートを作成できません",

View file

@ -5889,8 +5889,6 @@
"reporting.printablePdfV2.generateButtonLabel": "导出文件",
"reporting.printablePdfV2.helpText": "为此可视化选择您要导出的文件类型。",
"reporting.share.contextMenu.export.csvReportsButtonLabel": "导出",
"reporting.share.contextMenu.pdfReportsButtonLabel": "PDF 报告",
"reporting.share.contextMenu.pngReportsButtonLabel": "PNG 报告",
"reporting.share.csv.reporting.helpTextCSV": "导出此 {objectType} 的 CSV。",
"reporting.share.generateButtonLabelCSV": "生成 CSV",
"reporting.share.modalContent.notification.reportingErrorTitle": "无法创建报告",

View file

@ -127,6 +127,34 @@ export default function ({
expect(res.get('content-type')).to.equal('application/pdf');
await share.closeShareModal();
});
it('provides a button to copy POST URL', async () => {
// The "clipboard-read" permission of the Permissions API must be granted
if (!(await browser.checkBrowserPermission('clipboard-read'))) {
return;
}
await dashboard.navigateToApp();
await dashboard.loadSavedDashboard('Ecom Dashboard');
await reporting.openExportTab();
await reporting.checkUsePrintLayout();
await testSubjects.click('shareReportingCopyURL');
const postUrl = await browser.getClipboardValue();
expect(postUrl).to.contain('printablePdfV2');
const [, jobParams] = postUrl.split('jobParams=');
expect(decodeURIComponent(jobParams)).to.contain('browserTimezone:UTC,');
expect(decodeURIComponent(jobParams)).to.match(
/layout:\(dimensions:\(height:1\d{3},width:1\d{3}\),id:print\),/
);
expect(decodeURIComponent(jobParams)).to.match(
/objectType:dashboard,title:'Ecom Dashboard',/
);
expect(decodeURIComponent(jobParams)).to.match(
/locatorParams:.*id:DASHBOARD_APP_LOCATOR,params:\(dashboardId:'6c263e00-1c6d-11ea-a100-8589bb9d7c6b',/
);
});
});
describe('Print PNG button', () => {
@ -153,9 +181,37 @@ export default function ({
expect(await reporting.isGenerateReportButtonDisabled()).to.be(null);
await (await testSubjects.find('kibanaChrome')).clickMouseButton(); // close popover
});
it('provides a button to copy POST URL', async () => {
// The "clipboard-read" permission of the Permissions API must be granted
if (!(await browser.checkBrowserPermission('clipboard-read'))) {
return;
}
await dashboard.navigateToApp();
await dashboard.loadSavedDashboard('Ecom Dashboard');
await reporting.openExportTab();
await testSubjects.click('pngV2-radioOption');
await testSubjects.click('shareReportingCopyURL');
const postUrl = await browser.getClipboardValue();
expect(postUrl).to.contain('pngV2');
const [, jobParams] = postUrl.split('jobParams=');
expect(decodeURIComponent(jobParams)).to.contain('browserTimezone:UTC,');
expect(decodeURIComponent(jobParams)).to.match(
/layout:\(dimensions:\(height:1\d{3},width:1\d{3}\),id:preserve_layout\),/
);
expect(decodeURIComponent(jobParams)).to.match(
/objectType:dashboard,title:'Ecom Dashboard',/
);
expect(decodeURIComponent(jobParams)).to.match(
/locatorParams:.*id:DASHBOARD_APP_LOCATOR,params:\(dashboardId:'6c263e00-1c6d-11ea-a100-8589bb9d7c6b',/
);
});
});
describe.skip('Preserve Layout', () => {
describe('Preserve Layout', () => {
before(async () => {
await loadEcommerce();
});
@ -180,6 +236,33 @@ export default function ({
expect(res.get('content-type')).to.equal('application/pdf');
await kibanaServer.uiSettings.replace({});
});
it('provides a button to copy POST URL', async () => {
// The "clipboard-read" permission of the Permissions API must be granted
if (!(await browser.checkBrowserPermission('clipboard-read'))) {
return;
}
await dashboard.navigateToApp();
await dashboard.loadSavedDashboard('Ecom Dashboard');
await reporting.openExportTab();
await testSubjects.click('shareReportingCopyURL');
const postUrl = await browser.getClipboardValue();
expect(postUrl).to.contain('printablePdfV2');
const [, jobParams] = postUrl.split('jobParams=');
expect(decodeURIComponent(jobParams)).to.contain('browserTimezone:UTC,');
expect(decodeURIComponent(jobParams)).to.match(
/layout:\(dimensions:\(height:1\d{3},width:1\d{3}\),id:preserve_layout\),/
);
expect(decodeURIComponent(jobParams)).to.match(
/objectType:dashboard,title:'Ecom Dashboard',/
);
expect(decodeURIComponent(jobParams)).to.match(
/locatorParams:.*id:DASHBOARD_APP_LOCATOR,params:\(dashboardId:'6c263e00-1c6d-11ea-a100-8589bb9d7c6b',/
);
});
});
describe('Sample data from Kibana 7.6', () => {