mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Allow command extensions to open modals (#64029)
feat: make it possible to open modals from commands
This commit is contained in:
parent
09341a0cd6
commit
d44dc0f100
@ -56,6 +56,17 @@ export interface AppPluginMeta<T extends KeyValue = KeyValue> extends PluginMeta
|
||||
*/
|
||||
export type AppPluginExtensionLink = Pick<PluginExtensionLink, 'description' | 'path' | 'title'>;
|
||||
|
||||
// A list of helpers that can be used in the command handler
|
||||
export type AppPluginExtensionCommandHelpers = {
|
||||
// Opens a modal dialog and renders the provided React component inside it
|
||||
openModal: (options: {
|
||||
// The title of the modal
|
||||
title: string;
|
||||
// A React element that will be rendered inside the modal
|
||||
body: React.ElementType<{ onDismiss?: () => void }>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type AppPluginExtensionCommand = Pick<PluginExtensionCommand, 'description' | 'title'>;
|
||||
|
||||
export type AppPluginExtensionLinkConfig<C extends object = object> = {
|
||||
@ -70,7 +81,7 @@ export type AppPluginExtensionCommandConfig<C extends object = object> = {
|
||||
title: string;
|
||||
description: string;
|
||||
placement: string;
|
||||
handler: (context?: C) => void;
|
||||
handler: (context?: C, helpers?: AppPluginExtensionCommandHelpers) => void;
|
||||
configure?: (extension: AppPluginExtensionCommand, context?: C) => Partial<AppPluginExtensionCommand> | undefined;
|
||||
};
|
||||
|
||||
|
27
public/app/features/plugins/extensions/getModalWrapper.tsx
Normal file
27
public/app/features/plugins/extensions/getModalWrapper.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AppPluginExtensionCommandHelpers } from '@grafana/data';
|
||||
import { Modal } from '@grafana/ui';
|
||||
|
||||
export type ModalWrapperProps = {
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
// Wraps a component with a modal.
|
||||
// This way we can make sure that the modal is closable, and we also make the usage simpler.
|
||||
export const getModalWrapper = ({
|
||||
// The title of the modal (appears in the header)
|
||||
title,
|
||||
// A component that serves the body of the modal
|
||||
body: Body,
|
||||
}: Parameters<AppPluginExtensionCommandHelpers['openModal']>[0]) => {
|
||||
const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => {
|
||||
return (
|
||||
<Modal title={title} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
||||
<Body onDismiss={onDismiss} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return ModalWrapper;
|
||||
};
|
@ -269,18 +269,22 @@ describe('createPluginExtensionRegistry()', () => {
|
||||
describe('when registering commands', () => {
|
||||
const pluginId = 'belugacdn-app';
|
||||
// Sample command configurations to be used in tests
|
||||
const commandConfig1 = {
|
||||
placement: 'grafana/dashboard/panel/menu',
|
||||
title: 'Open incident',
|
||||
description: 'You can create an incident from this context',
|
||||
handler: () => {},
|
||||
};
|
||||
const commandConfig2 = {
|
||||
placement: 'plugins/grafana-slo-app/slo-breached',
|
||||
title: 'Open incident',
|
||||
description: 'You can create an incident from this context',
|
||||
handler: () => {},
|
||||
};
|
||||
let commandConfig1: AppPluginExtensionCommandConfig, commandConfig2: AppPluginExtensionCommandConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
commandConfig1 = {
|
||||
placement: 'grafana/dashboard/panel/menu',
|
||||
title: 'Open incident',
|
||||
description: 'You can create an incident from this context',
|
||||
handler: jest.fn(),
|
||||
};
|
||||
commandConfig2 = {
|
||||
placement: 'plugins/grafana-slo-app/slo-breached',
|
||||
title: 'Open incident',
|
||||
description: 'You can create an incident from this context',
|
||||
handler: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should register a command extension', () => {
|
||||
const registry = createPluginExtensionRegistry([
|
||||
@ -428,26 +432,25 @@ describe('createPluginExtensionRegistry()', () => {
|
||||
linkExtensions: [],
|
||||
commandExtensions: [
|
||||
{
|
||||
placement: 'grafana/dashboard/panel/menu',
|
||||
title: 'Open incident',
|
||||
description: 'You can create an incident from this context',
|
||||
handler: () => {},
|
||||
...commandConfig1,
|
||||
configure: () => ({}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const extensions = registry['grafana/dashboard/panel/menu'];
|
||||
const extensions = registry[commandConfig1.placement];
|
||||
const [configure] = extensions;
|
||||
const context = {};
|
||||
const extension = configure?.(context);
|
||||
const extension = configure(context);
|
||||
|
||||
assertPluginExtensionCommand(extension);
|
||||
|
||||
extension.callHandlerWithContext();
|
||||
|
||||
expect(commandErrorHandler).toBeCalledTimes(1);
|
||||
expect(commandErrorHandler).toBeCalledWith(expect.any(Function), context);
|
||||
expect(commandConfig1.handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should wrap handler function with extension error handling when no configure function is added', () => {
|
||||
@ -455,27 +458,52 @@ describe('createPluginExtensionRegistry()', () => {
|
||||
{
|
||||
pluginId,
|
||||
linkExtensions: [],
|
||||
commandExtensions: [
|
||||
{
|
||||
placement: 'grafana/dashboard/panel/menu',
|
||||
title: 'Open incident',
|
||||
description: 'You can create an incident from this context',
|
||||
handler: () => {},
|
||||
},
|
||||
],
|
||||
commandExtensions: [commandConfig1],
|
||||
},
|
||||
]);
|
||||
|
||||
const extensions = registry['grafana/dashboard/panel/menu'];
|
||||
const extensions = registry[commandConfig1.placement];
|
||||
const [configure] = extensions;
|
||||
const context = {};
|
||||
const extension = configure?.(context);
|
||||
const extension = configure(context);
|
||||
|
||||
assertPluginExtensionCommand(extension);
|
||||
|
||||
extension.callHandlerWithContext();
|
||||
|
||||
expect(commandErrorHandler).toBeCalledTimes(1);
|
||||
expect(commandErrorHandler).toBeCalledWith(expect.any(Function), context);
|
||||
expect(commandConfig1.handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the `handler()` function with the context and a `helpers` object', () => {
|
||||
const registry = createPluginExtensionRegistry([
|
||||
{
|
||||
pluginId,
|
||||
linkExtensions: [],
|
||||
commandExtensions: [commandConfig1, { ...commandConfig2, configure: () => ({}) }],
|
||||
},
|
||||
]);
|
||||
|
||||
const context = {};
|
||||
const command1 = registry[commandConfig1.placement][0](context);
|
||||
const command2 = registry[commandConfig2.placement][0](context);
|
||||
|
||||
assertPluginExtensionCommand(command1);
|
||||
assertPluginExtensionCommand(command2);
|
||||
|
||||
command1.callHandlerWithContext();
|
||||
command2.callHandlerWithContext();
|
||||
|
||||
expect(commandConfig1.handler).toBeCalledTimes(1);
|
||||
expect(commandConfig1.handler).toBeCalledWith(context, {
|
||||
openModal: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(commandConfig2.handler).toBeCalledTimes(1);
|
||||
expect(commandConfig2.handler).toBeCalledWith(context, {
|
||||
openModal: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
type AppPluginExtensionCommand,
|
||||
type AppPluginExtensionCommandConfig,
|
||||
type AppPluginExtensionCommandHelpers,
|
||||
type AppPluginExtensionLink,
|
||||
type AppPluginExtensionLinkConfig,
|
||||
type PluginExtension,
|
||||
@ -9,12 +10,15 @@ import {
|
||||
PluginExtensionTypes,
|
||||
} from '@grafana/data';
|
||||
import type { PluginExtensionRegistry, PluginExtensionRegistryItem } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import type { PluginPreloadResult } from '../pluginPreloader';
|
||||
|
||||
import { handleErrorsInHandler, handleErrorsInConfigure } from './errorHandling';
|
||||
import { getModalWrapper } from './getModalWrapper';
|
||||
import { PlacementsPerPlugin } from './placementsPerPlugin';
|
||||
import { ConfigureFunc } from './types';
|
||||
import { CommandHandlerFunc, ConfigureFunc } from './types';
|
||||
import { createLinkValidator, isValidLinkPath } from './validateLink';
|
||||
|
||||
export function createPluginExtensionRegistry(preloadResults: PluginPreloadResult[]): PluginExtensionRegistry {
|
||||
@ -69,6 +73,7 @@ function createCommandRegistryItem(
|
||||
config: AppPluginExtensionCommandConfig
|
||||
): PluginExtensionRegistryItem<PluginExtensionCommand> | undefined {
|
||||
const configure = config.configure ?? defaultConfigure;
|
||||
const helpers = getCommandHelpers();
|
||||
|
||||
const options = {
|
||||
pluginId: pluginId,
|
||||
@ -76,8 +81,9 @@ function createCommandRegistryItem(
|
||||
logger: console.warn,
|
||||
};
|
||||
|
||||
const handlerWithHelpers: CommandHandlerFunc = (context) => config.handler(context, helpers);
|
||||
const catchErrorsInHandler = handleErrorsInHandler(options);
|
||||
const handler = catchErrorsInHandler(config.handler);
|
||||
const handler = catchErrorsInHandler(handlerWithHelpers);
|
||||
|
||||
const extensionFactory = createCommandFactory(pluginId, config, handler);
|
||||
|
||||
@ -175,3 +181,11 @@ function hashKey(key: string): number {
|
||||
function defaultConfigure() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function getCommandHelpers() {
|
||||
const openModal: AppPluginExtensionCommandHelpers['openModal'] = ({ title, body }) => {
|
||||
appEvents.publish(new ShowModalReactEvent({ component: getModalWrapper({ title, body }) }));
|
||||
};
|
||||
|
||||
return { openModal };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user