Plugin Extensions: Clean up the deprecated APIs (#102102)
* PanelMenuBehaviour: stop using the deprecated `getPluginLinkExtensions()` API * Wip * grafana-runtime: remove deprecated APIs `usePluginExtensions()`, `usePluginLinkExtensions()` and `usePluginComponentExtensions()` * Wip * Wip * wip * wip * Chore: removed PluginExtensionLinkConfig * Chore: removed PluginExtensionComponentConfig * Chore: fixed grafana-pyroscope-datasource QueryEditor test * Chore: fixed PublicDashboardScenePage.test.tsx * Chore: fix PanelDataQueriesTab test * Chore: fix PanelMenuBehavior test * Chore: fix transformSceneToSaveModel test * Chore: fix last type errors * Chore: fix alerting/unified/testSetup/plugins.ts * Chore: break out types to separate file * feat(Extensions): expose an observable API for added links and components * chore: prettier fixes * Revert "chore: prettier fixes" This reverts commit53aa767664. * Revert "feat(Extensions): expose an observable API for added links and components" This reverts commitbdc588250e. --------- Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
This commit is contained in:
co-authored by
Hugo Häggmark
parent
c1cadc7d6f
commit
39dcff23f9
@@ -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.", "10"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
[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": [
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts:5381": [
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[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": [
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,20 +3,18 @@ import { Route, Routes } from 'react-router-dom';
|
|||||||
import { AppRootProps } from '@grafana/data';
|
import { AppRootProps } from '@grafana/data';
|
||||||
|
|
||||||
import { ROUTES } from '../../constants';
|
import { ROUTES } from '../../constants';
|
||||||
import { AddedComponents, AddedLinks, ExposedComponents, LegacyGetters, LegacyHooks } from '../../pages';
|
import { AddedComponents, AddedLinks, ExposedComponents } from '../../pages';
|
||||||
import { testIds } from '../../testIds';
|
import { testIds } from '../../testIds';
|
||||||
|
|
||||||
export function App(props: AppRootProps) {
|
export function App(props: AppRootProps) {
|
||||||
return (
|
return (
|
||||||
<div data-testid={testIds.container} style={{ marginTop: '5%' }}>
|
<div data-testid={testIds.container} style={{ marginTop: '5%' }}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={ROUTES.LegacyGetters} element={<LegacyGetters />} />
|
|
||||||
<Route path={ROUTES.LegacyHooks} element={<LegacyHooks />} />
|
|
||||||
<Route path={ROUTES.ExposedComponents} element={<ExposedComponents />} />
|
<Route path={ROUTES.ExposedComponents} element={<ExposedComponents />} />
|
||||||
<Route path={ROUTES.AddedComponents} element={<AddedComponents />} />
|
<Route path={ROUTES.AddedComponents} element={<AddedComponents />} />
|
||||||
<Route path={ROUTES.AddedLinks} element={<AddedLinks />} />
|
<Route path={ROUTES.AddedLinks} element={<AddedLinks />} />
|
||||||
|
|
||||||
<Route path={'*'} element={<LegacyGetters />} />
|
<Route path={'*'} element={<ExposedComponents />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import pluginJson from './plugin.json';
|
|||||||
|
|
||||||
export const plugin = new AppPlugin<{}>()
|
export const plugin = new AppPlugin<{}>()
|
||||||
.setRootPage(App)
|
.setRootPage(App)
|
||||||
.configureExtensionLink<PluginExtensionPanelContext>({
|
.addLink<PluginExtensionPanelContext>({
|
||||||
title: 'Open from time series or pie charts (path)',
|
title: 'Open from time series or pie charts (path)',
|
||||||
description: 'This link will only be visible on time series and pie charts',
|
description: 'This link will only be visible on time series and pie charts',
|
||||||
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
|
targets: PluginExtensionPoints.DashboardPanelMenu,
|
||||||
path: `/a/${pluginJson.id}/`,
|
path: `/a/${pluginJson.id}/`,
|
||||||
configure: (context) => {
|
configure: (context) => {
|
||||||
// Will only be visible for the Link Extensions dashboard
|
// Will only be visible for the Link Extensions dashboard
|
||||||
@@ -31,10 +31,10 @@ export const plugin = new AppPlugin<{}>()
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.configureExtensionLink<PluginExtensionPanelContext>({
|
.addLink<PluginExtensionPanelContext>({
|
||||||
title: 'Open from time series or pie charts (onClick)',
|
title: 'Open from time series or pie charts (onClick)',
|
||||||
description: 'This link will only be visible on time series and pie charts',
|
description: 'This link will only be visible on time series and pie charts',
|
||||||
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
|
targets: PluginExtensionPoints.DashboardPanelMenu,
|
||||||
onClick: (_, { openModal, context }) => {
|
onClick: (_, { openModal, context }) => {
|
||||||
const targets = context?.targets ?? [];
|
const targets = context?.targets ?? [];
|
||||||
const title = context?.title;
|
const title = context?.title;
|
||||||
|
|||||||
@@ -42,6 +42,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@grafana/runtime": "*"
|
"@grafana/runtime": "*"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@4.4.0"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ReusableComponentProps>({
|
|
||||||
extensionPointId: extensionPointId2,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PluginPage>
|
|
||||||
<Stack direction={'column'} gap={4} data-testid={testIds.legacyGettersPage.container}>
|
|
||||||
<section data-testid={testIds.legacyGettersPage.section1}>
|
|
||||||
<h3>
|
|
||||||
Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using
|
|
||||||
getPluginExtensions
|
|
||||||
</h3>
|
|
||||||
<ActionButton extensions={extensions} />
|
|
||||||
</section>
|
|
||||||
<section data-testid={testIds.legacyGettersPage.section2}>
|
|
||||||
<h3>Link extensions defined with configureExtensionLink and retrived using getPluginLinkExtensions</h3>
|
|
||||||
<ActionButton extensions={linkExtensions} />
|
|
||||||
</section>
|
|
||||||
<section data-testid={testIds.legacyGettersPage.section3}>
|
|
||||||
<h3>
|
|
||||||
Component extensions defined with configureExtensionComponent and retrived using
|
|
||||||
getPluginComponentExtensions
|
|
||||||
</h3>
|
|
||||||
{componentExtensions.map((extension) => {
|
|
||||||
const Component = extension.component;
|
|
||||||
return <Component key={extension.id} name="World" />;
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
</Stack>
|
|
||||||
</PluginPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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<ReusableComponentProps>({
|
|
||||||
extensionPointId: extensionPointId2,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PluginPage>
|
|
||||||
<Stack direction={'column'} gap={4} data-testid={testIds.legacyHooksPage.container}>
|
|
||||||
<section data-testid={testIds.legacyHooksPage.section1}>
|
|
||||||
<h3>
|
|
||||||
Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using
|
|
||||||
usePluginExtensions
|
|
||||||
</h3>
|
|
||||||
<ActionButton extensions={extensions} />
|
|
||||||
</section>
|
|
||||||
<section data-testid={testIds.legacyHooksPage.section2}>
|
|
||||||
<h3>Link extensions defined with configureExtensionLink and retrived using usePluginLinkExtensions</h3>
|
|
||||||
<ActionButton extensions={linkExtensions} />
|
|
||||||
</section>
|
|
||||||
<section data-testid={testIds.legacyHooksPage.section3}>
|
|
||||||
<h3>
|
|
||||||
Component extensions defined with configureExtensionComponent and retrived using
|
|
||||||
usePluginComponentExtensions
|
|
||||||
</h3>
|
|
||||||
{componentExtensions.map((extension) => {
|
|
||||||
const Component = extension.component;
|
|
||||||
return <Component key={extension.id} name="World" />;
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
</Stack>
|
|
||||||
</PluginPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
export { ExposedComponents } from './ExposedComponents';
|
export { ExposedComponents } from './ExposedComponents';
|
||||||
export { LegacyGetters } from './LegacyGetters';
|
|
||||||
export { LegacyHooks } from './LegacyHooks';
|
|
||||||
export { AddedComponents } from './AddedComponents';
|
export { AddedComponents } from './AddedComponents';
|
||||||
export { AddedLinks } from './AddedLinks';
|
export { AddedLinks } from './AddedLinks';
|
||||||
|
|||||||
@@ -19,22 +19,6 @@
|
|||||||
"updated": "%TODAY%"
|
"updated": "%TODAY%"
|
||||||
},
|
},
|
||||||
"includes": [
|
"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",
|
"type": "page",
|
||||||
"name": "Exposed components",
|
"name": "Exposed components",
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export class App extends React.PureComponent<AppRootProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div data-testid={testIds.appA.container} className="page-container">
|
<div data-testid={testIds.appA.container} className="page-container">
|
||||||
Hello Grafana!
|
Hello Grafana!!!!!
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
-6
@@ -7,12 +7,6 @@ import { App } from './components/App';
|
|||||||
|
|
||||||
export const plugin = new AppPlugin<{}>()
|
export const plugin = new AppPlugin<{}>()
|
||||||
.setRootPage(App)
|
.setRootPage(App)
|
||||||
.configureExtensionLink({
|
|
||||||
title: 'Go to A',
|
|
||||||
description: 'Navigating to pluging A',
|
|
||||||
extensionPointId: 'plugins/grafana-extensionstest-app/actions',
|
|
||||||
path: '/a/grafana-extensionexample1-app/',
|
|
||||||
})
|
|
||||||
.exposeComponent({
|
.exposeComponent({
|
||||||
id: 'grafana-extensionexample1-app/reusable-component/v1',
|
id: 'grafana-extensionexample1-app/reusable-component/v1',
|
||||||
title: 'Exposed component',
|
title: 'Exposed component',
|
||||||
|
|||||||
-17
@@ -7,23 +7,6 @@ import { App } from './components/App';
|
|||||||
|
|
||||||
export const plugin = new AppPlugin<{}>()
|
export const plugin = new AppPlugin<{}>()
|
||||||
.setRootPage(App)
|
.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: () => <div data-testid={testIds.appB.modal}>From plugin B</div>,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.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 }) => <div data-testid={testIds.appB.reusableComponent}>Hello {name}!</div>,
|
|
||||||
})
|
|
||||||
.addComponent<{ name: string }>({
|
.addComponent<{ name: string }>({
|
||||||
targets: 'plugins/grafana-extensionstest-app/addComponent/v1',
|
targets: 'plugins/grafana-extensionstest-app/addComponent/v1',
|
||||||
title: 'Added component from B',
|
title: 'Added component from B',
|
||||||
|
|||||||
+1
-7
@@ -1,4 +1,4 @@
|
|||||||
import { usePluginExtensions, usePluginLinks } from '@grafana/runtime';
|
import { usePluginLinks } from '@grafana/runtime';
|
||||||
import { Stack } from '@grafana/ui';
|
import { Stack } from '@grafana/ui';
|
||||||
import { testIds } from '../../../../testIds';
|
import { testIds } from '../../../../testIds';
|
||||||
import { ActionButton } from '../../../../components/ActionButton';
|
import { ActionButton } from '../../../../components/ActionButton';
|
||||||
@@ -7,18 +7,12 @@ export const LINKS_EXTENSION_POINT_ID = 'plugins/grafana-extensionstest-app/use-
|
|||||||
|
|
||||||
export function AddedLinks() {
|
export function AddedLinks() {
|
||||||
const { links } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
const { links } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
||||||
const { extensions } = usePluginExtensions({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction={'column'} gap={4}>
|
<Stack direction={'column'} gap={4}>
|
||||||
<section data-testid={testIds.appC.section1}>
|
<section data-testid={testIds.appC.section1}>
|
||||||
<h3>Link extensions defined with addLink and retrieved using usePluginLinks</h3>
|
<h3>Link extensions defined with addLink and retrieved using usePluginLinks</h3>
|
||||||
<ActionButton extensions={links} />
|
<ActionButton extensions={links} />
|
||||||
</section>
|
</section>
|
||||||
<section data-testid={testIds.appC.section2}>
|
|
||||||
<h3>Link extensions defined with addLink and retrieved using usePluginExtensions</h3>
|
|
||||||
<ActionButton extensions={extensions} />
|
|
||||||
</section>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
-17
@@ -6,23 +6,6 @@ import { App } from '../../components/App';
|
|||||||
|
|
||||||
export const plugin = new AppPlugin<{}>()
|
export const plugin = new AppPlugin<{}>()
|
||||||
.setRootPage(App)
|
.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: () => <div data-testid={testIds.appB.modal}>From plugin B</div>,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.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 }) => <div data-testid={testIds.appB.reusableComponent}>Hello {name}!</div>,
|
|
||||||
})
|
|
||||||
.addComponent<{ name: string }>({
|
.addComponent<{ name: string }>({
|
||||||
targets: ['plugins/grafana-extensionstest-app/addComponent/v1'],
|
targets: ['plugins/grafana-extensionstest-app/addComponent/v1'],
|
||||||
title: 'Added component (where meta data is missing)',
|
title: 'Added component (where meta data is missing)',
|
||||||
|
|||||||
-52
@@ -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!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-67
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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"');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
const container = await page.getByTestId(testIds.appC.section1);
|
||||||
await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible();
|
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"');
|
||||||
|
});
|
||||||
|
|||||||
@@ -552,10 +552,7 @@ export {
|
|||||||
type PluginExtensionComponent,
|
type PluginExtensionComponent,
|
||||||
type PluginExtensionComponentMeta,
|
type PluginExtensionComponentMeta,
|
||||||
type ComponentTypeWithExtensionMeta,
|
type ComponentTypeWithExtensionMeta,
|
||||||
type PluginExtensionConfig,
|
|
||||||
type PluginExtensionFunction,
|
type PluginExtensionFunction,
|
||||||
type PluginExtensionLinkConfig,
|
|
||||||
type PluginExtensionComponentConfig,
|
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
type PluginExtensionPanelContext,
|
type PluginExtensionPanelContext,
|
||||||
type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context,
|
type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context,
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { KeyValue } from './data';
|
|||||||
import { NavModel } from './navModel';
|
import { NavModel } from './navModel';
|
||||||
import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin';
|
import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin';
|
||||||
import {
|
import {
|
||||||
type PluginExtensionLinkConfig,
|
|
||||||
PluginExtensionComponentConfig,
|
|
||||||
PluginExtensionExposedComponentConfig,
|
PluginExtensionExposedComponentConfig,
|
||||||
PluginExtensionAddedComponentConfig,
|
PluginExtensionAddedComponentConfig,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
@@ -142,26 +140,6 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use .addLink() instead */
|
|
||||||
configureExtensionLink<Context extends object>(extension: Omit<PluginExtensionLinkConfig<Context>, 'type'>) {
|
|
||||||
this.addLink({
|
|
||||||
targets: [extension.extensionPointId],
|
|
||||||
...extension,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/** @deprecated Use .addComponent() instead */
|
|
||||||
configureExtensionComponent<Props = {}>(extension: Omit<PluginExtensionComponentConfig<Props>, 'type'>) {
|
|
||||||
this.addComponent({
|
|
||||||
targets: [extension.extensionPointId],
|
|
||||||
...extension,
|
|
||||||
component: extension.component,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -152,8 +152,6 @@ export type PluginExtensionExposedComponentConfig<Props = {}> = PluginExtensionC
|
|||||||
component: React.ComponentType<Props>;
|
component: React.ComponentType<Props>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
|
|
||||||
|
|
||||||
export type PluginExtensionOpenModalOptions = {
|
export type PluginExtensionOpenModalOptions = {
|
||||||
// The title of the modal
|
// The title of the modal
|
||||||
title: string;
|
title: string;
|
||||||
@@ -236,61 +234,3 @@ type Dashboard = {
|
|||||||
title: string;
|
title: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// deprecated types
|
|
||||||
|
|
||||||
/** @deprecated - use PluginExtensionAddedLinkConfig instead */
|
|
||||||
export type PluginExtensionLinkConfig<Context extends object = object> = {
|
|
||||||
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<Context>) => 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<Context>) =>
|
|
||||||
| Partial<{
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
path: string;
|
|
||||||
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => 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<Props = {}> = {
|
|
||||||
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<Props>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unique identifier of the Extension Point
|
|
||||||
* (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum)
|
|
||||||
*/
|
|
||||||
extensionPointId: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -9,24 +9,6 @@ export * from './appEvents';
|
|||||||
export * from './SidecarService_EXPERIMENTAL';
|
export * from './SidecarService_EXPERIMENTAL';
|
||||||
export * from './SidecarContext_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 {
|
export {
|
||||||
setPluginComponentHook,
|
setPluginComponentHook,
|
||||||
usePluginComponent,
|
usePluginComponent,
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } from '@grafana/data';
|
|
||||||
|
|
||||||
import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
|
|
||||||
|
|
||||||
export type GetPluginExtensions<T = PluginExtension> = (
|
|
||||||
options: GetPluginExtensionsOptions
|
|
||||||
) => GetPluginExtensionsResult<T>;
|
|
||||||
|
|
||||||
export type UsePluginExtensions<T = PluginExtension> = (
|
|
||||||
options: GetPluginExtensionsOptions
|
|
||||||
) => UsePluginExtensionsResult<T>;
|
|
||||||
|
|
||||||
export type GetPluginExtensionsOptions = {
|
|
||||||
extensionPointId: string;
|
|
||||||
// Make sure this object is properly memoized and not mutated.
|
|
||||||
context?: object | Record<string | symbol, unknown>;
|
|
||||||
limitPerPlugin?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetPluginExtensionsResult<T = PluginExtension> = {
|
|
||||||
extensions: T[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsePluginExtensionsResult<T = PluginExtension> = {
|
|
||||||
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<PluginExtensionLink> = (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 = <Props = {}>(options: {
|
|
||||||
extensionPointId: string;
|
|
||||||
limitPerPlugin?: number;
|
|
||||||
}): { extensions: Array<PluginExtensionComponent<Props>> } => {
|
|
||||||
const { extensions } = getPluginExtensions(options);
|
|
||||||
const componentExtensions = extensions.filter(isPluginExtensionComponent) as Array<PluginExtensionComponent<Props>>;
|
|
||||||
|
|
||||||
return {
|
|
||||||
extensions: componentExtensions,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<PluginExtensionLink> {
|
|
||||||
const { extensions, isLoading } = usePluginExtensions(options);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
return {
|
|
||||||
extensions: extensions.filter(isPluginExtensionLink),
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
}, [extensions, isLoading]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use usePluginComponents() instead.
|
|
||||||
*/
|
|
||||||
export function usePluginComponentExtensions<Props = {}>(
|
|
||||||
options: GetPluginExtensionsOptions
|
|
||||||
): { extensions: Array<PluginExtensionComponent<Props>>; isLoading: boolean } {
|
|
||||||
const { extensions, isLoading } = usePluginExtensions(options);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
extensions: extensions.filter(isPluginExtensionComponent) as Array<PluginExtensionComponent<Props>>,
|
|
||||||
isLoading,
|
|
||||||
}),
|
|
||||||
[extensions, isLoading]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -28,11 +28,9 @@ import {
|
|||||||
setQueryRunnerFactory,
|
setQueryRunnerFactory,
|
||||||
setRunRequest,
|
setRunRequest,
|
||||||
setPluginImportUtils,
|
setPluginImportUtils,
|
||||||
setPluginExtensionGetter,
|
|
||||||
setEmbeddedDashboard,
|
setEmbeddedDashboard,
|
||||||
setAppEvents,
|
setAppEvents,
|
||||||
setReturnToPreviousHook,
|
setReturnToPreviousHook,
|
||||||
setPluginExtensionsHook,
|
|
||||||
setPluginComponentHook,
|
setPluginComponentHook,
|
||||||
setPluginComponentsHook,
|
setPluginComponentsHook,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
@@ -83,14 +81,11 @@ import { PanelDataErrorView } from './features/panel/components/PanelDataErrorVi
|
|||||||
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
||||||
import { DatasourceSrv } from './features/plugins/datasource_srv';
|
import { DatasourceSrv } from './features/plugins/datasource_srv';
|
||||||
import {
|
import {
|
||||||
createPluginExtensionsGetter,
|
|
||||||
getObservablePluginComponents,
|
getObservablePluginComponents,
|
||||||
getObservablePluginLinks,
|
getObservablePluginLinks,
|
||||||
} from './features/plugins/extensions/getPluginExtensions';
|
} from './features/plugins/extensions/getPluginExtensions';
|
||||||
import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
|
||||||
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
|
||||||
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
||||||
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
||||||
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
||||||
@@ -226,8 +221,6 @@ export class GrafanaApp {
|
|||||||
await preloadPlugins(appPluginsToAwait);
|
await preloadPlugins(appPluginsToAwait);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPluginExtensionGetter(createPluginExtensionsGetter(pluginExtensionRegistries));
|
|
||||||
setPluginExtensionsHook(createUsePluginExtensions(pluginExtensionRegistries));
|
|
||||||
setPluginLinksHook(usePluginLinks);
|
setPluginLinksHook(usePluginLinks);
|
||||||
setPluginComponentHook(usePluginComponent);
|
setPluginComponentHook(usePluginComponent);
|
||||||
setPluginComponentsHook(usePluginComponents);
|
setPluginComponentsHook(usePluginComponents);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
|
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 { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||||
|
|
||||||
import { mockPluginLinkExtension } from '../mocks';
|
import { mockPluginLinkExtension } from '../mocks';
|
||||||
|
|
||||||
export function setupPluginsExtensionsHook() {
|
export function setupPluginsExtensionsHook() {
|
||||||
setPluginExtensionsHook(() => ({
|
setPluginLinksHook(() => ({
|
||||||
extensions: plugins.map((plugin) =>
|
links: plugins.map((plugin) =>
|
||||||
mockPluginLinkExtension({
|
mockPluginLinkExtension({
|
||||||
pluginId: plugin.id,
|
pluginId: plugin.id,
|
||||||
title: plugin.name,
|
title: plugin.name,
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
|||||||
import { PanelProps, systemDateFormats, SystemDateFormatsState } from '@grafana/data';
|
import { PanelProps, systemDateFormats, SystemDateFormatsState } from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test';
|
import { getPanelPlugin } from '@grafana/data/test';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import {
|
import { LocationServiceProvider, config, locationService, setPluginImportUtils } from '@grafana/runtime';
|
||||||
LocationServiceProvider,
|
|
||||||
config,
|
|
||||||
getPluginLinkExtensions,
|
|
||||||
locationService,
|
|
||||||
setPluginImportUtils,
|
|
||||||
} from '@grafana/runtime';
|
|
||||||
import { VizPanel } from '@grafana/scenes';
|
import { VizPanel } from '@grafana/scenes';
|
||||||
import { Dashboard } from '@grafana/schema';
|
import { Dashboard } from '@grafana/schema';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
@@ -32,7 +26,6 @@ import { getDashboardScenePageStateManager } from './DashboardScenePageStateMana
|
|||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
setPluginExtensionGetter: jest.fn(),
|
setPluginExtensionGetter: jest.fn(),
|
||||||
getPluginLinkExtensions: jest.fn(),
|
|
||||||
useChromeHeaderHeight: jest.fn().mockReturnValue(80),
|
useChromeHeaderHeight: jest.fn().mockReturnValue(80),
|
||||||
getBackendSrv: () => {
|
getBackendSrv: () => {
|
||||||
return {
|
return {
|
||||||
@@ -55,7 +48,11 @@ jest.mock('react-router-dom-v5-compat', () => ({
|
|||||||
useParams: jest.fn().mockReturnValue({ uid: 'my-dash-uid' }),
|
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<GrafanaRouteComponentProps> } = {}) {
|
function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps> } = {}) {
|
||||||
const context = getGrafanaContextMock();
|
const context = getGrafanaContextMock();
|
||||||
@@ -156,8 +153,8 @@ describe('DashboardScenePage', () => {
|
|||||||
// hacky way because mocking autosizer does not work
|
// hacky way because mocking autosizer does not work
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
|
||||||
getPluginLinkExtensionsMock.mockRestore();
|
getPluginExtensionsMock.mockRestore();
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
getPluginExtensionsMock.mockReturnValue({ extensions: [] });
|
||||||
store.delete(DASHBOARD_FROM_LS_KEY);
|
store.delete(DASHBOARD_FROM_LS_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { render } from 'test/test-utils';
|
|||||||
import { getDefaultTimeRange, LoadingState, PanelData, PanelProps } from '@grafana/data';
|
import { getDefaultTimeRange, LoadingState, PanelData, PanelProps } from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test';
|
import { getPanelPlugin } from '@grafana/data/test';
|
||||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
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 { Dashboard } from '@grafana/schema';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
import { DashboardRoutes } from 'app/types/dashboard';
|
import { DashboardRoutes } from 'app/types/dashboard';
|
||||||
@@ -18,8 +18,6 @@ import { PublicDashboardScenePage, Props as PublicDashboardSceneProps } from './
|
|||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
setPluginExtensionGetter: jest.fn(),
|
|
||||||
getPluginLinkExtensions: jest.fn(),
|
|
||||||
getDataSourceSrv: () => {
|
getDataSourceSrv: () => {
|
||||||
return {
|
return {
|
||||||
get: jest.fn().mockResolvedValue({}),
|
get: jest.fn().mockResolvedValue({}),
|
||||||
@@ -28,8 +26,6 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
|
|
||||||
|
|
||||||
function setup(token = 'an-access-token') {
|
function setup(token = 'an-access-token') {
|
||||||
const pubdashProps: PublicDashboardSceneProps = {
|
const pubdashProps: PublicDashboardSceneProps = {
|
||||||
...getRouteComponentProps({
|
...getRouteComponentProps({
|
||||||
@@ -126,8 +122,6 @@ describe('PublicDashboardScenePage', () => {
|
|||||||
// // hacky way because mocking autosizer does not work
|
// // hacky way because mocking autosizer does not work
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
|
||||||
getPluginLinkExtensionsMock.mockRestore();
|
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can render public dashboard', async () => {
|
it('can render public dashboard', async () => {
|
||||||
|
|||||||
+1
-6
@@ -18,7 +18,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test';
|
import { getPanelPlugin } from '@grafana/data/test';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
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 { PANEL_EDIT_LAST_USED_DATASOURCE } from 'app/features/dashboard/utils/dashboard';
|
||||||
import { InspectTab } from 'app/features/inspector/types';
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
|
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
|
||||||
@@ -56,11 +56,6 @@ async function createModelMock() {
|
|||||||
return queriesTab;
|
return queriesTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPluginExtensionsHook(() => ({
|
|
||||||
extensions: [],
|
|
||||||
isLoading: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
const result: PanelData = {
|
const result: PanelData = {
|
||||||
state: LoadingState.Loading,
|
state: LoadingState.Loading,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
urlUtil,
|
urlUtil,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test';
|
import { getPanelPlugin } from '@grafana/data/test';
|
||||||
import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
LocalValueVariable,
|
LocalValueVariable,
|
||||||
SceneQueryRunner,
|
SceneQueryRunner,
|
||||||
@@ -47,24 +47,17 @@ jest.mock('app/core/utils/explore', () => ({
|
|||||||
|
|
||||||
jest.mock('app/core/services/context_srv');
|
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', () => ({
|
jest.mock('app/store/store', () => ({
|
||||||
dispatch: jest.fn(),
|
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', () => {
|
describe('panelMenuBehavior', () => {
|
||||||
beforeEach(() => {
|
|
||||||
getPluginLinkExtensionsMock.mockRestore();
|
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
locationService.push('/d/dash-1?from=now-5m&to=now');
|
locationService.push('/d/dash-1?from=now-5m&to=now');
|
||||||
});
|
});
|
||||||
@@ -133,7 +126,7 @@ describe('panelMenuBehavior', () => {
|
|||||||
|
|
||||||
describe('when extending panel menu from plugins', () => {
|
describe('when extending panel menu from plugins', () => {
|
||||||
it('should contain menu item from link extension', async () => {
|
it('should contain menu item from link extension', async () => {
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({
|
getPluginExtensionsMock.mockReturnValue({
|
||||||
extensions: [
|
extensions: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -172,7 +165,7 @@ describe('panelMenuBehavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should truncate menu item title to 25 chars', async () => {
|
it('should truncate menu item title to 25 chars', async () => {
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({
|
getPluginExtensionsMock.mockReturnValue({
|
||||||
extensions: [
|
extensions: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -213,7 +206,7 @@ describe('panelMenuBehavior', () => {
|
|||||||
it('should pass onClick from plugin extension link to menu item', async () => {
|
it('should pass onClick from plugin extension link to menu item', async () => {
|
||||||
const expectedOnClick = jest.fn();
|
const expectedOnClick = jest.fn();
|
||||||
|
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({
|
getPluginExtensionsMock.mockReturnValue({
|
||||||
extensions: [
|
extensions: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -299,7 +292,7 @@ describe('panelMenuBehavior', () => {
|
|||||||
data,
|
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 () => {
|
it('should pass context with default time zone values when configuring extension', async () => {
|
||||||
@@ -356,11 +349,11 @@ describe('panelMenuBehavior', () => {
|
|||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getPluginLinkExtensionsMock).toBeCalledWith(expect.objectContaining({ context }));
|
expect(getPluginExtensionsMock).toBeCalledWith(expect.objectContaining({ context }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain menu item with category', async () => {
|
it('should contain menu item with category', async () => {
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({
|
getPluginExtensionsMock.mockReturnValue({
|
||||||
extensions: [
|
extensions: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -405,7 +398,7 @@ describe('panelMenuBehavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should truncate category to 25 chars', async () => {
|
it('should truncate category to 25 chars', async () => {
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({
|
getPluginExtensionsMock.mockReturnValue({
|
||||||
extensions: [
|
extensions: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -450,7 +443,7 @@ describe('panelMenuBehavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should contain menu item with category and append items without category after divider', async () => {
|
it('should contain menu item with category and append items without category after divider', async () => {
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({
|
getPluginExtensionsMock.mockReturnValue({
|
||||||
extensions: [
|
extensions: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
PanelPlugin,
|
PanelPlugin,
|
||||||
PluginExtensionPanelContext,
|
PluginExtensionPanelContext,
|
||||||
PluginExtensionPoints,
|
PluginExtensionPoints,
|
||||||
|
PluginExtensionTypes,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
} from '@grafana/data';
|
} 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 { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||||
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
|
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
|
||||||
import appEvents from 'app/core/app_events';
|
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 { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
|
||||||
import { InspectTab } from 'app/features/inspector/types';
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
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 { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||||
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
|
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
@@ -39,6 +43,18 @@ import { DashboardScene } from './DashboardScene';
|
|||||||
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
|
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
|
||||||
import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal';
|
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).
|
* 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));
|
items.push(getInspectMenuItem(plugin, panel, dashboard));
|
||||||
|
|
||||||
// TODO: make sure that this works reliably with the reactive extension registry
|
setupGetPluginExtensions();
|
||||||
// (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({
|
const { extensions } = getPluginExtensions({
|
||||||
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
|
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
|
||||||
context: createExtensionContext(panel, dashboard),
|
context: createExtensionContext(panel, dashboard),
|
||||||
limitPerPlugin: 3,
|
limitPerPlugin: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link);
|
||||||
|
|
||||||
if (extensions.length > 0 && !dashboard.state.isEditing) {
|
if (extensions.length > 0 && !dashboard.state.isEditing) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Extensions',
|
text: 'Extensions',
|
||||||
iconClassName: 'plug',
|
iconClassName: 'plug',
|
||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
subMenu: createExtensionSubMenu(extensions),
|
subMenu: createExtensionSubMenu(linkExtensions),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
VariableSupportType,
|
VariableSupportType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test';
|
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 { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes';
|
||||||
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
@@ -154,8 +154,6 @@ jest.mock('@grafana/data', () => ({
|
|||||||
setWeekStart: jest.fn(),
|
setWeekStart: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
|
|
||||||
|
|
||||||
jest.mock('@grafana/scenes', () => ({
|
jest.mock('@grafana/scenes', () => ({
|
||||||
...jest.requireActual('@grafana/scenes'),
|
...jest.requireActual('@grafana/scenes'),
|
||||||
sceneUtils: {
|
sceneUtils: {
|
||||||
@@ -165,11 +163,6 @@ jest.mock('@grafana/scenes', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('transformSceneToSaveModel', () => {
|
describe('transformSceneToSaveModel', () => {
|
||||||
beforeEach(() => {
|
|
||||||
getPluginLinkExtensionsMock.mockRestore();
|
|
||||||
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Given a simple scene with custom settings', () => {
|
describe('Given a simple scene with custom settings', () => {
|
||||||
it('Should transform back to persisted model', () => {
|
it('Should transform back to persisted model', () => {
|
||||||
const dashboardWithCustomSettings = {
|
const dashboardWithCustomSettings = {
|
||||||
|
|||||||
@@ -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: () => <div>Hello World1</div>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
targets: componentExtensionPointId,
|
|
||||||
title: 'Component 2',
|
|
||||||
description: '2',
|
|
||||||
component: () => <div>Hello World2</div>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<PluginExtension> {
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,7 @@ import * as React from 'react';
|
|||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type PluginExtensionLinkConfig,
|
|
||||||
type PluginExtensionConfig,
|
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
PluginExtensionTypes,
|
|
||||||
type PluginExtensionOpenModalOptions,
|
type PluginExtensionOpenModalOptions,
|
||||||
isDateTime,
|
isDateTime,
|
||||||
dateTime,
|
dateTime,
|
||||||
@@ -29,12 +26,6 @@ import { ExtensionsLog, log } from './logs/log';
|
|||||||
import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry';
|
import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry';
|
||||||
import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators';
|
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 = '') {
|
export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
|
||||||
return (...args: unknown[]) => {
|
return (...args: unknown[]) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { memo } from 'react';
|
|||||||
import {
|
import {
|
||||||
PluginContextType,
|
PluginContextType,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
PluginExtensionLinkConfig,
|
|
||||||
PluginExtensionPoints,
|
PluginExtensionPoints,
|
||||||
PluginLoadingStrategy,
|
PluginLoadingStrategy,
|
||||||
PluginType,
|
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', () => {
|
it('should throw an error if the configure() function is defined but is not a function', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
assertConfigureIsValid(
|
assertConfigureIsValid({
|
||||||
// @ts-ignore
|
title: 'Title',
|
||||||
{
|
description: 'Description',
|
||||||
title: 'Title',
|
extensionPointId: 'grafana/some-page/extension-point-a',
|
||||||
description: 'Description',
|
handler: () => {},
|
||||||
extensionPointId: 'grafana/some-page/extension-point-a',
|
configure: '() => {}',
|
||||||
handler: () => {},
|
} as unknown as PluginExtensionAddedLinkConfig); // We are casting to unknown to test it with a unvalid argument
|
||||||
configure: '() => {}',
|
|
||||||
} as PluginExtensionLinkConfig
|
|
||||||
);
|
|
||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { isFunction } from 'lodash';
|
|||||||
import { ComponentType, FC } from 'react';
|
import { ComponentType, FC } from 'react';
|
||||||
import * as React 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 { SandboxPluginMeta, SandboxedPluginObject } from './types';
|
||||||
import { isSandboxedPluginObject } from './utils';
|
import { isSandboxedPluginObject } from './utils';
|
||||||
@@ -58,17 +58,6 @@ export async function sandboxPluginComponents(
|
|||||||
Reflect.set(pluginObject, 'root', withSandboxWrapper(Reflect.get(pluginObject, 'root'), meta));
|
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
|
// config pages
|
||||||
if (Reflect.has(pluginObject, 'configPages')) {
|
if (Reflect.has(pluginObject, 'configPages')) {
|
||||||
const configPages: NonNullable<GrafanaPlugin['configPages']> = Reflect.get(pluginObject, 'configPages') ?? [];
|
const configPages: NonNullable<GrafanaPlugin['configPages']> = Reflect.get(pluginObject, 'configPages') ?? [];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NavModelItem } from '@grafana/data';
|
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 { Button, LinkButton, Stack, Text } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
@@ -46,21 +46,18 @@ export const TestStuffPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function LinkToBasicApp({ extensionPointId }: { extensionPointId: string }) {
|
function LinkToBasicApp({ extensionPointId }: { extensionPointId: string }) {
|
||||||
const { extensions } = getPluginExtensions({ extensionPointId });
|
const { links } = usePluginLinks({ extensionPointId });
|
||||||
|
|
||||||
if (extensions.length === 0) {
|
if (links.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{extensions.map((extension, i) => {
|
{links.map((link, i) => {
|
||||||
if (!isPluginExtensionLink(extension)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<LinkButton href={extension.path} title={extension.description} key={extension.id}>
|
<LinkButton href={link.path} title={link.description} key={link.id}>
|
||||||
{extension.title}
|
{link.title}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DataFrame, PanelMenuItem } from '@grafana/data';
|
import { DataFrame, PanelMenuItem } from '@grafana/data';
|
||||||
import { getPluginLinkExtensions } from '@grafana/runtime';
|
import { isPluginExtensionLink } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
sceneGraph,
|
sceneGraph,
|
||||||
@@ -11,6 +11,9 @@ import {
|
|||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { getExploreUrl } from 'app/core/utils/explore';
|
import { getExploreUrl } from 'app/core/utils/explore';
|
||||||
import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils';
|
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 { AddToExplorationButton, extensionPointId } from '../MetricSelect/AddToExplorationsButton';
|
||||||
import { getDataSource, getTrailFor } from '../utils';
|
import { getDataSource, getTrailFor } from '../utils';
|
||||||
@@ -28,6 +31,18 @@ interface PanelMenuState extends SceneObjectState {
|
|||||||
explorationsButton?: AddToExplorationButton;
|
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
|
* @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<PanelMenuState> implements VizPan
|
|||||||
this.state.explorationsButton?.activate();
|
this.state.explorationsButton?.activate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupGetPluginExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
addItem(item: PanelMenuItem): void {
|
addItem(item: PanelMenuItem): void {
|
||||||
@@ -119,12 +136,12 @@ export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPan
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getInvestigationLink = (addToExplorations: AddToExplorationButton) => {
|
const getInvestigationLink = (addToExplorations: AddToExplorationButton) => {
|
||||||
const links = getPluginLinkExtensions({
|
const links = getPluginExtensions({
|
||||||
extensionPointId: extensionPointId,
|
extensionPointId,
|
||||||
context: addToExplorations.state.context,
|
context: addToExplorations.state.context,
|
||||||
});
|
}).extensions.filter((ext) => isPluginExtensionLink(ext));
|
||||||
|
|
||||||
return links.extensions[0];
|
return links[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddToInvestigationClick = (event: React.MouseEvent, addToExplorations: AddToExplorationButton) => {
|
const onAddToInvestigationClick = (event: React.MouseEvent, addToExplorations: AddToExplorationButton) => {
|
||||||
|
|||||||
-2
@@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { CoreApp, PluginType } from '@grafana/data';
|
import { CoreApp, PluginType } from '@grafana/data';
|
||||||
import { setPluginExtensionsHook } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { PyroscopeDataSource } from '../datasource';
|
import { PyroscopeDataSource } from '../datasource';
|
||||||
import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test';
|
import { mockFetchPyroscopeDatasourceSettings } from '../datasource.test';
|
||||||
@@ -12,7 +11,6 @@ import { Props, QueryEditor } from './QueryEditor';
|
|||||||
|
|
||||||
describe('QueryEditor', () => {
|
describe('QueryEditor', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions
|
|
||||||
mockFetchPyroscopeDatasourceSettings();
|
mockFetchPyroscopeDatasourceSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
makeTimeRange,
|
makeTimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { setPluginExtensionsHook, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime';
|
import { getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
import { defaultPyroscopeQueryType } from './dataquery.gen';
|
import { defaultPyroscopeQueryType } from './dataquery.gen';
|
||||||
import { normalizeQuery, PyroscopeDataSource } from './datasource';
|
import { normalizeQuery, PyroscopeDataSource } from './datasource';
|
||||||
@@ -35,7 +35,6 @@ export function mockFetchPyroscopeDatasourceSettings(
|
|||||||
|
|
||||||
function setupDatasource() {
|
function setupDatasource() {
|
||||||
mockFetchPyroscopeDatasourceSettings();
|
mockFetchPyroscopeDatasourceSettings();
|
||||||
setPluginExtensionsHook(() => ({ extensions: [], isLoading: false })); // No extensions
|
|
||||||
const templateSrv = {
|
const templateSrv = {
|
||||||
replace: (query: string): string => {
|
replace: (query: string): string => {
|
||||||
return query.replace(/\$var/g, 'interpolated');
|
return query.replace(/\$var/g, 'interpolated');
|
||||||
|
|||||||
Reference in New Issue
Block a user