mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 14:43: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 PluginExtensionEventHelpers,
|
||||||
type PluginExtensionPanelContext,
|
type PluginExtensionPanelContext,
|
||||||
type PluginExtensionDataSourceConfigContext,
|
type PluginExtensionDataSourceConfigContext,
|
||||||
|
type PluginExtensionOpenModalOptions,
|
||||||
} from './pluginExtensions';
|
} from './pluginExtensions';
|
||||||
|
@ -95,15 +95,21 @@ export type PluginExtensionComponentConfig<Context extends object = object> = {
|
|||||||
|
|
||||||
export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
|
export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
|
||||||
|
|
||||||
|
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 }>;
|
||||||
|
// 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> = {
|
export type PluginExtensionEventHelpers<Context extends object = object> = {
|
||||||
context?: Readonly<Context>;
|
context?: Readonly<Context>;
|
||||||
// Opens a modal dialog and renders the provided React component inside it
|
// Opens a modal dialog and renders the provided React component inside it
|
||||||
openModal: (options: {
|
openModal: (options: PluginExtensionOpenModalOptions) => void;
|
||||||
// The title of the modal
|
|
||||||
title: string;
|
|
||||||
// A React element that will be rendered inside the modal
|
|
||||||
body: React.ElementType<{ onDismiss?: () => void }>;
|
|
||||||
}) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extension Points & Contexts
|
// 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('Plugin Extensions / Utils', () => {
|
||||||
describe('deepFreeze()', () => {
|
describe('deepFreeze()', () => {
|
||||||
@ -307,4 +313,88 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
expect(proxy.a()).toBe('testing');
|
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 { isArray, isObject } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
type PluginExtensionConfig,
|
type PluginExtensionConfig,
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
PluginExtensionTypes,
|
PluginExtensionTypes,
|
||||||
|
type PluginExtensionOpenModalOptions,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Modal } from '@grafana/ui';
|
import { Modal } from '@grafana/ui';
|
||||||
import appEvents from 'app/core/app_events';
|
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.
|
// 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 {
|
export function getEventHelpers(context?: Readonly<object>): PluginExtensionEventHelpers {
|
||||||
const openModal: PluginExtensionEventHelpers['openModal'] = ({ title, body }) => {
|
const openModal: PluginExtensionEventHelpers['openModal'] = (options) => {
|
||||||
appEvents.publish(new ShowModalReactEvent({ component: getModalWrapper({ title, body }) }));
|
const { title, body, width, height } = options;
|
||||||
|
|
||||||
|
appEvents.publish(
|
||||||
|
new ShowModalReactEvent({
|
||||||
|
component: getModalWrapper({ title, body, width, height }),
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { openModal, context };
|
return { openModal, context };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModalWrapperProps = {
|
type ModalWrapperProps = {
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wraps a component with a modal.
|
// Wraps a component with a modal.
|
||||||
// This way we can make sure that the modal is closable, and we also make the usage simpler.
|
// 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)
|
// The title of the modal (appears in the header)
|
||||||
title,
|
title,
|
||||||
// A component that serves the body of the modal
|
// A component that serves the body of the modal
|
||||||
body: Body,
|
body: Body,
|
||||||
}: Parameters<PluginExtensionEventHelpers['openModal']>[0]) => {
|
width,
|
||||||
|
height,
|
||||||
|
}: PluginExtensionOpenModalOptions) => {
|
||||||
|
const className = css({ width, height });
|
||||||
|
|
||||||
const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => {
|
const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => {
|
||||||
return (
|
return (
|
||||||
<Modal title={title} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
<Modal title={title} className={className} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
||||||
<Body onDismiss={onDismiss} />
|
<Body onDismiss={onDismiss} />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user