mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugin Extensions: Require meta-data to be defined in plugin.json
during development mode (#93429)
* feat: add extensions to the backend plugin model * feat: update the frontend plugin types * feat(pluginContext): return a `null` if there is no context found This will be necessary to understand if a certain hook is running inside a plugin context or not. * feat: add utility functions for checking extension configs * tests: fix failing tests due to the type updates * feat(AddedComponentsRegistry): validate plugin meta-info * feat(AddedLinksRegistry): validate meta-info * feat(ExposedComponentsRegistry): validate meta-info * feat(usePluginComponent): add meta-info validation * feat(usePluginComponents): add meta-info validation * feat(usePluginLinks): add meta-info validation * fix: only validate meta-info in registries if dev mode is enabled * tests: add unit tests for the restrictions functionality * tests: fix Go tests * fix(tests): revert accidental changes * fix: run goimports * fix: api tests * add nested app so that meta data can bested e2e tested * refactor(types): extract the ExtensionInfo into a separate type * refactor(extensions/utils): use Array.prototype.some() instead of .find() * refactor(usePluginLinks): update warning message * feat(usePluginExtensions()): validate plugin meta-info * Wip * fix(e2e): E2E tests for extensions * fix(extensions): allow multiple "/" slashes in the extension point id * fix(extensions/validators): stop validating the plugin id pattern --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
This commit is contained in:
parent
7188c13d22
commit
6096f46774
@ -17,3 +17,7 @@ apps:
|
|||||||
org_id: 1
|
org_id: 1
|
||||||
org_name: Main Org.
|
org_name: Main Org.
|
||||||
disabled: false
|
disabled: false
|
||||||
|
- type: grafana-extensionexample3-app
|
||||||
|
org_id: 1
|
||||||
|
org_name: Main Org.
|
||||||
|
disabled: false
|
||||||
|
@ -9,7 +9,7 @@ type ReusableComponentProps = {
|
|||||||
|
|
||||||
export function AddedComponents() {
|
export function AddedComponents() {
|
||||||
const { components } = usePluginComponents<ReusableComponentProps>({
|
const { components } = usePluginComponents<ReusableComponentProps>({
|
||||||
extensionPointId: 'plugins/grafana-extensionexample2-app/addComponent/v1',
|
extensionPointId: 'plugins/grafana-extensionstest-app/addComponent/v1',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,7 +16,7 @@ type ReusableComponentProps = {
|
|||||||
|
|
||||||
export function LegacyGetters() {
|
export function LegacyGetters() {
|
||||||
const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
|
const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
|
||||||
const extensionPointId2 = 'plugins/grafana-extensionexample2-app/configure-extension-component/v1';
|
const extensionPointId2 = 'plugins/grafana-extensionstest-app/configure-extension-component/v1';
|
||||||
const context: AppExtensionContext = {};
|
const context: AppExtensionContext = {};
|
||||||
|
|
||||||
const { extensions } = getPluginExtensions({
|
const { extensions } = getPluginExtensions({
|
||||||
|
@ -16,7 +16,7 @@ type ReusableComponentProps = {
|
|||||||
|
|
||||||
export function LegacyHooks() {
|
export function LegacyHooks() {
|
||||||
const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
|
const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
|
||||||
const extensionPointId2 = 'plugins/grafana-extensionexample2-app/configure-extension-component/v1';
|
const extensionPointId2 = 'plugins/grafana-extensionstest-app/configure-extension-component/v1';
|
||||||
const context: AppExtensionContext = {};
|
const context: AppExtensionContext = {};
|
||||||
|
|
||||||
const { extensions } = usePluginExtensions({
|
const { extensions } = usePluginExtensions({
|
||||||
|
@ -60,8 +60,43 @@
|
|||||||
"defaultNav": false
|
"defaultNav": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"extensions": {
|
||||||
|
"addedLinks": [
|
||||||
|
{
|
||||||
|
"targets": ["grafana/dashboard/panel/menu"],
|
||||||
|
"title": "Open from time series or pie charts (path)",
|
||||||
|
"description": "This link will only be visible on time series and pie charts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": ["grafana/dashboard/panel/menu"],
|
||||||
|
"title": "Open from time series or pie charts (onClick)",
|
||||||
|
"description": "This link will only be visible on time series and pie charts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extensionPoints": [
|
||||||
|
{
|
||||||
|
"id": "plugins/grafana-extensionstest-app/use-plugin-links/v1",
|
||||||
|
"title": "Extension point - links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins/grafana-extensionstest-app/addComponent/v1",
|
||||||
|
"title": "Extension point - components"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins/grafana-extensionstest-app/actions",
|
||||||
|
"title": "Legacy extension point - usePluginExtensions() and usePluginLinkExtensions()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins/grafana-extensionstest-app/configure-extension-component/v1",
|
||||||
|
"title": "Legacy extension point - usePluginComponentExtensions()"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": ">=10.4.0",
|
"grafanaDependency": ">=10.4.0",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": ["grafana-extensionexample1-app/reusable-component/v1"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,30 @@
|
|||||||
"defaultNav": false
|
"defaultNav": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": [
|
||||||
|
{
|
||||||
|
"id": "grafana-extensionexample1-app/reusable-component/v1",
|
||||||
|
"title": "Exposed component",
|
||||||
|
"description": "A component that can be reused by other app plugins."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addedLinks": [
|
||||||
|
{
|
||||||
|
"targets": [
|
||||||
|
"plugins/grafana-extensionstest-app/actions",
|
||||||
|
"plugins/grafana-extensionstest-app/use-plugin-links/v1"
|
||||||
|
],
|
||||||
|
"title": "Go to A",
|
||||||
|
"description": "Navigating to pluging A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": ["plugins/grafana-extensionstest-app/use-plugin-links/v1"],
|
||||||
|
"title": "Basic link",
|
||||||
|
"description": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": ">=10.3.3",
|
"grafanaDependency": ">=10.3.3",
|
||||||
"plugins": []
|
"plugins": []
|
||||||
|
@ -19,13 +19,13 @@ export const plugin = new AppPlugin<{}>()
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.configureExtensionComponent({
|
.configureExtensionComponent({
|
||||||
extensionPointId: 'plugins/grafana-extensionexample2-app/configure-extension-component/v1',
|
extensionPointId: 'plugins/grafana-extensionstest-app/configure-extension-component/v1',
|
||||||
title: 'Configure extension component from B',
|
title: 'Configure extension component from B',
|
||||||
description: 'A component that can be reused by other app plugins. Shared using configureExtensionComponent api',
|
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>,
|
component: ({ name }: { name: string }) => <div data-testid={testIds.appB.reusableComponent}>Hello {name}!</div>,
|
||||||
})
|
})
|
||||||
.addComponent<{ name: string }>({
|
.addComponent<{ name: string }>({
|
||||||
targets: 'plugins/grafana-extensionexample2-app/addComponent/v1',
|
targets: 'plugins/grafana-extensionstest-app/addComponent/v1',
|
||||||
title: 'Added component from B',
|
title: 'Added component from B',
|
||||||
description: 'A component that can be reused by other app plugins. Shared using addComponent api',
|
description: 'A component that can be reused by other app plugins. Shared using addComponent api',
|
||||||
component: ({ name }: { name: string }) => (
|
component: ({ name }: { name: string }) => (
|
||||||
|
@ -18,6 +18,30 @@
|
|||||||
"version": "%VERSION%",
|
"version": "%VERSION%",
|
||||||
"updated": "%TODAY%"
|
"updated": "%TODAY%"
|
||||||
},
|
},
|
||||||
|
"extensions": {
|
||||||
|
"addedLinks": [
|
||||||
|
{
|
||||||
|
"targets": [
|
||||||
|
"plugins/grafana-extensionstest-app/actions",
|
||||||
|
"plugins/grafana-extensionstest-app/use-plugin-links/v1"
|
||||||
|
],
|
||||||
|
"title": "Open from B",
|
||||||
|
"description": "Open a modal from plugin B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addedComponents": [
|
||||||
|
{
|
||||||
|
"targets": ["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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": ["plugins/grafana-extensionstest-app/addComponent/v1"],
|
||||||
|
"title": "Added component from B",
|
||||||
|
"description": "A component that can be reused by other app plugins. Shared using addComponent api"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"includes": [
|
"includes": [
|
||||||
{
|
{
|
||||||
"type": "page",
|
"type": "page",
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { usePluginExtensions, usePluginLinks } from '@grafana/runtime';
|
||||||
|
import { Stack } from '@grafana/ui';
|
||||||
|
import { testIds } from '../../../../testIds';
|
||||||
|
import { ActionButton } from '../../../../components/ActionButton';
|
||||||
|
|
||||||
|
export const LINKS_EXTENSION_POINT_ID = 'plugins/grafana-extensionstest-app/use-plugin-links/v1';
|
||||||
|
|
||||||
|
export function AddedLinks() {
|
||||||
|
const { links } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
||||||
|
const { extensions } = usePluginExtensions({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction={'column'} gap={4}>
|
||||||
|
<section data-testid={testIds.appC.section1}>
|
||||||
|
<h3>Link extensions defined with addLink and retrieved using usePluginLinks</h3>
|
||||||
|
<ActionButton extensions={links} />
|
||||||
|
</section>
|
||||||
|
<section data-testid={testIds.appC.section2}>
|
||||||
|
<h3>Link extensions defined with addLink and retrieved using usePluginExtensions</h3>
|
||||||
|
<ActionButton extensions={extensions} />
|
||||||
|
</section>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { AppRootProps } from '@grafana/data';
|
||||||
|
import { AddedLinks } from './AddedLinks';
|
||||||
|
import { testIds } from '../../../../testIds';
|
||||||
|
|
||||||
|
export class App extends React.PureComponent<AppRootProps> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div data-testid={testIds.appC.container} className="page-container">
|
||||||
|
Hello Grafana!
|
||||||
|
<AddedLinks></AddedLinks>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81.9 71.52"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="42.95" y1="16.88" x2="81.9" y2="16.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M55.46,62.43A2,2,0,0,1,54.07,59l4.72-4.54a2,2,0,0,1,2.2-.39l3.65,1.63,3.68-3.64a2,2,0,1,1,2.81,2.84l-4.64,4.6a2,2,0,0,1-2.22.41L60.6,58.26l-3.76,3.61A2,2,0,0,1,55.46,62.43Z"/><path class="cls-2" d="M37,0H2A2,2,0,0,0,0,2V31.76a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V2A2,2,0,0,0,37,0ZM4,29.76V8.84H35V29.76Z"/><path class="cls-3" d="M79.9,0H45a2,2,0,0,0-2,2V31.76a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V2A2,2,0,0,0,79.9,0ZM47,29.76V8.84h31V29.76Z"/><path class="cls-2" d="M37,37.76H2a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V39.76A2,2,0,0,0,37,37.76ZM4,67.52V46.6H35V67.52Z"/><path class="cls-2" d="M79.9,37.76H45a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V39.76A2,2,0,0,0,79.9,37.76ZM47,67.52V46.6h31V67.52Z"/><rect class="cls-1" x="10.48" y="56.95" width="4" height="5.79"/><rect class="cls-1" x="17.43" y="53.95" width="4" height="8.79"/><rect class="cls-1" x="24.47" y="50.95" width="4" height="11.79"/><path class="cls-1" d="M19.47,25.8a6.93,6.93,0,1,1,6.93-6.92A6.93,6.93,0,0,1,19.47,25.8Zm0-9.85a2.93,2.93,0,1,0,2.93,2.93A2.93,2.93,0,0,0,19.47,16Z"/></g></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,44 @@
|
|||||||
|
import { AppPlugin } from '@grafana/data';
|
||||||
|
|
||||||
|
import { LINKS_EXTENSION_POINT_ID } from '../../pages/AddedLinks';
|
||||||
|
import { testIds } from '../../testIds';
|
||||||
|
import { App } from '../../components/App';
|
||||||
|
|
||||||
|
export const plugin = new AppPlugin<{}>()
|
||||||
|
.setRootPage(App)
|
||||||
|
.configureExtensionLink({
|
||||||
|
title: 'configureExtensionLink (where meta data is missing)',
|
||||||
|
description: 'Open a modal from plugin B',
|
||||||
|
extensionPointId: 'plugins/grafana-extensionstest-app/actions',
|
||||||
|
onClick: (_, { openModal }) => {
|
||||||
|
openModal({
|
||||||
|
title: 'Modal from app B',
|
||||||
|
body: () => <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 }>({
|
||||||
|
targets: ['plugins/grafana-extensionstest-app/addComponent/v1'],
|
||||||
|
title: 'Added component (where meta data is missing)',
|
||||||
|
description: '.',
|
||||||
|
component: ({ name }: { name: string }) => (
|
||||||
|
<div data-testid={testIds.appB.reusableAddedComponent}>Hello {name}!</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.addLink({
|
||||||
|
title: 'Added link (where meta data is missing)',
|
||||||
|
description: '.',
|
||||||
|
targets: [LINKS_EXTENSION_POINT_ID],
|
||||||
|
onClick: (_, { openModal }) => {
|
||||||
|
openModal({
|
||||||
|
title: 'Modal from app C',
|
||||||
|
body: () => <div data-testid={testIds.appB.modal}>From plugin B</div>,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
|
||||||
|
"type": "app",
|
||||||
|
"name": "D App",
|
||||||
|
"id": "grafana-extensionexample3-app",
|
||||||
|
"preload": true,
|
||||||
|
"info": {
|
||||||
|
"keywords": ["app"],
|
||||||
|
"description": "Will extend root app with ui extensions",
|
||||||
|
"author": {
|
||||||
|
"name": "grafana"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/logo.svg",
|
||||||
|
"large": "img/logo.svg"
|
||||||
|
},
|
||||||
|
"screenshots": [],
|
||||||
|
"version": "%VERSION%",
|
||||||
|
"updated": "%TODAY%"
|
||||||
|
},
|
||||||
|
"includes": [
|
||||||
|
{
|
||||||
|
"type": "page",
|
||||||
|
"name": "Default",
|
||||||
|
"path": "/a/grafana-extensionexample3-app",
|
||||||
|
"role": "Admin",
|
||||||
|
"addToNav": false,
|
||||||
|
"defaultNav": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"grafanaDependency": ">=10.3.3",
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,11 @@ export const testIds = {
|
|||||||
reusableAddedComponent: 'b-app-add-component',
|
reusableAddedComponent: 'b-app-add-component',
|
||||||
exposedComponent: 'b-app-exposed-component',
|
exposedComponent: 'b-app-exposed-component',
|
||||||
},
|
},
|
||||||
|
appC: {
|
||||||
|
container: 'c-app-body',
|
||||||
|
section1: 'use-plugin-links',
|
||||||
|
section2: 'use-plugin-extensions',
|
||||||
|
},
|
||||||
legacyGettersPage: {
|
legacyGettersPage: {
|
||||||
container: 'data-testid pg-legacy-getters-container',
|
container: 'data-testid pg-legacy-getters-container',
|
||||||
section1: 'get-plugin-extensions',
|
section1: 'get-plugin-extensions',
|
||||||
|
@ -2,6 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
|||||||
|
|
||||||
import { testIds } from '../../testIds';
|
import { testIds } from '../../testIds';
|
||||||
import pluginJson from '../../plugin.json';
|
import pluginJson from '../../plugin.json';
|
||||||
|
import testApp3pluginJson from '../../plugins/grafana-extensionexample3-app/plugin.json';
|
||||||
|
|
||||||
test.describe('usePluginExtensions + configureExtensionLink', () => {
|
test.describe('usePluginExtensions + configureExtensionLink', () => {
|
||||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
||||||
@ -12,6 +13,17 @@ test.describe('usePluginExtensions + configureExtensionLink', () => {
|
|||||||
await page.getByTestId(testIds.modal.open).click();
|
await page.getByTestId(testIds.modal.open).click();
|
||||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
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.describe('usePluginExtensions + configureExtensionComponent', () => {
|
||||||
@ -43,3 +55,13 @@ test.describe('usePluginComponentExtensions + configureExtensionComponent', () =
|
|||||||
).toHaveText('Hello World!');
|
).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,6 +1,7 @@
|
|||||||
import { test, expect } from '@grafana/plugin-e2e';
|
import { test, expect } from '@grafana/plugin-e2e';
|
||||||
|
|
||||||
import pluginJson from '../plugin.json';
|
import pluginJson from '../plugin.json';
|
||||||
|
import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json';
|
||||||
import { testIds } from '../testIds';
|
import { testIds } from '../testIds';
|
||||||
|
|
||||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
||||||
@ -28,3 +29,11 @@ test('should extend main app with basic link extension from app A', async ({ pag
|
|||||||
await page.getByTestId(testIds.modal.open).click();
|
await page.getByTestId(testIds.modal.open).click();
|
||||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto(`/a/${testApp3pluginJson.id}`);
|
||||||
|
const container = await page.getByTestId(testIds.appC.section1);
|
||||||
|
await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
@ -2,10 +2,14 @@ import { useContext } from 'react';
|
|||||||
|
|
||||||
import { Context, PluginContextType } from './PluginContext';
|
import { Context, PluginContextType } from './PluginContext';
|
||||||
|
|
||||||
export function usePluginContext(): PluginContextType {
|
export function usePluginContext(): PluginContextType | null {
|
||||||
const context = useContext(Context);
|
const context = useContext(Context);
|
||||||
|
|
||||||
|
// The extensions hooks (e.g. `usePluginLinks()`) are using this hook to check
|
||||||
|
// if they are inside a plugin or not (core Grafana), so we should be able to return an empty state as well (`null`).
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('usePluginContext must be used within a PluginContextProvider');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
@ -586,6 +586,7 @@ export {
|
|||||||
type AngularMeta,
|
type AngularMeta,
|
||||||
type PluginMeta,
|
type PluginMeta,
|
||||||
type PluginDependencies,
|
type PluginDependencies,
|
||||||
|
type PluginExtensions,
|
||||||
type PluginInclude,
|
type PluginInclude,
|
||||||
type PluginBuildInfo,
|
type PluginBuildInfo,
|
||||||
type ScreenshotInfo,
|
type ScreenshotInfo,
|
||||||
|
@ -98,6 +98,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
|||||||
angular?: AngularMeta;
|
angular?: AngularMeta;
|
||||||
angularDetected?: boolean;
|
angularDetected?: boolean;
|
||||||
loadingStrategy?: PluginLoadingStrategy;
|
loadingStrategy?: PluginLoadingStrategy;
|
||||||
|
extensions?: PluginExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginDependencyInfo {
|
interface PluginDependencyInfo {
|
||||||
@ -111,6 +112,38 @@ export interface PluginDependencies {
|
|||||||
grafanaDependency?: string;
|
grafanaDependency?: string;
|
||||||
grafanaVersion: string;
|
grafanaVersion: string;
|
||||||
plugins: PluginDependencyInfo[];
|
plugins: PluginDependencyInfo[];
|
||||||
|
extensions: {
|
||||||
|
// A list of exposed component IDs
|
||||||
|
exposedComponents: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtensionInfo = {
|
||||||
|
targets: string | string[];
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PluginExtensions {
|
||||||
|
// The component extensions that the plugin registers
|
||||||
|
addedComponents: ExtensionInfo[];
|
||||||
|
|
||||||
|
// The link extensions that the plugin registers
|
||||||
|
addedLinks: ExtensionInfo[];
|
||||||
|
|
||||||
|
// The React components that the plugin exposes
|
||||||
|
exposedComponents: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// The extension points that the plugin provides
|
||||||
|
extensionPoints: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PluginIncludeType {
|
export enum PluginIncludeType {
|
||||||
|
@ -12,6 +12,13 @@ export function usePluginInteractionReporter(): typeof reportInteraction {
|
|||||||
const context = usePluginContext();
|
const context = usePluginContext();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
// Happens when the hook is not used inside a plugin (e.g. in core Grafana)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`No PluginContext found. The usePluginInteractionReporter() hook can only be used from a plugin.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const info = isDataSourcePluginContext(context)
|
const info = isDataSourcePluginContext(context)
|
||||||
? createDataSourcePluginEventProperties(context.instanceSettings)
|
? createDataSourcePluginEventProperties(context.instanceSettings)
|
||||||
: createPluginEventProperties(context.meta);
|
: createPluginEventProperties(context.meta);
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
getThemeById,
|
getThemeById,
|
||||||
AngularMeta,
|
AngularMeta,
|
||||||
PluginLoadingStrategy,
|
PluginLoadingStrategy,
|
||||||
|
PluginDependencies,
|
||||||
|
PluginExtensions,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
export interface AzureSettings {
|
export interface AzureSettings {
|
||||||
@ -42,6 +44,8 @@ export type AppPluginConfig = {
|
|||||||
preload: boolean;
|
preload: boolean;
|
||||||
angular: AngularMeta;
|
angular: AngularMeta;
|
||||||
loadingStrategy: PluginLoadingStrategy;
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
|
dependencies: PluginDependencies;
|
||||||
|
extensions: PluginExtensions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PreinstalledPlugin = {
|
export type PreinstalledPlugin = {
|
||||||
|
@ -17,6 +17,7 @@ type PluginSetting struct {
|
|||||||
Info plugins.Info `json:"info"`
|
Info plugins.Info `json:"info"`
|
||||||
Includes []*plugins.Includes `json:"includes"`
|
Includes []*plugins.Includes `json:"includes"`
|
||||||
Dependencies plugins.Dependencies `json:"dependencies"`
|
Dependencies plugins.Dependencies `json:"dependencies"`
|
||||||
|
Extensions plugins.Extensions `json:"extensions"`
|
||||||
JsonData map[string]any `json:"jsonData"`
|
JsonData map[string]any `json:"jsonData"`
|
||||||
SecureJsonFields map[string]bool `json:"secureJsonFields"`
|
SecureJsonFields map[string]bool `json:"secureJsonFields"`
|
||||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||||
|
@ -561,6 +561,8 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin,
|
|||||||
Preload: false,
|
Preload: false,
|
||||||
Angular: plugin.Angular,
|
Angular: plugin.Angular,
|
||||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
||||||
|
Extensions: plugin.Extensions,
|
||||||
|
Dependencies: plugin.Dependencies,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Enabled {
|
if settings.Enabled {
|
||||||
|
@ -209,6 +209,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
|||||||
SecureJsonFields: map[string]bool{},
|
SecureJsonFields: map[string]bool{},
|
||||||
AngularDetected: plugin.Angular.Detected,
|
AngularDetected: plugin.Angular.Detected,
|
||||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
||||||
|
Extensions: plugin.Extensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
if plugin.IsApp() {
|
if plugin.IsApp() {
|
||||||
|
@ -50,6 +50,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
Backend: true,
|
Backend: true,
|
||||||
@ -82,6 +91,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
|
||||||
@ -104,6 +122,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
|
||||||
@ -150,6 +177,9 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
{ID: "graphite", Type: "datasource", Name: "Graphite", Version: "1.0.0"},
|
{ID: "graphite", Type: "datasource", Name: "Graphite", Version: "1.0.0"},
|
||||||
{ID: "graph", Type: "panel", Name: "Graph", Version: "1.0.0"},
|
{ID: "graph", Type: "panel", Name: "Graph", Version: "1.0.0"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{
|
{
|
||||||
@ -169,6 +199,12 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Action: "plugins.app:access"},
|
{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Action: "plugins.app:access"},
|
||||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "includes-symlinks")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "includes-symlinks")),
|
||||||
},
|
},
|
||||||
@ -197,6 +233,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
|
||||||
@ -219,6 +264,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
|
||||||
@ -241,6 +295,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
Backend: true,
|
Backend: true,
|
||||||
@ -272,6 +335,15 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
Backend: true,
|
Backend: true,
|
||||||
|
@ -98,6 +98,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Category: "cloud",
|
Category: "cloud",
|
||||||
Annotations: true,
|
Annotations: true,
|
||||||
@ -141,6 +150,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Executable: "test",
|
Executable: "test",
|
||||||
Backend: true,
|
Backend: true,
|
||||||
@ -195,6 +213,9 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{
|
{
|
||||||
@ -228,6 +249,12 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Slug: "nginx-datasource",
|
Slug: "nginx-datasource",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Class: plugins.ClassExternal,
|
Class: plugins.ClassExternal,
|
||||||
Module: "public/plugins/test-app/module.js",
|
Module: "public/plugins/test-app/module.js",
|
||||||
@ -266,6 +293,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Backend: true,
|
Backend: true,
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
@ -312,6 +348,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Backend: true,
|
Backend: true,
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
@ -393,11 +438,20 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
GrafanaDependency: ">=8.0.0",
|
GrafanaDependency: ">=8.0.0",
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-memory"},
|
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-memory"},
|
||||||
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
Backend: false,
|
Backend: false,
|
||||||
},
|
},
|
||||||
DefaultNavURL: "/plugins/test-app/page/root-page-react",
|
DefaultNavURL: "/plugins/test-app/page/root-page-react",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@ -42,9 +43,100 @@ func (e DuplicateError) Is(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Dependencies struct {
|
type Dependencies struct {
|
||||||
GrafanaDependency string `json:"grafanaDependency"`
|
GrafanaDependency string `json:"grafanaDependency"`
|
||||||
GrafanaVersion string `json:"grafanaVersion"`
|
GrafanaVersion string `json:"grafanaVersion"`
|
||||||
Plugins []Dependency `json:"plugins"`
|
Plugins []Dependency `json:"plugins"`
|
||||||
|
Extensions ExtensionsDependencies `json:"extensions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need different versions for the Extensions struct because there is a now deprecated plugin.json schema out there, where the "extensions" prop
|
||||||
|
// is in a different format (Extensions V1). In order to support those as well while reading the plugin.json, we need to add a custom unmarshaling logic for extensions.
|
||||||
|
type ExtensionV1 struct {
|
||||||
|
ExtensionPointID string `json:"extensionPointId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtensionsV2 struct {
|
||||||
|
AddedLinks []AddedLink `json:"addedLinks"`
|
||||||
|
AddedComponents []AddedComponent `json:"addedComponents"`
|
||||||
|
ExposedComponents []ExposedComponent `json:"exposedComponents"`
|
||||||
|
ExtensionPoints []ExtensionPoint `json:"extensionPoints"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Extensions ExtensionsV2
|
||||||
|
|
||||||
|
func (e *Extensions) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
var extensionsV2 ExtensionsV2
|
||||||
|
|
||||||
|
if err = json.Unmarshal(data, &extensionsV2); err == nil {
|
||||||
|
e.AddedComponents = extensionsV2.AddedComponents
|
||||||
|
e.AddedLinks = extensionsV2.AddedLinks
|
||||||
|
e.ExposedComponents = extensionsV2.ExposedComponents
|
||||||
|
e.ExtensionPoints = extensionsV2.ExtensionPoints
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (V1)
|
||||||
|
var extensionsV1 []ExtensionV1
|
||||||
|
if err = json.Unmarshal(data, &extensionsV1); err == nil {
|
||||||
|
// Trying to process old format and add them to `AddedLinks` and `AddedComponents`
|
||||||
|
for _, extensionV1 := range extensionsV1 {
|
||||||
|
if extensionV1.Type == "link" {
|
||||||
|
extensionV2 := AddedLink{
|
||||||
|
Targets: []string{extensionV1.ExtensionPointID},
|
||||||
|
Title: extensionV1.Title,
|
||||||
|
Description: extensionV1.Description,
|
||||||
|
}
|
||||||
|
e.AddedLinks = append(e.AddedLinks, extensionV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if extensionV1.Type == "component" {
|
||||||
|
extensionV2 := AddedComponent{
|
||||||
|
Targets: []string{extensionV1.ExtensionPointID},
|
||||||
|
Title: extensionV1.Title,
|
||||||
|
Description: extensionV1.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
e.AddedComponents = append(e.AddedComponents, extensionV2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddedLink struct {
|
||||||
|
Targets []string `json:"targets"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddedComponent struct {
|
||||||
|
Targets []string `json:"targets"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExposedComponent struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtensionPoint struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtensionsDependencies struct {
|
||||||
|
ExposedComponents []string `json:"exposedComponents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Includes struct {
|
type Includes struct {
|
||||||
@ -231,6 +323,8 @@ type AppDTO struct {
|
|||||||
Preload bool `json:"preload"`
|
Preload bool `json:"preload"`
|
||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
|
Extensions Extensions `json:"extensions"`
|
||||||
|
Dependencies Dependencies `json:"dependencies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -109,7 +109,8 @@ type JSONData struct {
|
|||||||
SkipDataQuery bool `json:"skipDataQuery"`
|
SkipDataQuery bool `json:"skipDataQuery"`
|
||||||
|
|
||||||
// App settings
|
// App settings
|
||||||
AutoEnabled bool `json:"autoEnabled"`
|
AutoEnabled bool `json:"autoEnabled"`
|
||||||
|
Extensions Extensions `json:"extensions"`
|
||||||
|
|
||||||
// Datasource settings
|
// Datasource settings
|
||||||
Annotations bool `json:"annotations"`
|
Annotations bool `json:"annotations"`
|
||||||
@ -173,6 +174,26 @@ func ReadPluginJSON(reader io.Reader) (JSONData, error) {
|
|||||||
plugin.Dependencies.GrafanaVersion = "*"
|
plugin.Dependencies.GrafanaVersion = "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(plugin.Dependencies.Extensions.ExposedComponents) == 0 {
|
||||||
|
plugin.Dependencies.Extensions.ExposedComponents = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugin.Extensions.AddedLinks == nil {
|
||||||
|
plugin.Extensions.AddedLinks = []AddedLink{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugin.Extensions.AddedComponents == nil {
|
||||||
|
plugin.Extensions.AddedComponents = []AddedComponent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugin.Extensions.ExposedComponents == nil {
|
||||||
|
plugin.Extensions.ExposedComponents = []ExposedComponent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugin.Extensions.ExtensionPoints == nil {
|
||||||
|
plugin.Extensions.ExtensionPoints = []ExtensionPoint{}
|
||||||
|
}
|
||||||
|
|
||||||
for _, include := range plugin.Includes {
|
for _, include := range plugin.Includes {
|
||||||
if include.Role == "" {
|
if include.Role == "" {
|
||||||
include.Role = org.RoleViewer
|
include.Role = org.RoleViewer
|
||||||
|
@ -52,13 +52,25 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Updated: "2015-02-10",
|
Updated: "2015-02-10",
|
||||||
Keywords: []string{"test"},
|
Keywords: []string{"test"},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{},
|
||||||
|
AddedComponents: []AddedComponent{},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
Dependencies: Dependencies{
|
Dependencies: Dependencies{
|
||||||
GrafanaVersion: "3.x.x",
|
GrafanaVersion: "3.x.x",
|
||||||
Plugins: []Dependency{
|
Plugins: []Dependency{
|
||||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||||
},
|
},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Includes: []*Includes{
|
Includes: []*Includes{
|
||||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
|
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
|
||||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
|
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
|
||||||
@ -94,10 +106,22 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
ID: "grafana-piechart-panel",
|
ID: "grafana-piechart-panel",
|
||||||
Type: TypePanel,
|
Type: TypePanel,
|
||||||
Name: "Pie Chart (old)",
|
Name: "Pie Chart (old)",
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{},
|
||||||
|
AddedComponents: []AddedComponent{},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
Dependencies: Dependencies{
|
Dependencies: Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []Dependency{},
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Includes: []*Includes{
|
Includes: []*Includes{
|
||||||
{Name: "Pie Charts", Path: "dashboards/demo.json", Type: "dashboard", Role: org.RoleViewer},
|
{Name: "Pie Charts", Path: "dashboards/demo.json", Type: "dashboard", Role: org.RoleViewer},
|
||||||
},
|
},
|
||||||
@ -117,10 +141,21 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
ID: "grafana-pyroscope-datasource",
|
ID: "grafana-pyroscope-datasource",
|
||||||
AliasIDs: []string{"phlare"}, // Hardcoded from the parser
|
AliasIDs: []string{"phlare"}, // Hardcoded from the parser
|
||||||
Type: TypeDataSource,
|
Type: TypeDataSource,
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{},
|
||||||
|
AddedComponents: []AddedComponent{},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
Dependencies: Dependencies{
|
Dependencies: Dependencies{
|
||||||
GrafanaDependency: "",
|
GrafanaDependency: "",
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []Dependency{},
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -142,18 +177,257 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Dependencies: Dependencies{},
|
Dependencies: Dependencies{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "can read the latest versions of extensions information (v2)",
|
||||||
|
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||||
|
pJSON := `{
|
||||||
|
"id": "myorg-extensions-app",
|
||||||
|
"name": "Extensions App",
|
||||||
|
"type": "app",
|
||||||
|
"extensions": {
|
||||||
|
"addedLinks": [
|
||||||
|
{
|
||||||
|
"title": "Added link 1",
|
||||||
|
"description": "Added link 1 description",
|
||||||
|
"targets": ["grafana/dashboard/panel/menu"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addedComponents": [
|
||||||
|
{
|
||||||
|
"title": "Added component 1",
|
||||||
|
"description": "Added component 1 description",
|
||||||
|
"targets": ["grafana/user/profile/tab"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exposedComponents": [
|
||||||
|
{
|
||||||
|
"title": "Exposed component 1",
|
||||||
|
"description": "Exposed component 1 description",
|
||||||
|
"id": "myorg-extensions-app/component-1/v1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extensionPoints": [
|
||||||
|
{
|
||||||
|
"title": "Extension point 1",
|
||||||
|
"description": "Extension points 1 description",
|
||||||
|
"id": "myorg-extensions-app/extensions-point-1/v1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
return io.NopCloser(strings.NewReader(pJSON))
|
||||||
|
},
|
||||||
|
expected: JSONData{
|
||||||
|
ID: "myorg-extensions-app",
|
||||||
|
Name: "Extensions App",
|
||||||
|
Type: TypeApp,
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{
|
||||||
|
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||||
|
},
|
||||||
|
AddedComponents: []AddedComponent{
|
||||||
|
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||||
|
},
|
||||||
|
ExposedComponents: []ExposedComponent{
|
||||||
|
{Id: "myorg-extensions-app/component-1/v1", Title: "Exposed component 1", Description: "Exposed component 1 description"},
|
||||||
|
},
|
||||||
|
ExtensionPoints: []ExtensionPoint{
|
||||||
|
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Dependencies: Dependencies{
|
||||||
|
GrafanaVersion: "*",
|
||||||
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can read deprecated extensions info (v1) and parse it as v2",
|
||||||
|
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||||
|
pJSON := `{
|
||||||
|
"id": "myorg-extensions-app",
|
||||||
|
"name": "Extensions App",
|
||||||
|
"type": "app",
|
||||||
|
"extensions": [
|
||||||
|
{
|
||||||
|
"extensionPointId": "grafana/dashboard/panel/menu",
|
||||||
|
"title": "Added link 1",
|
||||||
|
"description": "Added link 1 description",
|
||||||
|
"type": "link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extensionPointId": "grafana/dashboard/panel/menu",
|
||||||
|
"title": "Added link 2",
|
||||||
|
"description": "Added link 2 description",
|
||||||
|
"type": "link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extensionPointId": "grafana/user/profile/tab",
|
||||||
|
"title": "Added component 1",
|
||||||
|
"description": "Added component 1 description",
|
||||||
|
"type": "component"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
return io.NopCloser(strings.NewReader(pJSON))
|
||||||
|
},
|
||||||
|
expected: JSONData{
|
||||||
|
ID: "myorg-extensions-app",
|
||||||
|
Name: "Extensions App",
|
||||||
|
Type: TypeApp,
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{
|
||||||
|
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||||
|
{Title: "Added link 2", Description: "Added link 2 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||||
|
},
|
||||||
|
AddedComponents: []AddedComponent{
|
||||||
|
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||||
|
},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
|
Dependencies: Dependencies{
|
||||||
|
GrafanaVersion: "*",
|
||||||
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "works if extensions info is empty",
|
||||||
|
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||||
|
pJSON := `{
|
||||||
|
"id": "myorg-extensions-app",
|
||||||
|
"name": "Extensions App",
|
||||||
|
"type": "app",
|
||||||
|
"extensions": []
|
||||||
|
}`
|
||||||
|
return io.NopCloser(strings.NewReader(pJSON))
|
||||||
|
},
|
||||||
|
expected: JSONData{
|
||||||
|
ID: "myorg-extensions-app",
|
||||||
|
Name: "Extensions App",
|
||||||
|
Type: TypeApp,
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{},
|
||||||
|
AddedComponents: []AddedComponent{},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
|
Dependencies: Dependencies{
|
||||||
|
GrafanaVersion: "*",
|
||||||
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "works if extensions info is completely missing",
|
||||||
|
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||||
|
pJSON := `{
|
||||||
|
"id": "myorg-extensions-app",
|
||||||
|
"name": "Extensions App",
|
||||||
|
"type": "app"
|
||||||
|
}`
|
||||||
|
return io.NopCloser(strings.NewReader(pJSON))
|
||||||
|
},
|
||||||
|
expected: JSONData{
|
||||||
|
ID: "myorg-extensions-app",
|
||||||
|
Name: "Extensions App",
|
||||||
|
Type: TypeApp,
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{},
|
||||||
|
AddedComponents: []AddedComponent{},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
|
Dependencies: Dependencies{
|
||||||
|
GrafanaVersion: "*",
|
||||||
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can read extensions related dependencies",
|
||||||
|
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||||
|
pJSON := `{
|
||||||
|
"id": "myorg-extensions-app",
|
||||||
|
"name": "Extensions App",
|
||||||
|
"type": "app",
|
||||||
|
"dependencies": {
|
||||||
|
"grafanaDependency": "10.0.0",
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": ["myorg-extensions-app/component-1/v1"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
return io.NopCloser(strings.NewReader(pJSON))
|
||||||
|
},
|
||||||
|
expected: JSONData{
|
||||||
|
ID: "myorg-extensions-app",
|
||||||
|
Name: "Extensions App",
|
||||||
|
Type: TypeApp,
|
||||||
|
|
||||||
|
Extensions: Extensions{
|
||||||
|
AddedLinks: []AddedLink{},
|
||||||
|
AddedComponents: []AddedComponent{},
|
||||||
|
ExposedComponents: []ExposedComponent{},
|
||||||
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
|
},
|
||||||
|
|
||||||
|
Dependencies: Dependencies{
|
||||||
|
GrafanaVersion: "*",
|
||||||
|
GrafanaDependency: "10.0.0",
|
||||||
|
Plugins: []Dependency{},
|
||||||
|
Extensions: ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{"myorg-extensions-app/component-1/v1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
p := tt.pluginJSON(t)
|
p := tt.pluginJSON(t)
|
||||||
got, err := ReadPluginJSON(p)
|
got, err := ReadPluginJSON(p)
|
||||||
|
|
||||||
|
// Check if the test returns the same error as expected
|
||||||
|
// (unneccary to check further if there is an error at this point)
|
||||||
|
if tt.err == nil && err != nil {
|
||||||
|
t.Errorf("Error while reading pluginJSON: %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the test returns the same error as expected
|
||||||
if tt.err != nil {
|
if tt.err != nil {
|
||||||
require.ErrorIs(t, err, tt.err)
|
require.ErrorIs(t, err, tt.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the test returns the expected pluginJSONData
|
||||||
if !cmp.Equal(got, tt.expected) {
|
if !cmp.Equal(got, tt.expected) {
|
||||||
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected))
|
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should be able to close the reader
|
||||||
require.NoError(t, p.Close())
|
require.NoError(t, p.Close())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Category: "cloud",
|
Category: "cloud",
|
||||||
Annotations: true,
|
Annotations: true,
|
||||||
@ -141,6 +150,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Executable: "test",
|
Executable: "test",
|
||||||
Backend: true,
|
Backend: true,
|
||||||
@ -195,6 +213,9 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{
|
{
|
||||||
@ -228,6 +249,12 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Slug: "nginx-datasource",
|
Slug: "nginx-datasource",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Class: plugins.ClassExternal,
|
Class: plugins.ClassExternal,
|
||||||
Module: "public/plugins/test-app/module.js",
|
Module: "public/plugins/test-app/module.js",
|
||||||
@ -266,6 +293,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Backend: true,
|
Backend: true,
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
@ -318,6 +354,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Backend: true,
|
Backend: true,
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
@ -423,6 +468,15 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
GrafanaDependency: ">=8.0.0",
|
GrafanaDependency: ">=8.0.0",
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-memory"},
|
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-memory"},
|
||||||
@ -497,6 +551,15 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
IAM: &pfs.IAM{
|
IAM: &pfs.IAM{
|
||||||
Permissions: []pfs.Permission{
|
Permissions: []pfs.Permission{
|
||||||
@ -599,6 +662,15 @@ func TestLoader_Load_CustomSource(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "3.x.x",
|
GrafanaVersion: "3.x.x",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "cdn/plugin")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "cdn/plugin")),
|
||||||
@ -671,6 +743,15 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Backend: true,
|
Backend: true,
|
||||||
Executable: "test",
|
Executable: "test",
|
||||||
@ -767,6 +848,15 @@ func TestLoader_Load_RBACReady(t *testing.T) {
|
|||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
GrafanaDependency: ">=8.0.0",
|
GrafanaDependency: ">=8.0.0",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{},
|
Includes: []*plugins.Includes{},
|
||||||
Roles: []plugins.RoleRegistration{
|
Roles: []plugins.RoleRegistration{
|
||||||
@ -840,10 +930,18 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
},
|
},
|
||||||
State: plugins.ReleaseStateAlpha,
|
State: plugins.ReleaseStateAlpha,
|
||||||
Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}},
|
Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}, Extensions: plugins.ExtensionsDependencies{
|
||||||
Backend: true,
|
ExposedComponents: []string{},
|
||||||
Executable: "test",
|
}},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
|
Backend: true,
|
||||||
|
Executable: "test",
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")),
|
||||||
Class: plugins.ClassExternal,
|
Class: plugins.ClassExternal,
|
||||||
@ -913,6 +1011,15 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
|||||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-connections"},
|
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-connections"},
|
||||||
@ -994,6 +1101,9 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
|||||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-connections"},
|
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-connections"},
|
||||||
@ -1001,6 +1111,12 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
|||||||
{Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-panel"},
|
{Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-panel"},
|
||||||
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
Backend: false,
|
Backend: false,
|
||||||
},
|
},
|
||||||
FS: mustNewStaticFSForTests(t, pluginDir1),
|
FS: mustNewStaticFSForTests(t, pluginDir1),
|
||||||
@ -1208,6 +1324,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
Backend: true,
|
Backend: true,
|
||||||
},
|
},
|
||||||
@ -1241,6 +1366,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
Dependencies: plugins.Dependencies{
|
Dependencies: plugins.Dependencies{
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Module: "public/plugins/test-datasource/nested/module.js",
|
Module: "public/plugins/test-datasource/nested/module.js",
|
||||||
@ -1336,6 +1470,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
GrafanaVersion: "7.0.0",
|
GrafanaVersion: "7.0.0",
|
||||||
GrafanaDependency: ">=7.0.0",
|
GrafanaDependency: ">=7.0.0",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Includes: []*plugins.Includes{
|
Includes: []*plugins.Includes{
|
||||||
{
|
{
|
||||||
@ -1382,6 +1519,12 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
Slug: "lots-of-stats",
|
Slug: "lots-of-stats",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
|
},
|
||||||
Backend: false,
|
Backend: false,
|
||||||
},
|
},
|
||||||
Module: "public/plugins/myorgid-simple-app/module.js",
|
Module: "public/plugins/myorgid-simple-app/module.js",
|
||||||
@ -1421,6 +1564,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
GrafanaDependency: ">=7.0.0",
|
GrafanaDependency: ">=7.0.0",
|
||||||
GrafanaVersion: "*",
|
GrafanaVersion: "*",
|
||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
|
Extensions: plugins.ExtensionsDependencies{
|
||||||
|
ExposedComponents: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: plugins.Extensions{
|
||||||
|
AddedLinks: []plugins.AddedLink{},
|
||||||
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Module: "public/plugins/myorgid-simple-app/child/module.js",
|
Module: "public/plugins/myorgid-simple-app/child/module.js",
|
||||||
|
@ -25,7 +25,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -75,7 +78,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -113,7 +119,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -179,7 +188,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0",
|
"grafanaDependency": "\u003e=10.3.0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -217,7 +229,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -255,7 +270,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -298,7 +316,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -336,7 +357,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -377,7 +401,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -415,7 +442,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -453,7 +483,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -503,7 +536,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -541,7 +577,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -579,7 +618,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -617,7 +659,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -655,7 +700,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -693,7 +741,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -744,7 +795,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0-0",
|
"grafanaDependency": "\u003e=10.3.0-0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -782,7 +836,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -829,7 +886,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -867,7 +927,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -910,7 +973,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -948,7 +1014,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -995,7 +1064,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0-0",
|
"grafanaDependency": "\u003e=10.3.0-0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1033,7 +1105,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1080,7 +1155,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1118,7 +1196,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.4.0",
|
"grafanaDependency": "\u003e=10.4.0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1156,7 +1237,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.4.0",
|
"grafanaDependency": "\u003e=10.4.0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1194,7 +1278,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1232,7 +1319,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1270,7 +1360,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1318,7 +1411,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0-0",
|
"grafanaDependency": "\u003e=10.3.0-0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1356,7 +1452,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1394,7 +1493,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1437,7 +1539,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1475,7 +1580,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1513,7 +1621,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1551,7 +1662,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1589,7 +1703,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1627,7 +1744,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1670,7 +1790,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0-0",
|
"grafanaDependency": "\u003e=10.3.0-0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1708,7 +1831,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0-0",
|
"grafanaDependency": "\u003e=10.3.0-0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1746,7 +1872,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1784,7 +1913,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1822,7 +1954,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1860,7 +1995,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1898,7 +2036,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1939,7 +2080,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
@ -1982,7 +2126,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "\u003e=10.3.0-0",
|
"grafanaDependency": "\u003e=10.3.0-0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"extensions": {
|
||||||
|
"exposedComponents": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"latestVersion": "",
|
"latestVersion": "",
|
||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
|
@ -19,6 +19,19 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
|
|||||||
version: info.version,
|
version: info.version,
|
||||||
angular: angular ?? { detected: false, hideDeprecation: false },
|
angular: angular ?? { detected: false, hideDeprecation: false },
|
||||||
loadingStrategy: PluginLoadingStrategy.script,
|
loadingStrategy: PluginLoadingStrategy.script,
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,6 +49,19 @@ describe('getRuleOrigin', () => {
|
|||||||
preload: true,
|
preload: true,
|
||||||
angular: { detected: false, hideDeprecation: false },
|
angular: { detected: false, hideDeprecation: false },
|
||||||
loadingStrategy: PluginLoadingStrategy.script,
|
loadingStrategy: PluginLoadingStrategy.script,
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const rule = mockCombinedRule({
|
const rule = mockCombinedRule({
|
||||||
|
@ -43,6 +43,9 @@ export default {
|
|||||||
grafanaDependency: '>=7.3.0',
|
grafanaDependency: '>=7.3.0',
|
||||||
grafanaVersion: '7.3',
|
grafanaVersion: '7.3',
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
links: [],
|
links: [],
|
||||||
|
@ -1,15 +1,62 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||||
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
...jest.requireActual('../utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AddedComponentsRegistry', () => {
|
describe('AddedComponentsRegistry', () => {
|
||||||
const consoleWarn = jest.fn();
|
const consoleWarn = jest.fn();
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.console.warn = consoleWarn;
|
global.console.warn = consoleWarn;
|
||||||
consoleWarn.mockReset();
|
consoleWarn.mockReset();
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty registry when no extensions registered', async () => {
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
@ -20,15 +67,14 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to register added components in the registry', async () => {
|
it('should be possible to register added components in the registry', async () => {
|
||||||
const pluginId = 'grafana-basic-app';
|
const extensionPointId = `${pluginId}/hello-world/v1`;
|
||||||
const id = `${pluginId}/hello-world/v1`;
|
|
||||||
const reactiveRegistry = new AddedComponentsRegistry();
|
const reactiveRegistry = new AddedComponentsRegistry();
|
||||||
|
|
||||||
reactiveRegistry.register({
|
reactiveRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
targets: [id],
|
targets: [extensionPointId],
|
||||||
title: 'not important',
|
title: 'not important',
|
||||||
description: 'not important',
|
description: 'not important',
|
||||||
component: () => React.createElement('div', null, 'Hello World'),
|
component: () => React.createElement('div', null, 'Hello World'),
|
||||||
@ -39,15 +85,17 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
const registry = await reactiveRegistry.getState();
|
const registry = await reactiveRegistry.getState();
|
||||||
|
|
||||||
expect(Object.keys(registry)).toHaveLength(1);
|
expect(Object.keys(registry)).toHaveLength(1);
|
||||||
expect(registry[id][0]).toMatchObject({
|
expect(registry[extensionPointId][0]).toMatchObject({
|
||||||
pluginId,
|
pluginId,
|
||||||
title: 'not important',
|
title: 'not important',
|
||||||
description: 'not important',
|
description: 'not important',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to asynchronously register component extensions for the same extension point (different plugins)', async () => {
|
it('should be possible to asynchronously register component extensions for the same extension point (different plugins)', async () => {
|
||||||
const pluginId1 = 'grafana-basic-app';
|
const pluginId1 = 'grafana-basic-app';
|
||||||
const pluginId2 = 'grafana-basic-app2';
|
const pluginId2 = 'grafana-basic-app2';
|
||||||
|
const extensionPointId = 'grafana/alerting/home';
|
||||||
const reactiveRegistry = new AddedComponentsRegistry();
|
const reactiveRegistry = new AddedComponentsRegistry();
|
||||||
|
|
||||||
// Register extensions for the first plugin
|
// Register extensions for the first plugin
|
||||||
@ -65,7 +113,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
|
|
||||||
const registry1 = await reactiveRegistry.getState();
|
const registry1 = await reactiveRegistry.getState();
|
||||||
expect(Object.keys(registry1)).toHaveLength(1);
|
expect(Object.keys(registry1)).toHaveLength(1);
|
||||||
expect(registry1['grafana/alerting/home'][0]).toMatchObject({
|
expect(registry1[extensionPointId][0]).toMatchObject({
|
||||||
pluginId: pluginId1,
|
pluginId: pluginId1,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
@ -78,7 +126,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 2 title',
|
title: 'Component 2 title',
|
||||||
description: 'Component 2 description',
|
description: 'Component 2 description',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -86,7 +134,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
|
|
||||||
const registry2 = await reactiveRegistry.getState();
|
const registry2 = await reactiveRegistry.getState();
|
||||||
expect(Object.keys(registry2)).toHaveLength(1);
|
expect(Object.keys(registry2)).toHaveLength(1);
|
||||||
expect(registry2['grafana/alerting/home']).toEqual(
|
expect(registry2[extensionPointId]).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId1,
|
pluginId: pluginId1,
|
||||||
@ -105,6 +153,8 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
it('should be possible to asynchronously register component extensions for a different extension points (different plugin)', async () => {
|
it('should be possible to asynchronously register component extensions for a different extension points (different plugin)', async () => {
|
||||||
const pluginId1 = 'grafana-basic-app';
|
const pluginId1 = 'grafana-basic-app';
|
||||||
const pluginId2 = 'grafana-basic-app2';
|
const pluginId2 = 'grafana-basic-app2';
|
||||||
|
const extensionPointId1 = 'grafana/alerting/home';
|
||||||
|
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||||
const reactiveRegistry = new AddedComponentsRegistry();
|
const reactiveRegistry = new AddedComponentsRegistry();
|
||||||
|
|
||||||
// Register extensions for the first plugin
|
// Register extensions for the first plugin
|
||||||
@ -114,7 +164,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId1],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -122,7 +172,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
|
|
||||||
const registry1 = await reactiveRegistry.getState();
|
const registry1 = await reactiveRegistry.getState();
|
||||||
expect(registry1).toEqual({
|
expect(registry1).toEqual({
|
||||||
'grafana/alerting/home': expect.arrayContaining([
|
[extensionPointId1]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId1,
|
pluginId: pluginId1,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
@ -138,7 +188,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 2 title',
|
title: 'Component 2 title',
|
||||||
description: 'Component 2 description',
|
description: 'Component 2 description',
|
||||||
targets: ['grafana/user/profile/tab'],
|
targets: [extensionPointId2],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -147,14 +197,14 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
const registry2 = await reactiveRegistry.getState();
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
expect(registry2).toEqual({
|
expect(registry2).toEqual({
|
||||||
'grafana/alerting/home': expect.arrayContaining([
|
[extensionPointId1]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId1,
|
pluginId: pluginId1,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
'grafana/user/profile/tab': expect.arrayContaining([
|
[extensionPointId2]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId2,
|
pluginId: pluginId2,
|
||||||
title: 'Component 2 title',
|
title: 'Component 2 title',
|
||||||
@ -165,8 +215,8 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => {
|
it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => {
|
||||||
const pluginId = 'grafana-basic-app';
|
|
||||||
const reactiveRegistry = new AddedComponentsRegistry();
|
const reactiveRegistry = new AddedComponentsRegistry();
|
||||||
|
const extensionPointId = 'grafana/alerting/home';
|
||||||
|
|
||||||
// Register extensions for the first extension point
|
// Register extensions for the first extension point
|
||||||
reactiveRegistry.register({
|
reactiveRegistry.register({
|
||||||
@ -175,20 +225,20 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Component 2 title',
|
title: 'Component 2 title',
|
||||||
description: 'Component 2 description',
|
description: 'Component 2 description',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World2'),
|
component: () => React.createElement('div', null, 'Hello World2'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const registry1 = await reactiveRegistry.getState();
|
const registry1 = await reactiveRegistry.getState();
|
||||||
expect(registry1).toEqual({
|
expect(registry1).toEqual({
|
||||||
'grafana/alerting/home': expect.arrayContaining([
|
[extensionPointId]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
@ -204,8 +254,9 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to register one extension component targeting multiple extension points', async () => {
|
it('should be possible to register one extension component targeting multiple extension points', async () => {
|
||||||
const pluginId = 'grafana-basic-app';
|
|
||||||
const reactiveRegistry = new AddedComponentsRegistry();
|
const reactiveRegistry = new AddedComponentsRegistry();
|
||||||
|
const extensionPointId1 = 'grafana/alerting/home';
|
||||||
|
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||||
|
|
||||||
reactiveRegistry.register({
|
reactiveRegistry.register({
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
@ -213,21 +264,21 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
targets: ['grafana/alerting/home', 'grafana/user/profile/tab'],
|
targets: [extensionPointId1, extensionPointId2],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const registry1 = await reactiveRegistry.getState();
|
const registry1 = await reactiveRegistry.getState();
|
||||||
expect(registry1).toEqual({
|
expect(registry1).toEqual({
|
||||||
'grafana/alerting/home': expect.arrayContaining([
|
[extensionPointId1]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
'grafana/user/profile/tab': expect.arrayContaining([
|
[extensionPointId2]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
@ -239,7 +290,9 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
|
|
||||||
it('should notify subscribers when the registry changes', async () => {
|
it('should notify subscribers when the registry changes', async () => {
|
||||||
const pluginId1 = 'grafana-basic-app';
|
const pluginId1 = 'grafana-basic-app';
|
||||||
const pluginId2 = 'another-plugin';
|
const pluginId2 = 'myorg-extensions-app';
|
||||||
|
const extensionPointId1 = 'grafana/alerting/home';
|
||||||
|
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||||
const reactiveRegistry = new AddedComponentsRegistry();
|
const reactiveRegistry = new AddedComponentsRegistry();
|
||||||
const observable = reactiveRegistry.asObservable();
|
const observable = reactiveRegistry.asObservable();
|
||||||
const subscribeCallback = jest.fn();
|
const subscribeCallback = jest.fn();
|
||||||
@ -252,7 +305,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId1],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -266,7 +319,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
{
|
{
|
||||||
title: 'Component 2 title',
|
title: 'Component 2 title',
|
||||||
description: 'Component 2 description',
|
description: 'Component 2 description',
|
||||||
targets: ['grafana/user/profile/tab'],
|
targets: [extensionPointId2],
|
||||||
component: () => React.createElement('div', null, 'Hello World2'),
|
component: () => React.createElement('div', null, 'Hello World2'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -277,14 +330,14 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
const registry = subscribeCallback.mock.calls[2][0];
|
const registry = subscribeCallback.mock.calls[2][0];
|
||||||
|
|
||||||
expect(registry).toEqual({
|
expect(registry).toEqual({
|
||||||
'grafana/alerting/home': expect.arrayContaining([
|
[extensionPointId1]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId1,
|
pluginId: pluginId1,
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
'grafana/user/profile/tab': expect.arrayContaining([
|
[extensionPointId2]: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
pluginId: pluginId2,
|
pluginId: pluginId2,
|
||||||
title: 'Component 2 title',
|
title: 'Component 2 title',
|
||||||
@ -294,43 +347,24 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip registering component and log a warning when id is not prefixed with plugin id or grafana', async () => {
|
|
||||||
const registry = new AddedComponentsRegistry();
|
|
||||||
registry.register({
|
|
||||||
pluginId: 'grafana-basic-app',
|
|
||||||
configs: [
|
|
||||||
{
|
|
||||||
title: 'Component 1 title',
|
|
||||||
description: 'Component 1 description',
|
|
||||||
targets: ['alerting/home'],
|
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(consoleWarn).toHaveBeenCalledWith(
|
|
||||||
"[Plugin Extensions] Could not register added component with id 'alerting/home'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id or grafana. e.g '<grafana|myorg-basic-app>/my-component-id/v1'."
|
|
||||||
);
|
|
||||||
const currentState = await registry.getState();
|
|
||||||
expect(Object.keys(currentState)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should log a warning when added component id is not suffixed with component version', async () => {
|
it('should log a warning when added component id is not suffixed with component version', async () => {
|
||||||
const registry = new AddedComponentsRegistry();
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const extensionPointId = 'grafana/test/home';
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId: 'grafana-basic-app',
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: 'Component 1 description',
|
description: 'Component 1 description',
|
||||||
targets: ['grafana/test/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(consoleWarn).toHaveBeenCalledWith(
|
expect(consoleWarn).toHaveBeenCalledWith(
|
||||||
"[Plugin Extensions] Added component with id 'grafana/test/home' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'."
|
`[Plugin Extensions] Added component "Component 1 title": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
|
||||||
);
|
);
|
||||||
const currentState = await registry.getState();
|
const currentState = await registry.getState();
|
||||||
expect(Object.keys(currentState)).toHaveLength(1);
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
@ -338,13 +372,15 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
|
|
||||||
it('should not register component when description is missing', async () => {
|
it('should not register component when description is missing', async () => {
|
||||||
const registry = new AddedComponentsRegistry();
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const extensionPointId = 'grafana/alerting/home';
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId: 'grafana-basic-app',
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: '',
|
description: '',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -359,13 +395,15 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
|
|
||||||
it('should not register component when title is missing', async () => {
|
it('should not register component when title is missing', async () => {
|
||||||
const registry = new AddedComponentsRegistry();
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const extensionPointId = 'grafana/alerting/home';
|
||||||
|
|
||||||
registry.register({
|
registry.register({
|
||||||
pluginId: 'grafana-basic-app',
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: '',
|
description: '',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -382,15 +420,16 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
it('should not be possible to register a component on a read-only registry', async () => {
|
it('should not be possible to register a component on a read-only registry', async () => {
|
||||||
const registry = new AddedComponentsRegistry();
|
const registry = new AddedComponentsRegistry();
|
||||||
const readOnlyRegistry = registry.readOnly();
|
const readOnlyRegistry = registry.readOnly();
|
||||||
|
const extensionPointId = 'grafana/alerting/home';
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
readOnlyRegistry.register({
|
readOnlyRegistry.register({
|
||||||
pluginId: 'grafana-basic-app',
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
title: 'Component 1 title',
|
title: 'Component 1 title',
|
||||||
description: '',
|
description: '',
|
||||||
targets: ['grafana/alerting/home'],
|
targets: [extensionPointId],
|
||||||
component: () => React.createElement('div', null, 'Hello World1'),
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -434,4 +473,105 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['grafana/alerting/home']);
|
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['grafana/alerting/home']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not register a component added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
targets: ['grafana/alerting/home'],
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedComponents = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
expect(consoleWarn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a component added by a core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
targets: ['grafana/alerting/home'],
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId: 'grafana',
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a component added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||||
|
// Production mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
|
||||||
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
targets: ['grafana/alerting/home'],
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedComponents = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a component added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
targets: ['grafana/alerting/home'],
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedComponents = [componentConfig];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,13 +2,8 @@ import { ReplaySubject } from 'rxjs';
|
|||||||
|
|
||||||
import { PluginExtensionAddedComponentConfig } from '@grafana/data';
|
import { PluginExtensionAddedComponentConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { logWarning, wrapWithPluginContext } from '../utils';
|
import { isAddedComponentMetaInfoMissing, isGrafanaDevMode, logWarning, wrapWithPluginContext } from '../utils';
|
||||||
import {
|
import { extensionPointEndsWithVersion, isGrafanaCoreExtensionPoint, isReactComponent } from '../validators';
|
||||||
extensionPointEndsWithVersion,
|
|
||||||
isExtensionPointIdValid,
|
|
||||||
isGrafanaCoreExtensionPoint,
|
|
||||||
isReactComponent,
|
|
||||||
} from '../validators';
|
|
||||||
|
|
||||||
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
|
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
|
||||||
|
|
||||||
@ -56,18 +51,15 @@ export class AddedComponentsRegistry extends Registry<
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedComponentMetaInfoMissing(pluginId, config)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets];
|
const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets];
|
||||||
for (const extensionPointId of extensionPointIds) {
|
for (const extensionPointId of extensionPointIds) {
|
||||||
if (!isExtensionPointIdValid(pluginId, extensionPointId)) {
|
|
||||||
logWarning(
|
|
||||||
`Could not register added component with id '${extensionPointId}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id or grafana. e.g '<grafana|myorg-basic-app>/my-component-id/v1'.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) {
|
if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) {
|
||||||
logWarning(
|
logWarning(
|
||||||
`Added component with id '${extensionPointId}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.`
|
`Added component "${config.title}": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,61 @@
|
|||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
|
||||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||||
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
...jest.requireActual('../utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AddedLinksRegistry', () => {
|
describe('AddedLinksRegistry', () => {
|
||||||
|
const originalApps = config.apps;
|
||||||
const consoleWarn = jest.fn();
|
const consoleWarn = jest.fn();
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.console.warn = consoleWarn;
|
global.console.warn = consoleWarn;
|
||||||
consoleWarn.mockReset();
|
consoleWarn.mockReset();
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty registry when no extensions registered', async () => {
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
@ -19,7 +66,6 @@ describe('AddedLinksRegistry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be possible to register link extensions in the registry', async () => {
|
it('should be possible to register link extensions in the registry', async () => {
|
||||||
const pluginId = 'grafana-basic-app';
|
|
||||||
const addedLinksRegistry = new AddedLinksRegistry();
|
const addedLinksRegistry = new AddedLinksRegistry();
|
||||||
|
|
||||||
addedLinksRegistry.register({
|
addedLinksRegistry.register({
|
||||||
@ -580,4 +626,109 @@ describe('AddedLinksRegistry', () => {
|
|||||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['plugins/myorg-basic-app/start']);
|
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['plugins/myorg-basic-app/start']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not register a link added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedLinksRegistry();
|
||||||
|
const linkConfig = {
|
||||||
|
title: 'Link 1',
|
||||||
|
description: 'Link 1 description',
|
||||||
|
path: `/a/${pluginId}/declare-incident`,
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
configure: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedLinks = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [linkConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
expect(consoleWarn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a link added by core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedLinksRegistry();
|
||||||
|
const linkConfig = {
|
||||||
|
title: 'Link 1',
|
||||||
|
description: 'Link 1 description',
|
||||||
|
path: `/a/grafana/declare-incident`,
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
configure: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId: 'grafana',
|
||||||
|
configs: [linkConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a link added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||||
|
// Production mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
|
||||||
|
const registry = new AddedLinksRegistry();
|
||||||
|
const linkConfig = {
|
||||||
|
title: 'Link 1',
|
||||||
|
description: 'Link 1 description',
|
||||||
|
path: `/a/${pluginId}/declare-incident`,
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
configure: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedLinks = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [linkConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a link added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedLinksRegistry();
|
||||||
|
const linkConfig = {
|
||||||
|
title: 'Link 1',
|
||||||
|
description: 'Link 1 description',
|
||||||
|
path: `/a/${pluginId}/declare-incident`,
|
||||||
|
targets: ['grafana/dashboard/panel/menu'],
|
||||||
|
configure: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedLinks = [linkConfig];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [linkConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,11 +3,10 @@ import { ReplaySubject } from 'rxjs';
|
|||||||
import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data';
|
import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data';
|
||||||
import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/src/types/pluginExtensions';
|
import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/src/types/pluginExtensions';
|
||||||
|
|
||||||
import { logWarning } from '../utils';
|
import { isAddedLinkMetaInfoMissing, isGrafanaDevMode, logWarning } from '../utils';
|
||||||
import {
|
import {
|
||||||
extensionPointEndsWithVersion,
|
extensionPointEndsWithVersion,
|
||||||
isConfigureFnValid,
|
isConfigureFnValid,
|
||||||
isExtensionPointIdValid,
|
|
||||||
isGrafanaCoreExtensionPoint,
|
isGrafanaCoreExtensionPoint,
|
||||||
isLinkPathValid,
|
isLinkPathValid,
|
||||||
} from '../validators';
|
} from '../validators';
|
||||||
@ -73,18 +72,15 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedLinkMetaInfoMissing(pluginId, config)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const extensionPointIds = Array.isArray(targets) ? targets : [targets];
|
const extensionPointIds = Array.isArray(targets) ? targets : [targets];
|
||||||
for (const extensionPointId of extensionPointIds) {
|
for (const extensionPointId of extensionPointIds) {
|
||||||
if (!isExtensionPointIdValid(pluginId, extensionPointId)) {
|
|
||||||
logWarning(
|
|
||||||
`Could not register added link with id '${extensionPointId}'. Reason: Target extension point id must start with grafana, plugins or plugin id.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) {
|
if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) {
|
||||||
logWarning(
|
logWarning(
|
||||||
`Added component with id '${extensionPointId}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.`
|
`Added link "${config.title}: it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,62 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
|
||||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||||
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
...jest.requireActual('../utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('ExposedComponentsRegistry', () => {
|
describe('ExposedComponentsRegistry', () => {
|
||||||
const consoleWarn = jest.fn();
|
const consoleWarn = jest.fn();
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.console.warn = consoleWarn;
|
global.console.warn = consoleWarn;
|
||||||
consoleWarn.mockReset();
|
consoleWarn.mockReset();
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty registry when no exposed components have been registered', async () => {
|
it('should return empty registry when no exposed components have been registered', async () => {
|
||||||
@ -397,4 +444,105 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual([`${pluginId}/hello-world/v1`]);
|
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual([`${pluginId}/hello-world/v1`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not register an exposed component added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new ExposedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
id: `${pluginId}/exposed-component/v1`,
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.exposedComponents = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
expect(consoleWarn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register an exposed component added by a core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new ExposedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
id: `${pluginId}/exposed-component/v1`,
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId: 'grafana',
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register an exposed component added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||||
|
// Production mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
|
||||||
|
const registry = new ExposedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
id: `${pluginId}/exposed-component/v1`,
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.exposedComponents = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register an exposed component added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new ExposedComponentsRegistry();
|
||||||
|
const componentConfig = {
|
||||||
|
id: `${pluginId}/exposed-component/v1`,
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
component: () => React.createElement('div', null, 'Hello World1'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.exposedComponents = [componentConfig];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [componentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(consoleWarn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { ReplaySubject } from 'rxjs';
|
|||||||
|
|
||||||
import { PluginExtensionExposedComponentConfig } from '@grafana/data';
|
import { PluginExtensionExposedComponentConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { logWarning } from '../utils';
|
import { isExposedComponentMetaInfoMissing, isGrafanaDevMode, logWarning } from '../utils';
|
||||||
import { extensionPointEndsWithVersion } from '../validators';
|
import { extensionPointEndsWithVersion } from '../validators';
|
||||||
|
|
||||||
import { Registry, RegistryType, PluginExtensionConfigs } from './Registry';
|
import { Registry, RegistryType, PluginExtensionConfigs } from './Registry';
|
||||||
@ -68,6 +68,10 @@ export class ExposedComponentsRegistry extends Registry<
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pluginId !== 'grafana' && isGrafanaDevMode() && isExposedComponentMetaInfoMissing(pluginId, config)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
registry[id] = { ...config, pluginId };
|
registry[id] = { ...config, pluginId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
|
import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import { setupPluginExtensionRegistries } from './registry/setup';
|
import { setupPluginExtensionRegistries } from './registry/setup';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
import { usePluginComponent } from './usePluginComponent';
|
import { usePluginComponent } from './usePluginComponent';
|
||||||
import * as utils from './utils';
|
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||||
|
|
||||||
const wrapWithPluginContext = jest.spyOn(utils, 'wrapWithPluginContext');
|
|
||||||
|
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn().mockResolvedValue({
|
getPluginSettings: jest.fn().mockResolvedValue({
|
||||||
@ -20,20 +21,111 @@ jest.mock('app/features/plugins/pluginSettings', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('./utils', () => ({
|
||||||
|
...jest.requireActual('./utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('usePluginComponent()', () => {
|
describe('usePluginComponent()', () => {
|
||||||
let registries: PluginExtensionRegistries;
|
let registries: PluginExtensionRegistries;
|
||||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||||
|
let pluginMeta: PluginMeta;
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const exposedComponentId = `${pluginId}/exposed-component/v1`;
|
||||||
|
const exposedComponentConfig = {
|
||||||
|
id: exposedComponentId,
|
||||||
|
title: 'Exposed component',
|
||||||
|
description: 'Exposed component description',
|
||||||
|
component: () => <div>Hello World</div>,
|
||||||
|
};
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
// This is necessary, so we can register exposed components to the registry during the tests
|
||||||
|
// (Otherwise the registry would reject it in the imitated production mode)
|
||||||
|
exposedComponents: [exposedComponentConfig],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
registries = setupPluginExtensionRegistries();
|
registries = setupPluginExtensionRegistries();
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
wrapWithPluginContext.mockClear();
|
jest.mocked(wrapWithPluginContext).mockClear();
|
||||||
|
|
||||||
|
pluginMeta = {
|
||||||
|
id: pluginId,
|
||||||
|
name: 'Extensions App',
|
||||||
|
type: PluginType.app,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'MyOrg',
|
||||||
|
},
|
||||||
|
description: 'App for testing extensions',
|
||||||
|
links: [],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: [],
|
||||||
|
updated: '2023-10-26T18:25:01Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
|
||||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
it('should return null if there are no component exposed for the id', () => {
|
it('should return null if there are no component exposed for the id', () => {
|
||||||
const { result } = renderHook(() => usePluginComponent('foo/bar'), { wrapper });
|
const { result } = renderHook(() => usePluginComponent('foo/bar'), { wrapper });
|
||||||
|
|
||||||
@ -42,15 +134,12 @@ describe('usePluginComponent()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return component, that can be rendered, from the registry', async () => {
|
it('should return component, that can be rendered, from the registry', async () => {
|
||||||
const id = 'my-app-plugin/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
|
|
||||||
registries.exposedComponentsRegistry.register({
|
registries.exposedComponentsRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [{ id, title: 'not important', description: 'not important', component: () => <div>Hello World</div> }],
|
configs: [exposedComponentConfig],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() => usePluginComponent(id), { wrapper });
|
const { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
|
||||||
const Component = result.current.component;
|
const Component = result.current.component;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -63,9 +152,7 @@ describe('usePluginComponent()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should dynamically update when component is registered to the registry', async () => {
|
it('should dynamically update when component is registered to the registry', async () => {
|
||||||
const id = 'my-app-plugin/foo/bar/v1';
|
const { result, rerender } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
const { result, rerender } = renderHook(() => usePluginComponent(id), { wrapper });
|
|
||||||
|
|
||||||
// No extensions yet
|
// No extensions yet
|
||||||
expect(result.current.component).toBeNull();
|
expect(result.current.component).toBeNull();
|
||||||
@ -75,14 +162,7 @@ describe('usePluginComponent()', () => {
|
|||||||
act(() => {
|
act(() => {
|
||||||
registries.exposedComponentsRegistry.register({
|
registries.exposedComponentsRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [exposedComponentConfig],
|
||||||
{
|
|
||||||
id,
|
|
||||||
title: 'not important',
|
|
||||||
description: 'not important',
|
|
||||||
component: () => <div>Hello World</div>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,26 +181,132 @@ describe('usePluginComponent()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only render the hook once', async () => {
|
it('should only render the hook once', async () => {
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
const id = `${pluginId}/foo/v1`;
|
|
||||||
|
|
||||||
// Add extensions to the registry
|
// Add extensions to the registry
|
||||||
act(() => {
|
act(() => {
|
||||||
registries.exposedComponentsRegistry.register({
|
registries.exposedComponentsRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [exposedComponentConfig],
|
||||||
{
|
|
||||||
id,
|
|
||||||
title: 'not important',
|
|
||||||
description: 'not important',
|
|
||||||
component: () => <div>Hello World</div>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapWithPluginContext).toHaveBeenCalledTimes(0);
|
expect(wrapWithPluginContext).toHaveBeenCalledTimes(0);
|
||||||
renderHook(() => usePluginComponent(id), { wrapper });
|
renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
|
||||||
await waitFor(() => expect(wrapWithPluginContext).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(wrapWithPluginContext).toHaveBeenCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not validate the meta-info in production mode', () => {
|
||||||
|
// Empty list of exposed component ids in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
dependencies: {
|
||||||
|
...pluginMeta.dependencies!,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
registries.exposedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [exposedComponentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an exposed component that is not defined in the plugin meta
|
||||||
|
// (No restrictions due to isGrafanaDevMode() = false)
|
||||||
|
let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
|
||||||
|
expect(result.current.component).not.toBe(null);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the meta-info in core Grafana', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// No plugin context -> used in Grafana core
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
registries.exposedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [exposedComponentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
// (No restrictions due to isGrafanaDevMode() = false)
|
||||||
|
let { result } = renderHook(() => usePluginComponent(exposedComponentId), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.component).not.toBe(null);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate the meta-info in dev mode and if inside a plugin', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// Empty list of exposed component ids in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
dependencies: {
|
||||||
|
...pluginMeta.dependencies!,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
registries.exposedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [exposedComponentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shouldn't return the component, as it's not present in the plugin.json dependencies
|
||||||
|
let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
|
||||||
|
expect(result.current.component).toBe(null);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exposed component if the meta-info is correct and in dev mode', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
dependencies: {
|
||||||
|
...pluginMeta.dependencies!,
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [exposedComponentId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
registries.exposedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [exposedComponentConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
|
||||||
|
expect(result.current.component).not.toBe(null);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,33 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
import { UsePluginComponentResult } from '@grafana/runtime';
|
import { usePluginContext } from '@grafana/data';
|
||||||
|
import { logWarning, UsePluginComponentResult } from '@grafana/runtime';
|
||||||
|
|
||||||
import { useExposedComponentsRegistry } from './ExtensionRegistriesContext';
|
import { useExposedComponentsRegistry } from './ExtensionRegistriesContext';
|
||||||
import { wrapWithPluginContext } from './utils';
|
import { isExposedComponentDependencyMissing, isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||||
|
|
||||||
// Returns a component exposed by a plugin.
|
// Returns a component exposed by a plugin.
|
||||||
// (Exposed components can be defined in plugins by calling .exposeComponent() on the AppPlugin instance.)
|
// (Exposed components can be defined in plugins by calling .exposeComponent() on the AppPlugin instance.)
|
||||||
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
|
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
|
||||||
const registry = useExposedComponentsRegistry();
|
const registry = useExposedComponentsRegistry();
|
||||||
const registryState = useObservable(registry.asObservable());
|
const registryState = useObservable(registry.asObservable());
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
|
|
||||||
return useMemo(() => {
|
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;
|
||||||
|
|
||||||
|
if (enableRestrictions && isExposedComponentDependencyMissing(id, pluginContext)) {
|
||||||
|
logWarning(
|
||||||
|
`usePluginComponent("${id}") - The exposed component ("${id}") is missing from the dependencies[] in the "plugin.json" file.`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!registryState?.[id]) {
|
if (!registryState?.[id]) {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -26,5 +41,5 @@ export function usePluginComponent<Props extends object = {}>(id: string): UsePl
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
component: wrapWithPluginContext(registryItem.pluginId, registryItem.component),
|
component: wrapWithPluginContext(registryItem.pluginId, registryItem.component),
|
||||||
};
|
};
|
||||||
}, [id, registryState]);
|
}, [id, pluginContext, registryState]);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
|
import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data';
|
||||||
|
|
||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import { setupPluginExtensionRegistries } from './registry/setup';
|
import { setupPluginExtensionRegistries } from './registry/setup';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
import { usePluginComponents } from './usePluginComponents';
|
import { usePluginComponents } from './usePluginComponents';
|
||||||
import * as utils from './utils';
|
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
|
||||||
|
|
||||||
const wrapWithPluginContext = jest.spyOn(utils, 'wrapWithPluginContext');
|
|
||||||
|
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn().mockResolvedValue({
|
getPluginSettings: jest.fn().mockResolvedValue({
|
||||||
@ -20,17 +20,69 @@ jest.mock('app/features/plugins/pluginSettings', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('./utils', () => ({
|
||||||
|
...jest.requireActual('./utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('usePluginComponents()', () => {
|
describe('usePluginComponents()', () => {
|
||||||
let registries: PluginExtensionRegistries;
|
let registries: PluginExtensionRegistries;
|
||||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||||
|
let pluginMeta: PluginMeta;
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
registries = setupPluginExtensionRegistries();
|
registries = setupPluginExtensionRegistries();
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
wrapWithPluginContext.mockClear();
|
jest.mocked(wrapWithPluginContext).mockClear();
|
||||||
|
|
||||||
|
pluginMeta = {
|
||||||
|
id: pluginId,
|
||||||
|
name: 'Extensions App',
|
||||||
|
type: PluginType.app,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'MyOrg',
|
||||||
|
},
|
||||||
|
description: 'App for testing extensions',
|
||||||
|
links: [],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: [],
|
||||||
|
updated: '2023-10-26T18:25:01Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
<PluginContextProvider meta={pluginMeta}>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,9 +99,6 @@ describe('usePluginComponents()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only return the plugin extension components for the given extension point ids', async () => {
|
it('should only return the plugin extension components for the given extension point ids', async () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
|
|
||||||
registries.addedComponentsRegistry.register({
|
registries.addedComponentsRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
@ -81,14 +130,13 @@ describe('usePluginComponents()', () => {
|
|||||||
act(() => {
|
act(() => {
|
||||||
render(result.current.components.map((Component, index) => <Component key={index} />));
|
render(result.current.components.map((Component, index) => <Component key={index} />));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await screen.findByText('Hello World1')).toBeVisible();
|
expect(await screen.findByText('Hello World1')).toBeVisible();
|
||||||
expect(await screen.findByText('Hello World2')).toBeVisible();
|
expect(await screen.findByText('Hello World2')).toBeVisible();
|
||||||
expect(await screen.queryByText('Hello World3')).toBeNull();
|
expect(screen.queryByText('Hello World3')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dynamically update the extensions registered for a certain extension point', () => {
|
it('should dynamically update the extensions registered for a certain extension point', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper });
|
let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper });
|
||||||
|
|
||||||
// No extensions yet
|
// No extensions yet
|
||||||
@ -128,8 +176,7 @@ describe('usePluginComponents()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should honour the limitPerPlugin arg if its set', () => {
|
it('should honour the limitPerPlugin arg if its set', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
const plugins = ['my-awesome1-app', 'my-awesome2-app', 'my-awesome3-app'];
|
||||||
const plugins = ['my-app-plugin1', 'my-app-plugin2', 'my-app-plugin3'];
|
|
||||||
let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId, limitPerPlugin: 2 }), {
|
let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId, limitPerPlugin: 2 }), {
|
||||||
wrapper,
|
wrapper,
|
||||||
});
|
});
|
||||||
@ -144,19 +191,19 @@ describe('usePluginComponents()', () => {
|
|||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
targets: extensionPointId,
|
targets: [extensionPointId],
|
||||||
title: '1',
|
title: '1',
|
||||||
description: '1',
|
description: '1',
|
||||||
component: () => <div>Hello World1</div>,
|
component: () => <div>Hello World1</div>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
targets: extensionPointId,
|
targets: [extensionPointId],
|
||||||
title: '2',
|
title: '2',
|
||||||
description: '2',
|
description: '2',
|
||||||
component: () => <div>Hello World2</div>,
|
component: () => <div>Hello World2</div>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
targets: extensionPointId,
|
targets: [extensionPointId],
|
||||||
title: '3',
|
title: '3',
|
||||||
description: '3',
|
description: '3',
|
||||||
component: () => <div>Hello World3</div>,
|
component: () => <div>Hello World3</div>,
|
||||||
@ -171,4 +218,191 @@ describe('usePluginComponents()', () => {
|
|||||||
|
|
||||||
expect(result.current.components.length).toBe(6);
|
expect(result.current.components.length).toBe(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point meta-info in production mode', () => {
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
registries.addedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: extensionPointId,
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
component: () => <div>Component</div>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
// (No restrictions due to isGrafanaDevMode() = false)
|
||||||
|
let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper });
|
||||||
|
expect(result.current.components.length).toBe(1);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point id in production mode', () => {
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
// (No restrictions due to isGrafanaDevMode() = false)
|
||||||
|
let { result } = renderHook(() => usePluginComponents({ extensionPointId: 'invalid-extension-point-id' }), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
expect(result.current.components.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point meta-info if used in Grafana core (no plugin context)', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// No plugin context -> used in Grafana core
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adding an extension to the extension point
|
||||||
|
registries.addedComponentsRegistry.register({
|
||||||
|
pluginId: 'grafana', // Only core Grafana can register extensions without a plugin context
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: 'grafana/extension-point/v1',
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
component: () => <div>Component</div>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let { result } = renderHook(() => usePluginComponents({ extensionPointId: 'grafana/extension-point/v1' }), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
expect(result.current.components.length).toBe(1);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point id if used in Grafana core (no plugin context)', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// No plugin context -> used in Grafana core
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
let { result } = renderHook(() => usePluginComponents({ extensionPointId: 'invalid-extension-point-id' }), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
expect(result.current.components.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate if the extension point meta-info is correct if in dev-mode and used by a plugin', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adding an extension to the extension point - it should not be returned later
|
||||||
|
registries.addedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: extensionPointId,
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
component: () => <div>Component</div>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper });
|
||||||
|
expect(result.current.components.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log a warning if the extension point meta-info is correct if in dev-mode and used by a plugin', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// The extension point is listed in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [
|
||||||
|
{
|
||||||
|
id: extensionPointId,
|
||||||
|
title: 'Extension point',
|
||||||
|
description: 'Extension point description',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adding an extension to the extension point - it should not be returned later
|
||||||
|
registries.addedComponentsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: extensionPointId,
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
component: () => <div>Component</div>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper });
|
||||||
|
expect(result.current.components.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
|
import { usePluginContext } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
UsePluginComponentOptions,
|
UsePluginComponentOptions,
|
||||||
UsePluginComponentsResult,
|
UsePluginComponentsResult,
|
||||||
} from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions';
|
} from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions';
|
||||||
|
|
||||||
import { useAddedComponentsRegistry } from './ExtensionRegistriesContext';
|
import { useAddedComponentsRegistry } from './ExtensionRegistriesContext';
|
||||||
|
import { isExtensionPointMetaInfoMissing, isGrafanaDevMode, logWarning } from './utils';
|
||||||
|
import { isExtensionPointIdValid } from './validators';
|
||||||
|
|
||||||
// Returns an array of component extensions for the given extension point
|
// Returns an array of component extensions for the given extension point
|
||||||
export function usePluginComponents<Props extends object = {}>({
|
export function usePluginComponents<Props extends object = {}>({
|
||||||
@ -15,10 +18,34 @@ export function usePluginComponents<Props extends object = {}>({
|
|||||||
}: UsePluginComponentOptions): UsePluginComponentsResult<Props> {
|
}: UsePluginComponentOptions): UsePluginComponentsResult<Props> {
|
||||||
const registry = useAddedComponentsRegistry();
|
const registry = useAddedComponentsRegistry();
|
||||||
const registryState = useObservable(registry.asObservable());
|
const registryState = useObservable(registry.asObservable());
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
|
|
||||||
return useMemo(() => {
|
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;
|
||||||
const components: Array<React.ComponentType<Props>> = [];
|
const components: Array<React.ComponentType<Props>> = [];
|
||||||
const extensionsByPlugin: Record<string, number> = {};
|
const extensionsByPlugin: Record<string, number> = {};
|
||||||
|
const pluginId = pluginContext?.meta.id ?? '';
|
||||||
|
|
||||||
|
if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) {
|
||||||
|
logWarning(
|
||||||
|
`Extension point usePluginComponents("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
components: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) {
|
||||||
|
logWarning(
|
||||||
|
`usePluginComponents("${extensionPointId}") - The extension point is missing from the "plugin.json" file.`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
components: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (const registryItem of registryState?.[extensionPointId] ?? []) {
|
for (const registryItem of registryState?.[extensionPointId] ?? []) {
|
||||||
const { pluginId } = registryItem;
|
const { pluginId } = registryItem;
|
||||||
@ -40,5 +67,5 @@ export function usePluginComponents<Props extends object = {}>({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
components,
|
components,
|
||||||
};
|
};
|
||||||
}, [extensionPointId, limitPerPlugin, registryState]);
|
}, [extensionPointId, limitPerPlugin, pluginContext, registryState]);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import { createUsePluginExtensions } from './usePluginExtensions';
|
|||||||
|
|
||||||
describe('usePluginExtensions()', () => {
|
describe('usePluginExtensions()', () => {
|
||||||
let registries: PluginExtensionRegistries;
|
let registries: PluginExtensionRegistries;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
registries = {
|
registries = {
|
||||||
@ -30,9 +32,6 @@ describe('usePluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return the plugin link extensions from the registry', () => {
|
it('should return the plugin link extensions from the registry', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
|
|
||||||
registries.addedLinksRegistry.register({
|
registries.addedLinksRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
@ -60,21 +59,19 @@ describe('usePluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return the plugin component extensions from the registry', () => {
|
it('should return the plugin component extensions from the registry', () => {
|
||||||
const linkExtensionPointId = 'plugins/foo/bar/v1';
|
const componentExtensionPointId = `${pluginId}/component/v1`;
|
||||||
const componentExtensionPointId = 'plugins/component/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
|
|
||||||
registries.addedLinksRegistry.register({
|
registries.addedLinksRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
targets: linkExtensionPointId,
|
targets: extensionPointId,
|
||||||
title: '1',
|
title: '1',
|
||||||
description: '1',
|
description: '1',
|
||||||
path: `/a/${pluginId}/2`,
|
path: `/a/${pluginId}/2`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
targets: linkExtensionPointId,
|
targets: extensionPointId,
|
||||||
title: '2',
|
title: '2',
|
||||||
description: '2',
|
description: '2',
|
||||||
path: `/a/${pluginId}/2`,
|
path: `/a/${pluginId}/2`,
|
||||||
@ -109,8 +106,6 @@ describe('usePluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should dynamically update the extensions registered for a certain extension point', () => {
|
it('should dynamically update the extensions registered for a certain extension point', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
const usePluginExtensions = createUsePluginExtensions(registries);
|
const usePluginExtensions = createUsePluginExtensions(registries);
|
||||||
let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
let { result, rerender } = renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||||
|
|
||||||
@ -149,7 +144,6 @@ describe('usePluginExtensions()', () => {
|
|||||||
it('should only render the hook once', () => {
|
it('should only render the hook once', () => {
|
||||||
const addedComponentsRegistrySpy = jest.spyOn(registries.addedComponentsRegistry, 'asObservable');
|
const addedComponentsRegistrySpy = jest.spyOn(registries.addedComponentsRegistry, 'asObservable');
|
||||||
const addedLinksRegistrySpy = jest.spyOn(registries.addedLinksRegistry, 'asObservable');
|
const addedLinksRegistrySpy = jest.spyOn(registries.addedLinksRegistry, 'asObservable');
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const usePluginExtensions = createUsePluginExtensions(registries);
|
const usePluginExtensions = createUsePluginExtensions(registries);
|
||||||
|
|
||||||
renderHook(() => usePluginExtensions({ extensionPointId }));
|
renderHook(() => usePluginExtensions({ extensionPointId }));
|
||||||
@ -158,8 +152,6 @@ describe('usePluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return the same extensions object if the context object is the same', async () => {
|
it('should return the same extensions object if the context object is the same', async () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
const usePluginExtensions = createUsePluginExtensions(registries);
|
const usePluginExtensions = createUsePluginExtensions(registries);
|
||||||
|
|
||||||
// Add extensions to the registry
|
// Add extensions to the registry
|
||||||
@ -200,8 +192,6 @@ describe('usePluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a new extensions object if the context object is different', () => {
|
it('should return a new extensions object if the context object is different', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
const usePluginExtensions = createUsePluginExtensions(registries);
|
const usePluginExtensions = createUsePluginExtensions(registries);
|
||||||
|
|
||||||
// Add extensions to the registry
|
// Add extensions to the registry
|
||||||
@ -232,8 +222,6 @@ describe('usePluginExtensions()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a new extensions object if the registry changes but the context object is the same', () => {
|
it('should return a new extensions object if the registry changes but the context object is the same', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
const context = {};
|
const context = {};
|
||||||
const usePluginExtensions = createUsePluginExtensions(registries);
|
const usePluginExtensions = createUsePluginExtensions(registries);
|
||||||
|
|
||||||
|
@ -1,31 +1,59 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
import { PluginExtension } from '@grafana/data';
|
import { PluginExtension, usePluginContext } from '@grafana/data';
|
||||||
import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/runtime';
|
import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/runtime';
|
||||||
import { useSidecar } from 'app/core/context/SidecarContext';
|
import { useSidecar } from 'app/core/context/SidecarContext';
|
||||||
|
|
||||||
import { getPluginExtensions } from './getPluginExtensions';
|
import { getPluginExtensions } from './getPluginExtensions';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
|
import { isExtensionPointMetaInfoMissing, isGrafanaDevMode, logWarning } from './utils';
|
||||||
|
import { isExtensionPointIdValid } from './validators';
|
||||||
|
|
||||||
export function createUsePluginExtensions(registries: PluginExtensionRegistries) {
|
export function createUsePluginExtensions(registries: PluginExtensionRegistries) {
|
||||||
const observableAddedComponentsRegistry = registries.addedComponentsRegistry.asObservable();
|
const observableAddedComponentsRegistry = registries.addedComponentsRegistry.asObservable();
|
||||||
const observableAddedLinksRegistry = registries.addedLinksRegistry.asObservable();
|
const observableAddedLinksRegistry = registries.addedLinksRegistry.asObservable();
|
||||||
|
|
||||||
return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult<PluginExtension> {
|
return function usePluginExtensions(options: GetPluginExtensionsOptions): UsePluginExtensionsResult<PluginExtension> {
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
const addedComponentsRegistry = useObservable(observableAddedComponentsRegistry);
|
const addedComponentsRegistry = useObservable(observableAddedComponentsRegistry);
|
||||||
const addedLinksRegistry = useObservable(observableAddedLinksRegistry);
|
const addedLinksRegistry = useObservable(observableAddedLinksRegistry);
|
||||||
const { activePluginId } = useSidecar();
|
const { activePluginId } = useSidecar();
|
||||||
|
const { extensionPointId, context, limitPerPlugin } = options;
|
||||||
|
|
||||||
const { extensions } = useMemo(() => {
|
const { extensions } = 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 ?? '';
|
||||||
|
|
||||||
if (!addedLinksRegistry && !addedComponentsRegistry) {
|
if (!addedLinksRegistry && !addedComponentsRegistry) {
|
||||||
return { extensions: [], isLoading: false };
|
return { extensions: [], isLoading: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) {
|
||||||
|
logWarning(
|
||||||
|
`Extension point usePluginExtensions("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
extensions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) {
|
||||||
|
logWarning(
|
||||||
|
`Invalid extension point. Reason: The extension point is not declared in the "plugin.json" file. ExtensionPointId: "${extensionPointId}"`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
extensions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return getPluginExtensions({
|
return getPluginExtensions({
|
||||||
extensionPointId: options.extensionPointId,
|
extensionPointId,
|
||||||
context: options.context,
|
context,
|
||||||
limitPerPlugin: options.limitPerPlugin,
|
limitPerPlugin,
|
||||||
addedComponentsRegistry,
|
addedComponentsRegistry,
|
||||||
addedLinksRegistry,
|
addedLinksRegistry,
|
||||||
});
|
});
|
||||||
@ -36,10 +64,11 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries)
|
|||||||
}, [
|
}, [
|
||||||
addedLinksRegistry,
|
addedLinksRegistry,
|
||||||
addedComponentsRegistry,
|
addedComponentsRegistry,
|
||||||
options.extensionPointId,
|
extensionPointId,
|
||||||
options.context,
|
context,
|
||||||
options.limitPerPlugin,
|
limitPerPlugin,
|
||||||
activePluginId,
|
activePluginId,
|
||||||
|
pluginContext,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { extensions, isLoading: false };
|
return { extensions, isLoading: false };
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { act } from '@testing-library/react';
|
import { act } from '@testing-library/react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
|
import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data';
|
||||||
|
|
||||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||||
import { setupPluginExtensionRegistries } from './registry/setup';
|
import { setupPluginExtensionRegistries } from './registry/setup';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
import { usePluginLinks } from './usePluginLinks';
|
import { usePluginLinks } from './usePluginLinks';
|
||||||
|
import { isGrafanaDevMode } from './utils';
|
||||||
|
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
getPluginSettings: jest.fn().mockResolvedValue({
|
getPluginSettings: jest.fn().mockResolvedValue({
|
||||||
@ -17,15 +20,66 @@ jest.mock('app/features/plugins/pluginSettings', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('./utils', () => ({
|
||||||
|
...jest.requireActual('./utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('usePluginLinks()', () => {
|
describe('usePluginLinks()', () => {
|
||||||
let registries: PluginExtensionRegistries;
|
let registries: PluginExtensionRegistries;
|
||||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||||
|
let pluginMeta: PluginMeta;
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
registries = setupPluginExtensionRegistries();
|
registries = setupPluginExtensionRegistries();
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
|
pluginMeta = {
|
||||||
|
id: pluginId,
|
||||||
|
name: 'Extensions App',
|
||||||
|
type: PluginType.app,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'MyOrg',
|
||||||
|
},
|
||||||
|
description: 'App for testing extensions',
|
||||||
|
links: [],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: [],
|
||||||
|
updated: '2023-10-26T18:25:01Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
<PluginContextProvider meta={pluginMeta}>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -42,9 +96,6 @@ describe('usePluginLinks()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only return the link extensions for the given extension point ids', async () => {
|
it('should only return the link extensions for the given extension point ids', async () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
|
|
||||||
registries.addedLinksRegistry.register({
|
registries.addedLinksRegistry.register({
|
||||||
pluginId,
|
pluginId,
|
||||||
configs: [
|
configs: [
|
||||||
@ -77,8 +128,6 @@ describe('usePluginLinks()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should dynamically update the extensions registered for a certain extension point', () => {
|
it('should dynamically update the extensions registered for a certain extension point', () => {
|
||||||
const extensionPointId = 'plugins/foo/bar/v1';
|
|
||||||
const pluginId = 'my-app-plugin';
|
|
||||||
let { result, rerender } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper });
|
let { result, rerender } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper });
|
||||||
|
|
||||||
// No extensions yet
|
// No extensions yet
|
||||||
@ -112,4 +161,179 @@ describe('usePluginLinks()', () => {
|
|||||||
expect(result.current.links[0].title).toBe('1');
|
expect(result.current.links[0].title).toBe('1');
|
||||||
expect(result.current.links[1].title).toBe('2');
|
expect(result.current.links[1].title).toBe('2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point meta-info in production mode', () => {
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
registries.addedLinksRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: extensionPointId,
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
path: `/a/${pluginId}/2`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
// (No restrictions due to isGrafanaDevMode() = false)
|
||||||
|
let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper });
|
||||||
|
expect(result.current.links.length).toBe(1);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point id in production mode', () => {
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
// (No restrictions due to isGrafanaDevMode() = false)
|
||||||
|
let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'invalid-extension-point-id' }), { wrapper });
|
||||||
|
expect(result.current.links.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point meta-info if used in Grafana core (no plugin context)', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// No plugin context -> used in Grafana core
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adding an extension to the extension point
|
||||||
|
registries.addedLinksRegistry.register({
|
||||||
|
pluginId: 'grafana', // Only core Grafana can register extensions without a plugin context
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: 'grafana/extension-point/v1',
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
path: `/a/grafana/${pluginId}/2`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'grafana/extension-point/v1' }), { wrapper });
|
||||||
|
expect(result.current.links.length).toBe(1);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate the extension point id if used in Grafana core (no plugin context)', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// No plugin context -> used in Grafana core
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'invalid-extension-point-id' }), { wrapper });
|
||||||
|
expect(result.current.links.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate if the extension point meta-info is correct if in dev-mode and used by a plugin', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adding an extension to the extension point - it should not be returned later
|
||||||
|
registries.addedLinksRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: extensionPointId,
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
path: `/a/${pluginId}/2`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper });
|
||||||
|
expect(result.current.links.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log a warning if the extension point meta-info is correct if in dev-mode and used by a plugin', () => {
|
||||||
|
// Imitate running in dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
// Empty list of extension points in the plugin meta (from plugin.json)
|
||||||
|
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<PluginContextProvider
|
||||||
|
meta={{
|
||||||
|
...pluginMeta,
|
||||||
|
extensions: {
|
||||||
|
...pluginMeta.extensions!,
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||||
|
</PluginContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adding an extension to the extension point - it should not be returned later
|
||||||
|
registries.addedLinksRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
targets: extensionPointId,
|
||||||
|
title: '1',
|
||||||
|
description: '1',
|
||||||
|
path: `/a/${pluginId}/2`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trying to render an extension point that is not defined in the plugin meta
|
||||||
|
let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper });
|
||||||
|
expect(result.current.links.length).toBe(0);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { isString } from 'lodash';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
import { PluginExtensionLink, PluginExtensionTypes } from '@grafana/data';
|
import { PluginExtensionLink, PluginExtensionTypes, usePluginContext } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
UsePluginLinksOptions,
|
UsePluginLinksOptions,
|
||||||
UsePluginLinksResult,
|
UsePluginLinksResult,
|
||||||
@ -15,7 +15,11 @@ import {
|
|||||||
getLinkExtensionOverrides,
|
getLinkExtensionOverrides,
|
||||||
getLinkExtensionPathWithTracking,
|
getLinkExtensionPathWithTracking,
|
||||||
getReadOnlyProxy,
|
getReadOnlyProxy,
|
||||||
|
isExtensionPointMetaInfoMissing,
|
||||||
|
isGrafanaDevMode,
|
||||||
|
logWarning,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import { isExtensionPointIdValid } from './validators';
|
||||||
|
|
||||||
// Returns an array of component extensions for the given extension point
|
// Returns an array of component extensions for the given extension point
|
||||||
export function usePluginLinks({
|
export function usePluginLinks({
|
||||||
@ -24,9 +28,34 @@ export function usePluginLinks({
|
|||||||
context,
|
context,
|
||||||
}: UsePluginLinksOptions): UsePluginLinksResult {
|
}: UsePluginLinksOptions): UsePluginLinksResult {
|
||||||
const registry = useAddedLinksRegistry();
|
const registry = useAddedLinksRegistry();
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
const registryState = useObservable(registry.asObservable());
|
const registryState = useObservable(registry.asObservable());
|
||||||
|
|
||||||
return useMemo(() => {
|
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 ?? '';
|
||||||
|
|
||||||
|
if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) {
|
||||||
|
logWarning(
|
||||||
|
`Extension point usePluginLinks("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) {
|
||||||
|
logWarning(
|
||||||
|
`Invalid extension point. Reason: The extension point is not declared in the "plugin.json" file. ExtensionPointId: "${extensionPointId}"`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!registryState || !registryState[extensionPointId]) {
|
if (!registryState || !registryState[extensionPointId]) {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -80,5 +109,5 @@ export function usePluginLinks({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
links: extensions,
|
links: extensions,
|
||||||
};
|
};
|
||||||
}, [context, extensionPointId, limitPerPlugin, registryState]);
|
}, [context, extensionPointId, limitPerPlugin, registryState, pluginContext]);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { type Unsubscribable } from 'rxjs';
|
import { type Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
import { dateTime, usePluginContext } from '@grafana/data';
|
import {
|
||||||
|
dateTime,
|
||||||
|
PluginContextType,
|
||||||
|
PluginExtensionPoints,
|
||||||
|
PluginLoadingStrategy,
|
||||||
|
PluginType,
|
||||||
|
usePluginContext,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { ShowModalReactEvent } from 'app/types/events';
|
import { ShowModalReactEvent } from 'app/types/events';
|
||||||
|
|
||||||
@ -11,6 +19,11 @@ import {
|
|||||||
getReadOnlyProxy,
|
getReadOnlyProxy,
|
||||||
createOpenModalFunction,
|
createOpenModalFunction,
|
||||||
wrapWithPluginContext,
|
wrapWithPluginContext,
|
||||||
|
isAddedLinkMetaInfoMissing,
|
||||||
|
isAddedComponentMetaInfoMissing,
|
||||||
|
isExposedComponentMetaInfoMissing,
|
||||||
|
isExposedComponentDependencyMissing,
|
||||||
|
isExtensionPointMetaInfoMissing,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||||
@ -396,7 +409,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
const ModalContent = () => {
|
const ModalContent = () => {
|
||||||
const context = usePluginContext();
|
const context = usePluginContext();
|
||||||
|
|
||||||
return <div>Version: {context.meta.info.version}</div>;
|
return <div>Version: {context!.meta.info.version}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
@ -415,13 +428,13 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ExampleComponent = (props: ExampleComponentProps) => {
|
const ExampleComponent = (props: ExampleComponentProps) => {
|
||||||
const { meta } = usePluginContext();
|
const pluginContext = usePluginContext();
|
||||||
|
|
||||||
const audience = props.audience || 'Grafana';
|
const audience = props.audience || 'Grafana';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Hello {audience}!</h1> Version: {meta.info.version}
|
<h1>Hello {audience}!</h1> Version: {pluginContext!.meta.info.version}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -446,4 +459,446 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
|
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isAddedLinkMetaInfoMissing()', () => {
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const extensionConfig = {
|
||||||
|
targets: [PluginExtensionPoints.DashboardPanelMenu],
|
||||||
|
title: 'Link title',
|
||||||
|
description: 'Link description',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||||
|
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||||
|
|
||||||
|
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(false);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the app config is not found', () => {
|
||||||
|
delete config.apps[pluginId];
|
||||||
|
|
||||||
|
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch("couldn't find app plugin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the link has no meta-info in the plugin.json', () => {
|
||||||
|
config.apps[pluginId].extensions.addedLinks = [];
|
||||||
|
|
||||||
|
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('not registered in the plugin.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the "targets" do not match', () => {
|
||||||
|
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||||
|
|
||||||
|
const returnValue = isAddedLinkMetaInfoMissing(pluginId, {
|
||||||
|
...extensionConfig,
|
||||||
|
targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"targets" don\'t match');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the "description" does not match', () => {
|
||||||
|
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||||
|
|
||||||
|
const returnValue = isAddedLinkMetaInfoMissing(pluginId, {
|
||||||
|
...extensionConfig,
|
||||||
|
description: 'Link description UPDATED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"description" doesn\'t match');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAddedComponentMetaInfoMissing()', () => {
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const extensionConfig = {
|
||||||
|
targets: [PluginExtensionPoints.DashboardPanelMenu],
|
||||||
|
title: 'Component title',
|
||||||
|
description: 'Component description',
|
||||||
|
component: () => <div>Component content</div>,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||||
|
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||||
|
|
||||||
|
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(false);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the app config is not found', () => {
|
||||||
|
delete config.apps[pluginId];
|
||||||
|
|
||||||
|
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch("couldn't find app plugin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the Component has no meta-info in the plugin.json', () => {
|
||||||
|
config.apps[pluginId].extensions.addedComponents = [];
|
||||||
|
|
||||||
|
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('not registered in the plugin.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the "targets" do not match', () => {
|
||||||
|
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||||
|
|
||||||
|
const returnValue = isAddedComponentMetaInfoMissing(pluginId, {
|
||||||
|
...extensionConfig,
|
||||||
|
targets: [PluginExtensionPoints.ExploreToolbarAction],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"targets" don\'t match');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the "description" does not match', () => {
|
||||||
|
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||||
|
|
||||||
|
const returnValue = isAddedComponentMetaInfoMissing(pluginId, {
|
||||||
|
...extensionConfig,
|
||||||
|
description: 'UPDATED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"description" doesn\'t match');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isExposedComponentMetaInfoMissing()', () => {
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const exposedComponentConfig = {
|
||||||
|
id: `${pluginId}/component/v1`,
|
||||||
|
title: 'Exposed component',
|
||||||
|
description: 'Exposed component description',
|
||||||
|
component: () => <div>Component content</div>,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||||
|
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(false);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the app config is not found', () => {
|
||||||
|
delete config.apps[pluginId];
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch("couldn't find app plugin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the exposed component has no meta-info in the plugin.json', () => {
|
||||||
|
config.apps[pluginId].extensions.exposedComponents = [];
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('not registered in the plugin.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the title does not match', () => {
|
||||||
|
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentMetaInfoMissing(pluginId, {
|
||||||
|
...exposedComponentConfig,
|
||||||
|
title: 'UPDATED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"title" doesn\'t match');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the "description" does not match', () => {
|
||||||
|
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentMetaInfoMissing(pluginId, {
|
||||||
|
...exposedComponentConfig,
|
||||||
|
description: 'UPDATED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"description" doesn\'t match');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isExposedComponentDependencyMissing()', () => {
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
let pluginContext: PluginContextType;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const exposedComponentId = `${pluginId}/component/v1`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
pluginContext = {
|
||||||
|
meta: {
|
||||||
|
id: pluginId,
|
||||||
|
name: 'Extensions App',
|
||||||
|
type: PluginType.app,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'MyOrg',
|
||||||
|
},
|
||||||
|
description: 'App for testing extensions',
|
||||||
|
links: [],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: [],
|
||||||
|
updated: '2023-10-26T18:25:01Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||||
|
pluginContext.meta.dependencies?.extensions.exposedComponents.push(exposedComponentId);
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(false);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the dependencies are missing', () => {
|
||||||
|
delete pluginContext.meta.dependencies;
|
||||||
|
|
||||||
|
const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the exposed component id is not specified in the list of dependencies', () => {
|
||||||
|
const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isExtensionPointMetaInfoMissing()', () => {
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
let pluginContext: PluginContextType;
|
||||||
|
const pluginId = 'myorg-extensions-app';
|
||||||
|
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||||
|
const extensionPointConfig = {
|
||||||
|
id: extensionPointId,
|
||||||
|
title: 'Extension point title',
|
||||||
|
description: 'Extension point description',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
pluginContext = {
|
||||||
|
meta: {
|
||||||
|
id: pluginId,
|
||||||
|
name: 'Extensions App',
|
||||||
|
type: PluginType.app,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'MyOrg',
|
||||||
|
},
|
||||||
|
description: 'App for testing extensions',
|
||||||
|
links: [],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: [],
|
||||||
|
updated: '2023-10-26T18:25:01Z',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||||
|
pluginContext.meta.extensions?.extensionPoints.push(extensionPointConfig);
|
||||||
|
|
||||||
|
const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(false);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return TRUE and log a warning if the extension point id is not recorded in the plugin.json', () => {
|
||||||
|
const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext);
|
||||||
|
|
||||||
|
expect(returnValue).toBe(true);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.mock.calls[0][0]).toMatch(`Extension point "${extensionPointId}"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,8 +16,11 @@ import {
|
|||||||
PanelMenuItem,
|
PanelMenuItem,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
|
PluginContextType,
|
||||||
|
PluginExtensionExposedComponentConfig,
|
||||||
|
PluginExtensionAddedComponentConfig,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction, config } from '@grafana/runtime';
|
||||||
import { Modal } from '@grafana/ui';
|
import { Modal } from '@grafana/ui';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
// TODO: instead of depending on the service as a singleton, inject it as an argument from the React context
|
// TODO: instead of depending on the service as a singleton, inject it as an argument from the React context
|
||||||
@ -408,3 +411,145 @@ export const openAppInSideview = (pluginId: string) => sidecarService.openApp(pl
|
|||||||
export const closeAppInSideview = (pluginId: string) => sidecarService.closeApp(pluginId);
|
export const closeAppInSideview = (pluginId: string) => sidecarService.closeApp(pluginId);
|
||||||
|
|
||||||
export const isAppOpened = (pluginId: string) => sidecarService.isAppOpened(pluginId);
|
export const isAppOpened = (pluginId: string) => sidecarService.isAppOpened(pluginId);
|
||||||
|
|
||||||
|
// Comes from the `app_mode` setting in the Grafana config (defaults to "development")
|
||||||
|
// Can be set with the `GF_DEFAULT_APP_MODE` environment variable
|
||||||
|
export const isGrafanaDevMode = () => config.buildInfo.env === 'development';
|
||||||
|
|
||||||
|
// Checks if the meta information is missing from the plugin's plugin.json file
|
||||||
|
export const isExtensionPointMetaInfoMissing = (extensionPointId: string, pluginContext: PluginContextType) => {
|
||||||
|
const pluginId = pluginContext.meta?.id;
|
||||||
|
const extensionPoints = pluginContext.meta?.extensions?.extensionPoints;
|
||||||
|
|
||||||
|
if (!extensionPoints || !extensionPoints.some((ep) => ep.id === extensionPointId)) {
|
||||||
|
logWarning(
|
||||||
|
`Extension point "${extensionPointId}" - it's not recorded in the "plugin.json" for "${pluginId}". Please add it under "extensions.extensionPoints[]".`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks if an exposed component that the plugin is depending on is missing from the `dependencies` in the plugin.json file
|
||||||
|
export const isExposedComponentDependencyMissing = (id: string, pluginContext: PluginContextType) => {
|
||||||
|
const pluginId = pluginContext.meta?.id;
|
||||||
|
const exposedComponentsDependencies = pluginContext.meta?.dependencies?.extensions?.exposedComponents;
|
||||||
|
|
||||||
|
if (!exposedComponentsDependencies || !exposedComponentsDependencies.includes(id)) {
|
||||||
|
logWarning(
|
||||||
|
`Using exposed component "${id}" - it's not recorded in the "plugin.json" for "${pluginId}". Please add it under "dependencies.extensions.exposedComponents[]".`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAddedLinkMetaInfoMissing = (pluginId: string, metaInfo: PluginExtensionAddedLinkConfig) => {
|
||||||
|
const app = config.apps[pluginId];
|
||||||
|
const logPrefix = `Added-link "${metaInfo.title}" from "${pluginId}" -`;
|
||||||
|
const pluginJsonMetaInfo = app ? app.extensions.addedLinks.find(({ title }) => title === metaInfo.title) : null;
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
logWarning(`${logPrefix} couldn't find app plugin "${pluginId}"`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginJsonMetaInfo) {
|
||||||
|
logWarning(`${logPrefix} not registered in the plugin.json under "extensions.addedLinks[]".`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
||||||
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
||||||
|
logWarning(`${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedLinks[]".`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
||||||
|
logWarning(
|
||||||
|
`${logPrefix} the "description" doesn't match with one in the plugin.json under "extensions.addedLinks[]".`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAddedComponentMetaInfoMissing = (pluginId: string, metaInfo: PluginExtensionAddedComponentConfig) => {
|
||||||
|
const app = config.apps[pluginId];
|
||||||
|
const logPrefix = `Added component "${metaInfo.title}" -`;
|
||||||
|
const pluginJsonMetaInfo = app ? app.extensions.addedComponents.find(({ title }) => title === metaInfo.title) : null;
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
logWarning(`${logPrefix} couldn't find app plugin "${pluginId}"`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginJsonMetaInfo) {
|
||||||
|
logWarning(`${logPrefix} not registered in the plugin.json under "extensions.addedComponents[]".`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
||||||
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
||||||
|
logWarning(
|
||||||
|
`${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedComponents[]".`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
||||||
|
logWarning(
|
||||||
|
`${logPrefix} the "description" doesn't match with one in the plugin.json under "extensions.addedComponents[]".`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isExposedComponentMetaInfoMissing = (
|
||||||
|
pluginId: string,
|
||||||
|
metaInfo: PluginExtensionExposedComponentConfig
|
||||||
|
) => {
|
||||||
|
const app = config.apps[pluginId];
|
||||||
|
const logPrefix = `Exposed component "${metaInfo.id}" -`;
|
||||||
|
const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.find(({ id }) => id === metaInfo.id) : null;
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
logWarning(`${logPrefix} couldn't find app plugin: "${pluginId}"`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginJsonMetaInfo) {
|
||||||
|
logWarning(`${logPrefix} not registered in the plugin.json under "extensions.exposedComponents[]".`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginJsonMetaInfo.title !== metaInfo.title) {
|
||||||
|
logWarning(
|
||||||
|
`${logPrefix} the "title" doesn't match with one in the plugin.json under "extensions.exposedComponents[]".`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
||||||
|
logWarning(
|
||||||
|
`${logPrefix} the "description" doesn't match with one in the plugin.json under "extensions.exposedComponents[]".`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
assertConfigureIsValid,
|
assertConfigureIsValid,
|
||||||
assertLinkPathIsValid,
|
assertLinkPathIsValid,
|
||||||
assertStringProps,
|
assertStringProps,
|
||||||
|
isExtensionPointIdValid,
|
||||||
isGrafanaCoreExtensionPoint,
|
isGrafanaCoreExtensionPoint,
|
||||||
isReactComponent,
|
isReactComponent,
|
||||||
} from './validators';
|
} from './validators';
|
||||||
@ -184,4 +185,50 @@ describe('Plugin Extension Validators', () => {
|
|||||||
expect(isGrafanaCoreExtensionPoint('grafana/dashboard/alertingrule/action')).toBe(false);
|
expect(isGrafanaCoreExtensionPoint('grafana/dashboard/alertingrule/action')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isExtensionPointIdValid()', () => {
|
||||||
|
test.each([
|
||||||
|
// We (for now allow core Grafana extension points to run without a version)
|
||||||
|
['grafana/extension-point', ''],
|
||||||
|
['grafana/extension-point', 'grafana'],
|
||||||
|
['myorg-extensions-app/extension-point', 'myorg-extensions-app'],
|
||||||
|
['myorg-extensions-app/extension-point/v1', 'myorg-extensions-app'],
|
||||||
|
['plugins/myorg-extensions-app/extension-point/v1', 'myorg-extensions-app'],
|
||||||
|
['plugins/myorg-basic-app/start', 'myorg-basic-app'],
|
||||||
|
['myorg-extensions-app/extension-point/v1', 'myorg-extensions-app'],
|
||||||
|
['plugins/myorg-extensions-app/extension-point/v1', 'myorg-extensions-app'],
|
||||||
|
['plugins/grafana-app-observability-app/service/action', 'grafana-app-observability-app'],
|
||||||
|
['plugins/grafana-k8s-app/cluster/action', 'grafana-k8s-app'],
|
||||||
|
['plugins/grafana-oncall-app/alert-group/action', 'grafana-oncall-app'],
|
||||||
|
['plugins/grafana-oncall-app/alert-group/action/v1', 'grafana-oncall-app'],
|
||||||
|
['plugins/grafana-oncall-app/alert-group/action/v1.0.0', 'grafana-oncall-app'],
|
||||||
|
])('should return TRUE if the extension point id is valid ("%s", "%s")', (extensionPointId, pluginId) => {
|
||||||
|
expect(
|
||||||
|
isExtensionPointIdValid({
|
||||||
|
extensionPointId,
|
||||||
|
pluginId,
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[
|
||||||
|
// Plugin id mismatch
|
||||||
|
'myorg-extensions-app/extension-point/v1',
|
||||||
|
'myorgs-other-app',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Missing plugin id prefix
|
||||||
|
'extension-point/v1',
|
||||||
|
'myorgs-extensions-app',
|
||||||
|
],
|
||||||
|
])('should return FALSE if the extension point id is invalid ("%s", "%s")', (extensionPointId, pluginId) => {
|
||||||
|
expect(
|
||||||
|
isExtensionPointIdValid({
|
||||||
|
extensionPointId,
|
||||||
|
pluginId,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -53,12 +53,18 @@ export function isLinkPathValid(pluginId: string, path: string) {
|
|||||||
return Boolean(typeof path === 'string' && path.length > 0 && path.startsWith(`/a/${pluginId}/`));
|
return Boolean(typeof path === 'string' && path.length > 0 && path.startsWith(`/a/${pluginId}/`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isExtensionPointIdValid(pluginId: string, extensionPointId: string) {
|
export function isExtensionPointIdValid({
|
||||||
return Boolean(
|
extensionPointId,
|
||||||
extensionPointId.startsWith('grafana/') ||
|
pluginId,
|
||||||
extensionPointId?.startsWith('plugins/') ||
|
}: {
|
||||||
extensionPointId?.startsWith(pluginId)
|
extensionPointId: string;
|
||||||
);
|
pluginId: string;
|
||||||
|
}) {
|
||||||
|
if (extensionPointId.startsWith('grafana/')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(extensionPointId.startsWith(`plugins/${pluginId}/`) || extensionPointId.startsWith(`${pluginId}/`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extensionPointEndsWithVersion(extensionPointId: string) {
|
export function extensionPointEndsWithVersion(extensionPointId: string) {
|
||||||
|
@ -7,7 +7,7 @@ content_security_policy_template = """require-trusted-types-for 'script'; script
|
|||||||
enable = publicDashboards
|
enable = publicDashboards
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app,
|
allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app,grafana-extensionexample3-app
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
type=sqlite3
|
type=sqlite3
|
||||||
|
Loading…
Reference in New Issue
Block a user