UI Extensions: Add openInNewTab property to link extensions (#114831)

feat: add `openInNewTab` property for link extensions
This commit is contained in:
Levente Balogh
2025-12-08 12:09:56 +01:00
committed by GitHub
parent 7c5457a75e
commit 5a8a730cfe
6 changed files with 20 additions and 1 deletions
@@ -30,6 +30,7 @@ export type PluginExtensionLink = PluginExtensionBase & {
onClick?: (event?: React.MouseEvent) => void; onClick?: (event?: React.MouseEvent) => void;
icon?: IconName; icon?: IconName;
category?: string; category?: string;
openInNewTab?: boolean;
}; };
export type PluginExtensionComponentMeta = Omit<PluginExtensionComponent, 'component'>; export type PluginExtensionComponentMeta = Omit<PluginExtensionComponent, 'component'>;
@@ -86,6 +87,7 @@ export type PluginExtensionAddedComponentConfig<Props = {}> = PluginExtensionCon
*/ */
component: React.ComponentType<Props>; component: React.ComponentType<Props>;
}; };
export type PluginExtensionAddedFunctionConfig<Signature = unknown> = PluginExtensionConfigBase & { export type PluginExtensionAddedFunctionConfig<Signature = unknown> = PluginExtensionConfigBase & {
/** /**
* The target extension points where the component will be added * The target extension points where the component will be added
@@ -106,6 +108,7 @@ export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Re
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void; onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
icon: IconName; icon: IconName;
category: string; category: string;
openInNewTab: boolean;
}> }>
| undefined; | undefined;
@@ -137,6 +140,10 @@ export type PluginExtensionAddedLinkConfig<Context extends object = object> = Pl
// (Optional) A category to be used when grouping the options in the ui // (Optional) A category to be used when grouping the options in the ui
category?: string; category?: string;
// (Optional) If true, opens the link in a new tab (renders with target="_blank")
// (Important: this is not guaranteed, depends on the extension point if it implements it.)
openInNewTab?: boolean;
}; };
export type PluginExtensionExposedComponentConfig<Props = {}> = PluginExtensionConfigBase & { export type PluginExtensionExposedComponentConfig<Props = {}> = PluginExtensionConfigBase & {
@@ -95,6 +95,7 @@ describe('AddedLinksRegistry', () => {
path: `/a/${pluginId}/declare-incident`, path: `/a/${pluginId}/declare-incident`,
targets: 'plugins/myorg-basic-app/start', targets: 'plugins/myorg-basic-app/start',
configure: jest.fn().mockImplementation((context) => ({ title: context?.title })), configure: jest.fn().mockImplementation((context) => ({ title: context?.title })),
openInNewTab: true,
}, },
], ],
}); });
@@ -120,6 +121,7 @@ describe('AddedLinksRegistry', () => {
path: `/a/${pluginId}/declare-incident`, path: `/a/${pluginId}/declare-incident`,
extensionPointId: 'plugins/myorg-basic-app/start', extensionPointId: 'plugins/myorg-basic-app/start',
configure: expect.any(Function), configure: expect.any(Function),
openInNewTab: true,
}, },
], ],
}); });
@@ -21,6 +21,7 @@ export type AddedLinkRegistryItem<Context extends object = object> = {
configure?: PluginAddedLinksConfigureFunc<Context>; configure?: PluginAddedLinksConfigureFunc<Context>;
icon?: IconName; icon?: IconName;
category?: string; category?: string;
openInNewTab?: boolean;
}; };
export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], PluginExtensionAddedLinkConfig> { export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], PluginExtensionAddedLinkConfig> {
@@ -40,13 +41,14 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
const { pluginId, configs } = item; const { pluginId, configs } = item;
for (const config of configs) { for (const config of configs) {
const { path, title, description, configure, onClick, targets } = config; const { path, title, description, configure, onClick, targets, openInNewTab } = config;
const configLog = this.logger.child({ const configLog = this.logger.child({
path: path ?? '', path: path ?? '',
description: description ?? '', description: description ?? '',
title, title,
pluginId, pluginId,
onClick: typeof onClick, onClick: typeof onClick,
openInNewTab: openInNewTab ? 'true' : 'false',
}); });
if (!title) { if (!title) {
@@ -167,6 +167,7 @@ describe('usePluginLinks()', () => {
title: '2', title: '2',
description: '2', description: '2',
path: `/a/${pluginId}/2`, path: `/a/${pluginId}/2`,
openInNewTab: true,
}, },
{ {
targets: 'plugins/another-extension/v1', targets: 'plugins/another-extension/v1',
@@ -181,7 +182,9 @@ describe('usePluginLinks()', () => {
expect(result.current.links.length).toBe(2); expect(result.current.links.length).toBe(2);
expect(result.current.links[0].title).toBe('1'); expect(result.current.links[0].title).toBe('1');
expect(result.current.links[0].openInNewTab).toBeUndefined();
expect(result.current.links[1].title).toBe('2'); expect(result.current.links[1].title).toBe('2');
expect(result.current.links[1].openInNewTab).toBe(true);
}); });
it('should dynamically update the extensions registered for a certain extension point', () => { it('should dynamically update the extensions registered for a certain extension point', () => {
@@ -51,6 +51,7 @@ export function usePluginLinks({
title: addedLink.title, title: addedLink.title,
description: addedLink.description ?? '', description: addedLink.description ?? '',
onClick: typeof addedLink.onClick, onClick: typeof addedLink.onClick,
openInNewTab: addedLink.openInNewTab ? 'true' : 'false',
}); });
// Only limit if the `limitPerPlugin` is set // Only limit if the `limitPerPlugin` is set
@@ -84,6 +85,7 @@ export function usePluginLinks({
description: overrides?.description || addedLink.description || '', description: overrides?.description || addedLink.description || '',
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined, path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined,
category: overrides?.category || addedLink.category, category: overrides?.category || addedLink.category,
openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab,
}; };
extensions.push(extension); extensions.push(extension);
@@ -474,12 +474,14 @@ export function getLinkExtensionOverrides(
return undefined; return undefined;
} }
// Only allowing to override the following properties
let { let {
title = config.title, title = config.title,
description = config.description, description = config.description,
path = config.path, path = config.path,
icon = config.icon, icon = config.icon,
category = config.category, category = config.category,
openInNewTab = config.openInNewTab,
...rest ...rest
} = overrides; } = overrides;
@@ -504,6 +506,7 @@ export function getLinkExtensionOverrides(
path, path,
icon, icon,
category, category,
openInNewTab,
}; };
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {