PluginExtensions: Made it possible to control modal size from extension (#76232)

* Added possibility to change modal size from UI extension.

* added tests for openModal.

* fixed typings.

* added test to verify default modal size.
This commit is contained in:
Marcus Andersson
2023-10-12 10:56:08 +02:00
committed by GitHub
parent 420fb56fda
commit f012b75a3a
4 changed files with 123 additions and 14 deletions

View File

@ -63,4 +63,5 @@ export {
type PluginExtensionEventHelpers,
type PluginExtensionPanelContext,
type PluginExtensionDataSourceConfigContext,
type PluginExtensionOpenModalOptions,
} from './pluginExtensions';

View File

@ -95,15 +95,21 @@ export type PluginExtensionComponentConfig<Context extends object = object> = {
export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
export type PluginExtensionEventHelpers<Context extends object = object> = {
context?: Readonly<Context>;
// Opens a modal dialog and renders the provided React component inside it
openModal: (options: {
export type PluginExtensionOpenModalOptions = {
// The title of the modal
title: string;
// A React element that will be rendered inside the modal
body: React.ElementType<{ onDismiss?: () => void }>;
}) => void;
// Width of the modal in pixels or percentage
width?: string | number;
// Height of the modal in pixels or percentage
height?: string | number;
};
export type PluginExtensionEventHelpers<Context extends object = object> = {
context?: Readonly<Context>;
// Opens a modal dialog and renders the provided React component inside it
openModal: (options: PluginExtensionOpenModalOptions) => void;
};
// Extension Points & Contexts

View File

@ -1,6 +1,12 @@
import { PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { type Unsubscribable } from 'rxjs';
import { deepFreeze, isPluginExtensionLinkConfig, handleErrorsInFn, getReadOnlyProxy } from './utils';
import { type PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { ShowModalReactEvent } from 'app/types/events';
import { deepFreeze, isPluginExtensionLinkConfig, handleErrorsInFn, getReadOnlyProxy, getEventHelpers } from './utils';
describe('Plugin Extensions / Utils', () => {
describe('deepFreeze()', () => {
@ -307,4 +313,88 @@ describe('Plugin Extensions / Utils', () => {
expect(proxy.a()).toBe('testing');
});
});
describe('getEventHelpers', () => {
describe('openModal', () => {
let renderModalSubscription: Unsubscribable | undefined;
beforeAll(() => {
renderModalSubscription = appEvents.subscribe(ShowModalReactEvent, (event) => {
const { payload } = event;
const Modal = payload.component;
render(<Modal />);
});
});
afterAll(() => {
renderModalSubscription?.unsubscribe();
});
it('should open modal with provided title and body', async () => {
const { openModal } = getEventHelpers();
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
});
expect(screen.getByRole('dialog')).toBeVisible();
expect(screen.getByRole('heading')).toHaveTextContent('Title in modal');
expect(screen.getByText('Text in body')).toBeVisible();
});
it('should open modal with default width if not specified', async () => {
const { openModal } = getEventHelpers();
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
});
const modal = screen.getByRole('dialog');
const style = window.getComputedStyle(modal);
expect(style.width).toBe('750px');
expect(style.height).toBe('');
});
it('should open modal with specified width', async () => {
const { openModal } = getEventHelpers();
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
width: '70%',
});
const modal = screen.getByRole('dialog');
const style = window.getComputedStyle(modal);
expect(style.width).toBe('70%');
});
it('should open modal with specified height', async () => {
const { openModal } = getEventHelpers();
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
height: 600,
});
const modal = screen.getByRole('dialog');
const style = window.getComputedStyle(modal);
expect(style.height).toBe('600px');
});
});
describe('context', () => {
it('should return same object as passed to getEventHelpers', () => {
const source = {};
const { context } = getEventHelpers(source);
expect(context).toBe(source);
});
});
});
});

View File

@ -1,3 +1,4 @@
import { css } from '@emotion/css';
import { isArray, isObject } from 'lodash';
import React from 'react';
@ -7,6 +8,7 @@ import {
type PluginExtensionConfig,
type PluginExtensionEventHelpers,
PluginExtensionTypes,
type PluginExtensionOpenModalOptions,
} from '@grafana/data';
import { Modal } from '@grafana/ui';
import appEvents from 'app/core/app_events';
@ -42,28 +44,38 @@ export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
// Event helpers are designed to make it easier to trigger "core actions" from an extension event handler, e.g. opening a modal or showing a notification.
export function getEventHelpers(context?: Readonly<object>): PluginExtensionEventHelpers {
const openModal: PluginExtensionEventHelpers['openModal'] = ({ title, body }) => {
appEvents.publish(new ShowModalReactEvent({ component: getModalWrapper({ title, body }) }));
const openModal: PluginExtensionEventHelpers['openModal'] = (options) => {
const { title, body, width, height } = options;
appEvents.publish(
new ShowModalReactEvent({
component: getModalWrapper({ title, body, width, height }),
})
);
};
return { openModal, context };
}
export type ModalWrapperProps = {
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 = ({
const getModalWrapper = ({
// The title of the modal (appears in the header)
title,
// A component that serves the body of the modal
body: Body,
}: Parameters<PluginExtensionEventHelpers['openModal']>[0]) => {
width,
height,
}: PluginExtensionOpenModalOptions) => {
const className = css({ width, height });
const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => {
return (
<Modal title={title} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
<Modal title={title} className={className} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
<Body onDismiss={onDismiss} />
</Modal>
);