mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Share Modal Redesign] Reporting Refactor Modals (#180009)
## Summary This PR refactors https://github.com/elastic/kibana/pull/179206 to have each export type be registered in Reporting and then passed into the share plugin. This PR is focused on the redesign in terms of the export modals. Test refactoring will be done in a separate PR. Partially closes https://github.com/elastic/kibana-team/issues/753 - [x] Need to refactor this PR to include @eokoneyo's general modal component - [x] Lens needs to have Export with all three report type options - to avoid circular dependencies move the Lens CSV stuff into the reporting plugin vs having it in Lens - [x] Canvas should not be affected by these changes (so the old share/reporting code has to stay for canvas) https://github.com/elastic/kibana/issues/151523 to keep in mind for the redesign Failed tests will be covered in this PR https://github.com/elastic/kibana/pull/180406 ### TO TEST Mark `share.new_version.enabled: true` in your kibana.dev.yml ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) --------- Co-authored-by: Eyo Okon Eyo <eyo.eyo@elastic.co> Co-authored-by: Tim Sullivan <tsullivan@users.noreply.github.com> Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
parent
e661eea406
commit
9579635c25
79 changed files with 1654 additions and 1282 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -646,7 +646,7 @@ packages/kbn-repo-path @elastic/kibana-operations
|
|||
packages/kbn-repo-source-classifier @elastic/kibana-operations
|
||||
packages/kbn-repo-source-classifier-cli @elastic/kibana-operations
|
||||
packages/kbn-reporting/common @elastic/appex-sharedux
|
||||
x-pack/examples/reporting_example @elastic/appex-sharedux
|
||||
packages/kbn-reporting/get_csv_panel_actions @elastic/appex-sharedux
|
||||
packages/kbn-reporting/export_types/csv @elastic/appex-sharedux
|
||||
packages/kbn-reporting/export_types/csv_common @elastic/appex-sharedux
|
||||
packages/kbn-reporting/export_types/pdf @elastic/appex-sharedux
|
||||
|
|
|
@ -651,7 +651,7 @@
|
|||
"@kbn/repo-info": "link:packages/kbn-repo-info",
|
||||
"@kbn/repo-packages": "link:packages/kbn-repo-packages",
|
||||
"@kbn/reporting-common": "link:packages/kbn-reporting/common",
|
||||
"@kbn/reporting-example-plugin": "link:x-pack/examples/reporting_example",
|
||||
"@kbn/reporting-csv-share-panel": "link:packages/kbn-reporting/get_csv_panel_actions",
|
||||
"@kbn/reporting-export-types-csv": "link:packages/kbn-reporting/export_types/csv",
|
||||
"@kbn/reporting-export-types-csv-common": "link:packages/kbn-reporting/export_types/csv_common",
|
||||
"@kbn/reporting-export-types-pdf": "link:packages/kbn-reporting/export_types/pdf",
|
||||
|
|
|
@ -115,7 +115,7 @@ pageLoadAssetSize:
|
|||
presentationUtil: 58834
|
||||
profiling: 36694
|
||||
remoteClusters: 51327
|
||||
reporting: 57003
|
||||
reporting: 58600
|
||||
rollup: 97204
|
||||
runtimeFields: 41752
|
||||
savedObjects: 108518
|
||||
|
@ -138,7 +138,7 @@ pageLoadAssetSize:
|
|||
serverlessObservability: 68747
|
||||
serverlessSearch: 72995
|
||||
sessionView: 77750
|
||||
share: 71239
|
||||
share: 88160
|
||||
slo: 37039
|
||||
snapshotRestore: 79032
|
||||
spaces: 57868
|
||||
|
|
9
packages/kbn-reporting/get_csv_panel_actions/index.ts
Normal file
9
packages/kbn-reporting/get_csv_panel_actions/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action';
|
13
packages/kbn-reporting/get_csv_panel_actions/jest.config.js
Normal file
13
packages/kbn-reporting/get_csv_panel_actions/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/kbn-reporting/get_csv_panel_actions'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/reporting-csv-share-panel",
|
||||
"owner": "@elastic/appex-sharedux"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/reporting-csv-share-panel",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -16,8 +16,8 @@ import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
|||
import { LicenseCheckState } from '@kbn/licensing-plugin/public';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { ReportingAPIClient } from '../..';
|
||||
import type { ClientConfigType } from '../../types';
|
||||
import { ReportingAPIClient } from '@kbn/reporting-public';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public/types';
|
||||
import {
|
||||
ActionContext,
|
||||
type PanelActionDependencies,
|
|
@ -28,9 +28,9 @@ import type { UiActionsActionDefinition as ActionDefinition } from '@kbn/ui-acti
|
|||
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import { CSV_REPORTING_ACTION, JobAppParamsCSV } from '@kbn/reporting-export-types-csv-common';
|
||||
import type { ClientConfigType } from '../../types';
|
||||
import { checkLicense } from '../../license_check';
|
||||
import type { ReportingAPIClient } from '../../reporting_api_client';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public/types';
|
||||
import { checkLicense } from '@kbn/reporting-public/license_check';
|
||||
import type { ReportingAPIClient } from '@kbn/reporting-public/reporting_api_client';
|
||||
import { getI18nStrings } from './strings';
|
||||
|
||||
function isSavedSearchEmbeddable(
|
|
@ -9,7 +9,7 @@
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { ReportingAPIClient } from '../../reporting_api_client';
|
||||
import type { ReportingAPIClient } from '@kbn/reporting-public/reporting_api_client';
|
||||
|
||||
interface I18nStrings {
|
||||
displayName: string;
|
31
packages/kbn-reporting/get_csv_panel_actions/tsconfig.json
Normal file
31
packages/kbn-reporting/get_csv_panel_actions/tsconfig.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts", "**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/reporting-export-types-csv-common",
|
||||
"@kbn/licensing-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/saved-search-plugin",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/reporting-public",
|
||||
]
|
||||
}
|
|
@ -7,7 +7,9 @@
|
|||
*/
|
||||
|
||||
export { getSharedComponents } from './shared';
|
||||
export { reportingExportModalProvider } from './share_context_menu/register_pdf_png_modal_reporting';
|
||||
export { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
|
||||
export { reportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
|
||||
export { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action';
|
||||
export { reportingCsvShareProvider as reportingCsvShareModalProvider } from './share_context_menu/register_csv_modal_reporting';
|
||||
export type { ReportingPublicComponents } from './shared/get_shared_components';
|
||||
export type { JobParamsProviderOptions } from './share_context_menu';
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type {
|
||||
ApplicationStart,
|
||||
I18nStart,
|
||||
IUiSettingsClient,
|
||||
ThemeServiceSetup,
|
||||
ToastsSetup,
|
||||
|
@ -16,6 +17,16 @@ import { ILicense } from '@kbn/licensing-plugin/public';
|
|||
import type { LayoutParams } from '@kbn/screenshotting-plugin/common';
|
||||
import type { ReportingAPIClient } from '../../reporting_api_client';
|
||||
|
||||
export interface ExportModalShareOpts {
|
||||
apiClient: ReportingAPIClient;
|
||||
uiSettings: IUiSettingsClient;
|
||||
usesUiCapabilities: boolean;
|
||||
license: ILicense;
|
||||
application: ApplicationStart;
|
||||
theme: ThemeServiceSetup;
|
||||
i18n: I18nStart;
|
||||
}
|
||||
|
||||
export interface ExportPanelShareOpts {
|
||||
apiClient: ReportingAPIClient;
|
||||
toasts: ToastsSetup;
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { CSV_JOB_TYPE, CSV_JOB_TYPE_V2 } from '@kbn/reporting-export-types-csv-common';
|
||||
|
||||
import type { SearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { ShareContext, ShareMenuItem } from '@kbn/share-plugin/public';
|
||||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react';
|
||||
import type { ExportModalShareOpts } from '.';
|
||||
import { checkLicense } from '../..';
|
||||
|
||||
export const reportingCsvShareProvider = ({
|
||||
apiClient,
|
||||
application,
|
||||
license,
|
||||
usesUiCapabilities,
|
||||
i18n: i18nStart,
|
||||
theme,
|
||||
}: ExportModalShareOpts) => {
|
||||
const getShareMenuItems = ({ objectType, sharingData, toasts }: ShareContext) => {
|
||||
if ('search' !== objectType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// only csv v2 supports esql (isTextBased) reports
|
||||
// TODO: whole csv reporting should move to v2 https://github.com/elastic/kibana/issues/151190
|
||||
const reportType = sharingData.isTextBased ? CSV_JOB_TYPE_V2 : CSV_JOB_TYPE;
|
||||
|
||||
const getSearchSource = sharingData.getSearchSource as ({
|
||||
addGlobalTimeFilter,
|
||||
absoluteTime,
|
||||
}: {
|
||||
addGlobalTimeFilter?: boolean;
|
||||
absoluteTime?: boolean;
|
||||
}) => SearchSourceFields;
|
||||
|
||||
const jobParams = {
|
||||
title: sharingData.title as string,
|
||||
objectType,
|
||||
};
|
||||
|
||||
const getJobParams = (forShareUrl?: boolean) => {
|
||||
if (reportType === CSV_JOB_TYPE_V2) {
|
||||
// csv v2 uses locator params
|
||||
return {
|
||||
...jobParams,
|
||||
locatorParams: sharingData.locatorParams as [Record<string, unknown>],
|
||||
};
|
||||
}
|
||||
|
||||
// csv v1 uses search source and columns
|
||||
return {
|
||||
...jobParams,
|
||||
columns: sharingData.columns as string[] | undefined,
|
||||
searchSource: getSearchSource({
|
||||
addGlobalTimeFilter: true,
|
||||
absoluteTime: !forShareUrl,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const shareActions: ShareMenuItem[] = [];
|
||||
|
||||
const licenseCheck = checkLicense(license.check('reporting', 'basic'));
|
||||
const licenseToolTipContent = licenseCheck.message;
|
||||
const licenseHasCsvReporting = licenseCheck.showLinks;
|
||||
const licenseDisabled = !licenseCheck.enableLinks;
|
||||
|
||||
let capabilityHasCsvReporting = false;
|
||||
if (usesUiCapabilities) {
|
||||
capabilityHasCsvReporting = application.capabilities.discover?.generateCsv === true;
|
||||
} else {
|
||||
capabilityHasCsvReporting = true; // deprecated
|
||||
}
|
||||
|
||||
const generateReportingJobCSV = ({ intl }: { intl: InjectedIntl }) => {
|
||||
const decoratedJobParams = apiClient.getDecoratedJobParams(getJobParams());
|
||||
return apiClient
|
||||
.createReportingJob(reportType, decoratedJobParams)
|
||||
.then(() => {
|
||||
toasts.addSuccess({
|
||||
title: intl.formatMessage(
|
||||
{
|
||||
id: 'reporting.share.modalContent.successfullyQueuedReportNotificationTitle',
|
||||
defaultMessage: 'Queued report for {objectType}',
|
||||
},
|
||||
{ objectType }
|
||||
),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="reporting.share.modalContent.successfullyQueuedReportNotificationDescription"
|
||||
defaultMessage="Track its progress in {path}."
|
||||
values={{
|
||||
path: (
|
||||
<a href={apiClient.getManagementLink()}>
|
||||
<FormattedMessage
|
||||
id="reporting.share.publicNotifier.reportLink.reportingSectionUrlLinkLabel"
|
||||
defaultMessage="Stack Management > Reporting"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
'data-test-subj': 'queueReportSuccess',
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
toasts.addError(error, {
|
||||
title: intl.formatMessage({
|
||||
id: 'reporting.share.modalContent.notification.reportingErrorTitle',
|
||||
defaultMessage: 'Unable to create report',
|
||||
}),
|
||||
toastMessage: error.body?.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (licenseHasCsvReporting && capabilityHasCsvReporting) {
|
||||
const panelTitle = i18n.translate(
|
||||
'reporting.share.contextMenu.export.csvReportsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Export',
|
||||
}
|
||||
);
|
||||
|
||||
const reportingUrl = new URL(window.location.origin);
|
||||
|
||||
const relativePath = apiClient.getReportingPublicJobPath(
|
||||
reportType,
|
||||
apiClient.getDecoratedJobParams(getJobParams())
|
||||
);
|
||||
|
||||
const absoluteUrl = new URL(relativePath, window.location.href).toString();
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: panelTitle,
|
||||
toolTipContent: licenseToolTipContent,
|
||||
disabled: licenseDisabled,
|
||||
['data-test-subj']: 'Export',
|
||||
},
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="reporting.share.csv.reporting.helpTextCSV"
|
||||
defaultMessage="Export a CSV of this {objectType}"
|
||||
values={{ objectType }}
|
||||
/>
|
||||
),
|
||||
reportType,
|
||||
label: 'CSV',
|
||||
copyURLButton: {
|
||||
id: 'reporting.share.modalContent.csv.copyUrlButtonLabel',
|
||||
dataTestSubj: 'shareReportingCopyURL',
|
||||
label: 'Post URL',
|
||||
},
|
||||
generateReportButton: (
|
||||
<FormattedMessage
|
||||
id="reporting.share.generateButtonLabelCSV"
|
||||
data-test-subj="generateReportButton"
|
||||
defaultMessage="Generate CSV"
|
||||
/>
|
||||
),
|
||||
generateReport: generateReportingJobCSV,
|
||||
generateCopyUrl: reportingUrl,
|
||||
absoluteUrl,
|
||||
renderCopyURLButton: true,
|
||||
});
|
||||
}
|
||||
|
||||
return shareActions;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'csvReportsModal',
|
||||
getShareMenuItems,
|
||||
};
|
||||
};
|
|
@ -12,7 +12,7 @@ import React from 'react';
|
|||
import { CSV_JOB_TYPE, CSV_JOB_TYPE_V2 } from '@kbn/reporting-export-types-csv-common';
|
||||
|
||||
import type { SearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { ShareContext, ShareMenuProvider } from '@kbn/share-plugin/public';
|
||||
import { ShareContext, ShareMenuItem, ShareMenuProvider } from '@kbn/share-plugin/public';
|
||||
import type { ExportPanelShareOpts } from '.';
|
||||
import { checkLicense } from '../..';
|
||||
import { ReportingPanelContent } from './reporting_panel_content_lazy';
|
||||
|
@ -68,7 +68,7 @@ export const reportingCsvShareProvider = ({
|
|||
};
|
||||
};
|
||||
|
||||
const shareActions = [];
|
||||
const shareActions: ShareMenuItem[] = [];
|
||||
|
||||
const licenseCheck = checkLicense(license.check('reporting', 'basic'));
|
||||
const licenseToolTipContent = licenseCheck.message;
|
||||
|
|
|
@ -0,0 +1,461 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ShareContext, ShareMenuItem, ShareMenuProvider } from '@kbn/share-plugin/public';
|
||||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { checkLicense } from '../../license_check';
|
||||
import {
|
||||
ExportModalShareOpts,
|
||||
ExportPanelShareOpts,
|
||||
JobParamsProviderOptions,
|
||||
ReportingSharingData,
|
||||
} from '.';
|
||||
import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
|
||||
|
||||
const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printablePdfV2') => () => {
|
||||
const {
|
||||
objectType,
|
||||
sharingData: { title, layout, locatorParams },
|
||||
} = opts;
|
||||
|
||||
const baseParams = {
|
||||
objectType,
|
||||
layout,
|
||||
title,
|
||||
};
|
||||
|
||||
if (type === 'printablePdfV2') {
|
||||
// multi locator for PDF V2
|
||||
return { ...baseParams, locatorParams: [locatorParams] };
|
||||
}
|
||||
// single locator for PNG V2
|
||||
return { ...baseParams, locatorParams };
|
||||
};
|
||||
|
||||
/**
|
||||
* This is used by Canvas
|
||||
*/
|
||||
export const reportingScreenshotShareProvider = ({
|
||||
apiClient,
|
||||
toasts,
|
||||
uiSettings,
|
||||
license,
|
||||
application,
|
||||
usesUiCapabilities,
|
||||
theme,
|
||||
}: 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}
|
||||
toasts={toasts}
|
||||
uiSettings={uiSettings}
|
||||
reportType={'pngV2'}
|
||||
objectId={objectId}
|
||||
requiresSavedState={requiresSavedState}
|
||||
getJobParams={getJobParams(jobProviderOptions, 'pngV2')}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
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}
|
||||
toasts={toasts}
|
||||
uiSettings={uiSettings}
|
||||
reportType={'printablePdfV2'}
|
||||
objectId={objectId}
|
||||
requiresSavedState={requiresSavedState}
|
||||
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
|
||||
getJobParams={getJobParams(jobProviderOptions, 'printablePdfV2')}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
shareActions.push(panelPng);
|
||||
shareActions.push(panelPdf);
|
||||
return shareActions;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'screenCaptureReports',
|
||||
getShareMenuItems,
|
||||
};
|
||||
};
|
||||
|
||||
export const reportingExportModalProvider = ({
|
||||
apiClient,
|
||||
license,
|
||||
application,
|
||||
usesUiCapabilities,
|
||||
theme,
|
||||
i18n: i18nStart,
|
||||
}: ExportModalShareOpts): ShareMenuProvider => {
|
||||
const getShareMenuItems = ({
|
||||
objectType,
|
||||
objectId,
|
||||
isDirty,
|
||||
onClose,
|
||||
shareableUrl,
|
||||
shareableUrlForSavedObject,
|
||||
toasts,
|
||||
...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 [];
|
||||
}
|
||||
// for lens png pdf and csv are combined into one modal
|
||||
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 jobProviderOptions: JobParamsProviderOptions = {
|
||||
shareableUrl: isDirty ? shareableUrl : shareableUrlForSavedObject ?? shareableUrl,
|
||||
objectType,
|
||||
sharingData,
|
||||
};
|
||||
|
||||
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 decoratedJobParams = apiClient.getDecoratedJobParams({
|
||||
...getJobParams(jobProviderOptions, 'printablePdfV2')(),
|
||||
layout: { id: optimizedForPrinting ? 'print' : 'preserve_layout', dimensions },
|
||||
objectType,
|
||||
title: sharingData.title,
|
||||
});
|
||||
|
||||
return apiClient
|
||||
.createReportingJob('printablePdfV2', decoratedJobParams)
|
||||
.then(() => {
|
||||
toasts.addSuccess({
|
||||
title: intl.formatMessage(
|
||||
{
|
||||
id: 'reporting.share.modalContent.successfullyQueuedReportNotificationTitle',
|
||||
defaultMessage: 'Queued report for {objectType}',
|
||||
},
|
||||
{ objectType }
|
||||
),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="reporting.share.modalContent.successfullyQueuedReportNotificationDescription"
|
||||
defaultMessage="Track its progress in {path}."
|
||||
values={{
|
||||
path: (
|
||||
<a href={apiClient.getManagementLink()}>
|
||||
<FormattedMessage
|
||||
id="reporting.share.publicNotifier.reportLink.reportingSectionUrlLinkLabel"
|
||||
defaultMessage="Stack Management > Reporting"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
'data-test-subj': 'queueReportSuccess',
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toasts.addError(error, {
|
||||
title: intl.formatMessage({
|
||||
id: 'reporting.share.modalContent.notification.reportingErrorTitle',
|
||||
defaultMessage: 'Unable to create report',
|
||||
}),
|
||||
toastMessage: error.body?.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const generateReportPNG = ({ intl }: { intl: InjectedIntl }) => {
|
||||
const el = document.querySelector('[data-shared-items-container]');
|
||||
const { height, width } = el ? el.getBoundingClientRect() : { height: 768, width: 1024 };
|
||||
const dimensions = { height, width };
|
||||
|
||||
const decoratedJobParams = apiClient.getDecoratedJobParams({
|
||||
...getJobParams(jobProviderOptions, 'pngV2')(),
|
||||
layout: { id: 'preserve_layout', dimensions },
|
||||
objectType,
|
||||
title: sharingData.title,
|
||||
});
|
||||
return apiClient
|
||||
.createReportingJob('pngV2', decoratedJobParams)
|
||||
.then(() => {
|
||||
toasts.addSuccess({
|
||||
title: intl.formatMessage(
|
||||
{
|
||||
id: 'reporting.share.modalContent.successfullyQueuedReportNotificationTitle',
|
||||
defaultMessage: 'Queued report for {objectType}',
|
||||
},
|
||||
{ objectType }
|
||||
),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="reporting.share.modalContent.successfullyQueuedReportNotificationDescription"
|
||||
defaultMessage="Track its progress in {path}."
|
||||
values={{
|
||||
path: (
|
||||
<a href={apiClient.getManagementLink()}>
|
||||
<FormattedMessage
|
||||
id="reporting.share.publicNotifier.reportLink.reportingSectionUrlLinkLabel"
|
||||
defaultMessage="Stack Management > Reporting"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>,
|
||||
{ theme, i18n: i18nStart }
|
||||
),
|
||||
'data-test-subj': 'queueReportSuccess',
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toasts.addError(error, {
|
||||
title: intl.formatMessage({
|
||||
id: 'reporting.share.modalContent.notification.reportingErrorTitle',
|
||||
defaultMessage: 'Unable to create report',
|
||||
}),
|
||||
toastMessage: error.body?.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: i18n.translate('reporting.shareContextMenu.ExportsButtonLabel', {
|
||||
defaultMessage: 'PDF',
|
||||
}),
|
||||
toolTipContent: licenseToolTipContent,
|
||||
disabled: licenseDisabled || sharingData.reportingDisabled,
|
||||
['data-test-subj']: 'imageExports',
|
||||
},
|
||||
label: 'PDF' as const,
|
||||
generateReport: generateReportPDF,
|
||||
reportType: 'printablePdfV2',
|
||||
requiresSavedState,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="reporting.printablePdfV2.helpText"
|
||||
defaultMessage="Exports can take a few minutes to generate."
|
||||
/>
|
||||
),
|
||||
generateReportButton: (
|
||||
<FormattedMessage
|
||||
id="reporting.printablePdfV2.generateButtonLabel"
|
||||
data-test-subj="generateReportButton"
|
||||
defaultMessage="Generate export"
|
||||
/>
|
||||
),
|
||||
layoutOption: objectType === 'dashboard' ? ('print' as const) : undefined,
|
||||
theme,
|
||||
renderLayoutOptionSwitch: objectType === 'dashboard',
|
||||
renderCopyURLButton: true,
|
||||
absoluteUrl: new URL(relativePathPDF, window.location.href).toString(),
|
||||
});
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: i18n.translate('reporting.shareContextMenu.ExportsButtonLabelPNG', {
|
||||
defaultMessage: 'PNG export',
|
||||
}),
|
||||
toolTipContent: licenseToolTipContent,
|
||||
disabled: licenseDisabled || sharingData.reportingDisabled,
|
||||
['data-test-subj']: 'imageExports',
|
||||
},
|
||||
label: 'PNG' as const,
|
||||
generateReport: generateReportPNG,
|
||||
reportType: 'pngV2',
|
||||
requiresSavedState,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="reporting.pngV2.helpText"
|
||||
defaultMessage="Exports can take a few minutes to generate."
|
||||
/>
|
||||
),
|
||||
generateReportButton: (
|
||||
<FormattedMessage
|
||||
id="reporting.pngV2.generateButtonLabel"
|
||||
defaultMessage="Generate export"
|
||||
data-test-subj="generateReportButton"
|
||||
/>
|
||||
),
|
||||
layoutOption: objectType === 'dashboard' ? ('print' as const) : undefined,
|
||||
renderCopyURLButton: true,
|
||||
absoluteUrl: new URL(relativePathPNG, window.location.href).toString(),
|
||||
});
|
||||
|
||||
return shareActions;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'modalImageReports',
|
||||
getShareMenuItems,
|
||||
};
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ShareContext, ShareMenuProvider } from '@kbn/share-plugin/public';
|
||||
import { ShareContext, ShareMenuItem, ShareMenuProvider } from '@kbn/share-plugin/public';
|
||||
import React from 'react';
|
||||
import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
|
||||
import { ReportingAPIClient, checkLicense } from '../..';
|
||||
|
@ -113,7 +113,7 @@ export const reportingScreenshotShareProvider = ({
|
|||
}
|
||||
|
||||
const { sharingData } = shareOpts as unknown as { sharingData: ReportingSharingData };
|
||||
const shareActions = [];
|
||||
const shareActions: ShareMenuItem[] = [];
|
||||
|
||||
const pngPanelTitle = i18n.translate('reporting.share.contextMenu.pngReportsButtonLabel', {
|
||||
defaultMessage: 'PNG Reports',
|
||||
|
|
|
@ -7,21 +7,19 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from '@kbn/core/public';
|
||||
import React from 'react';
|
||||
|
||||
import { PDF_REPORT_TYPE, PDF_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-pdf-common';
|
||||
import { PDF_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-pdf-common';
|
||||
import { PNG_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-png-common';
|
||||
|
||||
import { ReportingAPIClient } from '../../reporting_api_client';
|
||||
import React from 'react';
|
||||
import { ReportingAPIClient } from '../..';
|
||||
import { ReportingPanelProps } from '../share_context_menu/reporting_panel_content';
|
||||
import { ScreenCapturePanelContent } from '../share_context_menu/screen_capture_panel_content_lazy';
|
||||
|
||||
/**
|
||||
* Properties for displaying a share menu with Reporting features.
|
||||
*/
|
||||
export interface ApplicationProps {
|
||||
/**
|
||||
* A function that Reporting calls to get the sharing data from the application.
|
||||
* Needed for CSV exports and Canvas PDF reports.
|
||||
*/
|
||||
getJobParams: ReportingPanelProps['getJobParams'];
|
||||
|
||||
|
@ -41,25 +39,12 @@ export interface ApplicationProps {
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React components used to display share menus with Reporting features in an application.
|
||||
*/
|
||||
export interface ReportingPublicComponents {
|
||||
/**
|
||||
* An element to display a form to export the page as PDF
|
||||
* @deprecated
|
||||
*/
|
||||
ReportingPanelPDF(props: ApplicationProps): JSX.Element;
|
||||
|
||||
/**
|
||||
* An element to display a form to export the page as PDF
|
||||
*/
|
||||
ReportingPanelPDFV2(props: ApplicationProps): JSX.Element;
|
||||
|
||||
/**
|
||||
* An element to display a form to export the page as PNG
|
||||
*/
|
||||
ReportingPanelPNGV2(props: ApplicationProps): JSX.Element;
|
||||
/** Needed for Canvas PDF reports */
|
||||
ReportingPanelPDFV2(props: ApplicationProps): JSX.Element | null;
|
||||
ReportingPanelPNGV2(props: ApplicationProps): JSX.Element | undefined;
|
||||
ReportingModalPDF(props: ApplicationProps): JSX.Element | undefined;
|
||||
ReportingModalPNG(props: ApplicationProps): JSX.Element | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,44 +57,79 @@ export function getSharedComponents(
|
|||
apiClient: ReportingAPIClient
|
||||
): ReportingPublicComponents {
|
||||
return {
|
||||
ReportingPanelPDF(props: ApplicationProps) {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PDF_REPORT_TYPE}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
ReportingPanelPDFV2(props: ApplicationProps) {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PDF_REPORT_TYPE_V2}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams'];
|
||||
if (props.layoutOption === 'canvas') {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PDF_REPORT_TYPE_V2}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
layoutOption={'canvas' as const}
|
||||
{...props}
|
||||
getJobParams={getJobParams}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
ReportingPanelPNGV2(props: ApplicationProps) {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PNG_REPORT_TYPE_V2}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams'];
|
||||
if (props.layoutOption === 'canvas') {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PNG_REPORT_TYPE_V2}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
layoutOption={'canvas' as const}
|
||||
{...props}
|
||||
getJobParams={getJobParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
ReportingModalPDF(props: ApplicationProps) {
|
||||
const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams'];
|
||||
if (props.layoutOption === 'canvas') {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PDF_REPORT_TYPE_V2}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
layoutOption={'canvas' as const}
|
||||
{...props}
|
||||
getJobParams={getJobParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
ReportingModalPNG(props: ApplicationProps) {
|
||||
const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams'];
|
||||
if (props.layoutOption === 'canvas') {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
requiresSavedState={false}
|
||||
reportType={PDF_REPORT_TYPE_V2}
|
||||
apiClient={apiClient}
|
||||
toasts={core.notifications.toasts}
|
||||
uiSettings={core.uiSettings}
|
||||
theme={core.theme}
|
||||
layoutOption={'canvas' as const}
|
||||
{...props}
|
||||
getJobParams={getJobParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,11 +30,6 @@
|
|||
"@kbn/screenshotting-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/saved-search-plugin",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/react-kibana-mount",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ export function ModalContextProvider<T extends Array<ITabDeclaration<Record<stri
|
|||
|
||||
const reducersMap = useMemo(
|
||||
() =>
|
||||
tabs?.reduce((result, { reducer, initialState, ...rest }) => {
|
||||
tabs.reduce((result, { reducer, initialState, ...rest }) => {
|
||||
initialModalState.current[rest.id] = initialState ?? {};
|
||||
// @ts-ignore
|
||||
modalTabDefinitions.current.push({ ...rest });
|
||||
|
|
|
@ -67,7 +67,7 @@ describe('TabbedModal', () => {
|
|||
|
||||
expect(screen.queryByText(tabDefinition.name)).toBeInTheDocument();
|
||||
|
||||
userEvent.click(await screen.findByTestId(tabDefinition.modalActionBtn.dataTestSubj));
|
||||
userEvent.click(await screen.findByTestId(tabDefinition.modalActionBtn!.dataTestSubj));
|
||||
|
||||
expect(mockedHandlerFn).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -52,7 +52,7 @@ export interface IModalTabDeclaration<S = {}> extends EuiTabProps, ITabDeclarati
|
|||
description?: string;
|
||||
'data-test-subj'?: string;
|
||||
content: IModalTabContent<S>;
|
||||
modalActionBtn: IModalTabActionBtn<S>;
|
||||
modalActionBtn?: IModalTabActionBtn<S>;
|
||||
}
|
||||
|
||||
export interface ITabbedModalInner extends Pick<ComponentProps<typeof EuiModal>, 'onClose'> {
|
||||
|
@ -70,35 +70,33 @@ const TabbedModalInner: FC<ITabbedModalInner> = ({ onClose, modalTitle, modalWid
|
|||
[selectedTabId, state]
|
||||
);
|
||||
|
||||
const {
|
||||
content: SelectedTabContent,
|
||||
modalActionBtn: { handler, dataTestSubj, label, style },
|
||||
} = useMemo(() => {
|
||||
const { content: SelectedTabContent, modalActionBtn } = useMemo(() => {
|
||||
return tabs.find((obj) => obj.id === selectedTabId)!;
|
||||
}, [selectedTabId, tabs]);
|
||||
|
||||
const onSelectedTabChanged = (id: string) => {
|
||||
dispatch({ type: 'META_selectedTabId', payload: id });
|
||||
};
|
||||
const onSelectedTabChanged = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'META_selectedTabId', payload: id });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const renderTabs = () => {
|
||||
return tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
key={index}
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
disabled={tab.disabled}
|
||||
prepend={tab.prepend}
|
||||
append={tab.append}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
));
|
||||
};
|
||||
|
||||
const btnClickHandler = useCallback(() => {
|
||||
handler({ state: selectedTabState });
|
||||
}, [handler, selectedTabState]);
|
||||
const renderTabs = useCallback(() => {
|
||||
return tabs.map((tab, index) => {
|
||||
return (
|
||||
<EuiTab
|
||||
key={index}
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
disabled={tab.disabled}
|
||||
prepend={tab.prepend}
|
||||
append={tab.append}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
);
|
||||
});
|
||||
}, [onSelectedTabChanged, selectedTabId, tabs]);
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
|
@ -118,17 +116,20 @@ const TabbedModalInner: FC<ITabbedModalInner> = ({ onClose, modalTitle, modalWid
|
|||
})}
|
||||
</Fragment>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton
|
||||
isDisabled={style ? style({ state: selectedTabState }) : false}
|
||||
fill
|
||||
data-test-subj={dataTestSubj}
|
||||
data-share-url={state.url}
|
||||
onClick={btnClickHandler}
|
||||
>
|
||||
{label}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
{modalActionBtn?.id !== undefined && selectedTabState && (
|
||||
<EuiModalFooter>
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj={modalActionBtn.dataTestSubj}
|
||||
data-share-url={state.url}
|
||||
onClick={() => {
|
||||
modalActionBtn.handler({ state: selectedTabState });
|
||||
}}
|
||||
>
|
||||
{modalActionBtn.label}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
)}
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -59,6 +59,7 @@ export function ShowShareModal({
|
|||
},
|
||||
},
|
||||
},
|
||||
notifications,
|
||||
share: { toggleShareContextMenu },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
|
@ -197,5 +198,6 @@ export function ShowShareModal({
|
|||
snapshotShareWarning: Boolean(unsavedDashboardState?.panels)
|
||||
? shareModalStrings.getSnapshotShareWarning()
|
||||
: undefined,
|
||||
toasts: notifications.toasts,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ export const getTopNavLinks = ({
|
|||
isTextBased
|
||||
);
|
||||
|
||||
const { locator } = services;
|
||||
const { locator, notifications } = services;
|
||||
const appState = state.appState.getState();
|
||||
const { timefilter } = services.data.query.timefilter;
|
||||
const timeRange = timefilter.getTime();
|
||||
|
@ -198,6 +198,7 @@ export const getTopNavLinks = ({
|
|||
onClose: () => {
|
||||
anchorElement?.focus();
|
||||
},
|
||||
toasts: notifications.toasts,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonProps, EuiBetaBadgeProps } from '@elastic/eui';
|
||||
import { InjectedIntl } from '@kbn/i18n-react';
|
||||
|
||||
export type TopNavMenuAction = (anchorElement: HTMLElement) => void;
|
||||
|
||||
|
@ -26,6 +27,7 @@ export interface TopNavMenuData {
|
|||
iconSide?: EuiButtonProps['iconSide'];
|
||||
target?: string;
|
||||
href?: string;
|
||||
intl?: InjectedIntl;
|
||||
}
|
||||
|
||||
export interface RegisteredTopNavMenuData extends TopNavMenuData {
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ThemeServiceSetup } from '@kbn/core-theme-browser';
|
||||
import { I18nStart } from '@kbn/core/public';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { AnonymousAccessServiceContract } from '../../../common';
|
||||
|
@ -16,6 +18,8 @@ import type {
|
|||
ShareContext,
|
||||
} from '../../types';
|
||||
|
||||
export type { ShareMenuItem } from '../../types';
|
||||
|
||||
export interface IShareContext extends ShareContext {
|
||||
allowEmbed: boolean;
|
||||
allowShortUrl: boolean;
|
||||
|
@ -26,6 +30,8 @@ export interface IShareContext extends ShareContext {
|
|||
snapshotShareWarning?: string;
|
||||
objectTypeTitle?: string;
|
||||
isEmbedded: boolean;
|
||||
theme: ThemeServiceSetup;
|
||||
i18n: I18nStart;
|
||||
}
|
||||
|
||||
export const ShareTabsContext = createContext<IShareContext | null>(null);
|
||||
|
|
|
@ -136,6 +136,7 @@ export class ShareContextMenu extends Component<ShareContextMenuProps> {
|
|||
});
|
||||
menuItems.push({
|
||||
...shareMenuItem,
|
||||
name: shareMenuItem!.name,
|
||||
panel: panelId,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { type FC } from 'react';
|
|||
import { TabbedModal } from '@kbn/shared-ux-tabbed-modal';
|
||||
|
||||
import { ShareTabsContext, useShareTabsContext, type IShareContext } from './context';
|
||||
import { linkTab, embedTab } from './tabs';
|
||||
import { linkTab, embedTab, exportTab } from './tabs';
|
||||
|
||||
export const ShareMenuV2: FC<{ shareContext: IShareContext }> = ({ shareContext }) => {
|
||||
return (
|
||||
|
@ -28,20 +28,28 @@ export const ShareMenuTabs = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { allowEmbed, objectType, onClose } = shareContext;
|
||||
const { allowEmbed, objectType, onClose, shareMenuItems } = shareContext;
|
||||
const tabs = [];
|
||||
|
||||
tabs.push(linkTab);
|
||||
|
||||
if (shareMenuItems.length > 0) {
|
||||
tabs.push(exportTab);
|
||||
}
|
||||
|
||||
if (allowEmbed) {
|
||||
tabs.push(embedTab);
|
||||
}
|
||||
|
||||
const formattedTitle =
|
||||
objectType === 'lens' ? `Share this Lens visualization` : `Share this ${objectType}`;
|
||||
|
||||
return (
|
||||
<TabbedModal
|
||||
tabs={tabs}
|
||||
modalWidth={483}
|
||||
modalWidth={498}
|
||||
onClose={onClose}
|
||||
modalTitle={`Share this ${objectType}`}
|
||||
modalTitle={formattedTitle}
|
||||
defaultSelectedTabId="link"
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiFlexItem,
|
||||
EuiCopy,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
|
@ -24,7 +33,7 @@ type EmbedProps = Pick<
|
|||
| 'embedUrlParamExtensions'
|
||||
| 'objectType'
|
||||
> & {
|
||||
onChange: (url: string) => void;
|
||||
setIsNotSaved: () => void;
|
||||
};
|
||||
|
||||
interface UrlParams {
|
||||
|
@ -43,8 +52,8 @@ export const EmbedContent = ({
|
|||
shareableUrlForSavedObject,
|
||||
shareableUrl,
|
||||
isEmbedded,
|
||||
onChange,
|
||||
objectType,
|
||||
setIsNotSaved,
|
||||
}: EmbedProps) => {
|
||||
const isMounted = useMountedState();
|
||||
const [urlParams, setUrlParams] = useState<UrlParams | undefined>(undefined);
|
||||
|
@ -56,8 +65,8 @@ export const EmbedContent = ({
|
|||
const [usePublicUrl] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(url);
|
||||
}, [url, onChange]);
|
||||
if (objectType !== 'dashboard') setIsNotSaved();
|
||||
}, [url, setIsNotSaved, objectType]);
|
||||
|
||||
const getUrlParamExtensions = useCallback(
|
||||
(tempUrl: string): string => {
|
||||
|
@ -224,7 +233,7 @@ export const EmbedContent = ({
|
|||
const helpText =
|
||||
objectType === 'dashboard' ? (
|
||||
<FormattedMessage
|
||||
id="share.embed.dashbord.helpText"
|
||||
id="share.embed.dashboard.helpText"
|
||||
defaultMessage="Embed this dashboard into another webpage. Select which items to include in the embeddable view."
|
||||
/>
|
||||
) : (
|
||||
|
@ -236,12 +245,33 @@ export const EmbedContent = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">{helpText}</EuiText>
|
||||
<EuiSpacer />
|
||||
{renderUrlParamExtensions()}
|
||||
<EuiSpacer />
|
||||
</EuiForm>
|
||||
<>
|
||||
<EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">{helpText}</EuiText>
|
||||
<EuiSpacer />
|
||||
{renderUrlParamExtensions()}
|
||||
<EuiSpacer />
|
||||
</EuiForm>
|
||||
<EuiFlexGroup justifyContent="flexEnd" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCopy textToCopy={url}>
|
||||
{(copy) => (
|
||||
<EuiButton
|
||||
data-test-subj="copyEmbedUrlButton"
|
||||
onClick={copy}
|
||||
data-share-url={url}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="share.link.copyEmbedCodeButton"
|
||||
defaultMessage="Copy Embed Code"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,47 +8,45 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback } from 'react';
|
||||
import { copyToClipboard } from '@elastic/eui';
|
||||
import { type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal';
|
||||
import { EmbedContent } from './embed_content';
|
||||
import { useShareTabsContext } from '../../context';
|
||||
|
||||
const EMBED_TAB_ACTIONS = {
|
||||
SET_EMBED_URL: 'SET_EMBED_URL',
|
||||
SET_IS_NOT_SAVED: 'SET_IS_NOT_SAVED',
|
||||
};
|
||||
|
||||
type IEmbedTab = IModalTabDeclaration<{ url: string }>;
|
||||
type IEmbedTab = IModalTabDeclaration<{ url: string; isNotSaved: boolean }>;
|
||||
|
||||
const embedTabReducer: IEmbedTab['reducer'] = (state = { url: '' }, action) => {
|
||||
const embedTabReducer: IEmbedTab['reducer'] = (state = { url: '', isNotSaved: false }, action) => {
|
||||
switch (action.type) {
|
||||
case EMBED_TAB_ACTIONS.SET_EMBED_URL:
|
||||
case EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED:
|
||||
return {
|
||||
...state,
|
||||
url: action.payload,
|
||||
isNotSaved: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const EmbedTabContent: NonNullable<IEmbedTab['content']> = ({ dispatch }) => {
|
||||
const EmbedTabContent: NonNullable<IEmbedTab['content']> = ({ state, dispatch }) => {
|
||||
const {
|
||||
embedUrlParamExtensions,
|
||||
shareableUrlForSavedObject,
|
||||
shareableUrl,
|
||||
isEmbedded,
|
||||
objectType,
|
||||
isDirty,
|
||||
} = useShareTabsContext()!;
|
||||
|
||||
const onChange = useCallback(
|
||||
(shareUrl: string) => {
|
||||
dispatch({
|
||||
type: EMBED_TAB_ACTIONS.SET_EMBED_URL,
|
||||
payload: shareUrl,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const setIsNotSaved = useCallback(() => {
|
||||
dispatch({
|
||||
type: EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED,
|
||||
payload: objectType === 'dashboard' ? isDirty : false,
|
||||
});
|
||||
}, [dispatch, objectType, isDirty]);
|
||||
|
||||
return (
|
||||
<EmbedContent
|
||||
|
@ -58,8 +56,9 @@ const EmbedTabContent: NonNullable<IEmbedTab['content']> = ({ dispatch }) => {
|
|||
shareableUrl,
|
||||
isEmbedded,
|
||||
objectType,
|
||||
isNotSaved: state?.isNotSaved,
|
||||
setIsNotSaved,
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -75,14 +74,4 @@ export const embedTab: IEmbedTab = {
|
|||
}),
|
||||
reducer: embedTabReducer,
|
||||
content: EmbedTabContent,
|
||||
modalActionBtn: {
|
||||
id: 'embed',
|
||||
dataTestSubj: 'copyEmbedUrlButton',
|
||||
label: i18n.translate('share.link.copyEmbedCodeButton', {
|
||||
defaultMessage: 'Copy Embed',
|
||||
}),
|
||||
handler: ({ state }) => {
|
||||
copyToClipboard(state.url);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiIcon,
|
||||
EuiRadioGroup,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { ShareMenuItem } from '../../../types';
|
||||
import { type IShareContext } from '../../context';
|
||||
|
||||
type ExportProps = Pick<IShareContext, 'isDirty' | 'objectId' | 'objectType' | 'onClose'> & {
|
||||
layoutOption?: 'print';
|
||||
aggregateReportTypes: ShareMenuItem[];
|
||||
intl: InjectedIntl;
|
||||
};
|
||||
|
||||
type AllowedExports = 'pngV2' | 'printablePdfV2' | 'csv_v2' | 'csv_searchsource' | 'lens_csv';
|
||||
|
||||
const ExportContentUi = ({ isDirty, objectType, aggregateReportTypes, intl }: ExportProps) => {
|
||||
// needed for CSV in Discover
|
||||
const firstRadio =
|
||||
(aggregateReportTypes[0].reportType as AllowedExports) ?? ('printablePdfV2' as const);
|
||||
const [, setIsStale] = useState(false);
|
||||
const [isCreatingReport, setIsCreatingReport] = useState<boolean>(false);
|
||||
const [selectedRadio, setSelectedRadio] = useState<AllowedExports>(firstRadio);
|
||||
const [usePrintLayout, setPrintLayout] = useState(false);
|
||||
const isMounted = useMountedState();
|
||||
|
||||
const markAsStale = useCallback(() => {
|
||||
if (!isMounted) return;
|
||||
setIsStale(true);
|
||||
}, [isMounted]);
|
||||
|
||||
const getProperties = useCallback(() => {
|
||||
if (objectType === 'search') {
|
||||
return aggregateReportTypes[0];
|
||||
} else {
|
||||
return aggregateReportTypes?.filter(({ reportType }) => reportType === selectedRadio)[0];
|
||||
}
|
||||
}, [selectedRadio, aggregateReportTypes, objectType]);
|
||||
|
||||
const handlePrintLayoutChange = useCallback(
|
||||
(evt: EuiSwitchEvent) => {
|
||||
setPrintLayout(evt.target.checked);
|
||||
getProperties();
|
||||
},
|
||||
[setPrintLayout, getProperties]
|
||||
);
|
||||
|
||||
const {
|
||||
generateReportButton,
|
||||
helpText,
|
||||
renderCopyURLButton,
|
||||
generateReport,
|
||||
generateReportForPrinting,
|
||||
downloadCSVLens,
|
||||
absoluteUrl,
|
||||
renderLayoutOptionSwitch,
|
||||
} = getProperties();
|
||||
|
||||
const getRadioOptions = useCallback(() => {
|
||||
if (!aggregateReportTypes.length) {
|
||||
throw new Error('No content registered for this tab');
|
||||
}
|
||||
return aggregateReportTypes.map(({ reportType, label }) => {
|
||||
if (reportType == null) {
|
||||
throw new Error('expected reportType to be string!');
|
||||
}
|
||||
return { id: reportType, label, 'data-test-subj': `${reportType}-radioOption` };
|
||||
});
|
||||
}, [aggregateReportTypes]);
|
||||
|
||||
const renderLayoutOptionsSwitch = useCallback(() => {
|
||||
if (renderLayoutOptionSwitch) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="s" css={{ textWrap: 'nowrap' }}>
|
||||
<FormattedMessage
|
||||
id="share.screenCapturePanelContent.optimizeForPrintingLabel"
|
||||
defaultMessage="For printing"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
checked={usePrintLayout}
|
||||
onChange={handlePrintLayoutChange}
|
||||
data-test-subj="usePrintLayout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="share.screenCapturePanelContent.optimizeForPrintingHelpText"
|
||||
defaultMessage="Uses multiple pages, showing at most 2 visualizations per page "
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}, [usePrintLayout, renderLayoutOptionSwitch, handlePrintLayoutChange]);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted();
|
||||
getRadioOptions();
|
||||
renderLayoutOptionsSwitch();
|
||||
getProperties();
|
||||
markAsStale();
|
||||
}, [
|
||||
aggregateReportTypes,
|
||||
getProperties,
|
||||
getRadioOptions,
|
||||
renderLayoutOptionsSwitch,
|
||||
markAsStale,
|
||||
isMounted,
|
||||
]);
|
||||
|
||||
const showCopyURLButton = useCallback(() => {
|
||||
if (renderCopyURLButton)
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
isDirty ? (
|
||||
<FormattedMessage
|
||||
id="share.modalContent.unsavedStateErrorText"
|
||||
defaultMessage="Save your work before copying this URL."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="share.modalContent.savedStateErrorText"
|
||||
defaultMessage="Copy this POST URL to call generation from outside Kibana or from Watcher."
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiCopy textToCopy={absoluteUrl ?? ''}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty
|
||||
iconType="copy"
|
||||
flush="both"
|
||||
onClick={copy}
|
||||
data-test-subj="shareReportingCopyURL"
|
||||
>
|
||||
<EuiToolTip
|
||||
id="share.savePostURLMessage"
|
||||
content="Unsaved changes. This URL will not reflect later saved changes unless you save."
|
||||
>
|
||||
<FormattedMessage
|
||||
id="share.modalContent.copyUrlButtonLabel"
|
||||
defaultMessage="Post URL"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="share.postURLWatcherMessage"
|
||||
defaultMessage="Copy this POST URL to call generation from outside Kibana or from Watcher. Unsaved changes: URL may change if you upgrade Kibana"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [absoluteUrl, isDirty, renderCopyURLButton]);
|
||||
|
||||
const getReport = useCallback(() => {
|
||||
if (!generateReportForPrinting && !generateReport && !downloadCSVLens) {
|
||||
throw new Error('Report cannot be run due to no generate report method registered');
|
||||
}
|
||||
if (objectType === 'lens' && selectedRadio === 'lens_csv') {
|
||||
return downloadCSVLens!();
|
||||
}
|
||||
return usePrintLayout ? generateReportForPrinting!({ intl }) : generateReport!({ intl });
|
||||
}, [
|
||||
downloadCSVLens,
|
||||
generateReport,
|
||||
generateReportForPrinting,
|
||||
objectType,
|
||||
selectedRadio,
|
||||
usePrintLayout,
|
||||
intl,
|
||||
]);
|
||||
|
||||
const renderGenerateReportButton = useCallback(() => {
|
||||
return (
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setIsCreatingReport(true);
|
||||
getReport();
|
||||
setIsCreatingReport(false);
|
||||
}}
|
||||
data-test-subj="generateReportButton"
|
||||
isLoading={Boolean(isCreatingReport)}
|
||||
>
|
||||
{generateReportButton}
|
||||
</EuiButton>
|
||||
);
|
||||
}, [generateReportButton, getReport, isCreatingReport]);
|
||||
|
||||
const renderRadioOptions = () => {
|
||||
if (getRadioOptions().length > 1) {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent={'spaceBetween'}>
|
||||
<EuiRadioGroup
|
||||
options={getRadioOptions()}
|
||||
onChange={(id) => {
|
||||
setSelectedRadio(id as AllowedExports);
|
||||
getProperties();
|
||||
}}
|
||||
name="image reporting radio group"
|
||||
idSelected={selectedRadio}
|
||||
legend={{
|
||||
children: <FormattedMessage id="share.fileType" defaultMessage="File type" />,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getHelpText = () => {
|
||||
if (objectType === 'lens' && generateReport !== undefined) {
|
||||
return helpText;
|
||||
} else {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="share.helpText.goldLicense.roleNotPDFPNG"
|
||||
defaultMessage="Export a CSV of this visualization."
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiForm>
|
||||
<EuiSpacer size="l" />
|
||||
{getHelpText()}
|
||||
<EuiSpacer size="m" />
|
||||
{renderRadioOptions()}
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiForm>
|
||||
<EuiFlexGroup justifyContent="flexEnd" responsive={false}>
|
||||
{renderLayoutOptionsSwitch()}
|
||||
{showCopyURLButton()}
|
||||
{renderGenerateReportButton()}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExportContent = injectI18n(ExportContentUi);
|
36
src/plugins/share/public/components/tabs/export/index.tsx
Normal file
36
src/plugins/share/public/components/tabs/export/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
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 ShareMenuItem } from '../../context';
|
||||
|
||||
type IExportTab = IModalTabDeclaration;
|
||||
|
||||
const ExportTabContent = () => {
|
||||
const { shareMenuItems, objectType, isDirty, onClose } = useShareTabsContext()!;
|
||||
|
||||
return (
|
||||
<ExportContent
|
||||
objectType={objectType}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
aggregateReportTypes={shareMenuItems as unknown as ShareMenuItem[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const exportTab: IExportTab = {
|
||||
id: 'export',
|
||||
name: i18n.translate('share.contextMenu.exportCodeTab', {
|
||||
defaultMessage: 'Export',
|
||||
}),
|
||||
content: ExportTabContent,
|
||||
};
|
|
@ -8,3 +8,4 @@
|
|||
|
||||
export { linkTab } from './link';
|
||||
export { embedTab } from './embed';
|
||||
export { exportTab } from './export';
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { copyToClipboard } from '@elastic/eui';
|
||||
import { type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal';
|
||||
import { useShareTabsContext } from '../../context';
|
||||
import { LinkContent } from './link_content';
|
||||
|
@ -16,6 +15,7 @@ import { LinkContent } from './link_content';
|
|||
type ILinkTab = IModalTabDeclaration<{
|
||||
dashboardUrl: string;
|
||||
isNotSaved: boolean;
|
||||
setIsClicked: boolean;
|
||||
}>;
|
||||
|
||||
const LINK_TAB_ACTIONS = {
|
||||
|
@ -27,6 +27,7 @@ const linkTabReducer: ILinkTab['reducer'] = (
|
|||
state = {
|
||||
dashboardUrl: '',
|
||||
isNotSaved: false,
|
||||
setIsClicked: false,
|
||||
},
|
||||
action
|
||||
) => {
|
||||
|
@ -51,11 +52,11 @@ const LinkTabContent: ILinkTab['content'] = ({ state, dispatch }) => {
|
|||
objectType,
|
||||
objectId,
|
||||
isDirty,
|
||||
isEmbedded,
|
||||
shareableUrl,
|
||||
shareableUrlForSavedObject,
|
||||
urlService,
|
||||
shareableUrlLocatorParams,
|
||||
allowShortUrl,
|
||||
} = useShareTabsContext()!;
|
||||
|
||||
const setDashboardLink = useCallback(
|
||||
|
@ -68,9 +69,17 @@ const LinkTabContent: ILinkTab['content'] = ({ state, dispatch }) => {
|
|||
const setIsNotSaved = useCallback(() => {
|
||||
dispatch({
|
||||
type: LINK_TAB_ACTIONS.SET_IS_NOT_SAVED,
|
||||
payload: objectType === 'lens' ? isDirty : false,
|
||||
payload:
|
||||
objectType === 'lens' || (objectType === 'dashboard' && !allowShortUrl) ? isDirty : false,
|
||||
});
|
||||
}, [dispatch, objectType, isDirty]);
|
||||
}, [dispatch, objectType, isDirty, allowShortUrl]);
|
||||
|
||||
const setIsClicked = useCallback(() => {
|
||||
dispatch({
|
||||
type: LINK_TAB_ACTIONS.SET_IS_NOT_SAVED,
|
||||
payload: setIsClicked,
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<LinkContent
|
||||
|
@ -78,15 +87,16 @@ const LinkTabContent: ILinkTab['content'] = ({ state, dispatch }) => {
|
|||
objectType,
|
||||
objectId,
|
||||
isDirty,
|
||||
isEmbedded,
|
||||
shareableUrl,
|
||||
shareableUrlForSavedObject,
|
||||
urlService,
|
||||
shareableUrlLocatorParams,
|
||||
dashboardLink: state.dashboardUrl,
|
||||
isNotSaved: state.isNotSaved,
|
||||
dashboardLink: state?.dashboardUrl,
|
||||
setDashboardLink,
|
||||
isNotSaved: state?.isNotSaved,
|
||||
setIsNotSaved,
|
||||
allowShortUrl,
|
||||
setIsClicked: state?.setIsClicked,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -102,13 +112,4 @@ export const linkTab: ILinkTab = {
|
|||
}),
|
||||
content: LinkTabContent,
|
||||
reducer: linkTabReducer,
|
||||
modalActionBtn: {
|
||||
id: 'link',
|
||||
dataTestSubj: 'copyShareUrlButton',
|
||||
label: i18n.translate('share.link.copyLinkButton', { defaultMessage: 'Copy link' }),
|
||||
handler: ({ state }) => {
|
||||
copyToClipboard(state.dashboardUrl);
|
||||
},
|
||||
style: ({ state }) => state.isNotSaved,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,10 +6,20 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiForm, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
copyToClipboard,
|
||||
EuiButton,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { format as formatUrl, parse as parseUrl } from 'url';
|
||||
import { IShareContext } from '../../context';
|
||||
|
||||
|
@ -18,15 +28,12 @@ type LinkProps = Pick<
|
|||
| 'objectType'
|
||||
| 'objectId'
|
||||
| 'isDirty'
|
||||
| 'isEmbedded'
|
||||
| 'urlService'
|
||||
| 'shareableUrl'
|
||||
| 'shareableUrlForSavedObject'
|
||||
| 'shareableUrlLocatorParams'
|
||||
> & {
|
||||
setDashboardLink: (url: string) => void;
|
||||
setIsNotSaved: () => void;
|
||||
};
|
||||
| 'allowShortUrl'
|
||||
>;
|
||||
|
||||
interface UrlParams {
|
||||
[extensionName: string]: {
|
||||
|
@ -38,39 +45,20 @@ export const LinkContent = ({
|
|||
objectType,
|
||||
objectId,
|
||||
isDirty,
|
||||
isEmbedded,
|
||||
shareableUrl,
|
||||
shareableUrlForSavedObject,
|
||||
urlService,
|
||||
shareableUrlLocatorParams,
|
||||
setDashboardLink,
|
||||
setIsNotSaved,
|
||||
allowShortUrl,
|
||||
}: LinkProps) => {
|
||||
const isMounted = useMountedState();
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const [urlParams] = useState<UrlParams | undefined>(undefined);
|
||||
const [isTextCopied, setTextCopied] = useState(false);
|
||||
const [shortUrlCache, setShortUrlCache] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
// propagate url updates upwards to tab
|
||||
setDashboardLink(url);
|
||||
setIsNotSaved();
|
||||
}, [setDashboardLink, url, setIsNotSaved, isDirty]);
|
||||
|
||||
const isNotSaved = useCallback(() => {
|
||||
return objectId === undefined || objectId === '' || isDirty;
|
||||
}, [objectId, isDirty]);
|
||||
|
||||
const makeUrlEmbeddable = useCallback((tempUrl: string): string => {
|
||||
const embedParam = '?embed=true';
|
||||
const urlHasQueryString = tempUrl.indexOf('?') !== -1;
|
||||
|
||||
if (urlHasQueryString) {
|
||||
return tempUrl.replace('?', `${embedParam}&`);
|
||||
}
|
||||
|
||||
return `${tempUrl}${embedParam}`;
|
||||
}, []);
|
||||
return isDirty;
|
||||
}, [isDirty]);
|
||||
|
||||
const getUrlParamExtensions = useCallback(
|
||||
(tempUrl: string): string => {
|
||||
|
@ -93,12 +81,11 @@ export const LinkContent = ({
|
|||
|
||||
const updateUrlParams = useCallback(
|
||||
(tempUrl: string) => {
|
||||
tempUrl = isEmbedded ? makeUrlEmbeddable(tempUrl) : tempUrl;
|
||||
tempUrl = urlParams ? getUrlParamExtensions(tempUrl) : tempUrl;
|
||||
setUrl(tempUrl);
|
||||
return tempUrl;
|
||||
},
|
||||
[makeUrlEmbeddable, getUrlParamExtensions, urlParams, isEmbedded]
|
||||
[getUrlParamExtensions, urlParams]
|
||||
);
|
||||
|
||||
const getSnapshotUrl = useCallback(
|
||||
|
@ -148,56 +135,112 @@ export const LinkContent = ({
|
|||
return updateUrlParams(formattedUrl);
|
||||
}, [getSnapshotUrl, isNotSaved, updateUrlParams]);
|
||||
|
||||
const createShortUrl = useCallback(
|
||||
async (tempUrl: string) => {
|
||||
if (!isMounted || shortUrlCache) return;
|
||||
const shortUrl = shareableUrlLocatorParams
|
||||
? await urlService.shortUrls.get(null).createWithLocator(shareableUrlLocatorParams)
|
||||
: (await urlService.shortUrls.get(null).createFromLongUrl(tempUrl)).url;
|
||||
setShortUrlCache(shortUrl as string);
|
||||
setUrl(shortUrl as string);
|
||||
},
|
||||
[isMounted, shareableUrlLocatorParams, urlService.shortUrls, shortUrlCache]
|
||||
);
|
||||
|
||||
const setUrlHelper = useCallback(() => {
|
||||
let tempUrl: string | undefined;
|
||||
|
||||
if (objectType === 'dashboard' || objectType === 'search') {
|
||||
tempUrl = getSnapshotUrl();
|
||||
const createShortUrl = useCallback(async () => {
|
||||
if (shareableUrlLocatorParams) {
|
||||
const shortUrls = urlService.shortUrls.get(null);
|
||||
const shortUrl = await shortUrls.createWithLocator(shareableUrlLocatorParams);
|
||||
const urlWithLoc = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true });
|
||||
setShortUrlCache(urlWithLoc);
|
||||
return urlWithLoc;
|
||||
} else {
|
||||
tempUrl = getSavedObjectUrl();
|
||||
const snapshotUrl = getSnapshotUrl();
|
||||
const shortUrl = await urlService.shortUrls.get(null).createFromLongUrl(snapshotUrl);
|
||||
setShortUrlCache(shortUrl.url);
|
||||
return shortUrl.url;
|
||||
}
|
||||
return url === '' || objectType === 'lens' ? setUrl(tempUrl!) : createShortUrl(tempUrl!);
|
||||
}, [getSavedObjectUrl, getSnapshotUrl, createShortUrl, objectType, url]);
|
||||
}, [shareableUrlLocatorParams, urlService.shortUrls, getSnapshotUrl, setShortUrlCache]);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted();
|
||||
setUrlHelper();
|
||||
}, [isMounted, setUrlHelper]);
|
||||
const copyUrlHelper = useCallback(async () => {
|
||||
let urlToCopy = url;
|
||||
|
||||
if (!urlToCopy) {
|
||||
let tempUrl = '';
|
||||
|
||||
if (objectType === 'dashboard' || objectType === 'search') {
|
||||
tempUrl = getSnapshotUrl();
|
||||
} else if (objectType === 'lens') {
|
||||
tempUrl = getSavedObjectUrl() as string;
|
||||
}
|
||||
|
||||
urlToCopy = allowShortUrl ? await createShortUrl() : tempUrl;
|
||||
}
|
||||
|
||||
setUrl(() => {
|
||||
copyToClipboard(urlToCopy);
|
||||
setTextCopied(true);
|
||||
return urlToCopy;
|
||||
});
|
||||
}, [allowShortUrl, createShortUrl, getSavedObjectUrl, getSnapshotUrl, objectType, setUrl, url]);
|
||||
|
||||
const renderSaveState =
|
||||
objectType === 'lens' && isNotSaved() ? (
|
||||
<FormattedMessage
|
||||
id="share.link.lens.saveUrlBox"
|
||||
defaultMessage="There are unsaved changes. Before you generate a link, save the {objectType}."
|
||||
values={{ objectType }}
|
||||
/>
|
||||
) : objectType === 'lens' ? (
|
||||
shortUrlCache ?? shareableUrl
|
||||
) : (
|
||||
shareableUrl ?? shortUrlCache ?? ''
|
||||
);
|
||||
|
||||
const lensOnClick = () => {
|
||||
if (objectType === 'lens' && !isDirty) {
|
||||
return copyUrlHelper();
|
||||
} else {
|
||||
return copyUrlHelper();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="share.link.helpText"
|
||||
defaultMessage="Share a direct link to this {objectType}."
|
||||
values={{ objectType }}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiCodeBlock whiteSpace="pre" css={{ paddingRight: '30px' }}>
|
||||
{objectType === 'lens' && isNotSaved() ? (
|
||||
<>
|
||||
<EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="share.link.lens.saveUrlBox"
|
||||
defaultMessage="There are unsaved changes. Before you generate a link, save the lens."
|
||||
id="share.link.helpText"
|
||||
defaultMessage="Share a direct link to this {objectType}."
|
||||
values={{ objectType }}
|
||||
/>
|
||||
) : (
|
||||
shareableUrl ?? url ?? ''
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
{objectType !== 'dashboard' && (
|
||||
<EuiCodeBlock whiteSpace="pre" css={{ paddingRight: '30px' }}>
|
||||
{renderSaveState}
|
||||
</EuiCodeBlock>
|
||||
)}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
</EuiForm>
|
||||
<EuiSpacer />
|
||||
</EuiForm>
|
||||
<EuiFlexGroup justifyContent="flexEnd" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
isDirty && objectType === 'lens'
|
||||
? i18n.translate('share.link.unsaved', {
|
||||
defaultMessage:
|
||||
'There are unsaved changes. Before you generate a link, save the {objectType}.',
|
||||
values: {
|
||||
objectType,
|
||||
},
|
||||
})
|
||||
: isTextCopied
|
||||
? i18n.translate('share.link.copied', { defaultMessage: 'Text copied' })
|
||||
: null
|
||||
}
|
||||
>
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="copyShareUrlButton"
|
||||
data-share-url={url}
|
||||
onBlur={() => (objectType === 'lens' && isDirty ? null : setTextCopied(false))}
|
||||
onClick={lensOnClick}
|
||||
disabled={objectType === 'lens' && isDirty}
|
||||
>
|
||||
<FormattedMessage id="share.link.copyLinkButton" defaultMessage="Copy link" />
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ const createSetupContract = (): Setup => {
|
|||
url,
|
||||
navigate: jest.fn(),
|
||||
setAnonymousAccessServiceProvider: jest.fn(),
|
||||
isNewVersion: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
|
|
@ -42,6 +42,11 @@ export type SharePublicSetup = ShareMenuRegistrySetup & {
|
|||
* Sets the provider for the anonymous access service; this is consumed by the Security plugin to avoid a circular dependency.
|
||||
*/
|
||||
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessServiceContract) => void;
|
||||
/**
|
||||
* Allows for canvas to register the older versioned way whereas reporting for Discover/Lens/Dashboard
|
||||
* can use the new share version and show the share context modals
|
||||
*/
|
||||
isNewVersion: () => boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
@ -74,7 +79,7 @@ export class SharePlugin
|
|||
>
|
||||
{
|
||||
private config: ClientConfigType;
|
||||
private readonly shareMenuRegistry = new ShareMenuRegistry();
|
||||
private readonly shareMenuRegistry?: ShareMenuRegistry;
|
||||
private readonly shareContextMenu = new ShareMenuManager();
|
||||
private redirectManager?: RedirectManager;
|
||||
private url?: BrowserUrlService;
|
||||
|
@ -82,6 +87,9 @@ export class SharePlugin
|
|||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.get<ClientConfigType>();
|
||||
this.shareMenuRegistry = new ShareMenuRegistry({
|
||||
newVersionEnabled: this.config.new_version.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup): SharePublicSetup {
|
||||
|
@ -126,7 +134,7 @@ export class SharePlugin
|
|||
registrations.setup({ analytics });
|
||||
|
||||
return {
|
||||
...this.shareMenuRegistry.setup(),
|
||||
...this.shareMenuRegistry!.setup(),
|
||||
url: this.url,
|
||||
navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options),
|
||||
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessServiceContract) => {
|
||||
|
@ -135,6 +143,7 @@ export class SharePlugin
|
|||
}
|
||||
this.anonymousAccessServiceProvider = provider;
|
||||
},
|
||||
isNewVersion: () => this.config.new_version.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -143,7 +152,7 @@ export class SharePlugin
|
|||
const sharingContextMenuStart = this.shareContextMenu.start(
|
||||
core,
|
||||
this.url!,
|
||||
this.shareMenuRegistry.start(),
|
||||
this.shareMenuRegistry!.start(),
|
||||
disableEmbed,
|
||||
this.config.new_version.enabled ?? false,
|
||||
this.anonymousAccessServiceProvider
|
||||
|
|
|
@ -9,10 +9,9 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { CoreStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { CoreStart, OverlayStart, ThemeServiceStart, ToastsSetup } from '@kbn/core/public';
|
||||
import { EuiWrappingPopover } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
|
||||
import { ShareMenuRegistryStart } from './share_menu_registry';
|
||||
|
@ -58,6 +57,7 @@ export class ShareMenuManager {
|
|||
overlays: core.overlays,
|
||||
i18n: core.i18n,
|
||||
newVersionEnabled,
|
||||
toasts: core.notifications.toasts,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -92,6 +92,7 @@ export class ShareMenuManager {
|
|||
i18n,
|
||||
isDirty,
|
||||
newVersionEnabled,
|
||||
toasts,
|
||||
}: ShowShareMenuOptions & {
|
||||
anchorElement: HTMLElement;
|
||||
menuItems: ShareMenuItem[];
|
||||
|
@ -103,6 +104,7 @@ export class ShareMenuManager {
|
|||
i18n: CoreStart['i18n'];
|
||||
isDirty: boolean;
|
||||
newVersionEnabled: boolean;
|
||||
toasts: ToastsSetup;
|
||||
}) {
|
||||
if (this.isOpen) {
|
||||
onClose();
|
||||
|
@ -176,6 +178,9 @@ export class ShareMenuManager {
|
|||
onClose();
|
||||
session.close();
|
||||
},
|
||||
theme,
|
||||
i18n,
|
||||
toasts,
|
||||
}}
|
||||
/>,
|
||||
{ i18n, theme }
|
||||
|
|
|
@ -12,18 +12,18 @@ import { ShareMenuItem, ShareContext } from '../types';
|
|||
describe('ShareActionsRegistry', () => {
|
||||
describe('setup', () => {
|
||||
test('throws when registering duplicate id', () => {
|
||||
const setup = new ShareMenuRegistry().setup();
|
||||
const setup = new ShareMenuRegistry({ newVersionEnabled: false }).setup();
|
||||
setup.register({
|
||||
id: 'myTest',
|
||||
id: 'csvReports',
|
||||
getShareMenuItems: () => [],
|
||||
});
|
||||
expect(() =>
|
||||
setup.register({
|
||||
id: 'myTest',
|
||||
id: 'csvReports',
|
||||
getShareMenuItems: () => [],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Share menu provider with id [myTest] has already been registered. Use a unique id."`
|
||||
`"Share menu provider with id [csvReports] has already been registered. Use a unique id."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ describe('ShareActionsRegistry', () => {
|
|||
describe('start', () => {
|
||||
describe('getActions', () => {
|
||||
test('returns a flat list of actions returned by all providers', () => {
|
||||
const service = new ShareMenuRegistry();
|
||||
const service = new ShareMenuRegistry({ newVersionEnabled: false });
|
||||
const registerFunction = service.setup().register;
|
||||
const shareAction1 = {} as ShareMenuItem;
|
||||
const shareAction2 = {} as ShareMenuItem;
|
||||
|
@ -39,11 +39,11 @@ describe('ShareActionsRegistry', () => {
|
|||
const provider1Callback = jest.fn(() => [shareAction1]);
|
||||
const provider2Callback = jest.fn(() => [shareAction2, shareAction3]);
|
||||
registerFunction({
|
||||
id: 'myTest',
|
||||
id: 'csvReports',
|
||||
getShareMenuItems: provider1Callback,
|
||||
});
|
||||
registerFunction({
|
||||
id: 'myTest2',
|
||||
id: 'screenCaptureReports',
|
||||
getShareMenuItems: provider2Callback,
|
||||
});
|
||||
const context = {} as ShareContext;
|
||||
|
|
|
@ -10,6 +10,11 @@ import { ShareContext, ShareMenuProvider } from '../types';
|
|||
|
||||
export class ShareMenuRegistry {
|
||||
private readonly shareMenuProviders = new Map<string, ShareMenuProvider>();
|
||||
newVersionEnabled: boolean;
|
||||
|
||||
constructor({ newVersionEnabled }: { newVersionEnabled: boolean }) {
|
||||
this.newVersionEnabled = newVersionEnabled;
|
||||
}
|
||||
|
||||
public setup() {
|
||||
return {
|
||||
|
@ -21,13 +26,25 @@ export class ShareMenuRegistry {
|
|||
* @param shareMenuProvider
|
||||
*/
|
||||
register: (shareMenuProvider: ShareMenuProvider) => {
|
||||
if (this.shareMenuProviders.has(shareMenuProvider.id)) {
|
||||
throw new Error(
|
||||
`Share menu provider with id [${shareMenuProvider.id}] has already been registered. Use a unique id.`
|
||||
);
|
||||
if (
|
||||
!this.newVersionEnabled &&
|
||||
(shareMenuProvider.id === 'csvReports' ||
|
||||
shareMenuProvider.id === 'screenCaptureReports' ||
|
||||
shareMenuProvider.id === 'csvDownloadLens')
|
||||
) {
|
||||
if (this.shareMenuProviders.has(shareMenuProvider.id)) {
|
||||
throw new Error(
|
||||
`Share menu provider with id [${shareMenuProvider.id}] has already been registered. Use a unique id.`
|
||||
);
|
||||
}
|
||||
this.shareMenuProviders.set(shareMenuProvider.id, shareMenuProvider);
|
||||
} else if (
|
||||
shareMenuProvider.id === 'csvReportsModal' ||
|
||||
shareMenuProvider.id === 'modalImageReports' ||
|
||||
shareMenuProvider.id === 'csvDownloadLens'
|
||||
) {
|
||||
this.shareMenuProviders.set(shareMenuProvider.id, shareMenuProvider);
|
||||
}
|
||||
|
||||
this.shareMenuProviders.set(shareMenuProvider.id, shareMenuProvider);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { ComponentType, ReactElement } from 'react';
|
||||
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
|
||||
import { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/components/context_menu/context_menu';
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
import type { Capabilities, ThemeServiceSetup, ToastsSetup } from '@kbn/core/public';
|
||||
import type { UrlService, LocatorPublic } from '../common/url_service';
|
||||
import type { BrowserShortUrlClientFactoryCreateParams } from './url_service/short_urls/short_url_client_factory';
|
||||
import type { BrowserShortUrlClient } from './url_service/short_urls/short_url_client';
|
||||
|
@ -51,6 +51,7 @@ export interface ShareContext {
|
|||
onClose: () => void;
|
||||
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
|
||||
disabledShareUrl?: boolean;
|
||||
toasts: ToastsSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,10 +72,27 @@ export interface ShareContextMenuPanelItem
|
|||
* directly in the context menu. If the item is clicked, the `panel` is shown.
|
||||
* */
|
||||
export interface ShareMenuItem {
|
||||
shareMenuItem: ShareContextMenuPanelItem;
|
||||
panel: EuiContextMenuPanelDescriptor;
|
||||
shareMenuItem?: ShareContextMenuPanelItem;
|
||||
// needed for Canvas
|
||||
panel?: EuiContextMenuPanelDescriptor;
|
||||
label?: 'PDF' | 'CSV' | 'PNG';
|
||||
reportType?: string;
|
||||
requiresSavedState?: boolean;
|
||||
helpText?: ReactElement;
|
||||
copyURLButton?: { id: string; dataTestSubj: string; label: string };
|
||||
generateReportButton?: ReactElement;
|
||||
generateReport?: Function;
|
||||
generateReportForPrinting?: Function;
|
||||
theme?: ThemeServiceSetup;
|
||||
downloadCSVLens?: Function;
|
||||
renderLayoutOptionSwitch?: boolean;
|
||||
layoutOption?: 'print';
|
||||
absoluteUrl?: string;
|
||||
generateCopyUrl?: URL;
|
||||
renderCopyURLButton?: boolean;
|
||||
}
|
||||
|
||||
type ShareMenuItemType = Omit<ShareMenuItem, 'intl'>;
|
||||
/**
|
||||
* @public
|
||||
* A source for additional menu items shown in the share context menu. Any provider
|
||||
|
@ -84,8 +102,7 @@ export interface ShareMenuItem {
|
|||
* */
|
||||
export interface ShareMenuProvider {
|
||||
readonly id: string;
|
||||
|
||||
getShareMenuItems: (context: ShareContext) => ShareMenuItem[];
|
||||
getShareMenuItems: (context: ShareContext) => ShareMenuItemType[];
|
||||
}
|
||||
|
||||
interface UrlParamExtensionProps {
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"@kbn/shared-ux-prompt-not-found",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/shared-ux-tabbed-modal",
|
||||
"@kbn/core-theme-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, useEuiTheme } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
import { injectI18n } from '@kbn/i18n-react';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import React, { ReactNode, useRef } from 'react';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
@ -21,7 +21,6 @@ export interface Props {
|
|||
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||
className?: string;
|
||||
indexPatterns: DataView[];
|
||||
intl: InjectedIntl;
|
||||
timeRangeForSuggestionsOverride?: boolean;
|
||||
filtersForSuggestions?: Filter[];
|
||||
hiddenPanelOptions?: FilterItemsProps['hiddenPanelOptions'];
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
import type {
|
||||
VisualizeServices,
|
||||
VisualizeAppState,
|
||||
|
@ -60,7 +61,7 @@ const TopNav = ({
|
|||
embeddableId,
|
||||
onAppLeave,
|
||||
eventEmitter,
|
||||
}: VisualizeTopNavProps) => {
|
||||
}: VisualizeTopNavProps & { intl: InjectedIntl }) => {
|
||||
const { services } = useKibana<VisualizeServices>();
|
||||
const { TopNavMenu } = services.navigation.ui;
|
||||
const { setHeaderActionMenu, visualizeCapabilities } = services;
|
||||
|
@ -380,4 +381,4 @@ const TopNav = ({
|
|||
) : null;
|
||||
};
|
||||
|
||||
export const VisualizeTopNav = memo(TopNav);
|
||||
export const VisualizeTopNav = injectI18n(memo(TopNav));
|
||||
|
|
|
@ -410,6 +410,7 @@ export const getTopNavConfig = (
|
|||
},
|
||||
isDirty: hasUnappliedChanges || hasUnsavedChanges,
|
||||
showPublicUrlSwitch,
|
||||
toasts: toastNotifications,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1286,8 +1286,8 @@
|
|||
"@kbn/repo-source-classifier-cli/*": ["packages/kbn-repo-source-classifier-cli/*"],
|
||||
"@kbn/reporting-common": ["packages/kbn-reporting/common"],
|
||||
"@kbn/reporting-common/*": ["packages/kbn-reporting/common/*"],
|
||||
"@kbn/reporting-example-plugin": ["x-pack/examples/reporting_example"],
|
||||
"@kbn/reporting-example-plugin/*": ["x-pack/examples/reporting_example/*"],
|
||||
"@kbn/reporting-csv-share-panel": ["packages/kbn-reporting/get_csv_panel_actions"],
|
||||
"@kbn/reporting-csv-share-panel/*": ["packages/kbn-reporting/get_csv_panel_actions/*"],
|
||||
"@kbn/reporting-export-types-csv": ["packages/kbn-reporting/export_types/csv"],
|
||||
"@kbn/reporting-export-types-csv/*": ["packages/kbn-reporting/export_types/csv/*"],
|
||||
"@kbn/reporting-export-types-csv-common": ["packages/kbn-reporting/export_types/csv_common"],
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# Example Reporting integration!
|
||||
|
||||
Use this example code to understand how to add a "Generate Report" button to a
|
||||
Kibana page. This simple example shows that the end-to-end functionality of
|
||||
generating a screenshot report of a page just requires you to render a React
|
||||
component that you import from the Reportinng plugin.
|
||||
|
||||
A "reportable" Kibana page is one that has an **alternate version to show the data in a "screenshot-friendly" way**. The alternate version can be reached at a variation of the page's URL that the App team builds.
|
||||
|
||||
A "screenshot-friendly" page has **all interactive features turned off**. These are typically notifications, popups, tooltips, controls, autocomplete libraries, etc.
|
||||
|
||||
Turning off these features **keeps glitches out of the screenshot**, and makes the server-side headless browser **run faster and use less RAM**.
|
||||
|
||||
The URL that Reporting captures is controlled by the application, is a part of
|
||||
a "jobParams" object that gets passed to the React component imported from
|
||||
Reporting. The job params give the app control over the end-resulting report:
|
||||
|
||||
- Layout
|
||||
- Page dimensions
|
||||
- DOM attributes to select where the visualization container(s) is/are. The App team must add the attributes to DOM elements in their app.
|
||||
- DOM events that the page fires off and signals when the rendering is done. The App team must implement triggering the DOM events around rendering the data in their app.
|
||||
- Export type definition
|
||||
- Processes the jobParams into output data, which is stored in Elasticsearch in the Reporting system index.
|
||||
- Export type definitions are registered with the Reporting plugin at setup time.
|
||||
|
||||
The existing export type definitions are PDF, PNG, and CSV. They should be
|
||||
enough for nearly any use case.
|
||||
|
||||
If the existing options are too limited for a future use case, the AppServices
|
||||
team can assist the App team to implement a custom export type definition of
|
||||
their own, and register it using the Reporting plugin API **(documentation coming soon)**.
|
||||
|
||||
---
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = 'reportingExample';
|
||||
export const PLUGIN_NAME = 'reportingExample';
|
||||
|
||||
export type { MyForwardableState } from './types';
|
||||
|
||||
export type { ReportingExampleLocatorParams } from './locator';
|
||||
export { REPORTING_EXAMPLE_LOCATOR_ID, ReportingExampleLocatorDefinition } from './locator';
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LocatorDefinition } from '@kbn/share-plugin/public';
|
||||
import { PLUGIN_ID } from '.';
|
||||
import type { MyForwardableState } from '../public/types';
|
||||
|
||||
export const REPORTING_EXAMPLE_LOCATOR_ID = 'REPORTING_EXAMPLE_LOCATOR_ID';
|
||||
|
||||
export type ReportingExampleLocatorParams = SerializableRecord;
|
||||
|
||||
export class ReportingExampleLocatorDefinition implements LocatorDefinition<{}> {
|
||||
public readonly id = REPORTING_EXAMPLE_LOCATOR_ID;
|
||||
|
||||
migrations = {
|
||||
'1.0.0': (state: {}) => ({ ...state, migrated: true }),
|
||||
};
|
||||
|
||||
public readonly getLocation = async (params: MyForwardableState) => {
|
||||
const path = Boolean(params.captureTest) ? '/captureTest' : '/';
|
||||
return {
|
||||
app: PLUGIN_ID,
|
||||
path,
|
||||
state: params,
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Ensure, SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
export type MyForwardableState = Ensure<
|
||||
SerializableRecord & { captureTest: 'A' },
|
||||
SerializableRecord
|
||||
>;
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/reporting-example-plugin",
|
||||
"owner": "@elastic/appex-sharedux",
|
||||
"description": "Example integration code for applications to feature reports.",
|
||||
"plugin": {
|
||||
"id": "reportingExample",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"reporting",
|
||||
"developerExamples",
|
||||
"kibanaReact",
|
||||
"navigation",
|
||||
"screenshotMode",
|
||||
"share"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { CaptureTest } from './containers/capture_test';
|
||||
import { Main } from './containers/main';
|
||||
import { ApplicationContextProvider } from './application_context';
|
||||
import { SetupDeps, StartDeps, MyForwardableState } from './types';
|
||||
import { ROUTES } from './constants';
|
||||
|
||||
export const renderApp = (
|
||||
coreStart: CoreStart,
|
||||
deps: Omit<StartDeps & SetupDeps, 'developerExamples'>,
|
||||
{ appBasePath, element, history }: AppMountParameters, // FIXME: appBasePath is deprecated
|
||||
forwardedParams: MyForwardableState
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<ApplicationContextProvider forwardedState={forwardedParams}>
|
||||
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route path={ROUTES.captureTest} exact render={() => <CaptureTest />} />
|
||||
<Route render={() => <Main basename={appBasePath} {...coreStart} {...deps} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</KibanaThemeProvider>
|
||||
</ApplicationContextProvider>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useContext, createContext, FC } from 'react';
|
||||
|
||||
import type { MyForwardableState } from './types';
|
||||
|
||||
interface ContextValue {
|
||||
forwardedState?: MyForwardableState;
|
||||
}
|
||||
|
||||
const ApplicationContext = createContext<undefined | ContextValue>(undefined);
|
||||
|
||||
export const ApplicationContextProvider: FC<{ forwardedState: ContextValue['forwardedState'] }> = ({
|
||||
forwardedState,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<ApplicationContext.Provider value={{ forwardedState }}>{children}</ApplicationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useApplicationContext = (): ContextValue => {
|
||||
const ctx = useContext(ApplicationContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useApplicationContext called outside of ApplicationContext!');
|
||||
}
|
||||
return ctx;
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { TestImageA } from './test_image_a';
|
File diff suppressed because one or more lines are too long
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// Values based on A4 page size
|
||||
export const VIS = {
|
||||
width: 1950 / 2,
|
||||
height: 1200 / 2,
|
||||
};
|
||||
|
||||
export const ROUTES = {
|
||||
captureTest: '/captureTest',
|
||||
main: '/',
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
.reportingExample {
|
||||
&__captureContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
margin-top: $euiSizeM;
|
||||
margin-bottom: $euiSizeM;
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { parsePath } from 'history';
|
||||
import {
|
||||
EuiTabbedContent,
|
||||
EuiTabbedContentTab,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPageHeader,
|
||||
EuiPageBody,
|
||||
EuiPageSection,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { TestImageA } from '../components';
|
||||
import { useApplicationContext } from '../application_context';
|
||||
import { MyForwardableState } from '../types';
|
||||
import { ROUTES } from '../constants';
|
||||
|
||||
import './capture_test.scss';
|
||||
|
||||
const ItemsContainer: FunctionComponent<{ count: string }> = ({ count, children }) => (
|
||||
<div
|
||||
className="reportingExample__captureContainer"
|
||||
data-shared-items-container
|
||||
data-shared-items-count={count}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabs: Array<EuiTabbedContentTab & { id: MyForwardableState['captureTest'] }> = [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'Test A',
|
||||
content: (
|
||||
<ItemsContainer count="4">
|
||||
<TestImageA />
|
||||
<TestImageA />
|
||||
<TestImageA />
|
||||
<TestImageA />
|
||||
</ItemsContainer>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const CaptureTest: FunctionComponent = () => {
|
||||
const { forwardedState } = useApplicationContext();
|
||||
const tabToRender = forwardedState?.captureTest;
|
||||
const history = useHistory();
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageSection>
|
||||
<EuiPageHeader>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
href={history.createHref(parsePath(ROUTES.main))}
|
||||
>
|
||||
Back to main
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeader>
|
||||
</EuiPageSection>
|
||||
<EuiPageSection>
|
||||
<EuiSpacer />
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={
|
||||
tabToRender ? tabs.find((tab) => tab.id === tabToRender) : undefined
|
||||
}
|
||||
/>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
};
|
|
@ -1,378 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { parsePath } from 'history';
|
||||
import moment from 'moment';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
import { takeWhile } from 'rxjs';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiCodeBlock,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageHeader,
|
||||
EuiPageSection,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { JobParamsPDFDeprecated, JobParamsPDFV2 } from '@kbn/reporting-export-types-pdf-common';
|
||||
import { JobParamsPNGV2 } from '@kbn/reporting-export-types-png-common';
|
||||
import type { ReportingStart } from '@kbn/reporting-plugin/public';
|
||||
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public';
|
||||
import { BrowserRouter as Router, useHistory } from 'react-router-dom';
|
||||
|
||||
import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common';
|
||||
import { useApplicationContext } from '../application_context';
|
||||
import { ROUTES } from '../constants';
|
||||
import type { MyForwardableState } from '../types';
|
||||
|
||||
interface ReportingExampleAppProps {
|
||||
basename: string;
|
||||
reporting: ReportingStart;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
}
|
||||
|
||||
const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana'];
|
||||
|
||||
export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAppProps) => {
|
||||
const history = useHistory();
|
||||
const { forwardedState } = useApplicationContext();
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('forwardedState', forwardedState);
|
||||
}, [forwardedState]);
|
||||
|
||||
// Context Menu
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const onButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false);
|
||||
};
|
||||
|
||||
// Async Logos
|
||||
const [logos, setLogos] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
Rx.timer(2200)
|
||||
.pipe(takeWhile(() => logos.length < sourceLogos.length))
|
||||
.subscribe(() => {
|
||||
setLogos([...sourceLogos.slice(0, logos.length + 1)]);
|
||||
});
|
||||
});
|
||||
|
||||
const getPDFJobParamsDefault = (): JobParamsPDFDeprecated => {
|
||||
return {
|
||||
layout: { id: 'preserve_layout' },
|
||||
relativeUrls: ['/app/reportingExample#/intended-visualization'],
|
||||
objectType: 'develeloperExample',
|
||||
title: 'Reporting Developer Example',
|
||||
browserTimezone: 'UTC',
|
||||
version: '1',
|
||||
};
|
||||
};
|
||||
|
||||
const getPDFJobParamsDefaultV2 = (): JobParamsPDFV2 => {
|
||||
return {
|
||||
version: '8.0.0',
|
||||
layout: { id: 'preserve_layout' },
|
||||
locatorParams: [
|
||||
{ id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } },
|
||||
],
|
||||
objectType: 'develeloperExample',
|
||||
title: 'Reporting Developer Example',
|
||||
browserTimezone: moment.tz.guess(),
|
||||
};
|
||||
};
|
||||
|
||||
const getPNGJobParamsDefaultV2 = (): JobParamsPNGV2 => {
|
||||
return {
|
||||
version: '8.0.0',
|
||||
layout: { id: 'preserve_layout' },
|
||||
locatorParams: {
|
||||
id: REPORTING_EXAMPLE_LOCATOR_ID,
|
||||
version: '0.5.0',
|
||||
params: { myTestState: {} },
|
||||
},
|
||||
objectType: 'develeloperExample',
|
||||
title: 'Reporting Developer Example',
|
||||
browserTimezone: moment.tz.guess(),
|
||||
};
|
||||
};
|
||||
|
||||
const getCaptureTestPNGJobParams = (): JobParamsPNGV2 => {
|
||||
return {
|
||||
version: '8.0.0',
|
||||
layout: { id: 'preserve_layout' },
|
||||
locatorParams: {
|
||||
id: REPORTING_EXAMPLE_LOCATOR_ID,
|
||||
version: '0.5.0',
|
||||
params: { captureTest: 'A' } as MyForwardableState,
|
||||
},
|
||||
objectType: 'develeloperExample',
|
||||
title: 'Reporting Developer Example',
|
||||
browserTimezone: moment.tz.guess(),
|
||||
};
|
||||
};
|
||||
|
||||
const getCaptureTestPDFJobParams = (print: boolean) => (): JobParamsPDFV2 => {
|
||||
return {
|
||||
version: '8.0.0',
|
||||
layout: {
|
||||
id: print ? 'print' : 'preserve_layout',
|
||||
dimensions: {
|
||||
// Magic numbers based on height of components not rendered on this screen :(
|
||||
height: 2400,
|
||||
width: 1822,
|
||||
},
|
||||
},
|
||||
locatorParams: [
|
||||
{
|
||||
id: REPORTING_EXAMPLE_LOCATOR_ID,
|
||||
version: '0.5.0',
|
||||
params: { captureTest: 'A' } as MyForwardableState,
|
||||
},
|
||||
],
|
||||
objectType: 'develeloperExample',
|
||||
title: 'Reporting Developer Example',
|
||||
browserTimezone: moment.tz.guess(),
|
||||
};
|
||||
};
|
||||
|
||||
const panels: EuiContextMenuProps['panels'] = [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{ name: 'PDF Reports', icon: 'document', panel: 1 },
|
||||
{ name: 'PNG Reports', icon: 'document', panel: 7 },
|
||||
{ name: 'Capture test', icon: 'document', panel: 8, 'data-test-subj': 'captureTestPanel' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
initialFocusedItemIndex: 1,
|
||||
title: 'PDF Reports',
|
||||
items: [
|
||||
{ name: 'Default layout', icon: 'document', panel: 2 },
|
||||
{ name: 'Default layout V2', icon: 'document', panel: 4 },
|
||||
{ name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
initialFocusedItemIndex: 0,
|
||||
title: 'Capture test',
|
||||
items: [
|
||||
{
|
||||
name: 'Capture test A - PNG',
|
||||
icon: 'document',
|
||||
panel: 9,
|
||||
'data-test-subj': 'captureTestPNG',
|
||||
},
|
||||
{
|
||||
name: 'Capture test A - PDF',
|
||||
icon: 'document',
|
||||
panel: 10,
|
||||
'data-test-subj': 'captureTestPDF',
|
||||
},
|
||||
{
|
||||
name: 'Capture test A - PDF print optimized',
|
||||
icon: 'document',
|
||||
panel: 11,
|
||||
'data-test-subj': 'captureTestPDFPrint',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
initialFocusedItemIndex: 0,
|
||||
title: 'PNG Reports',
|
||||
items: [{ name: 'Default layout V2', icon: 'document', panel: 5 }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Default layout',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPDF
|
||||
getJobParams={getPDFJobParamsDefault}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Canvas Layout Option',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPDF
|
||||
layoutOption="canvas"
|
||||
getJobParams={getPDFJobParamsDefault}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Default layout V2',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPDFV2
|
||||
getJobParams={getPDFJobParamsDefaultV2}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Default layout V2',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPNGV2
|
||||
getJobParams={getPNGJobParamsDefaultV2}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: 'Test A',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPNGV2
|
||||
getJobParams={getCaptureTestPNGJobParams}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: 'Test A',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPDFV2
|
||||
getJobParams={getCaptureTestPDFJobParams(false)}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: 'Test A',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPDFV2
|
||||
layoutOption="print"
|
||||
getJobParams={getCaptureTestPDFJobParams(true)}
|
||||
onClose={closePopover}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Router basename={basename}>
|
||||
<I18nProvider>
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiTitle size="l">
|
||||
<h1>Reporting Example</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeader>
|
||||
<EuiPageSection>
|
||||
<EuiTitle>
|
||||
<h2>Example of a Sharing menu using components from Reporting</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiText>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="l">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="contextMenuExample"
|
||||
button={
|
||||
<EuiButton data-test-subj="shareButton" onClick={onButtonClick}>
|
||||
Share
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<EuiLink href={history.createHref(parsePath(ROUTES.captureTest))}>
|
||||
Go to capture test
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<div data-shared-items-container data-shared-items-count="5">
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
<EuiFlexItem data-shared-item>
|
||||
{forwardedState ? (
|
||||
<>
|
||||
<EuiText>
|
||||
<p>
|
||||
<strong>Forwarded app state</strong>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiCodeBlock>{JSON.stringify(forwardedState)}</EuiCodeBlock>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiText>
|
||||
<p>
|
||||
<strong>No forwarded app state found</strong>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiCodeBlock>{'{}'}</EuiCodeBlock>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{logos.map((item, index) => (
|
||||
<EuiFlexItem
|
||||
key={index}
|
||||
data-shared-item
|
||||
data-shared-render-error
|
||||
data-render-error="This is an example error"
|
||||
>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type={`logo${item}`} />}
|
||||
title={`Elastic ${item}`}
|
||||
description="Example of a card's description. Stick to one or two sentences."
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<p>Screenshot Mode is {screenshotMode.isScreenshotMode() ? 'ON' : 'OFF'}!</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</I18nProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReportingExamplePlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new ReportingExamplePlugin();
|
||||
}
|
||||
export type { PluginSetup, PluginStart } from './types';
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { PLUGIN_ID, PLUGIN_NAME, ReportingExampleLocatorDefinition } from '../common';
|
||||
import { SetupDeps, StartDeps, MyForwardableState } from './types';
|
||||
|
||||
export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
|
||||
public setup(core: CoreSetup, { developerExamples, screenshotMode, share }: SetupDeps): void {
|
||||
core.application.register({
|
||||
id: PLUGIN_ID,
|
||||
title: PLUGIN_NAME,
|
||||
visibleIn: [],
|
||||
async mount(params: AppMountParameters) {
|
||||
// Load application bundle
|
||||
const { renderApp } = await import('./application');
|
||||
const [coreStart, depsStart] = (await core.getStartServices()) as [
|
||||
CoreStart,
|
||||
StartDeps,
|
||||
unknown
|
||||
];
|
||||
// Render the application
|
||||
return renderApp(
|
||||
coreStart,
|
||||
{ ...depsStart, screenshotMode, share },
|
||||
params,
|
||||
params.history.location.state as MyForwardableState
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Show the app in Developer Examples
|
||||
developerExamples.register({
|
||||
appId: 'reportingExample',
|
||||
title: 'Reporting integration',
|
||||
description: 'Demonstrate how to put an Export button on a page and generate reports.',
|
||||
});
|
||||
|
||||
share.url.locators.create(new ReportingExampleLocatorDefinition());
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import { ReportingStart } from '@kbn/reporting-plugin/public';
|
||||
import type { MyForwardableState } from '../common';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PluginStart {}
|
||||
|
||||
export interface SetupDeps {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
share: SharePluginSetup;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
}
|
||||
export interface StartDeps {
|
||||
navigation: NavigationPublicPluginStart;
|
||||
reporting: ReportingStart;
|
||||
}
|
||||
|
||||
export type { MyForwardableState };
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"common/**/*.ts",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/navigation-plugin",
|
||||
"@kbn/screenshot-mode-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/reporting-plugin",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/reporting-export-types-pdf-common",
|
||||
"@kbn/reporting-export-types-png-common",
|
||||
]
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
"expressionTagcloud",
|
||||
"eventAnnotation",
|
||||
"unifiedSearch",
|
||||
"contentManagement"
|
||||
"contentManagement",
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"expressionLegacyMetricVis",
|
||||
|
@ -45,7 +45,8 @@
|
|||
"globalSearch",
|
||||
"savedObjectsTagging",
|
||||
"spaces",
|
||||
"serverless"
|
||||
"serverless",
|
||||
"licensing"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"unifiedSearch",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiForm, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { EuiButton, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
|
@ -21,32 +21,33 @@ export function DownloadPanelContent({
|
|||
warnings = [],
|
||||
}: DownloadPanelContentProps) {
|
||||
return (
|
||||
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareReportingForm">
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<>
|
||||
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareReportingForm">
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.application.csvPanelContent.generationDescription"
|
||||
defaultMessage="Download the data displayed in the visualization."
|
||||
/>
|
||||
</p>
|
||||
{warnings.map((warning, i) => (
|
||||
<p key={i}>{warning}</p>
|
||||
))}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiButton
|
||||
disabled={isDisabled}
|
||||
fullWidth
|
||||
fill
|
||||
onClick={onClick}
|
||||
data-test-subj="lnsApp_downloadCSVButton"
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.application.csvPanelContent.downloadButtonLabel"
|
||||
defaultMessage="Export as CSV"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
{warnings.map((warning, i) => (
|
||||
<p key={i}>{warning}</p>
|
||||
))}
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth={true}>
|
||||
<EuiButton
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
data-test-subj="lnsApp_downloadCSVButton"
|
||||
fill
|
||||
fullWidth
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.application.csvPanelContent.downloadButtonLabel"
|
||||
defaultMessage="Generate CSV"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import { tableHasFormulas } from '@kbn/data-plugin/common';
|
|||
import { downloadMultipleAs, ShareContext, ShareMenuProvider } from '@kbn/share-plugin/public';
|
||||
import { exporters } from '@kbn/data-plugin/public';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FormatFactory } from '../../../common/types';
|
||||
import { DownloadPanelContent } from './csv_download_panel_content_lazy';
|
||||
import { TableInspectorAdapter } from '../../editor_frame_service/types';
|
||||
import { DownloadPanelContent } from './csv_download_panel_content_lazy';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -96,13 +97,17 @@ function getWarnings(activeData: TableInspectorAdapter) {
|
|||
interface DownloadPanelShareOpts {
|
||||
uiSettings: IUiSettingsClient;
|
||||
formatFactoryFn: () => FormatFactory;
|
||||
atLeastGold: () => boolean;
|
||||
isNewVersion: boolean;
|
||||
}
|
||||
|
||||
export const downloadCsvShareProvider = ({
|
||||
uiSettings,
|
||||
formatFactoryFn,
|
||||
atLeastGold,
|
||||
isNewVersion,
|
||||
}: DownloadPanelShareOpts): ShareMenuProvider => {
|
||||
const getShareMenuItems = ({ objectType, sharingData, onClose }: ShareContext) => {
|
||||
const getShareMenuItems = ({ objectType, sharingData }: ShareContext) => {
|
||||
if ('lens' !== objectType) {
|
||||
return [];
|
||||
}
|
||||
|
@ -121,40 +126,87 @@ export const downloadCsvShareProvider = ({
|
|||
}
|
||||
);
|
||||
|
||||
const menuItemMetadata = {
|
||||
shareMenuItem: {
|
||||
name: panelTitle,
|
||||
icon: 'document',
|
||||
disabled: !csvEnabled,
|
||||
sortOrder: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const downloadCSVHandler = () =>
|
||||
downloadCSVs({
|
||||
title,
|
||||
formatFactory: formatFactoryFn(),
|
||||
activeData,
|
||||
uiSettings,
|
||||
columnsSorting,
|
||||
});
|
||||
|
||||
if (!isNewVersion) {
|
||||
return [
|
||||
{
|
||||
...menuItemMetadata,
|
||||
panel: {
|
||||
id: 'csvDownloadPanel',
|
||||
title: panelTitle,
|
||||
content: (
|
||||
<DownloadPanelContent
|
||||
isDisabled={!csvEnabled}
|
||||
warnings={getWarnings(activeData)}
|
||||
onClick={downloadCSVHandler}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
shareMenuItem: {
|
||||
name: panelTitle,
|
||||
icon: 'document',
|
||||
disabled: !csvEnabled,
|
||||
sortOrder: 1,
|
||||
},
|
||||
panel: {
|
||||
id: 'csvDownloadPanel',
|
||||
title: panelTitle,
|
||||
content: (
|
||||
<DownloadPanelContent
|
||||
isDisabled={!csvEnabled}
|
||||
warnings={getWarnings(activeData)}
|
||||
onClick={async () => {
|
||||
await downloadCSVs({
|
||||
title,
|
||||
formatFactory: formatFactoryFn(),
|
||||
activeData,
|
||||
uiSettings,
|
||||
columnsSorting,
|
||||
});
|
||||
onClose?.();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...menuItemMetadata,
|
||||
label: 'CSV' as const,
|
||||
reportType: 'lens_csv',
|
||||
downloadCSVLens: downloadCSVHandler,
|
||||
...(atLeastGold()
|
||||
? {
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.share.helpText"
|
||||
defaultMessage="Select the file type you would like to export for this visualization."
|
||||
/>
|
||||
),
|
||||
generateReportButton: (
|
||||
<FormattedMessage id="xpack.lens.share.export" defaultMessage="Generate export" />
|
||||
),
|
||||
renderLayoutOptionSwitch: false,
|
||||
getJobParams: undefined,
|
||||
showRadios: true,
|
||||
}
|
||||
: {
|
||||
isDisabled: !csvEnabled,
|
||||
warnings: getWarnings(activeData),
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.application.csvPanelContent.generationDescription"
|
||||
defaultMessage="Download the data displayed in the visualization."
|
||||
/>
|
||||
),
|
||||
generateReportButton: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.share.csvButton"
|
||||
data-test-subj="generateReportButton"
|
||||
defaultMessage="Download CSV"
|
||||
/>
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'csvDownload',
|
||||
id: 'csvDownloadLens',
|
||||
getShareMenuItems,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -297,6 +297,7 @@ export const LensTopNavMenu = ({
|
|||
dataViewFieldEditor,
|
||||
dataViewEditor,
|
||||
dataViews: dataViewsService,
|
||||
notifications,
|
||||
} = useKibana<LensAppServices>().services;
|
||||
|
||||
const {
|
||||
|
@ -639,6 +640,7 @@ export const LensTopNavMenu = ({
|
|||
onClose: () => {
|
||||
anchorElement?.focus();
|
||||
},
|
||||
toasts: notifications.toasts,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -793,6 +795,7 @@ export const LensTopNavMenu = ({
|
|||
isOnTextBasedMode,
|
||||
lensStore,
|
||||
theme$,
|
||||
notifications.toasts,
|
||||
]);
|
||||
|
||||
const onQuerySubmitWrapped = useCallback(
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { take } from 'rxjs';
|
||||
import type { AppMountParameters, CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
|
||||
import type { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
|
@ -63,6 +64,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import { registerSavedObjectToPanelMethod } from '@kbn/embeddable-plugin/public';
|
||||
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service';
|
||||
import type {
|
||||
FormBasedDatasource as FormBasedDatasourceType,
|
||||
|
@ -179,6 +181,7 @@ export interface LensPluginStartDependencies {
|
|||
eventAnnotationService: EventAnnotationServiceType;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
serverless?: ServerlessPluginStart;
|
||||
licensing?: LicensingPluginStart;
|
||||
}
|
||||
|
||||
export interface LensPublicSetup {
|
||||
|
@ -393,6 +396,17 @@ export class LensPlugin {
|
|||
downloadCsvShareProvider({
|
||||
uiSettings: core.uiSettings,
|
||||
formatFactoryFn: () => startServices().plugins.fieldFormats.deserialize,
|
||||
atLeastGold: () => {
|
||||
let isGold = false;
|
||||
startServices()
|
||||
.plugins.licensing?.license$.pipe(take(1))
|
||||
.subscribe((license) => {
|
||||
// need to make sure user has correct license and permissions to see PDF/PNG
|
||||
isGold = license.hasAtLeast('gold');
|
||||
});
|
||||
return isGold;
|
||||
},
|
||||
isNewVersion: share.isNewVersion(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -111,7 +111,8 @@
|
|||
"@kbn/presentation-publishing",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
"@kbn/unified-data-table",
|
||||
"@kbn/shared-ux-markdown"
|
||||
"@kbn/shared-ux-markdown",
|
||||
"@kbn/licensing-plugin",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
"taskManager",
|
||||
"screenshotMode",
|
||||
"share",
|
||||
"features"
|
||||
"features",
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"security",
|
||||
"spaces",
|
||||
"usageCollection",
|
||||
"screenshotting"
|
||||
"screenshotting",
|
||||
],
|
||||
"requiredBundles": [
|
||||
"embeddable",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { from, ReplaySubject } from 'rxjs';
|
||||
|
||||
import {
|
||||
CoreSetup,
|
||||
|
@ -30,11 +30,13 @@ import type { ClientConfigType } from '@kbn/reporting-public';
|
|||
import { ReportingAPIClient } from '@kbn/reporting-public';
|
||||
|
||||
import {
|
||||
ReportingCsvPanelAction,
|
||||
getSharedComponents,
|
||||
reportingCsvShareProvider,
|
||||
reportingCsvShareModalProvider,
|
||||
reportingExportModalProvider,
|
||||
reportingScreenshotShareProvider,
|
||||
} from '@kbn/reporting-public/share';
|
||||
import { ReportingCsvPanelAction } from '@kbn/reporting-csv-share-panel';
|
||||
import type { ReportingSetup, ReportingStart } from '.';
|
||||
import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler';
|
||||
|
||||
|
@ -70,7 +72,7 @@ export class ReportingPublicPlugin
|
|||
{
|
||||
private kibanaVersion: string;
|
||||
private apiClient?: ReportingAPIClient;
|
||||
private readonly stop$ = new Rx.ReplaySubject<void>(1);
|
||||
private readonly stop$ = new ReplaySubject<void>(1);
|
||||
private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', {
|
||||
defaultMessage: 'Reporting',
|
||||
});
|
||||
|
@ -123,7 +125,7 @@ export class ReportingPublicPlugin
|
|||
uiActions: uiActionsSetup,
|
||||
} = setupDeps;
|
||||
|
||||
const startServices$ = Rx.from(getStartServices());
|
||||
const startServices$ = from(getStartServices());
|
||||
const usesUiCapabilities = !this.config.roles.enabled;
|
||||
|
||||
const apiClient = this.getApiClient(core.http, core.uiSettings);
|
||||
|
@ -205,7 +207,7 @@ export class ReportingPublicPlugin
|
|||
const reportingStart = this.getContract(core);
|
||||
const { toasts } = core.notifications;
|
||||
|
||||
startServices$.subscribe(([{ application }, { licensing }]) => {
|
||||
startServices$.subscribe(([{ application, i18n: i18nStart }, { licensing }]) => {
|
||||
licensing.license$.subscribe((license) => {
|
||||
shareSetup.register(
|
||||
reportingCsvShareProvider({
|
||||
|
@ -218,8 +220,8 @@ export class ReportingPublicPlugin
|
|||
theme: core.theme,
|
||||
})
|
||||
);
|
||||
|
||||
if (this.config.export_types.pdf.enabled || this.config.export_types.png.enabled) {
|
||||
// needed for Canvas and legacy tests
|
||||
shareSetup.register(
|
||||
reportingScreenshotShareProvider({
|
||||
apiClient,
|
||||
|
@ -232,9 +234,35 @@ export class ReportingPublicPlugin
|
|||
})
|
||||
);
|
||||
}
|
||||
if (shareSetup.isNewVersion()) {
|
||||
shareSetup.register(
|
||||
reportingCsvShareModalProvider({
|
||||
apiClient,
|
||||
uiSettings,
|
||||
license,
|
||||
application,
|
||||
usesUiCapabilities,
|
||||
theme: core.theme,
|
||||
i18n: i18nStart,
|
||||
})
|
||||
);
|
||||
|
||||
if (this.config.export_types.pdf.enabled || this.config.export_types.png.enabled) {
|
||||
shareSetup.register(
|
||||
reportingExportModalProvider({
|
||||
apiClient,
|
||||
uiSettings,
|
||||
license,
|
||||
application,
|
||||
usesUiCapabilities,
|
||||
theme: core.theme,
|
||||
i18n: i18nStart,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return reportingStart;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"@kbn/core-http-request-handler-context-server",
|
||||
"@kbn/reporting-public",
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/reporting-csv-share-panel",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -26,7 +26,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
testFiles: [
|
||||
require.resolve('./search_examples'),
|
||||
require.resolve('./embedded_lens'),
|
||||
require.resolve('./reporting_examples'),
|
||||
require.resolve('./screenshotting'),
|
||||
require.resolve('./triggers_actions_ui_examples'),
|
||||
],
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import path from 'path';
|
||||
import type { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({
|
||||
getService,
|
||||
getPageObjects,
|
||||
updateBaselines,
|
||||
}: FtrProviderContext & { updateBaselines: boolean }) {
|
||||
const PageObjects = getPageObjects(['common', 'reporting']);
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const png = getService('png');
|
||||
const config = getService('config');
|
||||
const log = getService('log');
|
||||
const screenshotDir = config.get('screenshots.directory');
|
||||
|
||||
const appId = 'reportingExample';
|
||||
|
||||
const fixtures = {
|
||||
baselineAPng: path.resolve(__dirname, 'fixtures/baseline/capture_a.png'),
|
||||
baselineAPdf: path.resolve(__dirname, 'fixtures/baseline/capture_a.pdf'),
|
||||
baselineAPdfPrint: path.resolve(__dirname, 'fixtures/baseline/capture_a_print.pdf'),
|
||||
};
|
||||
|
||||
// NOTE: Occasionally, you may need to run the test and copy the "session" image file and replace the
|
||||
// "baseline" image file to reflect current renderings. The source and destination file paths can be found in
|
||||
// the INFO logs for this test run.
|
||||
describe('Captures', () => {
|
||||
before(async () => {
|
||||
await browser.setWindowSize(1600, 1000);
|
||||
});
|
||||
|
||||
it('PNG file matches the baseline image', async () => {
|
||||
await PageObjects.common.navigateToApp(appId);
|
||||
|
||||
await (await testSubjects.find('shareButton')).click();
|
||||
await (await testSubjects.find('captureTestPanel')).click();
|
||||
await (await testSubjects.find('captureTestPNG')).click();
|
||||
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
const url = await PageObjects.reporting.getReportURL(60000);
|
||||
const captureData = await PageObjects.reporting.getRawPdfReportData(url);
|
||||
|
||||
const pngSessionFilePath = await PageObjects.reporting.writeSessionReport(
|
||||
'capture_test_baseline_a',
|
||||
'png',
|
||||
captureData,
|
||||
screenshotDir
|
||||
);
|
||||
|
||||
log.info(
|
||||
`session image path: ${pngSessionFilePath}` +
|
||||
`, baseline image path: ${fixtures.baselineAPng}`
|
||||
);
|
||||
|
||||
expect(
|
||||
await png.compareAgainstBaseline(
|
||||
pngSessionFilePath,
|
||||
fixtures.baselineAPng,
|
||||
screenshotDir,
|
||||
updateBaselines
|
||||
)
|
||||
).to.be.lessThan(0.03);
|
||||
});
|
||||
});
|
||||
}
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 589 KiB |
Binary file not shown.
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginFunctionalProviderContext } from '../../../../test/plugin_functional/services';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
|
||||
describe('reporting examples', function () {
|
||||
loadTestFile(require.resolve('./capture_test'));
|
||||
});
|
||||
}
|
|
@ -5624,7 +5624,7 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/reporting-example-plugin@link:x-pack/examples/reporting_example":
|
||||
"@kbn/reporting-csv-share-panel@link:packages/kbn-reporting/get_csv_panel_actions":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue