diff --git a/.betterer.results b/.betterer.results
index 0f6c8fba7a6..c1f4b9f9ec8 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -489,18 +489,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
],
- "packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts:5381": [
- [0, 0, 0, "Do not use any type assertions.", "0"]
- ],
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
- "packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts:5381": [
- [0, 0, 0, "Do not use any type assertions.", "0"]
- ],
"packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx
index 2a413c665a6..d57a1476e19 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx
@@ -3,20 +3,18 @@ import { Route, Routes } from 'react-router-dom';
import { AppRootProps } from '@grafana/data';
import { ROUTES } from '../../constants';
-import { AddedComponents, AddedLinks, ExposedComponents, LegacyGetters, LegacyHooks } from '../../pages';
+import { AddedComponents, AddedLinks, ExposedComponents } from '../../pages';
import { testIds } from '../../testIds';
export function App(props: AppRootProps) {
return (
- } />
- } />
} />
} />
} />
- } />
+ } />
);
diff --git a/e2e/test-plugins/grafana-extensionstest-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/module.tsx
index f98d6629e48..25d69140a96 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/module.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/module.tsx
@@ -6,10 +6,10 @@ import pluginJson from './plugin.json';
export const plugin = new AppPlugin<{}>()
.setRootPage(App)
- .configureExtensionLink({
+ .addLink({
title: 'Open from time series or pie charts (path)',
description: 'This link will only be visible on time series and pie charts',
- extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
+ targets: PluginExtensionPoints.DashboardPanelMenu,
path: `/a/${pluginJson.id}/`,
configure: (context) => {
// Will only be visible for the Link Extensions dashboard
@@ -31,10 +31,10 @@ export const plugin = new AppPlugin<{}>()
}
},
})
- .configureExtensionLink({
+ .addLink({
title: 'Open from time series or pie charts (onClick)',
description: 'This link will only be visible on time series and pie charts',
- extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
+ targets: PluginExtensionPoints.DashboardPanelMenu,
onClick: (_, { openModal, context }) => {
const targets = context?.targets ?? [];
const title = context?.title;
diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json
index 2e7c91d08e6..d5f05088d35 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/package.json
+++ b/e2e/test-plugins/grafana-extensionstest-app/package.json
@@ -42,6 +42,5 @@
},
"peerDependencies": {
"@grafana/runtime": "*"
- },
- "packageManager": "yarn@4.4.0"
+ }
}
diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx
deleted file mode 100644
index 5cebc74d744..00000000000
--- a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import {
- PluginPage,
- getPluginComponentExtensions,
- getPluginExtensions,
- getPluginLinkExtensions,
-} from '@grafana/runtime';
-import { Stack } from '@grafana/ui';
-
-import { ActionButton } from '../components/ActionButton';
-import { testIds } from '../testIds';
-
-type AppExtensionContext = {};
-type ReusableComponentProps = {
- name: string;
-};
-
-export function LegacyGetters() {
- const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
- const extensionPointId2 = 'plugins/grafana-extensionstest-app/configure-extension-component/v1';
- const context: AppExtensionContext = {};
-
- const { extensions } = getPluginExtensions({
- extensionPointId: extensionPointId1,
- context,
- });
-
- const { extensions: linkExtensions } = getPluginLinkExtensions({
- extensionPointId: extensionPointId1,
- });
-
- const { extensions: componentExtensions } = getPluginComponentExtensions({
- extensionPointId: extensionPointId2,
- });
-
- return (
-
-
-
-
- Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using
- getPluginExtensions
-
-
-
-
- Link extensions defined with configureExtensionLink and retrived using getPluginLinkExtensions
-
-
-
-
- Component extensions defined with configureExtensionComponent and retrived using
- getPluginComponentExtensions
-
- {componentExtensions.map((extension) => {
- const Component = extension.component;
- return ;
- })}
-
-
-
- );
-}
diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx
deleted file mode 100644
index ef63caf9244..00000000000
--- a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import {
- PluginPage,
- usePluginComponentExtensions,
- usePluginExtensions,
- usePluginLinkExtensions,
-} from '@grafana/runtime';
-import { Stack } from '@grafana/ui';
-
-import { ActionButton } from '../components/ActionButton';
-import { testIds } from '../testIds';
-
-type AppExtensionContext = {};
-type ReusableComponentProps = {
- name: string;
-};
-
-export function LegacyHooks() {
- const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
- const extensionPointId2 = 'plugins/grafana-extensionstest-app/configure-extension-component/v1';
- const context: AppExtensionContext = {};
-
- const { extensions } = usePluginExtensions({
- extensionPointId: extensionPointId1,
- context,
- });
-
- const { extensions: linkExtensions } = usePluginLinkExtensions({
- extensionPointId: extensionPointId1,
- });
-
- const { extensions: componentExtensions } = usePluginComponentExtensions({
- extensionPointId: extensionPointId2,
- });
-
- return (
-
-
-
-
- Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using
- usePluginExtensions
-
-
-
-
- Link extensions defined with configureExtensionLink and retrived using usePluginLinkExtensions
-
-
-
-
- Component extensions defined with configureExtensionComponent and retrived using
- usePluginComponentExtensions
-
- {componentExtensions.map((extension) => {
- const Component = extension.component;
- return ;
- })}
-
-
-
- );
-}
diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx
index 6e955da302b..1326d3c7bdf 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx
@@ -1,5 +1,3 @@
export { ExposedComponents } from './ExposedComponents';
-export { LegacyGetters } from './LegacyGetters';
-export { LegacyHooks } from './LegacyHooks';
export { AddedComponents } from './AddedComponents';
export { AddedLinks } from './AddedLinks';
diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugin.json b/e2e/test-plugins/grafana-extensionstest-app/plugin.json
index 39252838461..90978c2c8a2 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/plugin.json
+++ b/e2e/test-plugins/grafana-extensionstest-app/plugin.json
@@ -19,22 +19,6 @@
"updated": "%TODAY%"
},
"includes": [
- {
- "type": "page",
- "name": "Legacy Getters",
- "path": "/a/grafana-extensionstest-app/legacy-getters",
- "role": "Admin",
- "addToNav": true,
- "defaultNav": false
- },
- {
- "type": "page",
- "name": "Legacy Hooks",
- "path": "/a/grafana-extensionstest-app/legacy-hooks",
- "role": "Admin",
- "addToNav": true,
- "defaultNav": false
- },
{
"type": "page",
"name": "Exposed components",
diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx
index cb3af35cd37..03d944bb881 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx
@@ -6,7 +6,7 @@ export class App extends React.PureComponent {
render() {
return (
- Hello Grafana!
+ Hello Grafana!!!!!
);
}
diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx
index 4b4dda98d1b..3f99d8f61f2 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx
@@ -7,12 +7,6 @@ import { App } from './components/App';
export const plugin = new AppPlugin<{}>()
.setRootPage(App)
- .configureExtensionLink({
- title: 'Go to A',
- description: 'Navigating to pluging A',
- extensionPointId: 'plugins/grafana-extensionstest-app/actions',
- path: '/a/grafana-extensionexample1-app/',
- })
.exposeComponent({
id: 'grafana-extensionexample1-app/reusable-component/v1',
title: 'Exposed component',
diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx
index 5a75974e1ed..12ca716ac46 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx
@@ -7,23 +7,6 @@ import { App } from './components/App';
export const plugin = new AppPlugin<{}>()
.setRootPage(App)
- .configureExtensionLink({
- title: 'Open from B',
- description: 'Open a modal from plugin B',
- extensionPointId: 'plugins/grafana-extensionstest-app/actions',
- onClick: (_, { openModal }) => {
- openModal({
- title: 'Modal from app B',
- body: () => From plugin B
,
- });
- },
- })
- .configureExtensionComponent({
- extensionPointId: 'plugins/grafana-extensionstest-app/configure-extension-component/v1',
- title: 'Configure extension component from B',
- description: 'A component that can be reused by other app plugins. Shared using configureExtensionComponent api',
- component: ({ name }: { name: string }) => Hello {name}!
,
- })
.addComponent<{ name: string }>({
targets: 'plugins/grafana-extensionstest-app/addComponent/v1',
title: 'Added component from B',
diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx
index 64df8da1575..422ed064163 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx
@@ -1,4 +1,4 @@
-import { usePluginExtensions, usePluginLinks } from '@grafana/runtime';
+import { usePluginLinks } from '@grafana/runtime';
import { Stack } from '@grafana/ui';
import { testIds } from '../../../../testIds';
import { ActionButton } from '../../../../components/ActionButton';
@@ -7,18 +7,12 @@ export const LINKS_EXTENSION_POINT_ID = 'plugins/grafana-extensionstest-app/use-
export function AddedLinks() {
const { links } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
- const { extensions } = usePluginExtensions({ extensionPointId: LINKS_EXTENSION_POINT_ID });
-
return (
Link extensions defined with addLink and retrieved using usePluginLinks
-
- Link extensions defined with addLink and retrieved using usePluginExtensions
-
-
);
}
diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx
index bba11717f86..fda97702d4f 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx
+++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx
@@ -6,23 +6,6 @@ import { App } from '../../components/App';
export const plugin = new AppPlugin<{}>()
.setRootPage(App)
- .configureExtensionLink({
- title: 'configureExtensionLink (where meta data is missing)',
- description: 'Open a modal from plugin B',
- extensionPointId: 'plugins/grafana-extensionstest-app/actions',
- onClick: (_, { openModal }) => {
- openModal({
- title: 'Modal from app B',
- body: () => From plugin B
,
- });
- },
- })
- .configureExtensionComponent({
- extensionPointId: 'plugins/grafana-extensionstest-app/configure-extension-component/v1',
- title: 'configureExtensionComponent (where meta data is missing)',
- description: 'A component that can be reused by other app plugins. Shared using configureExtensionComponent api',
- component: ({ name }: { name: string }) => Hello {name}!
,
- })
.addComponent<{ name: string }>({
targets: ['plugins/grafana-extensionstest-app/addComponent/v1'],
title: 'Added component (where meta data is missing)',
diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts
deleted file mode 100644
index 0d451b56977..00000000000
--- a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { test, expect } from '@grafana/plugin-e2e';
-
-import { ensureExtensionRegistryIsPopulated } from '../utils';
-import { testIds } from '../../testIds';
-import pluginJson from '../../plugin.json';
-
-test.describe('getPluginExtensions + configureExtensionLink', () => {
- test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-getters`);
- await ensureExtensionRegistryIsPopulated(page);
- const section = await page.getByTestId(testIds.legacyGettersPage.section1);
- await section.getByTestId(testIds.actions.button).click();
- await page.getByTestId(testIds.container).getByText('Go to A').click();
- await page.getByTestId(testIds.modal.open).click();
- await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
- });
-});
-
-test.describe('getPluginExtensions + configureExtensionComponent', () => {
- test('should extend main app with component extension from app B', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-getters`);
- await ensureExtensionRegistryIsPopulated(page);
- const section = await page.getByTestId(testIds.legacyGettersPage.section1);
- await section.getByTestId(testIds.actions.button).click();
- await page.getByTestId(testIds.container).getByText('Open from B').click();
- await expect(page.getByTestId(testIds.appB.modal)).toBeVisible();
- });
-});
-
-test.describe('getPluginLinkExtensions + configureExtensionLink', () => {
- test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-getters`);
- await ensureExtensionRegistryIsPopulated(page);
- const section = await page.getByTestId(testIds.legacyGettersPage.section2);
- await section.getByTestId(testIds.actions.button).click();
- await page.getByTestId(testIds.container).getByText('Go to A').click();
- await page.getByTestId(testIds.modal.open).click();
- await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
- });
-});
-
-test.describe('getPluginComponentExtensions + configureExtensionComponent', () => {
- test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-getters`);
- await ensureExtensionRegistryIsPopulated(page);
- await expect(
- page
- .getByTestId('configure-extension-component-get-plugin-component-extensions')
- .getByTestId(testIds.appB.reusableComponent)
- ).toHaveText('Hello World!');
- });
-});
diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts
deleted file mode 100644
index c4ae9d2d23a..00000000000
--- a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { test, expect } from '@grafana/plugin-e2e';
-
-import { testIds } from '../../testIds';
-import pluginJson from '../../plugin.json';
-import testApp3pluginJson from '../../plugins/grafana-extensionexample3-app/plugin.json';
-
-test.describe('usePluginExtensions + configureExtensionLink', () => {
- test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-hooks`);
- const section = await page.getByTestId(testIds.legacyHooksPage.section1);
- await section.getByTestId(testIds.actions.button).click();
- await page.getByTestId(testIds.container).getByText('Go to A').click();
- await page.getByTestId(testIds.modal.open).click();
- await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
- });
-
- test('should not display extensions that have not been declared in plugin.json when in development mode', async ({
- page,
- }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-hooks`);
- const section = await page.getByTestId(testIds.legacyHooksPage.section1);
- await section.getByTestId(testIds.actions.button).click();
- await expect(
- page.getByTestId(testIds.container).getByText('configureExtensionLink (where meta data is missing)')
- ).not.toBeVisible();
- });
-});
-
-test.describe('usePluginExtensions + configureExtensionComponent', () => {
- test('should extend main app with component extension from app B', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-hooks`);
- const section = await page.getByTestId(testIds.legacyHooksPage.section1);
- await section.getByTestId(testIds.actions.button).click();
- await page.getByTestId(testIds.container).getByText('Open from B').click();
- await expect(page.getByTestId(testIds.appB.modal)).toBeVisible();
- });
-});
-
-test.describe('usePluginLinkExtensions + configureExtensionLink', () => {
- test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-hooks`);
- const section = await page.getByTestId(testIds.legacyHooksPage.section2);
- await section.getByTestId(testIds.actions.button).click();
- await page.getByTestId(testIds.container).getByText('Go to A').click();
- await page.getByTestId(testIds.modal.open).click();
- await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
- });
-});
-
-test.describe('usePluginComponentExtensions + configureExtensionComponent', () => {
- test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => {
- await page.goto(`/a/${pluginJson.id}/legacy-hooks`);
- await expect(
- page.getByTestId(testIds.legacyHooksPage.section3).getByTestId(testIds.appB.reusableComponent)
- ).toHaveText('Hello World!');
- });
-});
-
-test.describe('usePluginExtensions + addLink', () => {
- test('should not display extensions in case extension point has not been declared in plugin json (dev mode only)', async ({
- page,
- }) => {
- await page.goto(`/a/${testApp3pluginJson.id}/legacy-hooks`);
- const section = await page.getByTestId(testIds.appC.section2);
- await expect(section.getByTestId(testIds.actions.button)).not.toBeVisible();
- });
-});
diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts
deleted file mode 100644
index 20be9c23d3f..00000000000
--- a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { expect, test } from '@grafana/plugin-e2e';
-import { ensureExtensionRegistryIsPopulated } from '../utils';
-
-const panelTitle = 'Link with defaults';
-const extensionTitle = 'Open from time series...';
-
-const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946';
-const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719';
-
-test.describe('configureExtensionLink targeting core extension points', () => {
- test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({
- gotoDashboardPage,
- page,
- }) => {
- const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid });
- await ensureExtensionRegistryIsPopulated(page);
- const panel = await dashboardPage.getPanelByTitle(panelTitle);
- await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' });
- await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible();
- });
-
- test('should add link extension (onclick) with defaults to time series panel', async ({
- gotoDashboardPage,
- page,
- }) => {
- const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid });
- await ensureExtensionRegistryIsPopulated(page);
- const panel = await dashboardPage.getPanelByTitle(panelTitle);
- await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' });
- await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"');
- });
-
- test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => {
- const panelTitle = 'Link with new name';
- const extensionTitle = 'Open from piechart';
- const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid });
- await ensureExtensionRegistryIsPopulated(page);
- const panel = await dashboardPage.getPanelByTitle(panelTitle);
- await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' });
- await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"');
- });
-});
diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts
index 2d471db1898..008d3ec5baf 100644
--- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts
+++ b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts
@@ -37,3 +37,35 @@ test('should not display any extensions when extension point is not declared in
const container = await page.getByTestId(testIds.appC.section1);
await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible();
});
+
+const panelTitle = 'Link with defaults';
+const extensionTitle = 'Open from time series...';
+
+const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946';
+const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719';
+
+test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({
+ gotoDashboardPage,
+ page,
+}) => {
+ const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid });
+ const panel = await dashboardPage.getPanelByTitle(panelTitle);
+ await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' });
+ await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible();
+});
+
+test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => {
+ const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid });
+ const panel = await dashboardPage.getPanelByTitle(panelTitle);
+ await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' });
+ await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"');
+});
+
+test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => {
+ const panelTitle = 'Link with new name';
+ const extensionTitle = 'Open from piechart';
+ const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid });
+ const panel = await dashboardPage.getPanelByTitle(panelTitle);
+ await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' });
+ await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"');
+});
diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts
index a3af39ded7c..631b4374ab6 100644
--- a/packages/grafana-data/src/index.ts
+++ b/packages/grafana-data/src/index.ts
@@ -552,10 +552,7 @@ export {
type PluginExtensionComponent,
type PluginExtensionComponentMeta,
type ComponentTypeWithExtensionMeta,
- type PluginExtensionConfig,
type PluginExtensionFunction,
- type PluginExtensionLinkConfig,
- type PluginExtensionComponentConfig,
type PluginExtensionEventHelpers,
type PluginExtensionPanelContext,
type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context,
diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts
index e9e792dfc7e..a3171ef76dd 100644
--- a/packages/grafana-data/src/types/app.ts
+++ b/packages/grafana-data/src/types/app.ts
@@ -6,8 +6,6 @@ import { KeyValue } from './data';
import { NavModel } from './navModel';
import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin';
import {
- type PluginExtensionLinkConfig,
- PluginExtensionComponentConfig,
PluginExtensionExposedComponentConfig,
PluginExtensionAddedComponentConfig,
PluginExtensionAddedLinkConfig,
@@ -142,26 +140,6 @@ export class AppPlugin extends GrafanaPlugin(extension: Omit, 'type'>) {
- this.addLink({
- targets: [extension.extensionPointId],
- ...extension,
- });
-
- return this;
- }
- /** @deprecated Use .addComponent() instead */
- configureExtensionComponent(extension: Omit, 'type'>) {
- this.addComponent({
- targets: [extension.extensionPointId],
- ...extension,
- component: extension.component,
- });
-
- return this;
- }
}
/**
diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts
index 8f9887a705a..ad5ef90be71 100644
--- a/packages/grafana-data/src/types/pluginExtensions.ts
+++ b/packages/grafana-data/src/types/pluginExtensions.ts
@@ -152,8 +152,6 @@ export type PluginExtensionExposedComponentConfig = PluginExtensionC
component: React.ComponentType;
};
-export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
-
export type PluginExtensionOpenModalOptions = {
// The title of the modal
title: string;
@@ -236,61 +234,3 @@ type Dashboard = {
title: string;
tags: string[];
};
-
-// deprecated types
-
-/** @deprecated - use PluginExtensionAddedLinkConfig instead */
-export type PluginExtensionLinkConfig = {
- type: PluginExtensionTypes.link;
- title: string;
- description: string;
-
- // A URL path that will be used as the href for the rendered link extension
- // (It is optional, because in some cases the action will be handled by the `onClick` handler instead of navigating to a new page)
- path?: string;
-
- // A function that will be called when the link is clicked
- // (It is called with the original event object)
- onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers) => void;
-
- /**
- * The unique identifier of the Extension Point
- * (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
- */
- extensionPointId: string;
-
- // (Optional) A function that can be used to configure the extension dynamically based on the extension point's context
- configure?: (context?: Readonly) =>
- | Partial<{
- title: string;
- description: string;
- path: string;
- onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers) => void;
- icon: IconName;
- category: string;
- }>
- | undefined;
-
- // (Optional) A icon that can be displayed in the ui for the extension option.
- icon?: IconName;
-
- // (Optional) A category to be used when grouping the options in the ui
- category?: string;
-};
-
-/** @deprecated - use PluginAddedLinkConfig instead */
-export type PluginExtensionComponentConfig = {
- type: PluginExtensionTypes.component;
- title: string;
- description: string;
-
- // The React component that will be rendered as the extension
- // (This component receives contextual information as props when it is rendered. You can just return `null` from the component to hide it.)
- component: React.ComponentType;
-
- /**
- * The unique identifier of the Extension Point
- * (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
- */
- extensionPointId: string;
-};
diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts
index c8bf24d6cb5..f1bf983ba1b 100644
--- a/packages/grafana-runtime/src/services/index.ts
+++ b/packages/grafana-runtime/src/services/index.ts
@@ -9,24 +9,6 @@ export * from './appEvents';
export * from './SidecarService_EXPERIMENTAL';
export * from './SidecarContext_EXPERIMENTAL';
-export {
- setPluginExtensionGetter,
- getPluginExtensions,
- getPluginLinkExtensions,
- getPluginComponentExtensions,
- type GetPluginExtensions,
- type GetPluginExtensionsOptions,
- type GetPluginExtensionsResult,
- type UsePluginExtensions,
- type UsePluginExtensionsResult,
-} from './pluginExtensions/getPluginExtensions';
-export {
- setPluginExtensionsHook,
- usePluginExtensions,
- usePluginLinkExtensions,
- usePluginComponentExtensions,
-} from './pluginExtensions/usePluginExtensions';
-
export {
setPluginComponentHook,
usePluginComponent,
diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.test.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.test.ts
deleted file mode 100644
index cb1c9e35e03..00000000000
--- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.test.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { setPluginExtensionGetter, type GetPluginExtensions, getPluginExtensions } from './getPluginExtensions';
-
-describe('Plugin Extensions / Get Plugin Extensions', () => {
- afterEach(() => {
- process.env.NODE_ENV = 'test';
- });
-
- test('should always return the same extension-getter function that was previously set', () => {
- const getter: GetPluginExtensions = jest.fn().mockReturnValue({ extensions: [] });
-
- setPluginExtensionGetter(getter);
- getPluginExtensions({ extensionPointId: 'panel-menu' });
-
- expect(getter).toHaveBeenCalledTimes(1);
- expect(getter).toHaveBeenCalledWith({ extensionPointId: 'panel-menu' });
- });
-
- test('should throw an error when trying to redefine the app-wide extension-getter function', () => {
- // By default, NODE_ENV is set to 'test' in jest.config.js, which allows to override the registry in tests.
- process.env.NODE_ENV = 'production';
-
- const getter: GetPluginExtensions = () => ({ extensions: [] });
-
- expect(() => {
- setPluginExtensionGetter(getter);
- setPluginExtensionGetter(getter);
- }).toThrowError();
- });
-
- test('should throw an error when trying to access the extension-getter function before it was set', () => {
- // "Unsetting" the registry
- // @ts-ignore
- setPluginExtensionGetter(undefined);
-
- expect(() => {
- getPluginExtensions({ extensionPointId: 'panel-menu' });
- }).toThrowError();
- });
-});
diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts
deleted file mode 100644
index edc5a48db6b..00000000000
--- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } from '@grafana/data';
-
-import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
-
-export type GetPluginExtensions = (
- options: GetPluginExtensionsOptions
-) => GetPluginExtensionsResult;
-
-export type UsePluginExtensions = (
- options: GetPluginExtensionsOptions
-) => UsePluginExtensionsResult;
-
-export type GetPluginExtensionsOptions = {
- extensionPointId: string;
- // Make sure this object is properly memoized and not mutated.
- context?: object | Record;
- limitPerPlugin?: number;
-};
-
-export type GetPluginExtensionsResult = {
- extensions: T[];
-};
-
-export type UsePluginExtensionsResult = {
- extensions: T[];
- isLoading: boolean;
-};
-
-let singleton: GetPluginExtensions | undefined;
-
-export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
- // We allow overriding the registry in tests
- if (singleton && process.env.NODE_ENV !== 'test') {
- throw new Error('setPluginExtensionGetter() function should only be called once, when Grafana is starting.');
- }
- singleton = instance;
-}
-
-function getPluginExtensionGetter(): GetPluginExtensions {
- if (!singleton) {
- throw new Error('getPluginExtensionGetter() can only be used after the Grafana instance has started.');
- }
- return singleton;
-}
-
-export const getPluginExtensions: GetPluginExtensions = (options) => getPluginExtensionGetter()(options);
-
-export const getPluginLinkExtensions: GetPluginExtensions = (options) => {
- const { extensions } = getPluginExtensions(options);
-
- return {
- extensions: extensions.filter(isPluginExtensionLink),
- };
-};
-
-// This getter doesn't support the `context` option (contextual information can be passed in as component props)
-export const getPluginComponentExtensions = (options: {
- extensionPointId: string;
- limitPerPlugin?: number;
-}): { extensions: Array> } => {
- const { extensions } = getPluginExtensions(options);
- const componentExtensions = extensions.filter(isPluginExtensionComponent) as Array>;
-
- return {
- extensions: componentExtensions,
- };
-};
diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.test.tsx b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.test.tsx
deleted file mode 100644
index d309e6f67af..00000000000
--- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.test.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import { renderHook } from '@testing-library/react';
-
-import { PluginExtension, PluginExtensionTypes } from '@grafana/data';
-
-import { UsePluginExtensions } from './getPluginExtensions';
-import {
- setPluginExtensionsHook,
- usePluginComponentExtensions,
- usePluginExtensions,
- usePluginLinkExtensions,
-} from './usePluginExtensions';
-
-describe('Plugin Extensions / usePluginExtensions', () => {
- afterEach(() => {
- process.env.NODE_ENV = 'test';
- });
-
- test('should always return the same extension-hook function that was previously set', () => {
- const hook: UsePluginExtensions = jest.fn().mockReturnValue({ extensions: [], isLoading: false });
-
- setPluginExtensionsHook(hook);
- usePluginExtensions({ extensionPointId: 'panel-menu' });
-
- expect(hook).toHaveBeenCalledTimes(1);
- expect(hook).toHaveBeenCalledWith({ extensionPointId: 'panel-menu' });
- });
-
- test('should throw an error when trying to redefine the app-wide extension-hook function', () => {
- // By default, NODE_ENV is set to 'test' in jest.config.js, which allows to override the registry in tests.
- process.env.NODE_ENV = 'production';
-
- const hook: UsePluginExtensions = () => ({ extensions: [], isLoading: false });
-
- expect(() => {
- setPluginExtensionsHook(hook);
- setPluginExtensionsHook(hook);
- }).toThrow();
- });
-
- test('should throw an error when trying to access the extension-hook function before it was set', () => {
- // "Unsetting" the registry
- // @ts-ignore
- setPluginExtensionsHook(undefined);
-
- expect(() => {
- usePluginExtensions({ extensionPointId: 'panel-menu' });
- }).toThrow();
- });
-
- describe('usePluginExtensionLinks()', () => {
- test('should return only links extensions', () => {
- const usePluginExtensionsMock: UsePluginExtensions = () => ({
- extensions: [
- {
- id: '1',
- pluginId: '',
- title: '',
- description: '',
- type: PluginExtensionTypes.component,
- component: () => undefined,
- },
- {
- id: '2',
- pluginId: '',
- title: '',
- description: '',
- path: '',
- type: PluginExtensionTypes.link,
- },
- {
- id: '3',
- pluginId: '',
- title: '',
- description: '',
- path: '',
- type: PluginExtensionTypes.link,
- },
- ],
- isLoading: false,
- });
-
- setPluginExtensionsHook(usePluginExtensionsMock);
-
- const { result } = renderHook(() => usePluginLinkExtensions({ extensionPointId: 'panel-menu' }));
- const { extensions } = result.current;
-
- expect(extensions).toHaveLength(2);
- expect(extensions[0].type).toBe('link');
- expect(extensions[1].type).toBe('link');
- expect(extensions.find(({ id }) => id === '2')).toBeDefined();
- expect(extensions.find(({ id }) => id === '3')).toBeDefined();
- });
-
- test('should return the same object if the extensions do not change', () => {
- const extensionPointId = 'foo';
- const extensions: PluginExtension[] = [
- {
- id: '1',
- pluginId: '',
- title: '',
- description: '',
- path: '',
- type: PluginExtensionTypes.link,
- },
- ];
-
- // Mimicing that the extensions do not change between renders
- const usePluginExtensionsMock: UsePluginExtensions = () => ({
- extensions,
- isLoading: false,
- });
-
- setPluginExtensionsHook(usePluginExtensionsMock);
-
- const { result, rerender } = renderHook(() => usePluginLinkExtensions({ extensionPointId }));
- const firstExtensions = result.current.extensions;
-
- rerender();
-
- const secondExtensions = result.current.extensions;
-
- expect(firstExtensions === secondExtensions).toBe(true);
- });
-
- test('should return a different object if the extensions do change', () => {
- const extensionPointId = 'foo';
-
- // Mimicing that the extensions is a new array object every time
- const usePluginExtensionsMock: UsePluginExtensions = () => ({
- extensions: [
- {
- id: '1',
- pluginId: '',
- title: '',
- description: '',
- path: '',
- type: PluginExtensionTypes.link,
- },
- ],
- isLoading: false,
- });
-
- setPluginExtensionsHook(usePluginExtensionsMock);
-
- const { result, rerender } = renderHook(() => usePluginLinkExtensions({ extensionPointId }));
- const firstExtensions = result.current.extensions;
-
- rerender();
-
- const secondExtensions = result.current.extensions;
-
- // The results differ
- expect(firstExtensions === secondExtensions).toBe(false);
- });
- });
-
- describe('usePluginExtensionComponents()', () => {
- test('should return only component extensions', () => {
- const hook: UsePluginExtensions = () => ({
- extensions: [
- {
- id: '1',
- pluginId: '',
- title: '',
- description: '',
- type: PluginExtensionTypes.component,
- component: () => undefined,
- },
- {
- id: '2',
- pluginId: '',
- title: '',
- description: '',
- path: '',
- type: PluginExtensionTypes.link,
- },
- {
- id: '3',
- pluginId: '',
- title: '',
- description: '',
- path: '',
- type: PluginExtensionTypes.link,
- },
- ],
- isLoading: false,
- });
-
- setPluginExtensionsHook(hook);
-
- const hookRender = renderHook(() => usePluginComponentExtensions({ extensionPointId: 'panel-menu' }));
- const { extensions } = hookRender.result.current;
-
- expect(extensions).toHaveLength(1);
- expect(extensions[0].type).toBe('component');
- expect(extensions.find(({ id }) => id === '1')).toBeDefined();
- });
-
- test('should return the same object if the extensions do not change', () => {
- const extensionPointId = 'foo';
- const extensions: PluginExtension[] = [
- {
- id: '1',
- pluginId: '',
- title: '',
- description: '',
- type: PluginExtensionTypes.component,
- component: () => undefined,
- },
- ];
-
- // Mimicing that the extensions do not change between renders
- const usePluginExtensionsMock: UsePluginExtensions = () => ({
- extensions,
- isLoading: false,
- });
-
- setPluginExtensionsHook(usePluginExtensionsMock);
-
- const { result, rerender } = renderHook(() => usePluginComponentExtensions({ extensionPointId }));
- const firstExtensions = result.current.extensions;
-
- rerender();
-
- const secondExtensions = result.current.extensions;
-
- // The results are the same
- expect(firstExtensions === secondExtensions).toBe(true);
- });
-
- test('should return a different object if the extensions do change', () => {
- const extensionPointId = 'foo';
-
- // Mimicing that the extensions is a new array object every time
- const usePluginExtensionsMock: UsePluginExtensions = () => ({
- extensions: [
- {
- id: '1',
- pluginId: '',
- title: '',
- description: '',
- type: PluginExtensionTypes.component,
- component: () => undefined,
- },
- ],
- isLoading: false,
- });
-
- setPluginExtensionsHook(usePluginExtensionsMock);
-
- const { result, rerender } = renderHook(() => usePluginComponentExtensions({ extensionPointId }));
- const firstExtensions = result.current.extensions;
-
- rerender();
-
- const secondExtensions = result.current.extensions;
-
- // The results differ
- expect(firstExtensions === secondExtensions).toBe(false);
- });
- });
-});
diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts
deleted file mode 100644
index f195707efc4..00000000000
--- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useMemo } from 'react';
-
-import { PluginExtensionComponent, PluginExtensionLink } from '@grafana/data';
-
-import { GetPluginExtensionsOptions, UsePluginExtensions, UsePluginExtensionsResult } from './getPluginExtensions';
-import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
-
-let singleton: UsePluginExtensions | undefined;
-
-export function setPluginExtensionsHook(hook: UsePluginExtensions): void {
- // We allow overriding the registry in tests
- if (singleton && process.env.NODE_ENV !== 'test') {
- throw new Error('setPluginExtensionsHook() function should only be called once, when Grafana is starting.');
- }
- singleton = hook;
-}
-
-/**
- * @deprecated Use either usePluginLinks() or usePluginComponents() instead.
- */
-export function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult {
- if (!singleton) {
- throw new Error('usePluginExtensions(options) can only be used after the Grafana instance has started.');
- }
- return singleton(options);
-}
-
-/**
- * @deprecated Use usePluginLinks() instead.
- */
-export function usePluginLinkExtensions(
- options: GetPluginExtensionsOptions
-): UsePluginExtensionsResult {
- const { extensions, isLoading } = usePluginExtensions(options);
-
- return useMemo(() => {
- return {
- extensions: extensions.filter(isPluginExtensionLink),
- isLoading,
- };
- }, [extensions, isLoading]);
-}
-
-/**
- * @deprecated Use usePluginComponents() instead.
- */
-export function usePluginComponentExtensions(
- options: GetPluginExtensionsOptions
-): { extensions: Array>; isLoading: boolean } {
- const { extensions, isLoading } = usePluginExtensions(options);
-
- return useMemo(
- () => ({
- extensions: extensions.filter(isPluginExtensionComponent) as Array>,
- isLoading,
- }),
- [extensions, isLoading]
- );
-}
diff --git a/public/app/app.ts b/public/app/app.ts
index 9ba23db15e7..c21cfabd4c8 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -28,11 +28,9 @@ import {
setQueryRunnerFactory,
setRunRequest,
setPluginImportUtils,
- setPluginExtensionGetter,
setEmbeddedDashboard,
setAppEvents,
setReturnToPreviousHook,
- setPluginExtensionsHook,
setPluginComponentHook,
setPluginComponentsHook,
setCurrentUser,
@@ -83,14 +81,11 @@ import { PanelDataErrorView } from './features/panel/components/PanelDataErrorVi
import { PanelRenderer } from './features/panel/components/PanelRenderer';
import { DatasourceSrv } from './features/plugins/datasource_srv';
import {
- createPluginExtensionsGetter,
getObservablePluginComponents,
getObservablePluginLinks,
} from './features/plugins/extensions/getPluginExtensions';
-import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
-import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
@@ -226,8 +221,6 @@ export class GrafanaApp {
await preloadPlugins(appPluginsToAwait);
}
- setPluginExtensionGetter(createPluginExtensionsGetter(pluginExtensionRegistries));
- setPluginExtensionsHook(createUsePluginExtensions(pluginExtensionRegistries));
setPluginLinksHook(usePluginLinks);
setPluginComponentHook(usePluginComponent);
setPluginComponentsHook(usePluginComponents);
diff --git a/public/app/features/alerting/unified/testSetup/plugins.ts b/public/app/features/alerting/unified/testSetup/plugins.ts
index 72a94187ef0..68c3b81944a 100644
--- a/public/app/features/alerting/unified/testSetup/plugins.ts
+++ b/public/app/features/alerting/unified/testSetup/plugins.ts
@@ -1,12 +1,12 @@
import { PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
-import { AppPluginConfig, setPluginComponentsHook, setPluginExtensionsHook } from '@grafana/runtime';
+import { AppPluginConfig, setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
import { mockPluginLinkExtension } from '../mocks';
export function setupPluginsExtensionsHook() {
- setPluginExtensionsHook(() => ({
- extensions: plugins.map((plugin) =>
+ setPluginLinksHook(() => ({
+ links: plugins.map((plugin) =>
mockPluginLinkExtension({
pluginId: plugin.id,
title: plugin.name,
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx
index 3ca8b9c3e05..0597b9e076b 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx
@@ -8,13 +8,7 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { PanelProps, systemDateFormats, SystemDateFormatsState } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { selectors } from '@grafana/e2e-selectors';
-import {
- LocationServiceProvider,
- config,
- getPluginLinkExtensions,
- locationService,
- setPluginImportUtils,
-} from '@grafana/runtime';
+import { LocationServiceProvider, config, locationService, setPluginImportUtils } from '@grafana/runtime';
import { VizPanel } from '@grafana/scenes';
import { Dashboard } from '@grafana/schema';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
@@ -32,7 +26,6 @@ import { getDashboardScenePageStateManager } from './DashboardScenePageStateMana
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
setPluginExtensionGetter: jest.fn(),
- getPluginLinkExtensions: jest.fn(),
useChromeHeaderHeight: jest.fn().mockReturnValue(80),
getBackendSrv: () => {
return {
@@ -55,7 +48,11 @@ jest.mock('react-router-dom-v5-compat', () => ({
useParams: jest.fn().mockReturnValue({ uid: 'my-dash-uid' }),
}));
-const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
+const getPluginExtensionsMock = jest.fn().mockReturnValue({ extensions: [] });
+jest.mock('app/features/plugins/extensions/getPluginExtensions', () => ({
+ ...jest.requireActual('app/features/plugins/extensions/getPluginExtensions'),
+ createPluginExtensionsGetter: () => getPluginExtensionsMock,
+}));
function setup({ routeProps }: { routeProps?: Partial } = {}) {
const context = getGrafanaContextMock();
@@ -156,8 +153,8 @@ describe('DashboardScenePage', () => {
// hacky way because mocking autosizer does not work
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
- getPluginLinkExtensionsMock.mockRestore();
- getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
+ getPluginExtensionsMock.mockRestore();
+ getPluginExtensionsMock.mockReturnValue({ extensions: [] });
store.delete(DASHBOARD_FROM_LS_KEY);
});
diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx
index b5c05c830b8..7d1371e2cdd 100644
--- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx
+++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx
@@ -6,7 +6,7 @@ import { render } from 'test/test-utils';
import { getDefaultTimeRange, LoadingState, PanelData, PanelProps } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
-import { config, getPluginLinkExtensions, setPluginImportUtils, setRunRequest } from '@grafana/runtime';
+import { config, setPluginImportUtils, setRunRequest } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { DashboardRoutes } from 'app/types/dashboard';
@@ -18,8 +18,6 @@ import { PublicDashboardScenePage, Props as PublicDashboardSceneProps } from './
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
- setPluginExtensionGetter: jest.fn(),
- getPluginLinkExtensions: jest.fn(),
getDataSourceSrv: () => {
return {
get: jest.fn().mockResolvedValue({}),
@@ -28,8 +26,6 @@ jest.mock('@grafana/runtime', () => ({
},
}));
-const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
-
function setup(token = 'an-access-token') {
const pubdashProps: PublicDashboardSceneProps = {
...getRouteComponentProps({
@@ -126,8 +122,6 @@ describe('PublicDashboardScenePage', () => {
// // hacky way because mocking autosizer does not work
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
- getPluginLinkExtensionsMock.mockRestore();
- getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
});
it('can render public dashboard', async () => {
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx
index ba6f6f48461..c76896c4afd 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx
@@ -18,7 +18,7 @@ import {
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { selectors } from '@grafana/e2e-selectors';
-import { config, locationService, setPluginExtensionsHook } from '@grafana/runtime';
+import { config, locationService } from '@grafana/runtime';
import { PANEL_EDIT_LAST_USED_DATASOURCE } from 'app/features/dashboard/utils/dashboard';
import { InspectTab } from 'app/features/inspector/types';
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
@@ -56,11 +56,6 @@ async function createModelMock() {
return queriesTab;
}
-setPluginExtensionsHook(() => ({
- extensions: [],
- isLoading: false,
-}));
-
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
const result: PanelData = {
state: LoadingState.Loading,
diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx
index 182ebac330f..5f0e1ae0e18 100644
--- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx
+++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx
@@ -9,7 +9,7 @@ import {
urlUtil,
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
-import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime';
+import { config, locationService } from '@grafana/runtime';
import {
LocalValueVariable,
SceneQueryRunner,
@@ -47,24 +47,17 @@ jest.mock('app/core/utils/explore', () => ({
jest.mock('app/core/services/context_srv');
-jest.mock('@grafana/runtime', () => ({
- ...jest.requireActual('@grafana/runtime'),
- setPluginExtensionGetter: jest.fn(),
- getPluginLinkExtensions: jest.fn(),
-}));
-
jest.mock('app/store/store', () => ({
dispatch: jest.fn(),
}));
-const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
+const getPluginExtensionsMock = jest.fn().mockReturnValue({ extensions: [] });
+jest.mock('app/features/plugins/extensions/getPluginExtensions', () => ({
+ ...jest.requireActual('app/features/plugins/extensions/getPluginExtensions'),
+ createPluginExtensionsGetter: () => getPluginExtensionsMock,
+}));
describe('panelMenuBehavior', () => {
- beforeEach(() => {
- getPluginLinkExtensionsMock.mockRestore();
- getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
- });
-
beforeAll(() => {
locationService.push('/d/dash-1?from=now-5m&to=now');
});
@@ -133,7 +126,7 @@ describe('panelMenuBehavior', () => {
describe('when extending panel menu from plugins', () => {
it('should contain menu item from link extension', async () => {
- getPluginLinkExtensionsMock.mockReturnValue({
+ getPluginExtensionsMock.mockReturnValue({
extensions: [
{
id: '1',
@@ -172,7 +165,7 @@ describe('panelMenuBehavior', () => {
});
it('should truncate menu item title to 25 chars', async () => {
- getPluginLinkExtensionsMock.mockReturnValue({
+ getPluginExtensionsMock.mockReturnValue({
extensions: [
{
id: '1',
@@ -213,7 +206,7 @@ describe('panelMenuBehavior', () => {
it('should pass onClick from plugin extension link to menu item', async () => {
const expectedOnClick = jest.fn();
- getPluginLinkExtensionsMock.mockReturnValue({
+ getPluginExtensionsMock.mockReturnValue({
extensions: [
{
id: '1',
@@ -299,7 +292,7 @@ describe('panelMenuBehavior', () => {
data,
};
- expect(getPluginLinkExtensionsMock).toBeCalledWith(expect.objectContaining({ context }));
+ expect(getPluginExtensionsMock).toBeCalledWith(expect.objectContaining({ context }));
});
it('should pass context with default time zone values when configuring extension', async () => {
@@ -356,11 +349,11 @@ describe('panelMenuBehavior', () => {
data,
};
- expect(getPluginLinkExtensionsMock).toBeCalledWith(expect.objectContaining({ context }));
+ expect(getPluginExtensionsMock).toBeCalledWith(expect.objectContaining({ context }));
});
it('should contain menu item with category', async () => {
- getPluginLinkExtensionsMock.mockReturnValue({
+ getPluginExtensionsMock.mockReturnValue({
extensions: [
{
id: '1',
@@ -405,7 +398,7 @@ describe('panelMenuBehavior', () => {
});
it('should truncate category to 25 chars', async () => {
- getPluginLinkExtensionsMock.mockReturnValue({
+ getPluginExtensionsMock.mockReturnValue({
extensions: [
{
id: '1',
@@ -450,7 +443,7 @@ describe('panelMenuBehavior', () => {
});
it('should contain menu item with category and append items without category after divider', async () => {
- getPluginLinkExtensionsMock.mockReturnValue({
+ getPluginExtensionsMock.mockReturnValue({
extensions: [
{
id: '1',
diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx
index 1f8a9547c83..82909f80268 100644
--- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx
+++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx
@@ -6,9 +6,10 @@ import {
PanelPlugin,
PluginExtensionPanelContext,
PluginExtensionPoints,
+ PluginExtensionTypes,
urlUtil,
} from '@grafana/data';
-import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime';
+import { config, locationService } from '@grafana/runtime';
import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes';
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
import appEvents from 'app/core/app_events';
@@ -22,6 +23,9 @@ import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { InspectTab } from 'app/features/inspector/types';
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
+import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/getPluginExtensions';
+import { pluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup';
+import { GetPluginExtensions } from 'app/features/plugins/extensions/types';
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
import { dispatch } from 'app/store/store';
@@ -39,6 +43,18 @@ import { DashboardScene } from './DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal';
+let getPluginExtensions: GetPluginExtensions;
+
+function setupGetPluginExtensions() {
+ if (getPluginExtensions) {
+ return getPluginExtensions;
+ }
+
+ getPluginExtensions = createPluginExtensionsGetter(pluginExtensionRegistries);
+
+ return getPluginExtensions;
+}
+
/**
* Behavior is called when VizPanelMenu is activated (ie when it's opened).
*/
@@ -286,20 +302,22 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
items.push(getInspectMenuItem(plugin, panel, dashboard));
- // TODO: make sure that this works reliably with the reactive extension registry
- // (we need to be able to know in advance what extensions should be loaded for this extension point, and make it possible to await for them.)
- const { extensions } = getPluginLinkExtensions({
+ setupGetPluginExtensions();
+
+ const { extensions } = getPluginExtensions({
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
context: createExtensionContext(panel, dashboard),
limitPerPlugin: 3,
});
+ const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link);
+
if (extensions.length > 0 && !dashboard.state.isEditing) {
items.push({
text: 'Extensions',
iconClassName: 'plug',
type: 'submenu',
- subMenu: createExtensionSubMenu(extensions),
+ subMenu: createExtensionSubMenu(linkExtensions),
});
}
diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
index f1ed038dfb5..9b6ec578234 100644
--- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
@@ -14,7 +14,7 @@ import {
VariableSupportType,
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
-import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
+import { setPluginImportUtils } from '@grafana/runtime';
import { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes';
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@@ -154,8 +154,6 @@ jest.mock('@grafana/data', () => ({
setWeekStart: jest.fn(),
}));
-const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
-
jest.mock('@grafana/scenes', () => ({
...jest.requireActual('@grafana/scenes'),
sceneUtils: {
@@ -165,11 +163,6 @@ jest.mock('@grafana/scenes', () => ({
}));
describe('transformSceneToSaveModel', () => {
- beforeEach(() => {
- getPluginLinkExtensionsMock.mockRestore();
- getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
- });
-
describe('Given a simple scene with custom settings', () => {
it('Should transform back to persisted model', () => {
const dashboardWithCustomSettings = {
diff --git a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx b/public/app/features/plugins/extensions/usePluginExtensions.test.tsx
deleted file mode 100644
index 479e5cd4c6c..00000000000
--- a/public/app/features/plugins/extensions/usePluginExtensions.test.tsx
+++ /dev/null
@@ -1,273 +0,0 @@
-import { act, renderHook } from '@testing-library/react';
-
-import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
-import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
-import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
-import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
-import { PluginExtensionRegistries } from './registry/types';
-import { useLoadAppPlugins } from './useLoadAppPlugins';
-import { createUsePluginExtensions } from './usePluginExtensions';
-
-jest.mock('./useLoadAppPlugins');
-
-describe('usePluginExtensions()', () => {
- let registries: PluginExtensionRegistries;
- const pluginId = 'myorg-extensions-app';
- const extensionPointId = `${pluginId}/extension-point/v1`;
-
- beforeEach(() => {
- registries = {
- addedComponentsRegistry: new AddedComponentsRegistry(),
- addedLinksRegistry: new AddedLinksRegistry(),
- exposedComponentsRegistry: new ExposedComponentsRegistry(),
- addedFunctionsRegistry: new AddedFunctionsRegistry(),
- };
- jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
- });
-
- it('should return an empty array if there are no extensions registered for the extension point', () => {
- const usePluginExtensions = createUsePluginExtensions(registries);
- const { result } = renderHook(() =>
- usePluginExtensions({
- extensionPointId: 'foo/bar/v1',
- })
- );
-
- expect(result.current.extensions).toEqual([]);
- });
-
- it('should return the plugin link extensions from the registry', () => {
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- title: '1',
- description: '1',
- path: `/a/${pluginId}/2`,
- },
- {
- targets: extensionPointId,
- title: '2',
- description: '2',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
-
- const usePluginExtensions = createUsePluginExtensions(registries);
- const { result } = renderHook(() => usePluginExtensions({ extensionPointId }));
-
- expect(result.current.extensions.length).toBe(2);
- expect(result.current.extensions[0].title).toBe('1');
- expect(result.current.extensions[1].title).toBe('2');
- });
-
- it('should return the plugin component extensions from the registry', () => {
- const componentExtensionPointId = `${pluginId}/component/v1`;
-
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- title: '1',
- description: '1',
- path: `/a/${pluginId}/2`,
- },
- {
- targets: extensionPointId,
- title: '2',
- description: '2',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
-
- registries.addedComponentsRegistry.register({
- pluginId,
- configs: [
- {
- targets: componentExtensionPointId,
- title: 'Component 1',
- description: '1',
- component: () => Hello World1
,
- },
- {
- targets: componentExtensionPointId,
- title: 'Component 2',
- description: '2',
- component: () => Hello World2
,
- },
- ],
- });
-
- const usePluginExtensions = createUsePluginExtensions(registries);
- const { result } = renderHook(() => usePluginExtensions({ extensionPointId: componentExtensionPointId }));
-
- expect(result.current.extensions.length).toBe(2);
- expect(result.current.extensions[0].title).toBe('Component 1');
- expect(result.current.extensions[1].title).toBe('Component 2');
- });
-
- it('should dynamically update the extensions registered for a certain extension point', () => {
- const usePluginExtensions = createUsePluginExtensions(registries);
- let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId }));
-
- // No extensions yet
- expect(result.current.extensions.length).toBe(0);
-
- // Add extensions to the registry
- act(() => {
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- title: '1',
- description: '1',
- path: `/a/${pluginId}/2`,
- },
- {
- targets: extensionPointId,
- title: '2',
- description: '2',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
- });
-
- // Check if the hook returns the new extensions
- rerender();
-
- expect(result.current.extensions.length).toBe(2);
- expect(result.current.extensions[0].title).toBe('1');
- expect(result.current.extensions[1].title).toBe('2');
- });
-
- it('should only render the hook once', () => {
- const addedComponentsRegistrySpy = jest.spyOn(registries.addedComponentsRegistry, 'asObservable');
- const addedLinksRegistrySpy = jest.spyOn(registries.addedLinksRegistry, 'asObservable');
- const usePluginExtensions = createUsePluginExtensions(registries);
-
- renderHook(() => usePluginExtensions({ extensionPointId }));
- expect(addedComponentsRegistrySpy).toHaveBeenCalledTimes(1);
- expect(addedLinksRegistrySpy).toHaveBeenCalledTimes(1);
- });
-
- it('should return the same extensions object if the context object is the same', async () => {
- const usePluginExtensions = createUsePluginExtensions(registries);
-
- // Add extensions to the registry
- act(() => {
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- title: '1',
- description: '1',
- path: `/a/${pluginId}/2`,
- },
- {
- targets: extensionPointId,
- title: '2',
- description: '2',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
- });
-
- // Check if it returns the same extensions object in case nothing changes
- const context = {};
- const { rerender, result } = renderHook(usePluginExtensions, {
- initialProps: {
- extensionPointId,
- context,
- },
- });
- const firstResult = result.current;
-
- rerender({ context, extensionPointId });
- const secondResult = result.current;
-
- expect(firstResult.extensions).toBe(secondResult.extensions);
- });
-
- it('should return a new extensions object if the context object is different', () => {
- const usePluginExtensions = createUsePluginExtensions(registries);
-
- // Add extensions to the registry
- act(() => {
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- title: '1',
- description: '1',
- path: `/a/${pluginId}/2`,
- },
- {
- targets: extensionPointId,
- title: '2',
- description: '2',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
- });
-
- // Check if it returns a different extensions object in case the context object changes
- const firstResults = renderHook(() => usePluginExtensions({ extensionPointId, context: {} }));
- const secondResults = renderHook(() => usePluginExtensions({ extensionPointId, context: {} }));
- expect(firstResults.result.current.extensions === secondResults.result.current.extensions).toBe(false);
- });
-
- it('should return a new extensions object if the registry changes but the context object is the same', () => {
- const context = {};
- const usePluginExtensions = createUsePluginExtensions(registries);
-
- // Add the first extension
- act(() => {
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- title: '1',
- description: '1',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
- });
-
- const { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId, context }));
- const firstExtensions = result.current.extensions;
-
- // Add the second extension
- act(() => {
- registries.addedLinksRegistry.register({
- pluginId,
- configs: [
- {
- targets: extensionPointId,
- // extensionPointId: 'plugins/foo/bar/zed', // A different extension point (to be sure that it's also returning a new object when the actual extension point doesn't change)
- title: '2',
- description: '2',
- path: `/a/${pluginId}/2`,
- },
- ],
- });
- });
-
- rerender();
-
- const secondExtensions = result.current.extensions;
-
- expect(firstExtensions === secondExtensions).toBe(false);
- });
-});
diff --git a/public/app/features/plugins/extensions/usePluginExtensions.tsx b/public/app/features/plugins/extensions/usePluginExtensions.tsx
deleted file mode 100644
index 2dfc57e289e..00000000000
--- a/public/app/features/plugins/extensions/usePluginExtensions.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { useMemo } from 'react';
-import { useObservable } from 'react-use';
-
-import { PluginExtension, usePluginContext } from '@grafana/data';
-import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/runtime';
-
-import * as errors from './errors';
-import { getPluginExtensions } from './getPluginExtensions';
-import { log } from './logs/log';
-import { PluginExtensionRegistries } from './registry/types';
-import { useLoadAppPlugins } from './useLoadAppPlugins';
-import { getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
-import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
-
-export function createUsePluginExtensions(registries: PluginExtensionRegistries) {
- const observableAddedComponentsRegistry = registries.addedComponentsRegistry.asObservable();
- const observableAddedLinksRegistry = registries.addedLinksRegistry.asObservable();
-
- return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult {
- const pluginContext = usePluginContext();
- const addedComponentsRegistry = useObservable(observableAddedComponentsRegistry);
- const addedLinksRegistry = useObservable(observableAddedLinksRegistry);
- const { extensionPointId, context, limitPerPlugin } = options;
- const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
-
- return useMemo(() => {
- // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
- const enableRestrictions = isGrafanaDevMode() && pluginContext !== null;
- const pluginId = pluginContext?.meta.id ?? '';
- const pointLog = log.child({
- pluginId,
- extensionPointId,
- });
-
- if (!addedLinksRegistry && !addedComponentsRegistry) {
- return { extensions: [], isLoading: false };
- }
-
- if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) {
- pointLog.error(errors.INVALID_EXTENSION_POINT_ID);
- return {
- isLoading: false,
- extensions: [],
- };
- }
-
- if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) {
- pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING);
- return {
- isLoading: false,
- extensions: [],
- };
- }
-
- if (isLoadingAppPlugins) {
- return {
- isLoading: true,
- extensions: [],
- };
- }
-
- const { extensions } = getPluginExtensions({
- extensionPointId,
- context,
- limitPerPlugin,
- addedComponentsRegistry,
- addedLinksRegistry,
- });
-
- return { extensions, isLoading: false };
-
- // Doing the deps like this instead of just `option` because users probably aren't going to memoize the
- // options object so we are checking it's simple value attributes.
- // The context though still has to be memoized though and not mutated.
- // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: refactor `getPluginExtensions` to accept service dependencies as arguments instead of relying on the sidecar singleton under the hood
- }, [
- addedLinksRegistry,
- addedComponentsRegistry,
- extensionPointId,
- context,
- limitPerPlugin,
- pluginContext,
- isLoadingAppPlugins,
- ]);
- };
-}
diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx
index ff452777688..4ba153fe94d 100644
--- a/public/app/features/plugins/extensions/utils.tsx
+++ b/public/app/features/plugins/extensions/utils.tsx
@@ -4,10 +4,7 @@ import * as React from 'react';
import { useAsync } from 'react-use';
import {
- type PluginExtensionLinkConfig,
- type PluginExtensionConfig,
type PluginExtensionEventHelpers,
- PluginExtensionTypes,
type PluginExtensionOpenModalOptions,
isDateTime,
dateTime,
@@ -29,12 +26,6 @@ import { ExtensionsLog, log } from './logs/log';
import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry';
import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators';
-export function isPluginExtensionLinkConfig(
- extension: PluginExtensionConfig | undefined
-): extension is PluginExtensionLinkConfig {
- return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.link;
-}
-
export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
return (...args: unknown[]) => {
try {
diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx
index f083ff738ea..8e3350494d5 100644
--- a/public/app/features/plugins/extensions/validators.test.tsx
+++ b/public/app/features/plugins/extensions/validators.test.tsx
@@ -3,7 +3,6 @@ import { memo } from 'react';
import {
PluginContextType,
PluginExtensionAddedLinkConfig,
- PluginExtensionLinkConfig,
PluginExtensionPoints,
PluginLoadingStrategy,
PluginType,
@@ -92,16 +91,13 @@ describe('Plugin Extension Validators', () => {
it('should throw an error if the configure() function is defined but is not a function', () => {
expect(() => {
- assertConfigureIsValid(
- // @ts-ignore
- {
- title: 'Title',
- description: 'Description',
- extensionPointId: 'grafana/some-page/extension-point-a',
- handler: () => {},
- configure: '() => {}',
- } as PluginExtensionLinkConfig
- );
+ assertConfigureIsValid({
+ title: 'Title',
+ description: 'Description',
+ extensionPointId: 'grafana/some-page/extension-point-a',
+ handler: () => {},
+ configure: '() => {}',
+ } as unknown as PluginExtensionAddedLinkConfig); // We are casting to unknown to test it with a unvalid argument
}).toThrowError();
});
});
diff --git a/public/app/features/plugins/sandbox/sandbox_components.tsx b/public/app/features/plugins/sandbox/sandbox_components.tsx
index e1eeb590d35..7e11e8428bc 100644
--- a/public/app/features/plugins/sandbox/sandbox_components.tsx
+++ b/public/app/features/plugins/sandbox/sandbox_components.tsx
@@ -2,7 +2,7 @@ import { isFunction } from 'lodash';
import { ComponentType, FC } from 'react';
import * as React from 'react';
-import { GrafanaPlugin, PluginExtensionConfig, PluginType } from '@grafana/data';
+import { GrafanaPlugin, PluginType } from '@grafana/data';
import { SandboxPluginMeta, SandboxedPluginObject } from './types';
import { isSandboxedPluginObject } from './utils';
@@ -58,17 +58,6 @@ export async function sandboxPluginComponents(
Reflect.set(pluginObject, 'root', withSandboxWrapper(Reflect.get(pluginObject, 'root'), meta));
}
- // extension components
- if (Reflect.has(pluginObject, 'extensionConfigs')) {
- const extensions: PluginExtensionConfig[] = Reflect.get(pluginObject, 'extensionConfigs');
- for (const extension of extensions) {
- if (Reflect.has(extension, 'component')) {
- Reflect.set(extension, 'component', withSandboxWrapper(Reflect.get(extension, 'component'), meta));
- }
- }
- Reflect.set(pluginObject, 'extensionConfigs', extensions);
- }
-
// config pages
if (Reflect.has(pluginObject, 'configPages')) {
const configPages: NonNullable = Reflect.get(pluginObject, 'configPages') ?? [];
diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx
index 0e5526e4d4f..6d1adc6a2ef 100644
--- a/public/app/features/sandbox/TestStuffPage.tsx
+++ b/public/app/features/sandbox/TestStuffPage.tsx
@@ -1,5 +1,5 @@
import { NavModelItem } from '@grafana/data';
-import { getPluginExtensions, isPluginExtensionLink } from '@grafana/runtime';
+import { usePluginLinks } from '@grafana/runtime';
import { Button, LinkButton, Stack, Text } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useAppNotification } from 'app/core/copy/appNotification';
@@ -46,21 +46,18 @@ export const TestStuffPage = () => {
};
function LinkToBasicApp({ extensionPointId }: { extensionPointId: string }) {
- const { extensions } = getPluginExtensions({ extensionPointId });
+ const { links } = usePluginLinks({ extensionPointId });
- if (extensions.length === 0) {
+ if (links.length === 0) {
return null;
}
return (
- {extensions.map((extension, i) => {
- if (!isPluginExtensionLink(extension)) {
- return null;
- }
+ {links.map((link, i) => {
return (
-
- {extension.title}
+
+ {link.title}
);
})}
diff --git a/public/app/features/trails/Menu/PanelMenu.tsx b/public/app/features/trails/Menu/PanelMenu.tsx
index 2d100a8b36b..5cfe5ed7a8b 100644
--- a/public/app/features/trails/Menu/PanelMenu.tsx
+++ b/public/app/features/trails/Menu/PanelMenu.tsx
@@ -1,5 +1,5 @@
import { DataFrame, PanelMenuItem } from '@grafana/data';
-import { getPluginLinkExtensions } from '@grafana/runtime';
+import { isPluginExtensionLink } from '@grafana/runtime';
import {
SceneComponentProps,
sceneGraph,
@@ -11,6 +11,9 @@ import {
} from '@grafana/scenes';
import { getExploreUrl } from 'app/core/utils/explore';
import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils';
+import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/getPluginExtensions';
+import { pluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup';
+import { type GetPluginExtensions } from 'app/features/plugins/extensions/types';
import { AddToExplorationButton, extensionPointId } from '../MetricSelect/AddToExplorationsButton';
import { getDataSource, getTrailFor } from '../utils';
@@ -28,6 +31,18 @@ interface PanelMenuState extends SceneObjectState {
explorationsButton?: AddToExplorationButton;
}
+let getPluginExtensions: GetPluginExtensions;
+
+function setupGetPluginExtensions() {
+ if (getPluginExtensions) {
+ return getPluginExtensions;
+ }
+
+ getPluginExtensions = createPluginExtensionsGetter(pluginExtensionRegistries);
+
+ return getPluginExtensions;
+}
+
/**
* @todo the VizPanelMenu interface is overly restrictive, doesn't allow any member functions on this class, so everything is currently inlined
*/
@@ -93,6 +108,8 @@ export class PanelMenu extends SceneObjectBase implements VizPan
this.state.explorationsButton?.activate();
}
});
+
+ setupGetPluginExtensions();
}
addItem(item: PanelMenuItem): void {
@@ -119,12 +136,12 @@ export class PanelMenu extends SceneObjectBase implements VizPan
}
const getInvestigationLink = (addToExplorations: AddToExplorationButton) => {
- const links = getPluginLinkExtensions({
- extensionPointId: extensionPointId,
+ const links = getPluginExtensions({
+ extensionPointId,
context: addToExplorations.state.context,
- });
+ }).extensions.filter((ext) => isPluginExtensionLink(ext));
- return links.extensions[0];
+ return links[0];
};
const onAddToInvestigationClick = (event: React.MouseEvent, addToExplorations: AddToExplorationButton) => {
diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx
index a3030b619ff..5e7ed1e300d 100644
--- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx
+++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx
@@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoreApp, PluginType } from '@grafana/data';
-import { setPluginExtensionsHook } from '@grafana/runtime';
import { PyroscopeDataSource } from '../datasource';
import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test';
@@ -12,7 +11,6 @@ import { Props, QueryEditor } from './QueryEditor';
describe('QueryEditor', () => {
beforeEach(() => {
- setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions
mockFetchPyroscopeDatasourceSettings();
});
diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts
index 9d1c9246dba..8eb122b9258 100644
--- a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts
+++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.test.ts
@@ -7,7 +7,7 @@ import {
DataSourceJsonData,
makeTimeRange,
} from '@grafana/data';
-import { setPluginExtensionsHook, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime';
+import { getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime';
import { defaultPyroscopeQueryType } from './dataquery.gen';
import { normalizeQuery, PyroscopeDataSource } from './datasource';
@@ -35,7 +35,6 @@ export function mockFetchPyroscopeDatasourceSettings(
function setupDatasource() {
mockFetchPyroscopeDatasourceSettings();
- setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions
const templateSrv = {
replace: (query: string): string => {
return query.replace(/\$var/g, 'interpolated');