diff --git a/packages/grafana-data/src/context/plugins/usePluginContext.tsx b/packages/grafana-data/src/context/plugins/usePluginContext.tsx index 51723446b81..e2ea1056629 100644 --- a/packages/grafana-data/src/context/plugins/usePluginContext.tsx +++ b/packages/grafana-data/src/context/plugins/usePluginContext.tsx @@ -1,5 +1,7 @@ import { useContext } from 'react'; +import { PluginMeta } from '../../types'; + import { Context, PluginContextType } from './PluginContext'; export function usePluginContext(): PluginContextType { @@ -9,3 +11,21 @@ export function usePluginContext(): PluginContextType { } return context; } + +export function usePluginMeta(): PluginMeta { + const context = usePluginContext(); + + return context.meta; +} + +export function usePluginJsonData() { + const context = usePluginContext(); + + return context.meta.jsonData; +} + +export function usePluginVersion() { + const context = usePluginContext(); + + return context.meta.info.version; +} diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index cf4abcd403f..bd22739aa1a 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -3,7 +3,16 @@ import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; import { useLocation, useRouteMatch } from 'react-router-dom'; -import { AppEvents, AppPlugin, AppPluginMeta, NavModel, NavModelItem, OrgRole, PluginType } from '@grafana/data'; +import { + AppEvents, + AppPlugin, + AppPluginMeta, + NavModel, + NavModelItem, + OrgRole, + PluginType, + PluginContextProvider, +} from '@grafana/data'; import { config, locationSearchToObject } from '@grafana/runtime'; import { Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -76,13 +85,15 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { } const pluginRoot = plugin.root && ( - + + + ); // Because of the fallback at plugin routes, we need to check @@ -93,7 +104,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { return true; } - const pluginInclude = plugin.meta?.includes.find((include) => include.path === pluginRoot.props.path); + const pluginInclude = plugin.meta?.includes.find((include) => include.path === location.pathname); // Check if include configuration contains current path if (!pluginInclude) { return true; diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index 069635dea3d..85b35929969 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -189,7 +189,7 @@ function getLinkExtensionOnClick( category: config.category, }); - const result = onClick(event, getEventHelpers(context)); + const result = onClick(event, getEventHelpers(pluginId, context)); if (isPromise(result)) { result.catch((e) => { diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 60738f3a3ba..c7cc1169d8e 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -2,12 +2,17 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { type Unsubscribable } from 'rxjs'; -import { type PluginExtensionLinkConfig, PluginExtensionTypes, dateTime } from '@grafana/data'; +import { type PluginExtensionLinkConfig, PluginExtensionTypes, dateTime, usePluginContext } from '@grafana/data'; import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; import { deepFreeze, isPluginExtensionLinkConfig, handleErrorsInFn, getReadOnlyProxy, getEventHelpers } from './utils'; +jest.mock('app/features/plugins/pluginSettings', () => ({ + ...jest.requireActual('app/features/plugins/pluginSettings'), + getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' } }), +})); + describe('Plugin Extensions / Utils', () => { describe('deepFreeze()', () => { test('should not fail when called with primitive values', () => { @@ -341,27 +346,29 @@ describe('Plugin Extensions / Utils', () => { }); it('should open modal with provided title and body', async () => { - const { openModal } = getEventHelpers(); + const pluginId = 'grafana-worldmap-panel'; + const { openModal } = getEventHelpers(pluginId); openModal({ title: 'Title in modal', body: () =>
Text in body
, }); - expect(screen.getByRole('dialog')).toBeVisible(); + expect(await screen.findByRole('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(); + const pluginId = 'grafana-worldmap-panel'; + const { openModal } = getEventHelpers(pluginId); openModal({ title: 'Title in modal', body: () =>
Text in body
, }); - const modal = screen.getByRole('dialog'); + const modal = await screen.findByRole('dialog'); const style = window.getComputedStyle(modal); expect(style.width).toBe('750px'); @@ -369,7 +376,8 @@ describe('Plugin Extensions / Utils', () => { }); it('should open modal with specified width', async () => { - const { openModal } = getEventHelpers(); + const pluginId = 'grafana-worldmap-panel'; + const { openModal } = getEventHelpers(pluginId); openModal({ title: 'Title in modal', @@ -377,14 +385,15 @@ describe('Plugin Extensions / Utils', () => { width: '70%', }); - const modal = screen.getByRole('dialog'); + const modal = await screen.findByRole('dialog'); const style = window.getComputedStyle(modal); expect(style.width).toBe('70%'); }); it('should open modal with specified height', async () => { - const { openModal } = getEventHelpers(); + const pluginId = 'grafana-worldmap-panel'; + const { openModal } = getEventHelpers(pluginId); openModal({ title: 'Title in modal', @@ -392,17 +401,37 @@ describe('Plugin Extensions / Utils', () => { height: 600, }); - const modal = screen.getByRole('dialog'); + const modal = await screen.findByRole('dialog'); const style = window.getComputedStyle(modal); expect(style.height).toBe('600px'); }); + + it('should open modal with the plugin context being available', async () => { + const pluginId = 'grafana-worldmap-panel'; + const { openModal } = getEventHelpers(pluginId); + + const ModalContent = () => { + const context = usePluginContext(); + + return
Version: {context.meta.info.version}
; + }; + + openModal({ + title: 'Title in modal', + body: ModalContent, + }); + + const modal = await screen.findByRole('dialog'); + expect(modal).toHaveTextContent('Version: 1.0.0'); + }); }); describe('context', () => { it('should return same object as passed to getEventHelpers', () => { + const pluginId = 'grafana-worldmap-panel'; const source = {}; - const { context } = getEventHelpers(source); + const { context } = getEventHelpers(pluginId, source); expect(context).toBe(source); }); }); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 42dc799494f..be8fbb6762b 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -11,9 +11,12 @@ import { type PluginExtensionOpenModalOptions, isDateTime, dateTime, + PluginContextProvider, + PluginMeta, } from '@grafana/data'; import { Modal } from '@grafana/ui'; import appEvents from 'app/core/app_events'; +import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { ShowModalReactEvent } from 'app/types/events'; export function logWarning(message: string) { @@ -45,13 +48,14 @@ 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): PluginExtensionEventHelpers { - const openModal: PluginExtensionEventHelpers['openModal'] = (options) => { +export function getEventHelpers(pluginId: string, context?: Readonly): PluginExtensionEventHelpers { + const openModal: PluginExtensionEventHelpers['openModal'] = async (options) => { const { title, body, width, height } = options; + const pluginMeta = await getPluginSettings(pluginId); appEvents.publish( new ShowModalReactEvent({ - component: getModalWrapper({ title, body, width, height }), + component: getModalWrapper({ title, body, width, height, pluginMeta }), }) ); }; @@ -72,14 +76,17 @@ const getModalWrapper = ({ body: Body, width, height, -}: PluginExtensionOpenModalOptions) => { + pluginMeta, +}: { pluginMeta: PluginMeta } & PluginExtensionOpenModalOptions) => { const className = css({ width, height }); const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => { return ( - - - + + + + + ); }; diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index a714e39118d..6306a443183 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -1,6 +1,7 @@ import type { PluginExtensionConfig } from '@grafana/data'; import type { AppPluginConfig } from '@grafana/runtime'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; +import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import * as pluginLoader from './plugin_loader'; @@ -29,6 +30,11 @@ async function preload(config: AppPluginConfig): Promise { pluginId, }); const { extensionConfigs = [] } = plugin; + + // Fetching meta-information for the preloaded app plugin and caching it for later. + // (The function below returns a promise, but it's not awaited for a reason: we don't want to block the preload process, we would only like to cache the result for later.) + getPluginSettings(pluginId); + return { pluginId, extensionConfigs }; } catch (error) { console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error);