diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1f6dccc88bccf..5c8ac3ab58ae6 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -170,7 +170,7 @@ pageLoadAssetSize: serverlessSearch: 26287 serverlessWorkplaceAI: 5078 sessionView: 47912 - share: 58677 + share: 64618 slo: 40437 snapshotRestore: 22068 spaces: 28871 diff --git a/src/platform/packages/private/kbn-reporting/public/moon.yml b/src/platform/packages/private/kbn-reporting/public/moon.yml index fe2c872314cc7..f47f0a08172d7 100644 --- a/src/platform/packages/private/kbn-reporting/public/moon.yml +++ b/src/platform/packages/private/kbn-reporting/public/moon.yml @@ -39,6 +39,8 @@ dependsOn: - '@kbn/ui-actions-plugin' - '@kbn/actions-plugin' - '@kbn/licensing-types' + - '@kbn/es-query' + - '@kbn/utility-types' tags: - shared-browser - package diff --git a/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.test.tsx b/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.test.tsx new file mode 100644 index 0000000000000..2a9b7fc854dab --- /dev/null +++ b/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.test.tsx @@ -0,0 +1,134 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import crypto from 'crypto'; +import { NEVER } from 'rxjs'; + +jest.mock('../../shared/get_search_csv_job_params', () => ({ + getSearchCsvJobParams: jest.fn(() => ({ + reportType: 'csv_v2', + decoratedJobParams: {}, + })), +})); + +import { getSearchCsvJobParams } from '../../shared/get_search_csv_job_params'; +import { getCsvReportParams, getShareMenuItems } from './csv_export_config'; + +describe('csv export config', () => { + describe('getCsvReportParams', () => { + it('should return report params that use absolute time, when useAbsoluteTime is true', () => { + const reportParams = getCsvReportParams({ + sharingData: { + isTextBased: true, + locatorParams: [ + { + id: crypto.randomUUID(), + version: 'test', + params: { + timeRange: { + from: 'now-90d/d', + to: 'now', + }, + }, + }, + ], + getSearchSource: () => ({}), + columns: [], + absoluteTimeRange: { + from: '2021-01-01T00:00:00.000Z', + to: '2021-01-01T00:00:00.000Z', + }, + title: 'test', + }, + useAbsoluteTime: true, + }); + + expect(reportParams).toEqual( + expect.objectContaining({ + locatorParams: expect.arrayContaining([ + expect.objectContaining({ + params: expect.objectContaining({ + timeRange: { + from: '2021-01-01T00:00:00.000Z', + to: '2021-01-01T00:00:00.000Z', + }, + }), + }), + ]), + }) + ); + }); + }); + + describe('getShareMenuItems', () => { + describe('generateReportingJobCSV', () => { + it('invokes getSearchModeParams with useAbsoluteTime set to true', () => { + const absoluteTimeRange = { + from: '2021-01-01T00:00:00.000Z', + to: '2021-01-02T00:00:00.000Z', + }; + const sharingData = { + isTextBased: true, + locatorParams: [ + { + id: crypto.randomUUID(), + version: 'test', + params: { + timeRange: { from: 'now-90d/d', to: 'now' }, + }, + }, + ], + getSearchSource: jest.fn(), + columns: [], + absoluteTimeRange, + title: 'test', + }; + + const apiClient = { + createReportingShareJob: jest.fn(() => new Promise(() => {})), + getManagementLink: jest.fn(), + getReportingPublicJobPath: jest.fn(), + getDecoratedJobParams: jest.fn((params) => params), + }; + + const shareMenu = getShareMenuItems({ + apiClient, + startServices$: NEVER, + csvConfig: { maxRows: 0 }, + isServerless: false, + } as unknown as Parameters[0])({ + objectType: 'search', + sharingData, + shareableUrlLocatorParams: undefined, + } as unknown as Parameters>[0]); + + (getSearchCsvJobParams as jest.Mock).mockClear(); + + // attempt to generate the asset export + void shareMenu.generateAssetExport({ + intl: { formatMessage: jest.fn() }, + } as unknown as Parameters[0]); + + expect(getSearchCsvJobParams).toHaveBeenCalledWith( + expect.objectContaining({ + searchModeParams: { + isEsqlMode: true, + locatorParams: [ + expect.objectContaining({ + params: expect.objectContaining({ + timeRange: absoluteTimeRange, + }), + }), + ], + }, + }) + ); + }); + }); + }); +}); diff --git a/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.tsx b/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.tsx index 90ca5cc24a211..3065c192acac6 100644 --- a/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.tsx +++ b/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/csv_export_config.tsx @@ -11,41 +11,64 @@ import React from 'react'; import { firstValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; -import type { InjectedIntl } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { ShareContext, ExportShare } from '@kbn/share-plugin/public'; import type { LocatorParams } from '@kbn/reporting-common/types'; import { EuiCallOut, EuiText } from '@elastic/eui'; -import type { ReportParamsGetter, ReportParamsGetterOptions } from '../../../types'; +import type { TimeRange } from '@kbn/es-query'; +import type { ExportGenerationOpts } from '@kbn/share-plugin/public/types'; +import type { + ReportingCSVSharingData, + ReportParamsGetter, + ReportParamsGetterOptions, +} from '../../../types'; import type { CsvSearchModeParams } from '../../shared/get_search_csv_job_params'; import { getSearchCsvJobParams } from '../../shared/get_search_csv_job_params'; import type { ExportModalShareOpts } from '../../share_context_menu'; +const toAbsoluteTimeRange = ( + locatorParams: LocatorParams[], + absoluteTimeRange: TimeRange | undefined +): LocatorParams[] => { + return locatorParams.map((lp) => { + if (!absoluteTimeRange) { + return lp; + } + + return { + ...lp, + params: { + ...lp.params, + timeRange: absoluteTimeRange, + }, + }; + }); +}; + export const getCsvReportParams: ReportParamsGetter< - ReportParamsGetterOptions & { forShareUrl?: boolean }, + ReportParamsGetterOptions & { + forShareUrl?: boolean; + useAbsoluteTime?: boolean; + }, CsvSearchModeParams -> = ({ sharingData, forShareUrl = false }) => { - const getSearchSource = sharingData.getSearchSource as ({ - addGlobalTimeFilter, - absoluteTime, - }: { - addGlobalTimeFilter?: boolean; - absoluteTime?: boolean; - }) => SerializedSearchSourceFields; +> = ({ sharingData, forShareUrl = false, useAbsoluteTime = false }) => { + const getSearchSource = sharingData.getSearchSource; if (sharingData.isTextBased) { - // csv v2 uses locator params + const locatorParams = sharingData.locatorParams; + return { isEsqlMode: true, - locatorParams: sharingData.locatorParams as LocatorParams[], + locatorParams: useAbsoluteTime + ? toAbsoluteTimeRange(locatorParams, sharingData.absoluteTimeRange) + : locatorParams, }; } // csv v1 uses search source and columns return { isEsqlMode: false, - columns: sharingData.columns as string[] | undefined, + columns: sharingData.columns, searchSource: getSearchSource({ addGlobalTimeFilter: true, absoluteTime: !forShareUrl, @@ -61,15 +84,28 @@ export const getShareMenuItems = ({ objectType, sharingData, - }: ShareContext): ReturnType extends Promise ? R : never => { - const getSearchModeParams = (forShareUrl?: boolean): CsvSearchModeParams => - getCsvReportParams({ sharingData, forShareUrl }); + shareableUrlLocatorParams, + }: ShareContext): Awaited< + ReturnType['config']> + > => { + const getSearchModeParams = ({ + forShareUrl, + useAbsoluteTime, + }: { + forShareUrl?: boolean; + useAbsoluteTime?: boolean; + } = {}): CsvSearchModeParams => + getCsvReportParams({ + sharingData, + forShareUrl, + useAbsoluteTime, + }); - const generateReportingJobCSV = ({ intl }: { intl: InjectedIntl }) => { + const generateReportingJobCSV = ({ intl }: ExportGenerationOpts) => { const { reportType, decoratedJobParams } = getSearchCsvJobParams({ apiClient, - searchModeParams: getSearchModeParams(false), - title: sharingData.title as string, + searchModeParams: getSearchModeParams({ useAbsoluteTime: true }), + title: sharingData.title, }); return firstValueFrom(startServices$).then(([startServices]) => { @@ -128,21 +164,30 @@ export const getShareMenuItems = defaultMessage: 'Export', }); - const { reportType, decoratedJobParams } = getSearchCsvJobParams({ + const { reportType } = getSearchCsvJobParams({ apiClient, - searchModeParams: getSearchModeParams(true), - title: sharingData.title as string, + searchModeParams: getSearchModeParams({ forShareUrl: true }), + title: sharingData.title, }); - const relativePath = apiClient.getReportingPublicJobPath(reportType, decoratedJobParams); - - const absoluteUrl = new URL(relativePath, window.location.href).toString(); + const getAbsoluteUrl = () => { + const { reportType: _reportType, decoratedJobParams } = getSearchCsvJobParams({ + apiClient, + searchModeParams: getSearchModeParams({ + forShareUrl: true, + }), + title: sharingData.title, + }); + const relativePath = apiClient.getReportingPublicJobPath(_reportType, decoratedJobParams); + return new URL(relativePath, window.location.href).toString(); + }; return { name: panelTitle, exportType: reportType, label: 'CSV', icon: 'table', + supportsAbsoluteTime: true, generateAssetExport: generateReportingJobCSV, helpText: ( absoluteUrl, + generateAssetURIValue: getAbsoluteUrl, }, renderTotalHitsSizeWarning: (totalHits: number = 0): React.ReactNode => { const maxRows = csvConfig?.maxRows || 0; diff --git a/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/index.ts b/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/index.ts index c995d7cee6e27..073c7415c434c 100644 --- a/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/index.ts +++ b/src/platform/packages/private/kbn-reporting/public/share/integrations/csv/index.ts @@ -10,13 +10,14 @@ import type { ExportShare, RegisterShareIntegrationArgs } from '@kbn/share-plugin/public'; import type { ExportModalShareOpts } from '../../share_context_menu'; import { checkLicense } from '../../..'; +import type { ReportingCSVSharingData } from '../../../types'; export const reportingCsvExportShareIntegration = ({ apiClient, startServices$, csvConfig, isServerless, -}: ExportModalShareOpts): RegisterShareIntegrationArgs => { +}: ExportModalShareOpts): RegisterShareIntegrationArgs> => { return { id: 'csvReports', groupId: 'export', diff --git a/src/platform/packages/private/kbn-reporting/public/share/share_context_menu/index.ts b/src/platform/packages/private/kbn-reporting/public/share/share_context_menu/index.ts index ce37f02a0a7cd..9cce2c927ce8d 100644 --- a/src/platform/packages/private/kbn-reporting/public/share/share_context_menu/index.ts +++ b/src/platform/packages/private/kbn-reporting/public/share/share_context_menu/index.ts @@ -11,6 +11,7 @@ import type * as Rx from 'rxjs'; import type { ApplicationStart, CoreStart } from '@kbn/core/public'; import type { ILicense } from '@kbn/licensing-types'; +import type { SharingData } from '@kbn/share-plugin/public'; import type { ReportingAPIClient } from '../../reporting_api_client'; import type { ClientConfigType } from '../../types'; @@ -41,13 +42,8 @@ export interface ExportPanelShareOpts { startServices$: Rx.Observable; } -export interface ReportingSharingData { - title: string; +export interface ReportingSharingData extends SharingData { reportingDisabled?: boolean; - locatorParams: { - id: string; - params: unknown; - }; } export interface JobParamsProviderOptions { diff --git a/src/platform/packages/private/kbn-reporting/public/tsconfig.json b/src/platform/packages/private/kbn-reporting/public/tsconfig.json index fbdc5551f46e5..44c89ad7565ad 100644 --- a/src/platform/packages/private/kbn-reporting/public/tsconfig.json +++ b/src/platform/packages/private/kbn-reporting/public/tsconfig.json @@ -29,5 +29,7 @@ "@kbn/ui-actions-plugin", "@kbn/actions-plugin", "@kbn/licensing-types", + "@kbn/es-query", + "@kbn/utility-types" ] } diff --git a/src/platform/packages/private/kbn-reporting/public/types.ts b/src/platform/packages/private/kbn-reporting/public/types.ts index cdccd54ab62d9..85c3e6080f00a 100644 --- a/src/platform/packages/private/kbn-reporting/public/types.ts +++ b/src/platform/packages/private/kbn-reporting/public/types.ts @@ -11,8 +11,12 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { HomePublicPluginStart } from '@kbn/home-plugin/public'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { ManagementStart } from '@kbn/management-plugin/public'; -import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { SharePluginStart, SharingData } from '@kbn/share-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import type { LocatorParams } from '@kbn/reporting-common/types'; +import type { TimeRange } from '@kbn/es-query'; +import type { SerializableRecord } from '@kbn/utility-types'; export interface ReportingPublicPluginStartDependencies { home: HomePublicPluginStart; @@ -45,9 +49,24 @@ export interface ClientConfigType { statefulSettings: { enabled: boolean }; } -export interface ReportParamsGetterOptions { +export type ReportingCSVSharingDataLocatorParams = Array< + LocatorParams +>; + +export interface ReportingCSVSharingData extends SharingData { + locatorParams: ReportingCSVSharingDataLocatorParams; + isTextBased: boolean; + getSearchSource: (args: { + addGlobalTimeFilter?: boolean; + absoluteTime?: boolean; + }) => SerializedSearchSourceFields; + columns: string[] | undefined; + absoluteTimeRange: TimeRange | undefined; +} + +export interface ReportParamsGetterOptions { objectType?: string; - sharingData: any; + sharingData: S; } export type ReportParamsGetter< diff --git a/src/platform/plugins/shared/discover/moon.yml b/src/platform/plugins/shared/discover/moon.yml index f745535493695..e2afb2619471a 100644 --- a/src/platform/plugins/shared/discover/moon.yml +++ b/src/platform/plugins/shared/discover/moon.yml @@ -144,6 +144,7 @@ dependsOn: - '@kbn/presentation-util-plugin' - '@kbn/core-saved-objects-common' - '@kbn/as-code-filters-constants' + - '@kbn/reporting-public' tags: - plugin - prod diff --git a/src/platform/plugins/shared/discover/public/__mocks__/services.ts b/src/platform/plugins/shared/discover/public/__mocks__/services.ts index 94af432eef5a6..f5d3333bdd45e 100644 --- a/src/platform/plugins/shared/discover/public/__mocks__/services.ts +++ b/src/platform/plugins/shared/discover/public/__mocks__/services.ts @@ -263,6 +263,7 @@ export function createDiscoverServicesMock(): DiscoverServices { }, metadata: { branch: 'test', + version: 'major.minor.patch', }, theme, storage: new LocalStorageMock({}) as unknown as Storage, diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.test.ts b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.test.ts new file mode 100644 index 0000000000000..95e9dfb8ba700 --- /dev/null +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.test.ts @@ -0,0 +1,138 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { createDiscoverServicesMock } from '../../../../../__mocks__/services'; +import { buildShareOptions } from './get_share'; +import { + getDiscoverInternalStateMock, + type InternalStateMockToolkit, +} from '../../../../../__mocks__/discover_state.mock'; +import { FetchStatus } from '../../../../types'; +import { internalStateActions } from '../../../state_management/redux'; + +const mockDiscoverService = createDiscoverServicesMock(); + +describe('getShare', () => { + let toolkit: InternalStateMockToolkit; + + beforeAll(async () => { + toolkit = getDiscoverInternalStateMock({ + services: mockDiscoverService, + persistedDataViews: [dataViewMock], + }); + + await toolkit.initializeTabs(); + await toolkit.initializeSingleTab({ tabId: toolkit.getCurrentTab().id }); + + toolkit.internalState.dispatch( + internalStateActions.setDataView({ + tabId: toolkit.getCurrentTab().id, + dataView: dataViewMock, + }) + ); + }); + + it('should return the correct share options, without absolute time range set when in classic mode', async () => { + const shareOptions = await buildShareOptions({ + services: mockDiscoverService, + discoverParams: { + dataView: dataViewMock, + isEsqlMode: false, + adHocDataViews: [], + authorizedRuleTypeIds: [], + actions: { + updateAdHocDataViews: jest.fn(), + }, + }, + currentTab: toolkit.getCurrentTab(), + persistedDiscoverSession: undefined, + totalHitsState: { result: 0, fetchStatus: FetchStatus.COMPLETE }, + hasUnsavedChanges: false, + }); + + expect(shareOptions).toEqual( + expect.objectContaining({ + allowShortUrl: false, + shareableUrl: 'http://localhost/', + shareableUrlForSavedObject: '#?_g=()', + sharingData: expect.objectContaining({ + isTextBased: false, + absoluteTimeRange: undefined, + locatorParams: expect.arrayContaining([ + expect.objectContaining({ + id: undefined, + version: 'major.minor.patch', + params: expect.objectContaining({ + timeRange: expect.objectContaining({ + from: expect.any(String), + to: expect.any(String), + }), + dataViewId: dataViewMock.id, + }), + }), + ]), + }), + objectId: undefined, + objectType: 'search', + objectTypeAlias: 'Discover session', + }) + ); + }); + + it('should return the correct share options, with absolute time range set when in ES|QL mode', async () => { + const shareOptions = await buildShareOptions({ + services: mockDiscoverService, + discoverParams: { + dataView: dataViewMock, + isEsqlMode: true, + adHocDataViews: [], + authorizedRuleTypeIds: [], + actions: { + updateAdHocDataViews: jest.fn(), + }, + }, + currentTab: toolkit.getCurrentTab(), + persistedDiscoverSession: undefined, + totalHitsState: { result: 0, fetchStatus: FetchStatus.COMPLETE }, + hasUnsavedChanges: false, + }); + + expect(shareOptions).toEqual( + expect.objectContaining({ + allowShortUrl: false, + shareableUrl: 'http://localhost/', + shareableUrlForSavedObject: '#?_g=()', + sharingData: expect.objectContaining({ + isTextBased: true, + absoluteTimeRange: expect.objectContaining({ + from: expect.any(String), + to: expect.any(String), + }), + locatorParams: expect.arrayContaining([ + expect.objectContaining({ + id: undefined, + version: 'major.minor.patch', + params: expect.objectContaining({ + timeRange: expect.objectContaining({ + from: expect.any(String), + to: expect.any(String), + }), + dataViewId: dataViewMock.id, + }), + }), + ]), + }), + objectId: undefined, + objectType: 'search', + objectTypeAlias: 'Discover session', + }) + ); + }); +}); diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx index 9f358cd9ffde5..f010616133732 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_share.tsx @@ -11,12 +11,12 @@ import { AppMenuActionId } from '@kbn/discover-utils'; import { omit } from 'lodash'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { TimeRange } from '@kbn/es-query'; import type { DiscoverSession } from '@kbn/saved-search-plugin/common'; import type { AppMenuItemType, AppMenuPopoverItem } from '@kbn/core-chrome-app-menu-components'; import type { ShowShareMenuOptions } from '@kbn/share-plugin/public'; -import type { ShareActionIntents } from '@kbn/share-plugin/public/types'; +import type { ShareActionIntents, SharingData } from '@kbn/share-plugin/public/types'; import type { IntlShape } from '@kbn/i18n-react'; +import type { ReportingCSVSharingData } from '@kbn/reporting-public/types'; import type { DataTotalHitsMsg } from '../../../state_management/discover_data_state_container'; import { getSharingData, showPublicUrlSwitch } from '../../../../../utils/get_sharing_data'; import { createSearchSource } from '../../../state_management/utils/create_search_source'; @@ -34,17 +34,27 @@ interface BuildShareOptionsParams { hasUnsavedChanges: boolean; } +/** + * Specifies an explicit type for the sharing data of the Discover app. + */ +type DiscoverSharingData = SharingData & ReportingCSVSharingData; + /** * Builds share options for both share modal and export integrations */ -const buildShareOptions = async ({ +export const buildShareOptions = async ({ discoverParams, services, currentTab, persistedDiscoverSession, totalHitsState, hasUnsavedChanges, -}: BuildShareOptionsParams): Promise> => { +}: BuildShareOptionsParams): Promise< + Omit< + ShowShareMenuOptions, + 'anchorElement' | 'asExport' + > +> => { const { dataView, isEsqlMode } = discoverParams; const searchSource = createSearchSource({ @@ -64,18 +74,19 @@ const buildShareOptions = async ({ const { locator } = services; const { timefilter } = services.data.query.timefilter; const timeRange = timefilter.getTime(); + const absoluteTimeRange = timefilter.getAbsoluteTime(); const refreshInterval = timefilter.getRefreshInterval(); const filters = services.filterManager.getFilters(); // Share -> Get links -> Snapshot - const params: DiscoverAppLocatorParams & { timeRange: TimeRange | undefined } = { + const params: DiscoverSharingData['locatorParams'][number]['params'] = { ...omit(currentTab.appState, 'dataSource'), ...(persistedDiscoverSession?.id ? { savedSearchId: persistedDiscoverSession.id } : {}), ...(dataView?.isPersisted() ? { dataViewId: dataView?.id } : { dataViewSpec: dataView?.toMinimalSpec() }), filters, - timeRange: timeRange ?? undefined, + timeRange, refreshInterval, }; @@ -140,7 +151,7 @@ const buildShareOptions = async ({ }, sharingData: { isTextBased: isEsqlMode, - locatorParams: [{ id: locator.id, params }], + locatorParams: [{ id: locator.id, version: services.metadata.version, params }], ...searchSourceSharingData, // CSV reports can be generated without a saved search so we provide a fallback title title: @@ -149,6 +160,7 @@ const buildShareOptions = async ({ defaultMessage: 'Untitled Discover session', }), totalHits: totalHitsState.result || 0, + absoluteTimeRange: isEsqlMode ? absoluteTimeRange : undefined, }, isDirty: !persistedDiscoverSession?.id || hasUnsavedChanges, }; diff --git a/src/platform/plugins/shared/discover/public/build_services.ts b/src/platform/plugins/shared/discover/public/build_services.ts index d5b2195c4d00d..870931395a744 100644 --- a/src/platform/plugins/shared/discover/public/build_services.ts +++ b/src/platform/plugins/shared/discover/public/build_services.ts @@ -122,7 +122,7 @@ export interface DiscoverServices { fieldFormats: FieldFormatsStart; dataViews: DataViewsContract; inspector: InspectorPublicPluginStart; - metadata: { branch: string }; + metadata: { branch: string; version: string }; navigation: NavigationPublicPluginStart; share?: SharePluginStart; urlForwarding: UrlForwardingStart; @@ -227,6 +227,7 @@ export const buildServices = ({ inspector: plugins.inspector, metadata: { branch: context.env.packageInfo.branch, + version: context.env.packageInfo.version, }, navigation: plugins.navigation, share: plugins.share, diff --git a/src/platform/plugins/shared/discover/tsconfig.json b/src/platform/plugins/shared/discover/tsconfig.json index a4171a1d65a92..752cc6854df61 100644 --- a/src/platform/plugins/shared/discover/tsconfig.json +++ b/src/platform/plugins/shared/discover/tsconfig.json @@ -139,6 +139,7 @@ "@kbn/presentation-util-plugin", "@kbn/core-saved-objects-common", "@kbn/as-code-filters-constants", + "@kbn/reporting-public", ], "exclude": ["target/**/*"] } diff --git a/src/platform/plugins/shared/share/public/components/export_integrations/export_integrations.test.tsx b/src/platform/plugins/shared/share/public/components/export_integrations/export_integrations.test.tsx index 75f5b9720d351..3d75f609118b1 100644 --- a/src/platform/plugins/shared/share/public/components/export_integrations/export_integrations.test.tsx +++ b/src/platform/plugins/shared/share/public/components/export_integrations/export_integrations.test.tsx @@ -13,7 +13,7 @@ import { userEvent } from '@testing-library/user-event'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { EuiFlyout, EuiButton } from '@elastic/eui'; -import { ExportMenu, ManagedFlyout } from './export_integrations'; +import { ExportMenu, ManagedExportFlyout } from './export_integrations'; import type { IShareContext } from '../context'; import type { ExportShareConfig, ShareConfigs } from '../../types'; @@ -48,7 +48,7 @@ const mockShareContext: IShareContext = { }, }, objectType: 'type', - sharingData: { title: 'title', url: 'url' }, + sharingData: { title: 'title', url: 'url', locatorParams: { id: 'test', params: {} } }, isDirty: false, onClose: jest.fn(), }; @@ -201,7 +201,7 @@ describe('Export Integrations', () => { if (isFlyoutVisible) { flyout = ( setIsFlyoutVisible(false)} aria-label="Export"> - = ({ shareContext }) => { @@ -59,7 +59,7 @@ interface LayoutOptionsProps { printLayoutChange: (evt: EuiSwitchEvent) => void; } -export interface ManagedFlyoutProps { +export interface ManagedExportFlyoutProps { exportIntegration: ExportShareConfig; intl: InjectedIntl; isDirty: boolean; @@ -75,6 +75,7 @@ export interface ManagedFlyoutProps { sharingData: { [key: string]: unknown; }; + shareableUrlLocatorParams?: ShareContext['shareableUrlLocatorParams']; } function LayoutOptionsSwitch({ usePrintLayout, printLayoutChange }: LayoutOptionsProps) { @@ -128,7 +129,7 @@ function LayoutOptionsSwitch({ usePrintLayout, printLayoutChange }: LayoutOption ); } -export function ManagedFlyout({ +export function ManagedExportFlyout({ exportIntegration, intl, isDirty, @@ -140,7 +141,7 @@ export function ManagedFlyout({ onSave, isSaving, sharingData, -}: ManagedFlyoutProps) { +}: ManagedExportFlyoutProps) { const [usePrintLayout, setPrintLayout] = useState(false); const [isCreatingExport, setIsCreatingExport] = useState(false); @@ -296,6 +297,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) { objectTypeAlias, objectTypeMeta, sharingData, + shareableUrlLocatorParams, } = useShareTypeContext('integration', 'export'); const { shareMenuItems: exportDerivatives } = useShareTypeContext( 'integration', @@ -314,6 +316,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) { id: string; group: keyof typeof selectionOptions.current; }>(); + const selectedMenuItem = useMemo(() => { let result: ExportShareConfig | ExportShareDerivativesConfig | null = null; @@ -464,7 +467,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) { }} /> {selectedMenuItemMeta!.group === 'export' ? ( - ) : ( (selectedMenuItem as ExportShareDerivativesConfig)?.config.flyoutContent({ diff --git a/src/platform/plugins/shared/share/public/components/export_integrations/index.ts b/src/platform/plugins/shared/share/public/components/export_integrations/index.ts index 5714a199f8264..60e0d7ca72693 100644 --- a/src/platform/plugins/shared/share/public/components/export_integrations/index.ts +++ b/src/platform/plugins/shared/share/public/components/export_integrations/index.ts @@ -7,4 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { ExportMenu, ManagedFlyout, type ManagedFlyoutProps } from './export_integrations'; +export { + ExportMenu, + ManagedExportFlyout, + type ManagedExportFlyoutProps, +} from './export_integrations'; diff --git a/src/platform/plugins/shared/share/public/components/share_context_menu.tsx b/src/platform/plugins/shared/share/public/components/share_context_menu.tsx index f2763167ec78a..a4911e3d43ff3 100644 --- a/src/platform/plugins/shared/share/public/components/share_context_menu.tsx +++ b/src/platform/plugins/shared/share/public/components/share_context_menu.tsx @@ -22,7 +22,7 @@ import type { ShareMenuItemLegacy, ShareContextMenuPanelItem, UrlParamExtension, - ShareableUrlLocatorParams, + ShareableLocatorParams, } from '../types'; import type { AnonymousAccessServiceContract } from '../../common/anonymous_access'; import type { BrowserUrlService } from '../types'; @@ -36,7 +36,7 @@ export interface ShareContextMenuProps { shareableUrlForSavedObject?: string; shareableUrlLocatorParams?: { locator: LocatorPublic; - params: ShareableUrlLocatorParams; + params: ShareableLocatorParams; }; shareMenuItems: ShareMenuItemLegacy[]; sharingData: any; diff --git a/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx b/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx index 53ea598af3afa..8d7617c133425 100644 --- a/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx +++ b/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx @@ -81,7 +81,7 @@ const mockShareContext: IShareContext = { }, }, objectType: 'type', - sharingData: { title: 'title', url: 'url' }, + sharingData: { title: 'title', url: 'url', locatorParams: { id: 'test', params: {} } }, isDirty: false, onClose: jest.fn(), }; diff --git a/src/platform/plugins/shared/share/public/components/tabs/embed/embed_content.test.tsx b/src/platform/plugins/shared/share/public/components/tabs/embed/embed_content.test.tsx index 590c393991198..a191c075cdb57 100644 --- a/src/platform/plugins/shared/share/public/components/tabs/embed/embed_content.test.tsx +++ b/src/platform/plugins/shared/share/public/components/tabs/embed/embed_content.test.tsx @@ -50,7 +50,7 @@ const mockShareContext: IShareContext = { title: 'title', config: {}, }, - sharingData: { title: 'title', url: 'url' }, + sharingData: { title: 'title', url: 'url', locatorParams: { id: 'test', params: {} } }, }; const renderComponent = ( diff --git a/src/platform/plugins/shared/share/public/components/tabs/link/link_content.test.tsx b/src/platform/plugins/shared/share/public/components/tabs/link/link_content.test.tsx index 14b934076c9e7..338ccd73acafd 100644 --- a/src/platform/plugins/shared/share/public/components/tabs/link/link_content.test.tsx +++ b/src/platform/plugins/shared/share/public/components/tabs/link/link_content.test.tsx @@ -33,7 +33,7 @@ const mockShareContext: IShareContext = { title: 'title', config: {}, }, - sharingData: { title: 'title', url: 'url' }, + sharingData: { title: 'title', url: 'url', locatorParams: { id: 'test', params: {} } }, }; const renderComponent = ( diff --git a/src/platform/plugins/shared/share/public/components/url_panel_content.tsx b/src/platform/plugins/shared/share/public/components/url_panel_content.tsx index 03460edb9dd12..045596294eb34 100644 --- a/src/platform/plugins/shared/share/public/components/url_panel_content.tsx +++ b/src/platform/plugins/shared/share/public/components/url_panel_content.tsx @@ -32,7 +32,7 @@ import { i18n } from '@kbn/i18n'; import type { Capabilities } from '@kbn/core/public'; import type { LocatorPublic } from '../../common'; -import type { ShareableUrlLocatorParams, UrlParamExtension } from '../types'; +import type { ShareableLocatorParams, UrlParamExtension } from '../types'; import type { AnonymousAccessServiceContract, AnonymousAccessState, @@ -48,7 +48,7 @@ export interface UrlPanelContentProps { shareableUrlForSavedObject?: string; shareableUrlLocatorParams?: { locator: LocatorPublic; - params: ShareableUrlLocatorParams; + params: ShareableLocatorParams; }; urlParamExtensions?: UrlParamExtension[]; anonymousAccess?: AnonymousAccessServiceContract; diff --git a/src/platform/plugins/shared/share/public/index.ts b/src/platform/plugins/shared/share/public/index.ts index fb46230487bc0..9288f95160cf2 100644 --- a/src/platform/plugins/shared/share/public/index.ts +++ b/src/platform/plugins/shared/share/public/index.ts @@ -30,7 +30,9 @@ export type { ExportShareConfig, ExportShareDerivatives, RegisterShareIntegrationArgs, - ShareableUrlLocatorParams, + ShareableLocatorParams, + SharingData, + ShareActionConfigArgs, } from './types'; export type { RedirectOptions } from '../common/url_service'; @@ -40,6 +42,7 @@ import { SharePlugin } from './plugin'; export { downloadMultipleAs, downloadFileAs } from './lib/download_as'; export type { DownloadableContent } from './lib/download_as'; +export { convertRelativeTimeStringToAbsoluteTimeString } from './lib/time_utils'; export function plugin(ctx: PluginInitializerContext) { return new SharePlugin(ctx); diff --git a/src/platform/plugins/shared/share/public/services/share_menu_manager.tsx b/src/platform/plugins/shared/share/public/services/share_menu_manager.tsx index b95b92f612abc..aef7eea3ef40b 100644 --- a/src/platform/plugins/shared/share/public/services/share_menu_manager.tsx +++ b/src/platform/plugins/shared/share/public/services/share_menu_manager.tsx @@ -10,7 +10,7 @@ import React, { createRef } from 'react'; import ReactDOM from 'react-dom'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import type { CoreStart } from '@kbn/core/public'; +import type { CoreStart, OverlayFlyoutOpenOptions } from '@kbn/core/public'; import type { RenderingService } from '@kbn/core-rendering-browser'; import type { InjectedIntl } from '@kbn/i18n-react'; import type { @@ -23,8 +23,8 @@ import type { ShareRegistry } from './share_menu_registry'; import { ShareMenu } from '../components/share_tabs'; import { ExportMenu, - ManagedFlyout, - type ManagedFlyoutProps, + ManagedExportFlyout, + type ManagedExportFlyoutProps, } from '../components/export_integrations'; import { ShareProvider, type IShareContext } from '../components/context'; @@ -67,6 +67,7 @@ export class ShareMenuManager { core.rendering ); }, + /** * Returns a handler to trigger a specific export integration by ID. * For direct exports, executes immediately; otherwise opens a flyout. @@ -76,74 +77,74 @@ export class ShareMenuManager { exportId: string, intl: InjectedIntl ): Promise<(() => Promise) | null> => { - const onClose = () => { - this.onClose(); - options.onClose?.(); - }; - - const menuItems = await resolveShareItemsForShareContext({ - ...options, + return this.createExportHandler( + core, + options, + resolveShareItemsForShareContext, isServerless, - onClose, - }); - - const exportIntegration = menuItems.find( - (item) => - item.shareType === 'integration' && item.id === exportId && item.groupId === 'export' - ); + (menuItems, getFlyoutSession, onClose) => { + const exportIntegration = menuItems.find( + (item) => + item.shareType === 'integration' && + item.id === exportId && + item.groupId === 'export' + ); - if (!exportIntegration) { - return null; - } + if (!exportIntegration) { + return null; + } - const exportConfig = exportIntegration.config as ExportShareConfig['config']; - - // Direct export (no UI needed) - execute immediately - if ( - !exportConfig.copyAssetURIConfig && - !exportConfig.generateAssetComponent && - exportConfig.generateAssetExport - ) { - return async () => { - await exportConfig - .generateAssetExport({ intl, optimizedForPrinting: false }) - .finally(onClose); - }; - } + const exportConfig = exportIntegration.config as ExportShareConfig['config']; - // Export needs UI (flyout) - return async () => { - const flyoutSession = core.overlays.openFlyout( - toMountPoint( - { - flyoutSession.close(); - onClose(); - }} - publicAPIEnabled={!isServerless} - shareObjectType={options.objectType} - shareObjectTypeAlias={options.objectTypeAlias} - shareObjectTypeMeta={ - options.objectTypeMeta as ManagedFlyoutProps['shareObjectTypeMeta'] - } - onSave={options.onSave} - isSaving={false} - sharingData={options.sharingData} - />, - core - ), - { - 'data-test-subj': 'exportItemDetailsFlyout', - size: 's', - ownFocus: true, - container: null, // "global" flyout + // Direct export (no UI needed) - execute immediately + if ( + !exportConfig.copyAssetURIConfig && + !exportConfig.generateAssetComponent && + exportConfig.generateAssetExport + ) { + return async () => { + await exportConfig + .generateAssetExport({ + intl, + optimizedForPrinting: false, + }) + .finally(onClose); + }; } - ); - }; + + return { + flyoutContent: ( + { + getFlyoutSession().close(); + onClose(); + }} + publicAPIEnabled={!isServerless} + shareObjectType={options.objectType} + shareObjectTypeAlias={options.objectTypeAlias} + shareObjectTypeMeta={ + options.objectTypeMeta as ManagedExportFlyoutProps['shareObjectTypeMeta'] + } + onSave={options.onSave} + isSaving={false} + sharingData={options.sharingData} + shareableUrlLocatorParams={options.shareableUrlLocatorParams} + /> + ), + overlayOptions: { + 'data-test-subj': 'exportItemDetailsFlyout', + size: 's', + ownFocus: true, + container: null, // "global" flyout, + }, + }; + } + ); }, + /** * Returns a handler to trigger an export derivative by ID, opening its custom flyout. */ @@ -151,79 +152,127 @@ export class ShareMenuManager { options: Omit, derivativeId: string ): Promise<(() => Promise) | null> => { - const onClose = () => { - this.onClose(); - options.onClose?.(); - }; - - const menuItems = await resolveShareItemsForShareContext({ - ...options, + return this.createExportHandler( + core, + options, + resolveShareItemsForShareContext, isServerless, - onClose, - }); + (menuItems, getFlyoutSession, onClose) => { + const derivative = menuItems.find( + (item) => + item.shareType === 'integration' && + item.groupId === 'exportDerivatives' && + item.id === derivativeId + ); - const derivative = menuItems.find( - (item) => - item.shareType === 'integration' && - item.groupId === 'exportDerivatives' && - item.id === derivativeId + if (!derivative) { + return null; + } + + const exportItems = menuItems.filter( + (item) => item.shareType === 'integration' && item.groupId === 'export' + ) as ExportShareConfig[]; + + const derivativeConfig = (derivative as ExportShareDerivativesConfig).config; + + if (!derivativeConfig.shouldRender({ availableExportItems: exportItems })) { + return null; + } + + const flyoutRef = createRef(); + + return { + flyoutContent: derivativeConfig.flyoutContent({ + closeFlyout: () => { + getFlyoutSession().close(); + onClose(); + }, + flyoutRef, + }), + overlayOptions: { + 'data-test-subj': `exportDerivativeFlyout-${derivativeId}`, + ownFocus: true, + container: null, // "global" flyout, + ...(derivativeConfig.flyoutSizing || {}), + }, + }; + } ); + }, + }; + } - if (!derivative) { - return null; + /** + * Method for handling export operations flexibly. + */ + private async createExportHandler( + core: CoreStart, + options: Omit, + resolveShareObjectTypeItems: ShareRegistry['resolveShareItemsForShareContext'], + isServerless: boolean, + cb: ( + objectTypeMenuItems: ShareConfigs[], + /** + * Returns the flyout session object for the current export integration. + * Hence its return value will be undefined until the flyout has been created. + */ + getFlyoutSession: () => ReturnType, + onClose: () => void + ) => + | null + | (() => Promise) + | { + flyoutContent: React.ReactNode | null; + overlayOptions?: OverlayFlyoutOpenOptions; } + ) { + const onClose = () => { + this.onClose(); + options.onClose?.(); + }; - const exportItems = menuItems.filter( - (item) => item.shareType === 'integration' && item.groupId === 'export' - ) as ExportShareConfig[]; + const menuItems = await resolveShareObjectTypeItems({ + ...options, + isServerless, + onClose, + }); - const derivativeConfig = (derivative as ExportShareDerivativesConfig).config; + const shareContext: IShareContext = { + objectId: options.objectId, + objectType: options.objectType, + objectTypeAlias: options.objectTypeAlias, + objectTypeMeta: options.objectTypeMeta, + publicAPIEnabled: !isServerless, + allowShortUrl: options.allowShortUrl, + sharingData: options.sharingData, + shareableUrl: options.shareableUrl, + shareableUrlLocatorParams: options.shareableUrlLocatorParams, + isDirty: options.isDirty, + shareMenuItems: menuItems, + onClose, + onSave: options.onSave, + }; - if (!derivativeConfig.shouldRender({ availableExportItems: exportItems })) { - return null; - } + let flyoutSession: ReturnType; - return async () => { - const flyoutRef = createRef(); + const cbResult = await cb(menuItems, () => flyoutSession, onClose); - const shareContext: IShareContext = { - objectId: options.objectId, - objectType: options.objectType, - objectTypeAlias: options.objectTypeAlias, - objectTypeMeta: options.objectTypeMeta, - publicAPIEnabled: !isServerless, - allowShortUrl: options.allowShortUrl, - sharingData: options.sharingData, - shareableUrl: options.shareableUrl, - shareableUrlLocatorParams: options.shareableUrlLocatorParams, - isDirty: options.isDirty, - shareMenuItems: menuItems, - onClose, - onSave: options.onSave, - }; - - const flyoutSession = core.overlays.openFlyout( - toMountPoint( - - {derivativeConfig.flyoutContent({ - closeFlyout: () => { - flyoutSession.close(); - onClose(); - }, - flyoutRef, - })} - , - core - ), - { - 'data-test-subj': `exportDerivativeFlyout-${derivativeId}`, - ownFocus: true, - container: null, // "global" flyout - ...(derivativeConfig.flyoutSizing || {}), - } - ); - }; - }, + if (!cbResult) { + return null; + } + + if (typeof cbResult === 'function') { + return cbResult; + } + + return async () => { + flyoutSession = core.overlays.openFlyout( + toMountPoint( + {cbResult.flyoutContent}, + core + ), + cbResult.overlayOptions + ); }; } diff --git a/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts b/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts index ead8f820a040c..f7763cb91f835 100644 --- a/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts +++ b/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts @@ -8,9 +8,17 @@ */ import { ShareRegistry } from './share_menu_registry'; -import type { ShareContext, ShareRegistryApiStart, RegisterShareIntegrationArgs } from '../types'; +import type { + ShareContext, + ShareRegistryApiStart, + RegisterShareIntegrationArgs, + ShareIntegration, + SharingData, +} from '../types'; import { url } from '../mocks'; +type TestShareIntegration = ShareIntegration, SharingData>; + describe('ShareActionsRegistry', () => { const startDeps: ShareRegistryApiStart = { urlService: url, @@ -26,15 +34,15 @@ describe('ShareActionsRegistry', () => { test('throws when registering duplicate id', () => { const shareRegistrySetup = new ShareRegistry().setup(); - shareRegistrySetup.registerShareIntegration({ + shareRegistrySetup.registerShareIntegration({ id: 'csvReports', - getShareIntegrationConfig: () => Promise.resolve({}), + getShareIntegrationConfig: (..._args) => Promise.resolve({}), }); expect(() => - shareRegistrySetup.registerShareIntegration({ + shareRegistrySetup.registerShareIntegration({ id: 'csvReports', - getShareIntegrationConfig: () => Promise.resolve({}), + getShareIntegrationConfig: (..._args) => Promise.resolve({}), }) ).toThrowErrorMatchingInlineSnapshot( `"Share action with type [integration] for app [*] has already been registered."` @@ -51,7 +59,7 @@ describe('ShareActionsRegistry', () => { const { availableIntegrations } = shareRegistry.start(startDeps); // register a global integration without a prerequisite - registerShareIntegration({ + registerShareIntegration({ id: 'csvReports', getShareIntegrationConfig: () => Promise.resolve({}), }); @@ -69,7 +77,7 @@ describe('ShareActionsRegistry', () => { const prerequisiteCheckFn = jest.fn(() => false); // register a global integration with a prerequisiteCheck - registerShareIntegration({ + registerShareIntegration({ id: 'csvReports', getShareIntegrationConfig: () => Promise.resolve({}), prerequisiteCheck: prerequisiteCheckFn, @@ -95,7 +103,7 @@ describe('ShareActionsRegistry', () => { const prerequisiteCheckFn = jest.fn(() => true); // register a global integration with a prerequisiteCheck - registerShareIntegration({ + registerShareIntegration({ id: 'csvReports', getShareIntegrationConfig: () => Promise.resolve({}), prerequisiteCheck: prerequisiteCheckFn, @@ -119,7 +127,7 @@ describe('ShareActionsRegistry', () => { const { availableIntegrations } = shareRegistry.start(startDeps); // register a global integration with a groupId - registerShareIntegration({ + registerShareIntegration({ id: 'csvReports', groupId: 'export', getShareIntegrationConfig: () => Promise.resolve({}), @@ -136,7 +144,7 @@ describe('ShareActionsRegistry', () => { const { availableIntegrations } = shareRegistry.start(startDeps); // register a scoped integration with a groupId - registerShareIntegration('scoped', { + registerShareIntegration('scoped', { id: 'csvReports', groupId: 'export', getShareIntegrationConfig: () => Promise.resolve({}), @@ -205,9 +213,9 @@ describe('ShareActionsRegistry', () => { getShareIntegrationConfig: shareAction3ConfigFactory, }; - registerFunction(shareAction1); - registerFunction(shareAction2); - registerFunction(shareAction3); + registerFunction(shareAction1); + registerFunction(shareAction2); + registerFunction(shareAction3); const context = { objectType: 'anotherRandomShareObjectType' } as ShareContext; const isServerless = false; @@ -279,9 +287,9 @@ describe('ShareActionsRegistry', () => { getShareIntegrationConfig: shareAction3ConfigFactory, }; - registerFunction(context.objectType, shareAction1); - registerFunction('someOtherRandomObjectType', shareAction2); - registerFunction(context.objectType, shareAction3); + registerFunction(context.objectType, shareAction1); + registerFunction('someOtherRandomObjectType', shareAction2); + registerFunction(context.objectType, shareAction3); const { resolveShareItemsForShareContext } = service.start(startDeps); diff --git a/src/platform/plugins/shared/share/public/services/share_menu_registry.ts b/src/platform/plugins/shared/share/public/services/share_menu_registry.ts index f9169117a3fe4..48c9d6cdcbcab 100644 --- a/src/platform/plugins/shared/share/public/services/share_menu_registry.ts +++ b/src/platform/plugins/shared/share/public/services/share_menu_registry.ts @@ -16,7 +16,6 @@ import type { ShareRegistryPublicApi, ShareActionIntents, InternalShareActionIntent, - ShareIntegration, ShareRegistryApiStart, ShareMenuProviderLegacy, RegisterShareIntegrationArgs, @@ -126,7 +125,7 @@ export class ShareRegistry implements ShareRegistryPublicApi { }); } - private registerShareIntegration( + private registerShareIntegration( ...args: [string, RegisterShareIntegrationArgs] | [RegisterShareIntegrationArgs] ): void { const [shareObject, shareActionIntent] = diff --git a/src/platform/plugins/shared/share/public/types.ts b/src/platform/plugins/shared/share/public/types.ts index 5edf7d4566564..5c17a894f4b4b 100644 --- a/src/platform/plugins/shared/share/public/types.ts +++ b/src/platform/plugins/shared/share/public/types.ts @@ -15,6 +15,7 @@ import type { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/co import type { ILicense } from '@kbn/licensing-types'; import type { Capabilities } from '@kbn/core/public'; import type { TimeRange } from '@kbn/es-query'; +import type { SerializableRecord } from '@kbn/utility-types'; 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'; @@ -28,7 +29,7 @@ export interface ShareRegistryApiStart { getLicense: () => ILicense | undefined; } -type ShareActionConfigArgs = ShareContext & +export type ShareActionConfigArgs = ShareContext & Pick; export type ShareTypes = 'link' | 'embed' | 'legacy' | 'integration'; @@ -61,18 +62,19 @@ export interface ShareMenuProviderLegacy { type ShareImplementationFactory< T extends Omit, - C extends Record = Record + C extends Record = Record, + S extends SharingData = SharingData > = T extends 'integration' ? { id: string; groupId?: string; shareType: T; /** - * callback that yields the share configuration for the share as a promise, enables the possibility to dynamically fetch the share configuration + * Callback that yields the configured methods for the current share implementation as a promise, enables the possibility to dynamically fetch the share configuration */ - config: (ctx: ShareActionConfigArgs) => Promise; + config: (ctx: ShareActionConfigArgs) => Promise; /** - * when provided, this method will be used to evaluate if this integration should be available, + * When provided, this method will be used to evaluate if this integration should be available, * given the current license and capabilities of kibana */ prerequisiteCheck?: (args: { @@ -83,10 +85,10 @@ type ShareImplementationFactory< } : { shareType: T; - config: (ctx: ShareActionConfigArgs) => C | null; + config: (ctx: ShareActionConfigArgs) => C | null; }; -// New type definition to extract the config return type +// Type definition to resolve the configured share implementation methods from the implementation factory type ShareImplementation = Omit & { config: T extends ShareImplementationFactory ? R : never; }; @@ -102,7 +104,7 @@ export type LinkShare = ShareImplementationFactory< >; /** - * @description implementation definition for creating a share action for sharing embed links + * @description Implementation definition for creating a share action for sharing embed links */ export type EmbedShare = ShareImplementationFactory< 'embed', @@ -113,14 +115,15 @@ export type EmbedShare = ShareImplementationFactory< >; /** - * @description skeleton definition for implementing a share action integration + * @description Skeleton definition for implementing a share action integration */ export type ShareIntegration< - IntegrationParameters extends Record = Record -> = ShareImplementationFactory<'integration', IntegrationParameters>; + IntegrationParameters extends Record = Record, + S extends SharingData = SharingData +> = ShareImplementationFactory<'integration', IntegrationParameters, S>; /** - * @description implementation definition to support legacy share implementation + * @description Implementation definition to support legacy share implementation */ export interface ShareLegacy { shareType: 'legacy'; @@ -131,7 +134,7 @@ export interface ShareLegacy { /** * @description Share integration implementation definition for performing exports within kibana */ -export interface ExportShare +export interface ExportShare extends ShareIntegration< { /** @@ -158,6 +161,10 @@ export interface ExportShare requiresSavedState?: boolean; supportedLayoutOptions?: Array<'print'>; renderLayoutOptionSwitch?: boolean; + /** + * indicates if the export integration supports generating it exports with absolute time ranges + */ + supportsAbsoluteTime?: boolean; renderTotalHitsSizeWarning?: (totalHits?: number) => ReactNode | undefined; } & ( | { @@ -174,7 +181,8 @@ export interface ExportShare generateAssetComponent?: never; copyAssetURIConfig?: never; } - ) + ), + S > { groupId: 'export'; } @@ -253,29 +261,55 @@ export interface ShareUIConfig { }; } -export interface SharingData { +/** + * One locator (typical link/export flows) or several (e.g. CSV reporting). + */ +export type SharingDataLocatorParams

= + | { + id: string; + params: P; + version?: string; + } + | Array<{ + id: string; + params: P; + version?: string; + }>; + +export interface SharingData

+ extends Record { title: string; - locatorParams: { - id: string; - params: Record; - }; + locatorParams: SharingDataLocatorParams

; } export type ShareIntegrationMapKey = `integration-${string}`; -export interface RegisterShareIntegrationArgs - extends Pick { - getShareIntegrationConfig: I['config']; -} +/** + * Registration payload for a share integration. + * We infer sharing-data `S` from `I` instead of constraining `I extends ShareIntegration` (defaults on + * `ShareIntegration` use `SharingData`, and `config`'s `ctx` is contravariant in `S`, so + * `ExportShare` is not assignable to the default `ShareIntegration`). + */ +export type RegisterShareIntegrationArgs = I extends ShareIntegration< + infer P, + infer S +> + ? P extends Record + ? S extends SharingData + ? Pick & { + getShareIntegrationConfig: (ctx: ShareActionConfigArgs) => ReturnType; + } + : never + : never + : never; export interface ShareRegistryInternalApi { - registerShareIntegration( - shareObject: string, - arg: RegisterShareIntegrationArgs - ): void; - registerShareIntegration(arg: RegisterShareIntegrationArgs): void; + registerShareIntegration(shareObject: string, arg: RegisterShareIntegrationArgs): void; + registerShareIntegration(arg: RegisterShareIntegrationArgs): void; - resolveShareItemsForShareContext(args: ShareContext): Promise; + resolveShareItemsForShareContext( + args: ShareContext & { isServerless: boolean } + ): Promise; } export abstract class ShareRegistryPublicApi { @@ -296,9 +330,9 @@ export type BrowserUrlService = UrlService< BrowserShortUrlClient >; -export type ShareableUrlLocatorParams = { +export type ShareableLocatorParams = SerializableRecord & { timeRange: TimeRange | undefined; -} & Record; +}; /** * @public @@ -309,7 +343,7 @@ export type ShareableUrlLocatorParams = { * It is possible to use the static function `toggleShareContextMenu` * to render the menu as a popover. * */ -export interface ShareContext { +export interface ShareContext { /** * The type of the object to share. for example lens, dashboard, etc. */ @@ -346,10 +380,10 @@ export interface ShareContext { */ shareableUrlForSavedObject?: string; shareableUrlLocatorParams?: { - locator: LocatorPublic; - params: ShareableUrlLocatorParams; + locator: LocatorPublic; + params: ShareableLocatorParams; }; - sharingData: { [key: string]: unknown }; + sharingData: S; isDirty: boolean; onClose: () => void; } @@ -396,7 +430,16 @@ export interface UrlParamExtension { } /** @public */ -export interface ShowShareMenuOptions extends Omit { +export interface ShowShareMenuOptions< + /** + * Specifies the type of the locator params for the sharing data. + */ + P extends SerializableRecord = SerializableRecord, + /** + * Specifies the type of the sharing data. + */ + S extends Record = Record +> extends Omit & S>, 'onClose'> { asExport?: boolean; anchorElement?: HTMLElement; allowShortUrl: boolean; diff --git a/x-pack/platform/plugins/private/reporting/public/plugin.ts b/x-pack/platform/plugins/private/reporting/public/plugin.ts index a29955a28def1..94b7728c818e8 100644 --- a/x-pack/platform/plugins/private/reporting/public/plugin.ts +++ b/x-pack/platform/plugins/private/reporting/public/plugin.ts @@ -34,6 +34,7 @@ import { } from '@kbn/reporting-public/share'; import type { InjectedIntl } from '@kbn/i18n-react'; import type { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; +import type { ReportingCSVSharingData } from '@kbn/reporting-public/types'; import type { ReportingSetup, ReportingStart } from '.'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import type { StartServices } from './types'; @@ -218,7 +219,7 @@ export class ReportingPublicPlugin }); }); - shareSetup.registerShareIntegration( + shareSetup.registerShareIntegration>( 'search', // TODO: export the reporting pdf export provider for registration in the actual plugins that depend on it reportingCsvExportShareIntegration({