diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index 8902ce5640d2a..8c3f5e6fada0b 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -229,6 +229,12 @@ export function ShowShareModal({ ), }, embed: { + embedUrlParamExtensions: [ + { + paramName: 'embed', + component: EmbedUrlParamExtension, + }, + ], draftModeCallOut: ( ( ); } - const { shareMenuItems, ...rest } = context; + const { shareMenuItems, objectTypeMeta, ...rest } = context; let shareTypeImplementations = shareMenuItems; @@ -65,11 +57,15 @@ export const useShareTabsContext = ( // only integration share types can have multiple implementations shareTypeImplementations = ( shareType === 'integration' ? Array.prototype.filter : Array.prototype.find - ).call(shareMenuItems, (item) => item.shareType === shareType && item.groupId === groupId); + ).call(shareMenuItems, (item) => item.shareType === shareType && item?.groupId === groupId); } return { ...rest, + objectTypeMeta: { + ...objectTypeMeta, + config: shareType ? objectTypeMeta.config[shareType] : objectTypeMeta.config, + }, shareMenuItems: shareTypeImplementations, }; }; diff --git a/src/platform/plugins/shared/share/public/components/tabs/export/export_content.tsx b/src/platform/plugins/shared/share/public/components/tabs/export/export_content.tsx index a3adc10fb74e6..7e0cca2168de2 100644 --- a/src/platform/plugins/shared/share/public/components/tabs/export/export_content.tsx +++ b/src/platform/plugins/shared/share/public/components/tabs/export/export_content.tsx @@ -27,7 +27,7 @@ import { EuiToolTip, type EuiRadioGroupOption, } from '@elastic/eui'; -import { SupportedExportTypes, ShareMenuItemV2 } from '../../../types'; +import { ShareMenuItemV2 } from '../../../types'; import { type IShareContext } from '../../context'; type ExportProps = Pick & { @@ -58,9 +58,7 @@ const ExportContentUi = ({ }, []); }, [aggregateReportTypes]); - const [selectedRadio, setSelectedRadio] = useState( - radioOptions[0].id as SupportedExportTypes - ); + const [selectedRadio, setSelectedRadio] = useState(radioOptions[0].id); const { config: { diff --git a/src/platform/plugins/shared/share/public/components/tabs/link/index.tsx b/src/platform/plugins/shared/share/public/components/tabs/link/index.tsx index cb0d3692ac4cd..d7df4f444d6c2 100644 --- a/src/platform/plugins/shared/share/public/components/tabs/link/index.tsx +++ b/src/platform/plugins/shared/share/public/components/tabs/link/index.tsx @@ -58,7 +58,6 @@ const LinkTabContent: ILinkTab['content'] = ({ state, dispatch }) => { shareableUrlLocatorParams, allowShortUrl, shareMenuItems, - delegatedShareUrlHandler, } = useShareTabsContext('link'); const setDashboardLink = useCallback( @@ -87,7 +86,7 @@ const LinkTabContent: ILinkTab['content'] = ({ state, dispatch }) => { { setIsNotSaved, allowShortUrl, setIsClicked: state?.setIsClicked, - delegatedShareUrlHandler, }} /> ); diff --git a/src/platform/plugins/shared/share/public/components/tabs/link/link_content.tsx b/src/platform/plugins/shared/share/public/components/tabs/link/link_content.tsx index cb65e0b3f252f..68455e393c975 100644 --- a/src/platform/plugins/shared/share/public/components/tabs/link/link_content.tsx +++ b/src/platform/plugins/shared/share/public/components/tabs/link/link_content.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useState, useRef, useEffect } from 'react'; import type { IShareContext, ShareContextObjectTypeConfig } from '../../context'; -import type { LinkShare } from '../../../services/share_orchestrator'; +import type { LinkShare } from '../../../types'; type LinkProps = Pick< IShareContext, @@ -29,7 +29,6 @@ type LinkProps = Pick< | 'objectId' | 'isDirty' | 'shareableUrl' - | 'delegatedShareUrlHandler' | 'shareableUrlLocatorParams' | 'allowShortUrl' > & { @@ -46,12 +45,11 @@ interface UrlParams { export const LinkContent = ({ isDirty, objectType, - objectConfig = {}, + objectConfig, shareableUrl, shortUrlService, shareableUrlLocatorParams, allowShortUrl, - delegatedShareUrlHandler, }: LinkProps) => { const [snapshotUrl, setSnapshotUrl] = useState(''); const [isTextCopied, setTextCopied] = useState(false); @@ -60,6 +58,8 @@ export const LinkContent = ({ const urlToCopy = useRef(undefined); const copiedTextToolTipCleanupIdRef = useRef>(); + const { delegatedShareUrlHandler } = objectConfig; + const getUrlWithUpdatedParams = useCallback((tempUrl: string): string => { const urlWithUpdatedParams = urlParamsRef.current ? Object.keys(urlParamsRef.current).reduce((urlAccumulator, key) => { diff --git a/src/platform/plugins/shared/share/public/plugin.ts b/src/platform/plugins/shared/share/public/plugin.ts index ad68a3302f74e..5b0f01124d5af 100644 --- a/src/platform/plugins/shared/share/public/plugin.ts +++ b/src/platform/plugins/shared/share/public/plugin.ts @@ -11,8 +11,7 @@ import './index.scss'; import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { ShareMenuManager, ShareMenuManagerStart } from './services'; -import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; -import { ShareOptionsManager } from './services/share_options_manager'; +import { ShareRegistry /* , ShareMenuRegistrySetup */ } from './services'; import { UrlService } from '../common/url_service'; import { RedirectManager } from './url_service'; import type { RedirectOptions } from '../common/url_service/locators/redirect'; @@ -28,7 +27,7 @@ import { registrations } from './lib/registrations'; import type { BrowserUrlService } from './types'; /** @public */ -export type SharePublicSetup = ShareMenuRegistrySetup & { +export interface SharePublicSetup { /** * Utilities to work with URL locators and short URLs. */ @@ -45,8 +44,12 @@ export type SharePublicSetup = ShareMenuRegistrySetup & { */ setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessServiceContract) => void; - registerShareIntegration: ShareOptionsManager['registerShareIntegration']; -}; + registerShareIntegration: ShareRegistry['registerShareIntegration']; + /** + * @deprecated Use `registerShareIntegration` instead. + */ + register: ShareRegistry['register']; +} /** @public */ export type SharePublicStart = ShareMenuManagerStart & { @@ -77,8 +80,7 @@ export class SharePlugin SharePublicStartDependencies > { - private readonly shareMenuRegistry?: ShareMenuRegistry = new ShareMenuRegistry(); - private readonly shareOptionsManager = new ShareOptionsManager(); + private readonly shareRegistry = new ShareRegistry(); private readonly shareContextMenu = new ShareMenuManager(); private redirectManager?: RedirectManager; private url?: BrowserUrlService; @@ -128,10 +130,10 @@ export class SharePlugin registrations.setup({ analytics }); return { - registerShareIntegration: this.shareOptionsManager.registerShareIntegration.bind( - this.shareOptionsManager + registerShareIntegration: this.shareRegistry.registerShareIntegration.bind( + this.shareRegistry ), - ...this.shareMenuRegistry!.setup(), + register: this.shareRegistry.register.bind(this.shareRegistry), url: this.url, navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options), setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessServiceContract) => { @@ -146,16 +148,15 @@ export class SharePlugin public start(core: CoreStart): SharePublicStart { const disableEmbed = this.initializerContext.env.packageInfo.buildFlavor === 'serverless'; - this.shareOptionsManager.start({ + this.shareRegistry.start({ urlService: this.url!, anonymousAccessServiceProvider: () => this.anonymousAccessServiceProvider!(), }); const sharingContextMenuStart = this.shareContextMenu.start({ core, - shareRegistry: this.shareMenuRegistry!.start(), + shareRegistry: this.shareRegistry, disableEmbed, - shareOptionsManager: this.shareOptionsManager, }); return { diff --git a/src/platform/plugins/shared/share/public/services/index.ts b/src/platform/plugins/shared/share/public/services/index.ts index 87416ccd2e923..8132d592a9e0e 100644 --- a/src/platform/plugins/shared/share/public/services/index.ts +++ b/src/platform/plugins/shared/share/public/services/index.ts @@ -9,4 +9,3 @@ export * from './share_menu_registry'; export * from './share_menu_manager'; -export * from './share_options_manager'; 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 6833acfb3321a..b3d85f1563e81 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 @@ -12,25 +12,23 @@ import ReactDOM from 'react-dom'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { CoreStart, ThemeServiceStart, UserProfileService } from '@kbn/core/public'; import { ShowShareMenuOptions } from '../types'; -import { ShareMenuRegistryStart } from './share_menu_registry'; -import type { ShareMenuItemV2 } from '../types'; +import { ShareRegistry } from './share_menu_registry'; +import type { ShareConfigs } from '../types'; import { ShareMenu } from '../components/share_tabs'; -import { ShareOptionsManager } from './share_options_manager'; interface ShareMenuManagerStartDeps { core: CoreStart; - shareRegistry: ShareMenuRegistryStart; + shareRegistry: ShareRegistry; disableEmbed: boolean; - shareOptionsManager: ShareOptionsManager; } export class ShareMenuManager { private isOpen = false; - private shareOptionsManager?: ShareOptionsManager; + private shareRegistry?: ShareRegistry; private container = document.createElement('div'); - start({ core, shareOptionsManager, shareRegistry, disableEmbed }: ShareMenuManagerStartDeps) { - this.shareOptionsManager = shareOptionsManager; + start({ core, shareRegistry, disableEmbed }: ShareMenuManagerStartDeps) { + this.shareRegistry = shareRegistry; return { showShareDialog: this.showShareDialog.bind(this), @@ -46,14 +44,14 @@ export class ShareMenuManager { options.onClose?.(); }; - const menuItems = this.shareOptionsManager!.resolveShareItemsForShareContext({ + const menuItems = this.shareRegistry!.resolveShareItemsForShareContext({ ...options, onClose, }); this.toggleShareContextMenu({ ...options, - allowEmbed: disableEmbed ? false : options.allowEmbed, + allowEmbed: false, // disableEmbed ? false : options.allowEmbed, onClose, menuItems, publicAPIEnabled: !disableEmbed, @@ -68,16 +66,6 @@ export class ShareMenuManager { this.isOpen = false; }; - private showShareDialog(app: string) { - const shareOptions = this.shareOptionsManager?.getShareConfigOptionsForApp(app); - - if (!shareOptions) { - return; - } - - console.log('share options available for app', shareOptions); - } - private toggleShareContextMenu({ anchorElement, allowEmbed, @@ -89,18 +77,14 @@ export class ShareMenuManager { menuItems, shareableUrl, shareableUrlLocatorParams, - embedUrlParamExtensions, - showPublicUrlSwitch, - snapshotShareWarning, onClose, disabledShareUrl, isDirty, - delegatedShareUrlHandler, publicAPIEnabled, ...startServices }: ShowShareMenuOptions & { anchorElement: HTMLElement; - menuItems: ShareMenuItemV2[]; + menuItems: ShareConfigs[]; onClose: () => void; isDirty: boolean; userProfile: UserProfileService; @@ -130,10 +114,6 @@ export class ShareMenuManager { sharingData, shareableUrl, shareableUrlLocatorParams, - delegatedShareUrlHandler, - embedUrlParamExtensions, - showPublicUrlSwitch, - snapshotShareWarning, disabledShareUrl, isDirty, shareMenuItems: menuItems, diff --git a/src/platform/plugins/shared/share/public/services/share_menu_registry.mock.ts b/src/platform/plugins/shared/share/public/services/share_menu_registry.mock.ts index 0395062ff2984..5c29221a849ff 100644 --- a/src/platform/plugins/shared/share/public/services/share_menu_registry.mock.ts +++ b/src/platform/plugins/shared/share/public/services/share_menu_registry.mock.ts @@ -9,7 +9,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { - ShareMenuRegistry, + ShareRegistry, ShareMenuRegistrySetup, ShareMenuRegistryStart, } from './share_menu_registry'; @@ -29,7 +29,7 @@ const createStartMock = (): jest.Mocked => { return start; }; -const createMock = (): jest.Mocked> => { +const createMock = (): jest.Mocked> => { const service = { setup: jest.fn(), start: jest.fn(), 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 ae88251ac8d20..fa9d0cf0205da 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 @@ -7,13 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ShareMenuRegistry } from './share_menu_registry'; +import { ShareRegistry } from './share_menu_registry'; import { ShareMenuItemV2, ShareContext } from '../types'; describe('ShareActionsRegistry', () => { describe('setup', () => { test('throws when registering duplicate id', () => { - const setup = new ShareMenuRegistry().setup(); + const setup = new ShareRegistry().setup(); setup.register({ id: 'csvReports', getShareMenuItems: () => [], @@ -32,7 +32,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 ShareRegistry(); const registerFunction = service.setup().register; const shareAction1 = {} as ShareMenuItemV2; const shareAction2 = {} as ShareMenuItemV2; 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 c2c926fd0a081..420f638cdb0b3 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 @@ -7,50 +7,131 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { +import type { + BrowserUrlService, ShareContext, - ShareMenuProvider, - ShareMenuProviderV2, + ShareConfigs, + ShareRegistryApi, + ShareActionIntents, + InternalShareActionIntent, + ShareIntegration, + ShareRegistryApiStart, ShareMenuProviderLegacy, } from '../types'; +import type { AnonymousAccessServiceContract } from '../../common/anonymous_access'; -export class ShareMenuRegistry { - private readonly shareMenuProviders = new Map(); +export class ShareRegistry implements ShareRegistryApi { + private urlService?: BrowserUrlService; + private anonymousAccessServiceProvider?: () => AnonymousAccessServiceContract; - register(shareMenuProvider: ShareMenuProvider) { - if (this.shareMenuProviders.has(shareMenuProvider.id)) { + private readonly globalMarker: string = '*'; + + constructor() { + // register default share actions + this.registerLinkShareAction(); + this.registerEmbedShareAction(); + } + + private readonly shareOptionsRegistry: Record< + string, + Map + > = { + [this.globalMarker]: new Map(), + }; + + private registerShareIntentAction( + shareObject: string, + shareActionIntent: ShareActionIntents + ): void { + if (!this.shareOptionsRegistry[shareObject]) { + this.shareOptionsRegistry[shareObject] = new Map(); + } + + const shareContextMap = this.shareOptionsRegistry[shareObject]; + + const recordKey = + shareActionIntent.shareType === 'integration' + ? (`integration-${shareActionIntent.groupId || 'unknown'}-${shareActionIntent.id}` as const) + : shareActionIntent.shareType; + + if (shareContextMap.has(recordKey)) { throw new Error( - `Share menu provider with id [${shareMenuProvider.id}] has already been registered. Use a unique id.` + `Share action with type [${shareActionIntent.shareType}] for app [${shareObject}] has already been registered.` ); } - this.shareMenuProviders.set(shareMenuProvider.id, shareMenuProvider); - } - - public setup() { - return { - /** - * Register an additional source of items for share context menu items. All registered providers - * will be called if a consumer displays the context menu. Returned `ShareMenuItem`s will be shown - * in the context menu together with the default built-in share options. - * Each share provider needs a globally unique id. - * @param shareMenuProvider - */ - register: this.register.bind(this), - }; - } - - public start() { - return { - getShareMenuItems: (context: ShareContext) => - Array.from(this.shareMenuProviders.values()).flatMap((shareActionProvider) => - ( - (shareActionProvider as ShareMenuProviderV2).getShareMenuItems ?? - (shareActionProvider as ShareMenuProviderLegacy).getShareMenuItemsLegacy - ).call(shareActionProvider, context) - ), - }; + + shareContextMap.set(recordKey, shareActionIntent); + } + + private registerLinkShareAction(): void { + this.registerShareIntentAction(this.globalMarker, { + shareType: 'link', + config: ({ urlService }) => ({ + shortUrlService: urlService?.shortUrls.get(null)!, + }), + }); } -} -export type ShareMenuRegistrySetup = ReturnType; -export type ShareMenuRegistryStart = ReturnType; + private registerEmbedShareAction(): void { + this.registerShareIntentAction(this.globalMarker, { + shareType: 'embed', + config: ({ urlService }) => ({ + shortUrlService: urlService?.shortUrls.get(null)!, + }), + }); + } + + /** + * @deprecated use {@link registerShareIntegration} instead + */ + register(value: ShareMenuProviderLegacy) { + // implement backwards compatibility for the share plugin + } + + registerShareIntegration( + ...args: [string, Omit] | [Omit] + ): void { + const [shareObject, shareActionIntent] = + args.length === 1 ? [this.globalMarker, args[0]] : args; + this.registerShareIntentAction(shareObject, { + shareType: 'integration', + ...shareActionIntent, + }); + } + + start({ urlService, anonymousAccessServiceProvider }: ShareRegistryApiStart) { + this.urlService = urlService; + this.anonymousAccessServiceProvider = anonymousAccessServiceProvider; + } + + getShareConfigOptionsForObject( + objectType: ShareContext['objectType'] + ): Array { + const shareContextMap = this.shareOptionsRegistry[objectType]; + const globalOptions = Array.from(this.shareOptionsRegistry[this.globalMarker].values()); + + if (!shareContextMap) { + return globalOptions; + } + + return globalOptions.concat(Array.from(shareContextMap.values())); + } + + resolveShareItemsForShareContext({ objectType, ...shareContext }: ShareContext): ShareConfigs[] { + if (!this.urlService || !this.anonymousAccessServiceProvider) { + throw new Error('ShareOptionsManager#start was not been invoked'); + } + + return this.getShareConfigOptionsForObject(objectType) + .map((shareAction) => ({ + ...shareAction, + config: shareAction?.config?.call(null, { + urlService: this.urlService!, + anonymousAccessServiceProvider: this.anonymousAccessServiceProvider, + objectType, + ...shareContext, + }), + })) + .filter((shareAction) => shareAction.config); + } +} diff --git a/src/platform/plugins/shared/share/public/services/share_options_manager.ts b/src/platform/plugins/shared/share/public/services/share_options_manager.ts deleted file mode 100644 index 73a239f9c9219..0000000000000 --- a/src/platform/plugins/shared/share/public/services/share_options_manager.ts +++ /dev/null @@ -1,136 +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", 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 { - ShareOrchestrator, - type ShareActionIntents, - type InternalShareActionIntent, - type ShareIntegration, - type ShareOptionsManagerStart, -} from './share_orchestrator'; -import type { BrowserUrlService, ShareContext } from '../types'; -import type { AnonymousAccessServiceContract } from '../../common/anonymous_access'; - -export class ShareOptionsManager implements ShareOrchestrator { - private urlService?: BrowserUrlService; - private anonymousAccessServiceProvider?: () => AnonymousAccessServiceContract; - - private readonly globalMarker: string = '*'; - - private readonly restrictedShareActionIntent: readonly InternalShareActionIntent[] = [ - 'link', - 'embed', - ]; - - constructor() { - // register default share actions - this.registerLinkShareAction(); - this.registerEmbedShareAction(); - } - - private readonly shareOptionsRegistry: Record< - string, - Map - > = { - [this.globalMarker]: new Map(), - }; - - private registerShareIntentAction( - shareObject: string, - shareActionIntent: ShareActionIntents - ): void { - if (!this.shareOptionsRegistry[shareObject]) { - this.shareOptionsRegistry[shareObject] = new Map(); - } - - const shareContextMap = this.shareOptionsRegistry[shareObject]; - - const recordKey = - shareActionIntent.shareType === 'integration' - ? (`integration-${shareActionIntent.groupId || 'unknown'}-${shareActionIntent.id}` as const) - : shareActionIntent.shareType; - - if (shareContextMap.has(recordKey)) { - throw new Error( - `Share action with type [${shareActionIntent.shareType}] for app [${shareObject}] has already been registered.` - ); - } - - shareContextMap.set(recordKey, shareActionIntent); - } - - private registerLinkShareAction(): void { - this.registerShareIntentAction(this.globalMarker, { - shareType: 'link', - config: ({ urlService }) => ({ - shortUrlService: urlService?.shortUrls.get(null)!, - }), - }); - } - - private registerEmbedShareAction(): void { - this.registerShareIntentAction(this.globalMarker, { - shareType: 'embed', - config: ({ urlService }) => ({ - shortUrlService: urlService?.shortUrls.get(null)!, - }), - }); - } - - registerShareIntegration( - ...args: [string, Omit] | [Omit] - ): void { - const [shareObject, shareActionIntent] = - args.length === 1 ? [this.globalMarker, args[0]] : args; - this.registerShareIntentAction(shareObject, { - shareType: 'integration', - ...shareActionIntent, - }); - } - - start({ urlService, anonymousAccessServiceProvider }: ShareOptionsManagerStart) { - this.urlService = urlService; - this.anonymousAccessServiceProvider = anonymousAccessServiceProvider; - } - - getShareConfigOptionsForApp( - objectType: ShareContext['objectType'] - ): Array { - const shareContextMap = this.shareOptionsRegistry[objectType]; - const globalOptions = Array.from(this.shareOptionsRegistry[this.globalMarker].values()); - - if (!shareContextMap) { - return globalOptions; - } - - return globalOptions.concat(Array.from(shareContextMap.values())); - } - - resolveShareItemsForShareContext({ objectType, ...shareContext }: ShareContext) { - if (!this.urlService || !this.anonymousAccessServiceProvider) { - throw new Error('ShareOptionsManager#start was not been invoked'); - } - - return this.getShareConfigOptionsForApp(objectType) - .map((shareAction) => ({ - ...shareAction, - config: shareAction?.config?.call( - null, - { - objectType, - urlService: this.urlService!, - anonymousAccessServiceProvider: this.anonymousAccessServiceProvider, - ...shareContext, - }, - {} - ), - })) - .filter((shareAction) => shareAction.config); - } -} diff --git a/src/platform/plugins/shared/share/public/services/share_orchestrator.ts b/src/platform/plugins/shared/share/public/services/share_orchestrator.ts deleted file mode 100644 index 21d04e208d8be..0000000000000 --- a/src/platform/plugins/shared/share/public/services/share_orchestrator.ts +++ /dev/null @@ -1,139 +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", 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 type { ReactElement, ReactNode } from 'react'; -import type { EuiIconProps } from '@elastic/eui'; -import { AnonymousAccessServiceContract } from '../../common/anonymous_access'; -import { UrlParamExtension, ScreenshotExportOpts, ShareContext, BrowserUrlService } from '../types'; - -export interface ShareOptionsManagerStart { - urlService: BrowserUrlService; - anonymousAccessServiceProvider?: () => AnonymousAccessServiceContract; -} - -export type ShareTypes = 'link' | 'embed' | 'integration'; - -export type InternalShareActionIntent = Exclude; - -interface ShareActionUserInputBase = Record> { - /** - * The title of the share action - */ - title: string; - allowShortUrl?: boolean; - draftModeCallOut?: ReactNode; - helpText?: ReactElement; - CTAButtonConfig?: { - id: string; - dataTestSubj: string; - label: string; - }; -} - -type ShareImplementation< - T extends ShareTypes, - C extends Record = Record, - U extends Record = Record -> = T extends 'integration' - ? { - id: string; - groupId?: string; - shareType: T; - config: ( - ctx: ShareContext & ShareOptionsManagerStart, - userInput: ShareActionUserInputBase - ) => C; - } - : { - shareType: T; - config: ( - ctx: ShareContext & ShareOptionsManagerStart, - userInput: ShareActionUserInputBase - ) => C; - }; - -export type LinkShare = ShareImplementation< - 'link', - { - shortUrlService: ReturnType; - }, - { - delegatedShareUrlHandler?: () => string; - } ->; - -export type EmbedShare = ShareImplementation< - 'embed', - { - shortUrlService: ReturnType; - }, - { - allowEmbed: boolean; - embedUrlParamExtensions?: UrlParamExtension[]; - } ->; - -export type ShareIntegration< - IntegrationParameters extends Record = Record -> = ShareImplementation<'integration', IntegrationParameters, {}>; - -export type ShareActionIntents = LinkShare | EmbedShare | ShareIntegration; - -/** - * @description bundled share integration for performing exports - */ -export interface ExportShare - extends ShareIntegration<{ - /** - * @deprecated only kept around for legacy reasons - */ - name?: string; - /** - * @deprecated only kept around for legacy reasons - */ - icon?: EuiIconProps['type']; - /** - * @deprecated only kept around for legacy reasons - */ - sortOrder?: number; - label: string; - exportType: string; - generateAssetExport: (args: ScreenshotExportOpts) => Promise; - generateValueExport: (args: ScreenshotExportOpts) => string | undefined; - warnings?: Array<{ title: string; message: string }>; - requiresSavedState?: boolean; - supportedLayoutOptions: ['print']; - renderLayoutOptionSwitch?: boolean; - }> { - groupId: 'export'; -} - -export interface SharingData { - title: string; - locatorParams: { - id: string; - params: Record; - }; -} - -export abstract class ShareOrchestrator { - abstract start(args: ShareOptionsManagerStart): void; - - abstract registerShareIntegration(shareObject: string, arg: I): void; - abstract registerShareIntegration(arg: I): void; - - // abstract registerShareAction(app: string, args: ShareActionIntent[]): void; - // abstract registerShareAction(args: ShareActionIntent[]): void; - - // // when toggling the share menu we keep it simple, only passing in the minimum required data, instead of a sleuth of options - // // any extra check that is required to be done will happen behind the scenes for each share context - // abstract toggleShare(context: string, data: SharingData): void; - - // abstract fetchShareOptionForApp(sharingApp: string): Array; -} diff --git a/src/platform/plugins/shared/share/public/types.ts b/src/platform/plugins/shared/share/public/types.ts index 37ab26dea9f79..b15a7c12c3f05 100644 --- a/src/platform/plugins/shared/share/public/types.ts +++ b/src/platform/plugins/shared/share/public/types.ts @@ -11,23 +11,182 @@ import type { ComponentType, ReactElement, ReactNode } from 'react'; import type { InjectedIntl } from '@kbn/i18n-react'; import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/components/context_menu/context_menu'; -import type { Capabilities, ThemeServiceSetup, ToastsSetup } from '@kbn/core/public'; +import type { Capabilities, ToastsSetup } from '@kbn/core/public'; +import type { EuiIconProps } from '@elastic/eui'; 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'; +import { AnonymousAccessServiceContract } from '../common/anonymous_access'; -export type { ExportShare } from './services/share_orchestrator'; +export interface ShareRegistryApiStart { + urlService: BrowserUrlService; + anonymousAccessServiceProvider?: () => AnonymousAccessServiceContract; +} -export type BrowserUrlService = UrlService< - BrowserShortUrlClientFactoryCreateParams, - BrowserShortUrlClient ->; +export type ShareTypes = 'link' | 'embed' | 'integration'; + +export type InternalShareActionIntent = Exclude; -export interface ShareContextObjectTypeConfig { +type ShareActionUserInputBase = Record> = { + /** + * The title of the share action + */ draftModeCallOut?: ReactNode; + helpText?: ReactElement; + CTAButtonConfig?: { + id: string; + dataTestSubj: string; + label: string; + }; + snapshotShareWarning?: string; +} & E; + +type ShareImplementationFactory< + T extends ShareTypes, + C extends Record = Record +> = T extends 'integration' + ? { + id: string; + groupId?: string; + shareType: T; + config: (ctx: ShareContext & ShareRegistryApiStart) => C; + } + : { + shareType: T; + config: (ctx: ShareContext & ShareRegistryApiStart) => C; + }; + +// New type definition to extract the config return type +type ShareImplementation = Omit & { + config: T extends ShareImplementationFactory ? R : never; +}; + +/** + * @description implementation definition for creating a share action for sharing object links + */ +export type LinkShare = ShareImplementationFactory< + 'link', + { + shortUrlService: ReturnType; + } +>; + +/** + * @description implementation definition for creating a share action for sharing embed links + */ +export type EmbedShare = ShareImplementationFactory< + 'embed', + { + shortUrlService: ReturnType; + } +>; + +/** + * @description skeleton definition for implementing a share action integration + */ +export type ShareIntegration< + IntegrationParameters extends Record = Record +> = ShareImplementationFactory<'integration', IntegrationParameters>; + +/** + * @description Share integration implementation definition for performing exports within kibana + */ +export interface ExportShare + extends ShareIntegration<{ + /** + * @deprecated only kept around for legacy reasons + */ + name?: string; + /** + * @deprecated only kept around for legacy reasons + */ + icon?: EuiIconProps['type']; + /** + * @deprecated only kept around for legacy reasons + */ + sortOrder?: number; + label: string; + exportType: string; + generateAssetExport: (args: ScreenshotExportOpts) => Promise; + generateValueExport: (args: ScreenshotExportOpts) => string | undefined; + warnings?: Array<{ title: string; message: string }>; + requiresSavedState?: boolean; + supportedLayoutOptions: ['print']; + renderLayoutOptionSwitch?: boolean; + }> { + groupId: 'export'; +} + +export type ShareActionIntents = LinkShare | EmbedShare | ShareIntegration; + +// Example usage +type LinkShareConfig = ShareImplementation; +type EmbedShareConfig = ShareImplementation; +type ExportShareConfig = ShareImplementation; +type ShareIntegrationConfig = ShareImplementation; + +export type ShareConfigs = LinkShareConfig | EmbedShareConfig | ExportShareConfig; + +type LinkShareUIConfig = ShareActionUserInputBase<{ + /** + * + * @description allows a consumer to provide a custom method which when invoked + * handles providing a share url in the context of said consumer + */ + delegatedShareUrlHandler?: () => string; +}>; + +type EmbedShareUIConfig = ShareActionUserInputBase<{ + allowEmbed: boolean; + embedUrlParamExtensions?: UrlParamExtension[]; computeAnonymousCapabilities?: (anonymousUserCapabilities: Capabilities) => boolean; + /** + * @deprecated use computeAnonymousCapabilities defined on objectTypeMeta config + */ + showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean; +}>; + +type ExportShareUIConfig = ShareActionUserInputBase<{}>; + +export interface ShareUIConfig { + link: LinkShareUIConfig; + embed: EmbedShareUIConfig; + export: ExportShareUIConfig; +} + +export interface SharingData { + title: string; + locatorParams: { + id: string; + params: Record; + }; +} + +export abstract class ShareRegistryApi { + abstract start(args: ShareRegistryApiStart): void; + + abstract registerShareIntegration(shareObject: string, arg: I): void; + abstract registerShareIntegration(arg: I): void; + + abstract getShareConfigOptionsForObject( + objectType: string + ): Array; + + abstract resolveShareItemsForShareContext(args: ShareContext): ShareConfigs[]; + + // abstract registerShareAction(app: string, args: ShareActionIntent[]): void; + // abstract registerShareAction(args: ShareActionIntent[]): void; + + // // when toggling the share menu we keep it simple, only passing in the minimum required data, instead of a sleuth of options + // // any extra check that is required to be done will happen behind the scenes for each share context + // abstract toggleShare(context: string, data: SharingData): void; } +export type BrowserUrlService = UrlService< + BrowserShortUrlClientFactoryCreateParams, + BrowserShortUrlClient +>; + /** * @public * Properties of the current object to share. Registered share @@ -38,14 +197,20 @@ export interface ShareContextObjectTypeConfig { * to render the menu as a popover. * */ export interface ShareContext { + /** + * The type of the object to share. for example lens, dashboard, etc. + */ objectType: string; /** * Allows for passing contextual information that each consumer can provide to customize the share menu */ objectTypeMeta: { title: string; - config?: Partial>; + config: Partial; }; + /** + * Id of the object that's been attempted to be shared + */ objectId?: string; /** * Current url for sharing. This can be set in cases where `window.location.href` @@ -65,19 +230,9 @@ export interface ShareContext { locator: LocatorPublic; params: any; }; - /** - * - * @description allows a consumer to provide a custom method which when invoked - * handles providing a share url in the context of said consumer - */ - delegatedShareUrlHandler?: () => string; sharingData: { [key: string]: unknown }; isDirty: boolean; onClose: () => void; - /** - * @deprecated use computeAnonymousCapabilities defined on objectTypeMeta config - */ - showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean; disabledShareUrl?: boolean; toasts: ToastsSetup; } @@ -94,12 +249,13 @@ export interface ShareContextMenuPanelItem sortOrder?: number; } -export type SupportedExportTypes = - | 'pngV2' - | 'printablePdfV2' - | 'csv_v2' - | 'csv_searchsource' - | 'lens_csv'; +// we don't need this +// export type SupportedExportTypes = +// | 'pngV2' +// | 'printablePdfV2' +// | 'csv_v2' +// | 'csv_searchsource' +// | 'lens_csv'; /** * @public @@ -121,35 +277,36 @@ export interface ScreenshotExportOpts { intl: InjectedIntl; } -export interface ShareMenuItemV2 extends ShareMenuItemBase { - // extended props to support share modal - label: 'PDF' | 'CSV' | 'PNG'; - reportType?: SupportedExportTypes; - requiresSavedState?: boolean; - helpText?: ReactElement; - copyURLButton?: { id: string; dataTestSubj: string; label: string }; - generateExportButton?: ReactElement; - /** - * Function to trigger an export - */ - generateExport: (args: ScreenshotExportOpts) => Promise; - /** - * Function to generate a URL to be used for automating export - * Not applicable for exports that do not call a remote API (i.e Lens CSV export) - */ - generateExportUrl?: (args: ScreenshotExportOpts) => string | undefined; - theme?: ThemeServiceSetup; - renderLayoutOptionSwitch?: boolean; - layoutOption?: 'print'; - generateCopyUrl?: URL; - renderCopyURLButton?: boolean; - warnings?: Array<{ title: string; message: string }>; -} +// export interface ShareMenuItemV2 extends ShareMenuItemBase { +// // extended props to support share modal +// label: 'PDF' | 'CSV' | 'PNG'; +// // reportType?: SupportedExportTypes; +// requiresSavedState?: boolean; +// helpText?: ReactElement; +// copyURLButton?: { id: string; dataTestSubj: string; label: string }; +// generateExportButton?: ReactElement; +// /** +// * Function to trigger an export +// */ +// generateExport: (args: ScreenshotExportOpts) => Promise; +// /** +// * Function to generate a URL to be used for automating export +// * Not applicable for exports that do not call a remote API (i.e Lens CSV export) +// */ +// generateExportUrl?: (args: ScreenshotExportOpts) => string | undefined; +// theme?: ThemeServiceSetup; +// renderLayoutOptionSwitch?: boolean; +// layoutOption?: 'print'; +// generateCopyUrl?: URL; +// renderCopyURLButton?: boolean; +// warnings?: Array<{ title: string; message: string }>; +// } + +// export interface ShareMenuProviderV2 { +// readonly id: string; +// getShareMenuItems: (context: ShareContext) => ShareMenuItemV2[]; +// } -export interface ShareMenuProviderV2 { - readonly id: string; - getShareMenuItems: (context: ShareContext) => ShareMenuItemV2[]; -} export interface ShareMenuProviderLegacy { readonly id: string; getShareMenuItemsLegacy: (context: ShareContext) => ShareMenuItemLegacy[]; @@ -162,7 +319,7 @@ export interface ShareMenuProviderLegacy { * menu. Returned `ShareMenuItem`s will be shown in the context menu together with the * default built-in share options. Each share provider needs a globally unique id. * */ -export type ShareMenuProvider = ShareMenuProviderV2 | ShareMenuProviderLegacy; +// export type ShareMenuProvider = ShareMenuProviderLegacy; interface UrlParamExtensionProps { setParamValue: (values: {}) => void; @@ -176,10 +333,7 @@ export interface UrlParamExtension { /** @public */ export interface ShowShareMenuOptions extends Omit { anchorElement: HTMLElement; - allowEmbed: boolean; allowShortUrl: boolean; - embedUrlParamExtensions?: UrlParamExtension[]; - snapshotShareWarning?: string; onClose?: () => void; publicAPIEnabled?: boolean; } diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx index 36dc5ca1c8884..05d20f4ee8e1b 100644 --- a/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx @@ -634,13 +634,7 @@ export const LensTopNavMenu = ({ share.toggleShareContextMenu({ anchorElement, - allowEmbed: false, allowShortUrl: false, - delegatedShareUrlHandler: () => { - return isCurrentStateDirty || !currentDoc?.savedObjectId - ? shareableUrl! - : savedObjectURL.href; - }, objectId: currentDoc?.savedObjectId, objectType: 'lens', objectTypeMeta: { @@ -665,6 +659,15 @@ export const LensTopNavMenu = ({ /> ), + delegatedShareUrlHandler: () => { + return isCurrentStateDirty || !currentDoc?.savedObjectId + ? shareableUrl! + : savedObjectURL.href; + }, + }, + embed: { + allowEmbed: true, + showPublicUrlSwitch: () => false, }, }, }, @@ -674,7 +677,6 @@ export const LensTopNavMenu = ({ // disable the menu if both shortURL permission and the visualization has not been saved // TODO: improve here the disabling state with more specific checks disabledShareUrl: Boolean(!shareUrlEnabled && !currentDoc?.savedObjectId), - showPublicUrlSwitch: () => false, onClose: () => { anchorElement?.focus(); },