mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 07:01:49 +08:00
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:
@ -63,4 +63,5 @@ export {
|
||||
type PluginExtensionEventHelpers,
|
||||
type PluginExtensionPanelContext,
|
||||
type PluginExtensionDataSourceConfigContext,
|
||||
type PluginExtensionOpenModalOptions,
|
||||
} from './pluginExtensions';
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
Reference in New Issue
Block a user