diff --git a/examples/share_examples/public/plugin.tsx b/examples/share_examples/public/plugin.tsx index e408bbc09128d1..5fcc5d8229c6c8 100644 --- a/examples/share_examples/public/plugin.tsx +++ b/examples/share_examples/public/plugin.tsx @@ -22,7 +22,7 @@ export class ShareDemoPlugin implements Plugin public setup(core: CoreSetup, { share }: SetupDeps) { share.register({ id: 'demo', - getShareMenuItems: (context) => [ + getShareMenuItemsLegacy: (context) => [ { panel: { id: 'demo', diff --git a/packages/kbn-reporting/public/reporting_api_client.ts b/packages/kbn-reporting/public/reporting_api_client.ts index 247fb4b67d28df..48931fee1cf518 100644 --- a/packages/kbn-reporting/public/reporting_api_client.ts +++ b/packages/kbn-reporting/public/reporting_api_client.ts @@ -227,6 +227,9 @@ export class ReportingAPIClient implements IReportingAPI { }); } + /** + * Adds the browserTimezone and kibana version to report job params + */ public getDecoratedJobParams(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 diff --git a/packages/kbn-reporting/public/share/share_context_menu/index.ts b/packages/kbn-reporting/public/share/share_context_menu/index.ts index 73cc97e21f5cd1..d4d66ddf6378ed 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/index.ts +++ b/packages/kbn-reporting/public/share/share_context_menu/index.ts @@ -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; } diff --git a/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx b/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx index 762ce513084ca8..d0a4544c3b0e06 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx @@ -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, }); } diff --git a/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx b/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx index 5c47360bde1d26..949e7bc5930729 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx @@ -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; - }) => _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: ( - - ), - }, - }; - - 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: ( - - ), - }, - }; - - 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; diff --git a/src/plugins/share/public/components/context/index.tsx b/src/plugins/share/public/components/context/index.tsx index 7d858bf0665fa0..b75df40aaa41ab 100644 --- a/src/plugins/share/public/components/context/index.tsx +++ b/src/plugins/share/public/components/context/index.tsx @@ -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; diff --git a/src/plugins/share/public/components/share_context_menu.test.tsx b/src/plugins/share/public/components/share_context_menu.test.tsx index 12e6abdcdfc831..6e0bc1535b8dd5 100644 --- a/src/plugins/share/public/components/share_context_menu.test.tsx +++ b/src/plugins/share/public/components/share_context_menu.test.tsx @@ -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', diff --git a/src/plugins/share/public/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx index e5a141d4d3daf8..a768335da45e17 100644 --- a/src/plugins/share/public/components/share_context_menu.tsx +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -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; params: any; }; - shareMenuItems: ShareMenuItem[]; + shareMenuItems: ShareMenuItemLegacy[]; sharingData: any; onClose: () => void; embedUrlParamExtensions?: UrlParamExtension[]; diff --git a/src/plugins/share/public/components/share_tabs.test.tsx b/src/plugins/share/public/components/share_tabs.test.tsx index 52e73d7f1b5d3c..b4ad92fce84f93 100644 --- a/src/plugins/share/public/components/share_tabs.test.tsx +++ b/src/plugins/share/public/components/share_tabs.test.tsx @@ -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( @@ -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( @@ -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( diff --git a/src/plugins/share/public/components/tabs/export/export_content.tsx b/src/plugins/share/public/components/tabs/export/export_content.tsx index 602227a72074ff..109014d7784f51 100644 --- a/src/plugins/share/public/components/tabs/export/export_content.tsx +++ b/src/plugins/share/public/components/tabs/export/export_content.tsx @@ -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 ( @@ -160,7 +161,8 @@ const ExportContentUi = ({ ); - }, [absoluteUrl, renderCopyURLButton, publicAPIEnabled]); + } + }, [renderCopyURLButton, publicAPIEnabled, usePrintLayout, generateExportUrl, intl]); const renderGenerateReportButton = useCallback(() => { return ( diff --git a/src/plugins/share/public/components/tabs/export/index.tsx b/src/plugins/share/public/components/tabs/export/index.tsx index 9ada64a751a586..6c97dba37f8a6d 100644 --- a/src/plugins/share/public/components/tabs/export/index.tsx +++ b/src/plugins/share/public/components/tabs/export/index.tsx @@ -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} /> ); diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 2d84b57995fcc1..08199c9e9ca5b3 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -23,7 +23,8 @@ export type { export type { ShareContext, ShareMenuProvider, - ShareMenuItem, + ShareMenuItemLegacy, + ShareMenuItemV2, ShowShareMenuOptions, ShareContextMenuPanelItem, BrowserUrlService, diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx index 6f7a90bd9aa5e8..e5d838691f66c8 100644 --- a/src/plugins/share/public/services/share_menu_manager.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -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; diff --git a/src/plugins/share/public/services/share_menu_registry.mock.ts b/src/plugins/share/public/services/share_menu_registry.mock.ts index bab112214cbf55..0395062ff2984c 100644 --- a/src/plugins/share/public/services/share_menu_registry.mock.ts +++ b/src/plugins/share/public/services/share_menu_registry.mock.ts @@ -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 => { const setup = { @@ -24,7 +24,7 @@ const createSetupMock = (): jest.Mocked => { const createStartMock = (): jest.Mocked => { const start = { - getShareMenuItems: jest.fn((props: ShareContext) => [] as ShareMenuItem[]), + getShareMenuItems: jest.fn((_props: ShareContext) => [] as ShareMenuItemV2[]), }; return start; }; diff --git a/src/plugins/share/public/services/share_menu_registry.test.ts b/src/plugins/share/public/services/share_menu_registry.test.ts index 0f5ec29778f5c1..ae88251ac8d206 100644 --- a/src/plugins/share/public/services/share_menu_registry.test.ts +++ b/src/plugins/share/public/services/share_menu_registry.test.ts @@ -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({ diff --git a/src/plugins/share/public/services/share_menu_registry.ts b/src/plugins/share/public/services/share_menu_registry.ts index 3b5d2df8ac63d8..24dee378f6bfdd 100644 --- a/src/plugins/share/public/services/share_menu_registry.ts +++ b/src/plugins/share/public/services/share_menu_registry.ts @@ -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(); @@ -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) ), }; } diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index 03b63f873d9649..5c727dc2d299c2 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -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; + /** + * Function to trigger an export + */ + generateExport: (args: ScreenshotExportOpts) => Promise; + /** + * 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>; +} +export interface ShareMenuProviderLegacy { + readonly id: string; + getShareMenuItemsLegacy: (context: ShareContext) => ShareMenuItemLegacy[]; +} -type ShareMenuItemType = Omit; /** * @public * A source for additional menu items shown in the share context menu. Any provider @@ -134,10 +150,7 @@ type ShareMenuItemType = Omit; * 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; diff --git a/x-pack/plugins/lens/public/app_plugin/csv_download_provider/csv_download_provider.tsx b/x-pack/plugins/lens/public/app_plugin/csv_download_provider/csv_download_provider.tsx index e986080c94c932..db04a48ad3803f 100644 --- a/x-pack/plugins/lens/public/app_plugin/csv_download_provider/csv_download_provider.tsx +++ b/x-pack/plugins/lens/public/app_plugin/csv_download_provider/csv_download_provider.tsx @@ -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() ? { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 1627c24c5b4b21..c17b2fa3e66bee 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1112aaff99d201..df88db76dfaa50 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "レポートを作成できません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 96b43b0045dfd7..f3b9f95ec6601e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "无法创建报告", diff --git a/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts index 3f0bf511075c27..60a7f754933deb 100644 --- a/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts @@ -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', () => {