mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
28d6a22263
commit
38407ae6b0
22 changed files with 220 additions and 257 deletions
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,8 @@ export type {
|
|||
export type {
|
||||
ShareContext,
|
||||
ShareMenuProvider,
|
||||
ShareMenuItem,
|
||||
ShareMenuItemLegacy,
|
||||
ShareMenuItemV2,
|
||||
ShowShareMenuOptions,
|
||||
ShareContextMenuPanelItem,
|
||||
BrowserUrlService,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
? {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "レポートを作成できません",
|
||||
|
|
|
@ -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": "无法创建报告",
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue