mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
Plugins: replace config.apps in extension registries (#116581)
* Plugins: replace `config.apps` in extension registries * fix validator tests * fix readonly initialization * fix registry tests * fix pluginImporter tests * wip * chore: fix registry tests * chore: refactor tests * chore: refactor test fixture * chore: fix getPluginExtensions.test.tsx * chore: fix all remaining red tests * chore: refactor public/app/features/plugins/extensions/getPluginExtensions.test.tsx * chore: fix imports * chore: more refactor * chore: adds error handling * chore: update after PR feedback * chore: use getCachedPromise function * chore: undo test --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
This commit is contained in:
parent
971ce0ce47
commit
4b34d5ea67
30 changed files with 762 additions and 909 deletions
|
|
@ -2764,56 +2764,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginComponent.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginComponents.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginFunctions.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginLinks.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/validators.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 30
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/validators.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/sandbox/distortions.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import { RouteDescriptor } from './core/navigation/types';
|
|||
import { ThemeProvider } from './core/utils/ConfigProvider';
|
||||
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||
import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext';
|
||||
import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||
import { getPluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||
import { PluginExtensionRegistries } from './features/plugins/extensions/registry/types';
|
||||
import { ScopesContextProvider } from './features/scopes/ScopesContextProvider';
|
||||
import { RouterWrapper } from './routes/RoutesWrapper';
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ interface AppWrapperProps {
|
|||
|
||||
interface AppWrapperState {
|
||||
ready?: boolean;
|
||||
registries?: PluginExtensionRegistries;
|
||||
}
|
||||
|
||||
/** Used by enterprise */
|
||||
|
|
@ -55,7 +57,8 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({ ready: true });
|
||||
const registries = await getPluginExtensionRegistries();
|
||||
this.setState({ ready: true, registries });
|
||||
this.removePreloader();
|
||||
|
||||
// clear any old icon caches
|
||||
|
|
@ -93,7 +96,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
|
|||
|
||||
render() {
|
||||
const { context } = this.props;
|
||||
const { ready } = this.state;
|
||||
const { ready, registries } = this.state;
|
||||
|
||||
navigationLogger('AppWrapper', false, 'rendering');
|
||||
|
||||
|
|
@ -125,7 +128,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
|
|||
>
|
||||
<MaybeTimeRangeProvider>
|
||||
<ScopesContextProvider>
|
||||
<ExtensionRegistriesProvider registries={pluginExtensionRegistries}>
|
||||
<ExtensionRegistriesProvider registries={registries}>
|
||||
<ExtensionSidebarContextProvider>
|
||||
<UNSAFE_PortalProvider getContainer={getPortalContainer}>
|
||||
<GlobalStyles />
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ import {
|
|||
getObservablePluginComponents,
|
||||
getObservablePluginLinks,
|
||||
} from './features/plugins/extensions/getPluginExtensions';
|
||||
import { getPluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
|
||||
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
||||
|
|
@ -262,6 +263,8 @@ export class GrafanaApp {
|
|||
preloadPlugins(await getAppPluginsToPreload());
|
||||
}
|
||||
|
||||
getPluginExtensionRegistries();
|
||||
|
||||
setHelpNavItemHook(useHelpNode);
|
||||
setPluginLinksHook(usePluginLinks);
|
||||
setPluginComponentHook(usePluginComponent);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/co
|
|||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/getPluginExtensions';
|
||||
import { pluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup';
|
||||
import { getPluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup';
|
||||
import { PluginExtensionRegistries } from 'app/features/plugins/extensions/registry/types';
|
||||
import { GetPluginExtensions } from 'app/features/plugins/extensions/types';
|
||||
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
|
@ -47,12 +48,12 @@ import { PanelTimeRangeDrawer } from './panel-timerange/PanelTimeRangeDrawer';
|
|||
|
||||
let getPluginExtensions: GetPluginExtensions;
|
||||
|
||||
function setupGetPluginExtensions() {
|
||||
function setupGetPluginExtensions(registries: PluginExtensionRegistries) {
|
||||
if (getPluginExtensions) {
|
||||
return getPluginExtensions;
|
||||
}
|
||||
|
||||
getPluginExtensions = createPluginExtensionsGetter(pluginExtensionRegistries);
|
||||
getPluginExtensions = createPluginExtensionsGetter(registries);
|
||||
|
||||
return getPluginExtensions;
|
||||
}
|
||||
|
|
@ -300,7 +301,8 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
|||
});
|
||||
}
|
||||
|
||||
setupGetPluginExtensions();
|
||||
const registries = await getPluginExtensionRegistries();
|
||||
setupGetPluginExtensions(registries);
|
||||
|
||||
const { extensions } = getPluginExtensions({
|
||||
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
|
||||
|
|
|
|||
|
|
@ -89,10 +89,10 @@ function renderUnderRouter(page = '') {
|
|||
appPluginNavItem.parentItem = appsSection;
|
||||
|
||||
const registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry([]),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry([]),
|
||||
addedLinksRegistry: new AddedLinksRegistry([]),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry([]),
|
||||
};
|
||||
const pagePath = page ? `/${page}` : '';
|
||||
const route = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/regis
|
|||
import { PluginExtensionRegistries } from './registry/types';
|
||||
|
||||
export interface ExtensionRegistriesContextType {
|
||||
registries: PluginExtensionRegistries;
|
||||
registries?: PluginExtensionRegistries;
|
||||
}
|
||||
|
||||
// Using a different context for each registry to avoid unnecessary re-renders
|
||||
|
|
@ -53,6 +53,10 @@ export const ExtensionRegistriesProvider = ({
|
|||
registries,
|
||||
children,
|
||||
}: PropsWithChildren<ExtensionRegistriesContextType>) => {
|
||||
if (!registries) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AddedLinksRegistryContext.Provider value={registries.addedLinksRegistry}>
|
||||
<AddedComponentsRegistryContext.Provider value={registries.addedComponentsRegistry}>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ import {
|
|||
import { log } from './logs/log';
|
||||
import { resetLogMock } from './logs/testUtils';
|
||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||
import { pluginExtensionRegistries } from './registry/setup';
|
||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||
import { getPluginExtensionRegistries } from './registry/setup';
|
||||
import { isReadOnlyProxy } from './utils';
|
||||
import { assertPluginExtensionLink } from './validators';
|
||||
|
||||
|
|
@ -39,6 +41,13 @@ jest.mock('./logs/log', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('./registry/setup', () => ({
|
||||
...jest.requireActual('./registry/setup'),
|
||||
getPluginExtensionRegistries: jest.fn(),
|
||||
}));
|
||||
|
||||
const getPluginExtensionRegistriesMock = jest.mocked(getPluginExtensionRegistries);
|
||||
|
||||
async function createRegistries(
|
||||
preloadResults: Array<{
|
||||
pluginId: string;
|
||||
|
|
@ -46,8 +55,8 @@ async function createRegistries(
|
|||
addedLinkConfigs: PluginExtensionAddedLinkConfig[];
|
||||
}>
|
||||
) {
|
||||
const addedLinksRegistry = new AddedLinksRegistry();
|
||||
const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
const addedLinksRegistry = new AddedLinksRegistry([]);
|
||||
const addedComponentsRegistry = new AddedComponentsRegistry([]);
|
||||
|
||||
for (const { pluginId, addedLinkConfigs, addedComponentConfigs } of preloadResults) {
|
||||
addedLinksRegistry.register({
|
||||
|
|
@ -552,11 +561,27 @@ describe('getPluginExtensions()', () => {
|
|||
describe('getObservablePluginExtensions()', () => {
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu/v1';
|
||||
const pluginId = 'grafana-basic-app';
|
||||
let addedLinksRegistry: AddedLinksRegistry;
|
||||
let addedComponentsRegistry: AddedComponentsRegistry;
|
||||
let addedFunctionsRegistry: AddedFunctionsRegistry;
|
||||
let exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry();
|
||||
pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
pluginExtensionRegistries.addedLinksRegistry.register({
|
||||
addedLinksRegistry = new AddedLinksRegistry([]);
|
||||
addedComponentsRegistry = new AddedComponentsRegistry([]);
|
||||
addedFunctionsRegistry = new AddedFunctionsRegistry([]);
|
||||
exposedComponentsRegistry = new ExposedComponentsRegistry([]);
|
||||
|
||||
const registries = {
|
||||
addedComponentsRegistry,
|
||||
addedFunctionsRegistry,
|
||||
addedLinksRegistry,
|
||||
exposedComponentsRegistry,
|
||||
};
|
||||
|
||||
getPluginExtensionRegistriesMock.mockResolvedValue(registries);
|
||||
|
||||
addedLinksRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -569,7 +594,7 @@ describe('getObservablePluginExtensions()', () => {
|
|||
],
|
||||
});
|
||||
|
||||
pluginExtensionRegistries.addedComponentsRegistry.register({
|
||||
addedComponentsRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -599,7 +624,7 @@ describe('getObservablePluginExtensions()', () => {
|
|||
const observable = getObservablePluginExtensions({ extensionPointId }).pipe(take(2));
|
||||
|
||||
setTimeout(() => {
|
||||
pluginExtensionRegistries.addedLinksRegistry.register({
|
||||
addedLinksRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -631,11 +656,27 @@ describe('getObservablePluginExtensions()', () => {
|
|||
describe('getObservablePluginLinks()', () => {
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu/v1';
|
||||
const pluginId = 'grafana-basic-app';
|
||||
let addedLinksRegistry: AddedLinksRegistry;
|
||||
let addedComponentsRegistry: AddedComponentsRegistry;
|
||||
let addedFunctionsRegistry: AddedFunctionsRegistry;
|
||||
let exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry();
|
||||
pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
pluginExtensionRegistries.addedLinksRegistry.register({
|
||||
beforeEach(async () => {
|
||||
addedLinksRegistry = new AddedLinksRegistry([]);
|
||||
addedComponentsRegistry = new AddedComponentsRegistry([]);
|
||||
addedFunctionsRegistry = new AddedFunctionsRegistry([]);
|
||||
exposedComponentsRegistry = new ExposedComponentsRegistry([]);
|
||||
|
||||
const registries = {
|
||||
addedComponentsRegistry,
|
||||
addedFunctionsRegistry,
|
||||
addedLinksRegistry,
|
||||
exposedComponentsRegistry,
|
||||
};
|
||||
|
||||
getPluginExtensionRegistriesMock.mockResolvedValue(registries);
|
||||
|
||||
addedLinksRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -648,7 +689,7 @@ describe('getObservablePluginLinks()', () => {
|
|||
],
|
||||
});
|
||||
|
||||
pluginExtensionRegistries.addedComponentsRegistry.register({
|
||||
addedComponentsRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -685,7 +726,7 @@ describe('getObservablePluginLinks()', () => {
|
|||
|
||||
it('should be possible to receive the last state of the registry', async () => {
|
||||
// Register a new link
|
||||
pluginExtensionRegistries.addedLinksRegistry.register({
|
||||
addedLinksRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -709,8 +750,12 @@ describe('getObservablePluginLinks()', () => {
|
|||
});
|
||||
|
||||
it('should receive an empty array if there are no links', async () => {
|
||||
pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry();
|
||||
pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
getPluginExtensionRegistriesMock.mockResolvedValue({
|
||||
addedLinksRegistry: new AddedLinksRegistry([]),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry([]),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry([]),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry([]),
|
||||
});
|
||||
|
||||
const observable = getObservablePluginLinks({ extensionPointId }).pipe(first());
|
||||
const links = await firstValueFrom(observable);
|
||||
|
|
@ -722,11 +767,27 @@ describe('getObservablePluginLinks()', () => {
|
|||
describe('getObservablePluginComponents()', () => {
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu/v1';
|
||||
const pluginId = 'grafana-basic-app';
|
||||
let addedLinksRegistry: AddedLinksRegistry;
|
||||
let addedComponentsRegistry: AddedComponentsRegistry;
|
||||
let addedFunctionsRegistry: AddedFunctionsRegistry;
|
||||
let exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry();
|
||||
pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
pluginExtensionRegistries.addedLinksRegistry.register({
|
||||
beforeEach(async () => {
|
||||
addedLinksRegistry = new AddedLinksRegistry([]);
|
||||
addedComponentsRegistry = new AddedComponentsRegistry([]);
|
||||
addedFunctionsRegistry = new AddedFunctionsRegistry([]);
|
||||
exposedComponentsRegistry = new ExposedComponentsRegistry([]);
|
||||
|
||||
const registries = {
|
||||
addedComponentsRegistry,
|
||||
addedFunctionsRegistry,
|
||||
addedLinksRegistry,
|
||||
exposedComponentsRegistry,
|
||||
};
|
||||
|
||||
getPluginExtensionRegistriesMock.mockResolvedValue(registries);
|
||||
|
||||
addedLinksRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -739,7 +800,7 @@ describe('getObservablePluginComponents()', () => {
|
|||
],
|
||||
});
|
||||
|
||||
pluginExtensionRegistries.addedComponentsRegistry.register({
|
||||
addedComponentsRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -776,7 +837,7 @@ describe('getObservablePluginComponents()', () => {
|
|||
|
||||
it('should be possible to receive the last state of the registry', async () => {
|
||||
// Register a new component
|
||||
pluginExtensionRegistries.addedComponentsRegistry.register({
|
||||
addedComponentsRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
|
|
@ -801,8 +862,12 @@ describe('getObservablePluginComponents()', () => {
|
|||
});
|
||||
|
||||
it('should receive an empty array if there are no components', async () => {
|
||||
pluginExtensionRegistries.addedLinksRegistry = new AddedLinksRegistry();
|
||||
pluginExtensionRegistries.addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
getPluginExtensionRegistriesMock.mockResolvedValue({
|
||||
addedLinksRegistry: new AddedLinksRegistry([]),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry([]),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry([]),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry([]),
|
||||
});
|
||||
|
||||
const observable = getObservablePluginComponents({ extensionPointId }).pipe(first());
|
||||
const components = await firstValueFrom(observable);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { isString } from 'lodash';
|
||||
import { combineLatest, map, Observable } from 'rxjs';
|
||||
import { combineLatest, from, map, Observable, switchMap } from 'rxjs';
|
||||
|
||||
import {
|
||||
PluginExtensionTypes,
|
||||
|
|
@ -13,7 +13,7 @@ import { log } from './logs/log';
|
|||
import { AddedComponentRegistryItem } from './registry/AddedComponentsRegistry';
|
||||
import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry';
|
||||
import { RegistryType } from './registry/Registry';
|
||||
import { pluginExtensionRegistries } from './registry/setup';
|
||||
import { getPluginExtensionRegistries } from './registry/setup';
|
||||
import type { PluginExtensionRegistries } from './registry/types';
|
||||
import { GetExtensions, GetExtensionsOptions, GetPluginExtensions } from './types';
|
||||
import {
|
||||
|
|
@ -38,22 +38,25 @@ export const getObservablePluginExtensions = (
|
|||
options: Omit<GetExtensionsOptions, 'addedComponentsRegistry' | 'addedLinksRegistry'>
|
||||
): Observable<ReturnType<GetExtensions>> => {
|
||||
const { extensionPointId } = options;
|
||||
const { addedComponentsRegistry, addedLinksRegistry } = pluginExtensionRegistries;
|
||||
|
||||
return combineLatest([
|
||||
addedComponentsRegistry.asObservableSlice((state) => state[extensionPointId]),
|
||||
addedLinksRegistry.asObservableSlice((state) => state[extensionPointId]),
|
||||
]).pipe(
|
||||
map(([components, links]) =>
|
||||
getPluginExtensions({
|
||||
...options,
|
||||
addedComponentsRegistry: {
|
||||
[extensionPointId]: components,
|
||||
},
|
||||
addedLinksRegistry: {
|
||||
[extensionPointId]: links,
|
||||
},
|
||||
})
|
||||
return from(getPluginExtensionRegistries()).pipe(
|
||||
switchMap((registries) =>
|
||||
combineLatest([
|
||||
registries.addedComponentsRegistry.asObservableSlice((state) => state[extensionPointId]),
|
||||
registries.addedLinksRegistry.asObservableSlice((state) => state[extensionPointId]),
|
||||
]).pipe(
|
||||
map(([components, links]) =>
|
||||
getPluginExtensions({
|
||||
...options,
|
||||
addedComponentsRegistry: {
|
||||
[extensionPointId]: components,
|
||||
},
|
||||
addedLinksRegistry: {
|
||||
[extensionPointId]: links,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
import { basicApp } from '../test-fixtures/config.apps';
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||
|
|
@ -30,48 +30,17 @@ jest.mock('../logs/log', () => {
|
|||
});
|
||||
|
||||
describe('AddedComponentsRegistry', () => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
const pluginId = basicApp.id;
|
||||
const apps = [basicApp];
|
||||
const createRegistry = async (override: AppPluginConfig[] = apps) => new AddedComponentsRegistry(override);
|
||||
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const registry = await firstValueFrom(observable);
|
||||
expect(registry).toEqual({});
|
||||
|
|
@ -79,7 +48,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
|
||||
it('should be possible to register added components in the registry', async () => {
|
||||
const extensionPointId = `${pluginId}/hello-world/v1`;
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId,
|
||||
|
|
@ -107,7 +76,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -166,7 +135,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
const pluginId2 = 'grafana-basic-app2';
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -226,7 +195,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => {
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
// Register extensions for the first extension point
|
||||
|
|
@ -265,7 +234,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should be possible to register one extension component targeting multiple extension points', async () => {
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||
|
||||
|
|
@ -304,7 +273,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
const pluginId2 = 'myorg-extensions-app';
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -359,7 +328,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should not register component when title is missing', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -381,7 +350,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should not be possible to register a component on a read-only registry', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
|
|
@ -405,7 +374,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
|
||||
it('should pass down fresh registrations to the read-only version of the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const subscribeCallback = jest.fn();
|
||||
let readOnlyState;
|
||||
|
|
@ -441,7 +410,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
|
|
@ -449,9 +418,6 @@ describe('AddedComponentsRegistry', () => {
|
|||
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],
|
||||
|
|
@ -467,7 +433,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
|
|
@ -490,7 +456,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
// Production mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
|
|
@ -498,9 +464,6 @@ describe('AddedComponentsRegistry', () => {
|
|||
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],
|
||||
|
|
@ -516,16 +479,18 @@ describe('AddedComponentsRegistry', () => {
|
|||
// 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];
|
||||
const { description, targets, title } = componentConfig;
|
||||
const app = {
|
||||
...basicApp,
|
||||
extensions: { ...basicApp.extensions, addedComponents: [{ description, targets, title }] },
|
||||
};
|
||||
const registry = await createRegistry([app]);
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
|
@ -540,7 +505,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
|
||||
describe('asObservableSlice', () => {
|
||||
it('should return the selected slice from the registry', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -568,7 +533,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should return undefined when the selected key does not exist', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const observable = registry.asObservableSlice((state) => state['non-existent-key']).pipe(take(1));
|
||||
|
||||
await expect(observable).toEmitValuesWith((received) => {
|
||||
|
|
@ -578,7 +543,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should only emit when the selected slice changes', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -641,7 +606,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should deep freeze the selected slice', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -669,7 +634,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should work with read-only registries', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
|
|
@ -698,7 +663,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should emit immediately to new subscribers with the current slice value', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -725,7 +690,7 @@ describe('AddedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should not emit when Object.is returns true for the same value', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ReplaySubject } from 'rxjs';
|
||||
|
||||
import { PluginExtensionAddedComponentConfig } from '@grafana/data';
|
||||
import { AppPluginConfig, PluginExtensionAddedComponentConfig } from '@grafana/data';
|
||||
|
||||
import * as errors from '../errors';
|
||||
import { isGrafanaDevMode, wrapWithPluginContext } from '../utils';
|
||||
|
|
@ -22,12 +22,13 @@ export class AddedComponentsRegistry extends Registry<
|
|||
PluginExtensionAddedComponentConfig
|
||||
> {
|
||||
constructor(
|
||||
apps: AppPluginConfig[],
|
||||
options: {
|
||||
registrySubject?: ReplaySubject<RegistryType<AddedComponentRegistryItem[]>>;
|
||||
initialState?: RegistryType<AddedComponentRegistryItem[]>;
|
||||
} = {}
|
||||
) {
|
||||
super(options);
|
||||
super(apps, options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
|
|
@ -51,7 +52,7 @@ export class AddedComponentsRegistry extends Registry<
|
|||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
isAddedComponentMetaInfoMissing(pluginId, config, configLog)
|
||||
isAddedComponentMetaInfoMissing(pluginId, config, configLog, this.apps)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -85,7 +86,7 @@ export class AddedComponentsRegistry extends Registry<
|
|||
|
||||
// Returns a read-only version of the registry.
|
||||
readOnly() {
|
||||
return new AddedComponentsRegistry({
|
||||
return new AddedComponentsRegistry(this.apps, {
|
||||
registrySubject: this.registrySubject,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
import { basicApp } from '../test-fixtures/config.apps';
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
||||
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||
|
|
@ -29,55 +29,24 @@ jest.mock('../logs/log', () => {
|
|||
});
|
||||
|
||||
describe('addedFunctionsRegistry', () => {
|
||||
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: {
|
||||
addedFunctions: [],
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
const pluginId = basicApp.id;
|
||||
const apps = [basicApp];
|
||||
const createRegistry = async (override: AppPluginConfig[] = apps) => new AddedFunctionsRegistry(override);
|
||||
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||
const addedFunctionsRegistry = await createRegistry();
|
||||
const observable = addedFunctionsRegistry.asObservable();
|
||||
const registry = await firstValueFrom(observable);
|
||||
expect(registry).toEqual({});
|
||||
});
|
||||
|
||||
it('should be possible to register function extensions in the registry', async () => {
|
||||
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||
const addedFunctionsRegistry = await createRegistry();
|
||||
|
||||
addedFunctionsRegistry.register({
|
||||
pluginId,
|
||||
|
|
@ -122,7 +91,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should not emit when registering with empty configs', async () => {
|
||||
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||
const addedFunctionsRegistry = await createRegistry();
|
||||
const observable = addedFunctionsRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -147,7 +116,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should not change registry state when registering with empty configs after previous registrations', async () => {
|
||||
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||
const addedFunctionsRegistry = await createRegistry();
|
||||
|
||||
// First register some extensions
|
||||
addedFunctionsRegistry.register({
|
||||
|
|
@ -178,7 +147,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
it('should be possible to asynchronously register function extensions for the same placement (different plugins)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -245,7 +214,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
it('should be possible to asynchronously register function extensions for a different placement (different plugin)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -314,7 +283,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
it('should be possible to asynchronously register function extensions for the same placement (same plugin)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first extension point
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -368,7 +337,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
it('should be possible to asynchronously register function extensions for a different placement (same plugin)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first extension point
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -424,7 +393,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
it('should notify subscribers when the registry changes', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -484,7 +453,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
it('should give the last version of the registry for new subscribers', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -518,9 +487,9 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not register a function extension if it has an invalid fn function', () => {
|
||||
it('should not register a function extension if it has an invalid fn function', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -546,9 +515,9 @@ describe('addedFunctionsRegistry', () => {
|
|||
expect(registry).toEqual({});
|
||||
});
|
||||
|
||||
it('should not register a function extension if it has invalid properties (empty title)', () => {
|
||||
it('should not register a function extension if it has invalid properties (empty title)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -574,7 +543,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
it('should not be possible to register a function on a read-only registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
|
||||
expect(() => {
|
||||
|
|
@ -597,7 +566,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
it('should pass down fresh registrations to the read-only version of the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const subscribeCallback = jest.fn();
|
||||
let readOnlyState;
|
||||
|
|
@ -633,7 +602,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const fnConfig = {
|
||||
title: 'Function 1',
|
||||
description: 'Function 1 description',
|
||||
|
|
@ -641,9 +610,6 @@ describe('addedFunctionsRegistry', () => {
|
|||
fn: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedFunctions = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [fnConfig],
|
||||
|
|
@ -659,7 +625,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const fnConfig = {
|
||||
title: 'Function 1',
|
||||
description: 'Function 1 description',
|
||||
|
|
@ -682,7 +648,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
// Production mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const fnConfig = {
|
||||
title: 'Function 1',
|
||||
description: 'Function 1 description',
|
||||
|
|
@ -690,9 +656,6 @@ describe('addedFunctionsRegistry', () => {
|
|||
fn: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedFunctions = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [fnConfig],
|
||||
|
|
@ -708,16 +671,18 @@ describe('addedFunctionsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const fnConfig = {
|
||||
title: 'Function 1',
|
||||
description: 'Function 1 description',
|
||||
targets: ['grafana/dashboard/panel/menu'],
|
||||
fn: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedFunctions = [fnConfig];
|
||||
const { description, targets, title } = fnConfig;
|
||||
const app = {
|
||||
...basicApp,
|
||||
extensions: { ...basicApp.extensions, addedFunctions: [{ description, targets, title }] },
|
||||
};
|
||||
const registry = await createRegistry([app]);
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
|
@ -732,7 +697,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
|
||||
describe('asObservableSlice', () => {
|
||||
it('should return the selected slice from the registry', async () => {
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -760,7 +725,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should return undefined when the selected key does not exist', async () => {
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const observable = registry.asObservableSlice((state) => state['non-existent-key']).pipe(take(1));
|
||||
|
||||
await expect(observable).toEmitValuesWith((received) => {
|
||||
|
|
@ -770,7 +735,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should only emit when the selected slice changes (distinctUntilChanged)', async () => {
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -833,7 +798,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should deep freeze the selected slice', async () => {
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -861,7 +826,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should work with read-only registries', async () => {
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
|
|
@ -890,7 +855,7 @@ describe('addedFunctionsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should emit immediately to new subscribers with the current slice value', async () => {
|
||||
const registry = new AddedFunctionsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
registry.register({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { isFunction } from 'lodash';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
|
||||
import { PluginExtensionAddedFunctionConfig } from '@grafana/data';
|
||||
import { AppPluginConfig, PluginExtensionAddedFunctionConfig } from '@grafana/data';
|
||||
|
||||
import * as errors from '../errors';
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
|
@ -20,12 +20,13 @@ export type AddedFunctionsRegistryItem<Signature = unknown> = {
|
|||
|
||||
export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[], PluginExtensionAddedFunctionConfig> {
|
||||
constructor(
|
||||
apps: AppPluginConfig[],
|
||||
options: {
|
||||
registrySubject?: ReplaySubject<RegistryType<AddedFunctionsRegistryItem[]>>;
|
||||
initialState?: RegistryType<AddedFunctionsRegistryItem[]>;
|
||||
} = {}
|
||||
) {
|
||||
super(options);
|
||||
super(apps, options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
|
|
@ -49,7 +50,11 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
|
|||
continue;
|
||||
}
|
||||
|
||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) {
|
||||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
isAddedFunctionMetaInfoMissing(pluginId, config, configLog, this.apps)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +83,7 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
|
|||
|
||||
// Returns a read-only version of the registry.
|
||||
readOnly() {
|
||||
return new AddedFunctionsRegistry({
|
||||
return new AddedFunctionsRegistry(this.apps, {
|
||||
registrySubject: this.registrySubject,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
import { basicApp } from '../test-fixtures/config.apps';
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||
|
|
@ -29,55 +29,24 @@ jest.mock('../logs/log', () => {
|
|||
});
|
||||
|
||||
describe('AddedLinksRegistry', () => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
const pluginId = basicApp.id;
|
||||
const apps = [basicApp];
|
||||
const createRegistry = async (override: AppPluginConfig[] = apps) => new AddedLinksRegistry(override);
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
const addedLinksRegistry = new AddedLinksRegistry();
|
||||
const addedLinksRegistry = await createRegistry();
|
||||
const observable = addedLinksRegistry.asObservable();
|
||||
const registry = await firstValueFrom(observable);
|
||||
expect(registry).toEqual({});
|
||||
});
|
||||
|
||||
it('should be possible to register link extensions in the registry', async () => {
|
||||
const addedLinksRegistry = new AddedLinksRegistry();
|
||||
const addedLinksRegistry = await createRegistry();
|
||||
|
||||
addedLinksRegistry.register({
|
||||
pluginId,
|
||||
|
|
@ -129,7 +98,7 @@ describe('AddedLinksRegistry', () => {
|
|||
it('should be possible to asynchronously register link extensions for the same placement (different plugins)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -201,7 +170,7 @@ describe('AddedLinksRegistry', () => {
|
|||
it('should be possible to asynchronously register link extensions for a different placement (different plugin)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -275,7 +244,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
it('should be possible to asynchronously register link extensions for the same placement (same plugin)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first extension point
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -333,7 +302,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
it('should be possible to asynchronously register link extensions for a different placement (same plugin)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
// Register extensions for the first extension point
|
||||
reactiveRegistry.register({
|
||||
|
|
@ -393,7 +362,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
it('should notify subscribers when the registry changes', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -459,7 +428,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
it('should give the last version of the registry for new subscribers', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -496,9 +465,9 @@ describe('AddedLinksRegistry', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not register a link extension if it has an invalid configure() function', () => {
|
||||
it('should not register a link extension if it has an invalid configure() function', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -525,9 +494,9 @@ describe('AddedLinksRegistry', () => {
|
|||
expect(registry).toEqual({});
|
||||
});
|
||||
|
||||
it('should not register a link extension if it has invalid properties (empty title / description)', () => {
|
||||
it('should not register a link extension if it has invalid properties (empty title / description)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedLinksRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -555,7 +524,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
it('should not be possible to register a link on a read-only registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
|
||||
expect(() => {
|
||||
|
|
@ -579,7 +548,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
it('should pass down fresh registrations to the read-only version of the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const subscribeCallback = jest.fn();
|
||||
let readOnlyState;
|
||||
|
|
@ -616,7 +585,7 @@ describe('AddedLinksRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
|
|
@ -625,9 +594,6 @@ describe('AddedLinksRegistry', () => {
|
|||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [linkConfig],
|
||||
|
|
@ -643,7 +609,7 @@ describe('AddedLinksRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
|
|
@ -667,7 +633,7 @@ describe('AddedLinksRegistry', () => {
|
|||
// Production mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
|
|
@ -676,9 +642,6 @@ describe('AddedLinksRegistry', () => {
|
|||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [linkConfig],
|
||||
|
|
@ -694,7 +657,6 @@ describe('AddedLinksRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
|
|
@ -702,9 +664,12 @@ describe('AddedLinksRegistry', () => {
|
|||
targets: ['grafana/dashboard/panel/menu'],
|
||||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [linkConfig];
|
||||
const { description, targets, title } = linkConfig;
|
||||
const app = {
|
||||
...basicApp,
|
||||
extensions: { ...basicApp.extensions, addedLinks: [{ description, targets, title }] },
|
||||
};
|
||||
const registry = await createRegistry([app]);
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
|
@ -719,7 +684,7 @@ describe('AddedLinksRegistry', () => {
|
|||
|
||||
describe('asObservableSlice', () => {
|
||||
it('should return the selected slice from the registry', async () => {
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -748,7 +713,7 @@ describe('AddedLinksRegistry', () => {
|
|||
});
|
||||
|
||||
it('should return undefined when the selected key does not exist', async () => {
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const observable = registry.asObservableSlice((state) => state['non-existent-key']).pipe(take(1));
|
||||
|
||||
await expect(observable).toEmitValuesWith((received) => {
|
||||
|
|
@ -758,7 +723,7 @@ describe('AddedLinksRegistry', () => {
|
|||
});
|
||||
|
||||
it('should only emit when the selected slice changes (distinctUntilChanged)', async () => {
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -824,7 +789,7 @@ describe('AddedLinksRegistry', () => {
|
|||
});
|
||||
|
||||
it('should deep freeze the selected slice', async () => {
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -853,7 +818,7 @@ describe('AddedLinksRegistry', () => {
|
|||
});
|
||||
|
||||
it('should work with read-only registries', async () => {
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
|
|
@ -883,7 +848,7 @@ describe('AddedLinksRegistry', () => {
|
|||
});
|
||||
|
||||
it('should emit immediately to new subscribers with the current slice value', async () => {
|
||||
const registry = new AddedLinksRegistry();
|
||||
const registry = await createRegistry();
|
||||
const extensionPointId = 'grafana/dashboard/panel/menu';
|
||||
|
||||
registry.register({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ReplaySubject } from 'rxjs';
|
||||
|
||||
import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data';
|
||||
import { AppPluginConfig, IconName, PluginExtensionAddedLinkConfig } from '@grafana/data';
|
||||
import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/internal';
|
||||
|
||||
import * as errors from '../errors';
|
||||
|
|
@ -26,12 +26,13 @@ export type AddedLinkRegistryItem<Context extends object = object> = {
|
|||
|
||||
export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], PluginExtensionAddedLinkConfig> {
|
||||
constructor(
|
||||
apps: AppPluginConfig[],
|
||||
options: {
|
||||
registrySubject?: ReplaySubject<RegistryType<AddedLinkRegistryItem[]>>;
|
||||
initialState?: RegistryType<AddedLinkRegistryItem[]>;
|
||||
} = {}
|
||||
) {
|
||||
super(options);
|
||||
super(apps, options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
|
|
@ -66,7 +67,11 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
|||
continue;
|
||||
}
|
||||
|
||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedLinkMetaInfoMissing(pluginId, config, configLog)) {
|
||||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
isAddedLinkMetaInfoMissing(pluginId, config, configLog, this.apps)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +95,7 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
|||
|
||||
// Returns a read-only version of the registry.
|
||||
readOnly() {
|
||||
return new AddedLinksRegistry({
|
||||
return new AddedLinksRegistry(this.apps, {
|
||||
registrySubject: this.registrySubject,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
import { basicApp } from '../test-fixtures/config.apps';
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||
|
|
@ -30,48 +30,17 @@ jest.mock('../logs/log', () => {
|
|||
});
|
||||
|
||||
describe('ExposedComponentsRegistry', () => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
const pluginId = basicApp.id;
|
||||
const apps = [basicApp];
|
||||
const createRegistry = async (override: AppPluginConfig[] = apps) => new ExposedComponentsRegistry(override);
|
||||
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
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 () => {
|
||||
const reactiveRegistry = new ExposedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const registry = await firstValueFrom(observable);
|
||||
expect(registry).toEqual({});
|
||||
|
|
@ -80,7 +49,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
it('should be possible to register exposed components in the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const id = `${pluginId}/hello-world/v1`;
|
||||
const reactiveRegistry = new ExposedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId,
|
||||
|
|
@ -110,7 +79,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
const id1 = `${pluginId}/hello-world1/v1`;
|
||||
const id2 = `${pluginId}/hello-world2/v1`;
|
||||
const id3 = `${pluginId}/hello-world3/v1`;
|
||||
const reactiveRegistry = new ExposedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId,
|
||||
|
|
@ -151,7 +120,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
const id2 = `${pluginId1}/hello-world2/v1`;
|
||||
const id3 = `${pluginId2}/hello-world1/v1`;
|
||||
const id4 = `${pluginId2}/hello-world2/v1`;
|
||||
const reactiveRegistry = new ExposedComponentsRegistry();
|
||||
const reactiveRegistry = await createRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId1,
|
||||
|
|
@ -199,7 +168,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should notify subscribers when the registry changes', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const observable = registry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -241,7 +210,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should give the last version of the registry for new subscribers', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const observable = registry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -272,7 +241,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should log an error if another component with the same id already exists in the registry', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app1',
|
||||
configs: [
|
||||
|
|
@ -312,7 +281,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should skip registering component and log an error when id is not prefixed with plugin id', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app1',
|
||||
configs: [
|
||||
|
|
@ -333,7 +302,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should not register component when title is missing', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
|
|
@ -355,7 +324,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
|
||||
it('should not be possible to register a component on a read-only registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
|
||||
expect(() => {
|
||||
|
|
@ -378,7 +347,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
|
||||
it('should pass down fresh registrations to the read-only version of the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const subscribeCallback = jest.fn();
|
||||
let readOnlyState;
|
||||
|
|
@ -414,7 +383,6 @@ describe('ExposedComponentsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
id: `${pluginId}/exposed-component/v1`,
|
||||
title: 'Component title',
|
||||
|
|
@ -422,9 +390,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.exposedComponents = [];
|
||||
|
||||
const registry = await createRegistry();
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [componentConfig],
|
||||
|
|
@ -440,7 +406,6 @@ describe('ExposedComponentsRegistry', () => {
|
|||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
id: `${pluginId}/exposed-component/v1`,
|
||||
title: 'Component title',
|
||||
|
|
@ -448,6 +413,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
const registry = await createRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: [componentConfig],
|
||||
|
|
@ -463,7 +429,6 @@ describe('ExposedComponentsRegistry', () => {
|
|||
// Production mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
id: `${pluginId}/exposed-component/v1`,
|
||||
title: 'Component title',
|
||||
|
|
@ -471,9 +436,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.exposedComponents = [];
|
||||
|
||||
const registry = await createRegistry();
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [componentConfig],
|
||||
|
|
@ -489,17 +452,19 @@ describe('ExposedComponentsRegistry', () => {
|
|||
// 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'),
|
||||
};
|
||||
const { description, id, title } = componentConfig;
|
||||
const app = {
|
||||
...basicApp,
|
||||
extensions: { ...basicApp.extensions, exposedComponents: [{ description, id, title }] },
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.exposedComponents = [componentConfig];
|
||||
|
||||
const registry = await createRegistry([app]);
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [componentConfig],
|
||||
|
|
@ -513,7 +478,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
|
||||
describe('asObservableSlice', () => {
|
||||
it('should return the selected exposed component from the registry', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const componentId = 'test-plugin/exposed-component/v1';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -540,7 +505,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should deep freeze exposed components', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const componentId = 'test-plugin/exposed-component/v1';
|
||||
|
||||
registry.register({
|
||||
|
|
@ -566,7 +531,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||
});
|
||||
|
||||
it('should only emit when the selected exposed component changes', async () => {
|
||||
const registry = new ExposedComponentsRegistry();
|
||||
const registry = await createRegistry();
|
||||
const componentId1 = 'test-plugin/component1/v1';
|
||||
const componentId2 = 'test-plugin/component2/v1';
|
||||
const subscribeCallback = jest.fn();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ReplaySubject } from 'rxjs';
|
||||
|
||||
import { PluginExtensionExposedComponentConfig } from '@grafana/data';
|
||||
import { AppPluginConfig, PluginExtensionExposedComponentConfig } from '@grafana/data';
|
||||
|
||||
import * as errors from '../errors';
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
|
@ -22,12 +22,13 @@ export class ExposedComponentsRegistry extends Registry<
|
|||
PluginExtensionExposedComponentConfig
|
||||
> {
|
||||
constructor(
|
||||
apps: AppPluginConfig[],
|
||||
options: {
|
||||
registrySubject?: ReplaySubject<RegistryType<ExposedComponentRegistryItem>>;
|
||||
initialState?: RegistryType<ExposedComponentRegistryItem>;
|
||||
} = {}
|
||||
) {
|
||||
super(options);
|
||||
super(apps, options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
|
|
@ -65,7 +66,7 @@ export class ExposedComponentsRegistry extends Registry<
|
|||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog)
|
||||
isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog, this.apps)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -80,7 +81,7 @@ export class ExposedComponentsRegistry extends Registry<
|
|||
|
||||
// Returns a read-only version of the registry.
|
||||
readOnly() {
|
||||
return new ExposedComponentsRegistry({
|
||||
return new ExposedComponentsRegistry(this.apps, {
|
||||
registrySubject: this.registrySubject,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
|||
describe('Registry.asObservableSlice ', () => {
|
||||
describe('Shared behaviour', () => {
|
||||
it('should handle selecting a non-existent key that later gets added', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = new AddedComponentsRegistry([]);
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
const subscribeCallback = jest.fn();
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ describe('Registry.asObservableSlice ', () => {
|
|||
});
|
||||
|
||||
it('should handle multiple subscribers independently', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const registry = new AddedComponentsRegistry([]);
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/other/point';
|
||||
const callback1 = jest.fn();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Observable, ReplaySubject, Subject, distinctUntilChanged, firstValueFrom, map, scan, startWith } from 'rxjs';
|
||||
|
||||
import { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { ExtensionsLog, log } from '../logs/log';
|
||||
import { deepFreeze } from '../utils';
|
||||
|
||||
|
|
@ -25,11 +27,14 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
|
|||
// (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.)
|
||||
protected registrySubject: ReplaySubject<RegistryType<TRegistryValue>>;
|
||||
|
||||
constructor(options: {
|
||||
registrySubject?: ReplaySubject<RegistryType<TRegistryValue>>;
|
||||
initialState?: RegistryType<TRegistryValue>;
|
||||
log?: ExtensionsLog;
|
||||
}) {
|
||||
constructor(
|
||||
protected apps: AppPluginConfig[],
|
||||
options: {
|
||||
registrySubject?: ReplaySubject<RegistryType<TRegistryValue>>;
|
||||
initialState?: RegistryType<TRegistryValue>;
|
||||
log?: ExtensionsLog;
|
||||
}
|
||||
) {
|
||||
this.resultSubject = new Subject<PluginExtensionConfigs<TMapType>>();
|
||||
this.logger = options.log ?? log;
|
||||
this.isReadOnly = false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { MonitoringLogger } from '@grafana/runtime';
|
||||
import { getAppPluginMetas, invalidateCache, setLogger } from '@grafana/runtime/internal';
|
||||
|
||||
import { getPluginExtensionRegistries } from './setup';
|
||||
|
||||
jest.mock('@grafana/runtime/internal', () => ({
|
||||
...jest.requireActual('@grafana/runtime/internal'),
|
||||
getAppPluginMetas: jest.fn(),
|
||||
}));
|
||||
|
||||
const getAppPluginMetasMock = jest.mocked(getAppPluginMetas);
|
||||
let logger: MonitoringLogger;
|
||||
|
||||
describe('getPluginExtensionRegistries', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
invalidateCache();
|
||||
getAppPluginMetasMock.mockResolvedValue([]);
|
||||
logger = {
|
||||
logDebug: jest.fn(),
|
||||
logError: jest.fn(),
|
||||
logInfo: jest.fn(),
|
||||
logMeasurement: jest.fn(),
|
||||
logWarning: jest.fn(),
|
||||
};
|
||||
setLogger(logger);
|
||||
});
|
||||
|
||||
test('should only call getAppPluginMetas once', async () => {
|
||||
const promise1 = getPluginExtensionRegistries();
|
||||
const promise2 = getPluginExtensionRegistries();
|
||||
|
||||
await Promise.all([promise1, promise2]);
|
||||
|
||||
expect(getAppPluginMetasMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should return the same promise instance for concurrent calls', async () => {
|
||||
const promise1 = getPluginExtensionRegistries();
|
||||
const promise2 = getPluginExtensionRegistries();
|
||||
const promise3 = getPluginExtensionRegistries();
|
||||
|
||||
expect(promise1).toStrictEqual(promise2);
|
||||
expect(promise2).toStrictEqual(promise3);
|
||||
expect(promise3).toStrictEqual(promise1);
|
||||
|
||||
const [first, second, third] = await Promise.all([promise1, promise2, promise3]);
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(second).toBe(third);
|
||||
expect(third).toBe(first);
|
||||
});
|
||||
|
||||
test('should not cache promise if getAppPluginMetas throws', async () => {
|
||||
getAppPluginMetasMock.mockRejectedValueOnce(new Error('Some error'));
|
||||
|
||||
const first = await getPluginExtensionRegistries();
|
||||
const second = await getPluginExtensionRegistries();
|
||||
const third = await getPluginExtensionRegistries();
|
||||
|
||||
expect(getAppPluginMetasMock).toHaveBeenCalledTimes(2); // first + second (because first throws), third is cached
|
||||
expect(first).not.toBe(second);
|
||||
expect(first).not.toBe(third);
|
||||
expect(second).toBe(third);
|
||||
expect(logger.logError).toHaveBeenCalledTimes(1);
|
||||
expect(logger.logError).toHaveBeenCalledWith(new Error('Something failed while resolving a cached promise'), {
|
||||
message: 'Some error',
|
||||
stack: expect.any(String),
|
||||
key: 'initPluginExtensionRegistries',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { PluginExtensionExposedComponents } from '@grafana/data';
|
||||
/* eslint-disable @grafana/i18n/no-untranslated-strings */
|
||||
import { AppPluginConfig, PluginExtensionExposedComponents } from '@grafana/data';
|
||||
import { getAppPluginMetas, getCachedPromise } from '@grafana/runtime/internal';
|
||||
import CentralAlertHistorySceneExposedComponent from 'app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistorySceneExposedComponent';
|
||||
import { CreateAlertFromPanelExposedComponent } from 'app/features/alerting/unified/extensions/CreateAlertFromPanelExposedComponent';
|
||||
import { AddToDashboardFormExposedComponent } from 'app/features/dashboard-scene/addToDashboard/AddToDashboardFormExposedComponent';
|
||||
|
|
@ -12,50 +14,69 @@ import { AddedLinksRegistry } from './AddedLinksRegistry';
|
|||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './types';
|
||||
|
||||
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
||||
export const addedLinksRegistry = new AddedLinksRegistry();
|
||||
export const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
||||
addedComponentsRegistry,
|
||||
exposedComponentsRegistry,
|
||||
addedLinksRegistry,
|
||||
addedFunctionsRegistry,
|
||||
};
|
||||
function initRegistries(apps: AppPluginConfig[]): PluginExtensionRegistries {
|
||||
const addedComponentsRegistry = new AddedComponentsRegistry(apps);
|
||||
const exposedComponentsRegistry = new ExposedComponentsRegistry(apps);
|
||||
const addedLinksRegistry = new AddedLinksRegistry(apps);
|
||||
const addedFunctionsRegistry = new AddedFunctionsRegistry(apps);
|
||||
return { addedComponentsRegistry, addedFunctionsRegistry, addedLinksRegistry, exposedComponentsRegistry };
|
||||
}
|
||||
|
||||
// Registering core extension links
|
||||
addedLinksRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: getCoreExtensionConfigurations(),
|
||||
});
|
||||
function registerCoreExtensions({ addedLinksRegistry, exposedComponentsRegistry }: PluginExtensionRegistries) {
|
||||
// Registering core extension links
|
||||
addedLinksRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: getCoreExtensionConfigurations(),
|
||||
});
|
||||
|
||||
// Registering core exposed components
|
||||
exposedComponentsRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: [
|
||||
{
|
||||
id: PluginExtensionExposedComponents.CentralAlertHistorySceneV1,
|
||||
title: 'Central alert history scene',
|
||||
description: 'Central alert history scene',
|
||||
component: CentralAlertHistorySceneExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.AddToDashboardFormV1,
|
||||
title: 'Add to dashboard form',
|
||||
description: 'Add to dashboard form',
|
||||
component: AddToDashboardFormExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.CreateAlertFromPanelV1,
|
||||
title: 'Create alert from panel',
|
||||
description: 'Modal to create an alert rule from panel data',
|
||||
component: CreateAlertFromPanelExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.OpenQueryLibraryV1,
|
||||
title: 'Access to the Query Library',
|
||||
description: 'Access to the Query Library',
|
||||
component: OpenQueryLibraryExposedComponent,
|
||||
},
|
||||
],
|
||||
});
|
||||
// Registering core exposed components
|
||||
exposedComponentsRegistry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: [
|
||||
{
|
||||
id: PluginExtensionExposedComponents.CentralAlertHistorySceneV1,
|
||||
title: 'Central alert history scene',
|
||||
description: 'Central alert history scene',
|
||||
component: CentralAlertHistorySceneExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.AddToDashboardFormV1,
|
||||
title: 'Add to dashboard form',
|
||||
description: 'Add to dashboard form',
|
||||
component: AddToDashboardFormExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.CreateAlertFromPanelV1,
|
||||
title: 'Create alert from panel',
|
||||
description: 'Modal to create an alert rule from panel data',
|
||||
component: CreateAlertFromPanelExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.OpenQueryLibraryV1,
|
||||
title: 'Access to the Query Library',
|
||||
description: 'Access to the Query Library',
|
||||
component: OpenQueryLibraryExposedComponent,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function initPluginExtensionRegistries(): Promise<PluginExtensionRegistries> {
|
||||
const apps = await getAppPluginMetas();
|
||||
const registries = initRegistries(apps);
|
||||
registerCoreExtensions(registries);
|
||||
|
||||
return registries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin extension registries, initializing them on first call.
|
||||
* This function is safe to call concurrently - multiple simultaneous calls will
|
||||
* all receive the same Promise instance, ensuring only one initialization.
|
||||
* If initialization (including getAppPluginMetas) fails, the error is logged and
|
||||
* empty plugin extension registries are returned as a fallback.
|
||||
* @returns Promise resolving to the plugin extension registries
|
||||
*/
|
||||
export async function getPluginExtensionRegistries(): Promise<PluginExtensionRegistries> {
|
||||
return getCachedPromise(initPluginExtensionRegistries, { defaultValue: initRegistries([]) });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ describe('useRegistrySlice', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry([]),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry([]),
|
||||
addedLinksRegistry: new AddedLinksRegistry([]),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry([]),
|
||||
};
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { AngularMeta, AppPluginConfig, PluginLoadingStrategy } from '@grafana/data';
|
||||
import { AppPluginMetas } from '@grafana/runtime/internal';
|
||||
|
||||
const app: AppPluginConfig = cloneDeep({
|
||||
const app: AppPluginConfig = structuredClone({
|
||||
id: 'myorg-someplugin-app',
|
||||
path: 'public/plugins/myorg-someplugin-app/module.js',
|
||||
version: '1.0.0',
|
||||
|
|
@ -29,7 +27,7 @@ const app: AppPluginConfig = cloneDeep({
|
|||
buildMode: 'production',
|
||||
});
|
||||
|
||||
export const metas: AppPluginMetas = cloneDeep({
|
||||
export const metas: AppPluginMetas = structuredClone({
|
||||
'grafana-exploretraces-app': {
|
||||
id: 'grafana-exploretraces-app',
|
||||
path: 'public/plugins/grafana-exploretraces-app/module.js',
|
||||
|
|
@ -483,7 +481,7 @@ export const metas: AppPluginMetas = cloneDeep({
|
|||
|
||||
export const apps = Object.values(metas);
|
||||
|
||||
export const genericAppPluginConfig: Omit<AppPluginConfig, 'id'> = {
|
||||
export const genericAppPluginConfig: Omit<AppPluginConfig, 'id'> = structuredClone({
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
|
|
@ -506,4 +504,9 @@ export const genericAppPluginConfig: Omit<AppPluginConfig, 'id'> = {
|
|||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const basicApp: AppPluginConfig = structuredClone({
|
||||
...genericAppPluginConfig,
|
||||
id: 'grafana-basic-app',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
|
||||
import { AppPluginConfig, PluginContextProvider, PluginMeta, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
|
|
@ -12,6 +12,7 @@ import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
|||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './registry/types';
|
||||
import { basicApp } from './test-fixtures/config.apps';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginComponent } from './usePluginComponent';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
|
|
@ -51,8 +52,7 @@ describe('usePluginComponent()', () => {
|
|||
let registries: PluginExtensionRegistries;
|
||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||
let pluginMeta: PluginMeta;
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const pluginId = basicApp.id;
|
||||
const exposedComponentId = `${pluginId}/exposed-component/v1`;
|
||||
const exposedComponentConfig = {
|
||||
id: exposedComponentId,
|
||||
|
|
@ -61,39 +61,23 @@ describe('usePluginComponent()', () => {
|
|||
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: [],
|
||||
},
|
||||
},
|
||||
...basicApp,
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
...basicApp.extensions,
|
||||
// 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: [],
|
||||
},
|
||||
};
|
||||
let apps: AppPluginConfig[];
|
||||
|
||||
beforeEach(() => {
|
||||
apps = [appPluginConfig];
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(apps),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(apps),
|
||||
addedLinksRegistry: new AddedLinksRegistry(apps),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(apps),
|
||||
};
|
||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
|
@ -135,19 +119,11 @@ describe('usePluginComponent()', () => {
|
|||
},
|
||||
};
|
||||
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return null if there are no component exposed for the id', () => {
|
||||
const { result } = renderHook(() => usePluginComponent('foo/bar'), { wrapper });
|
||||
|
||||
|
|
@ -383,7 +359,7 @@ describe('usePluginComponent()', () => {
|
|||
|
||||
// Should log an error in dev mode
|
||||
expect(log.error).toHaveBeenCalledWith(
|
||||
'Attempted to mutate object property "c" from extension with id myorg-extensions-app and version unknown',
|
||||
'Attempted to mutate object property "c" from extension with id grafana-basic-app and version unknown',
|
||||
{
|
||||
stack: expect.any(String),
|
||||
}
|
||||
|
|
@ -439,7 +415,7 @@ describe('usePluginComponent()', () => {
|
|||
|
||||
// Should log a warning
|
||||
expect(log.warning).toHaveBeenCalledWith(
|
||||
'Attempted to mutate object property "c" from extension with id myorg-extensions-app and version unknown',
|
||||
'Attempted to mutate object property "c" from extension with id grafana-basic-app and version unknown',
|
||||
{
|
||||
stack: expect.any(String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import { act, render, renderHook, screen } from '@testing-library/react';
|
||||
import React, { type JSX } from 'react';
|
||||
|
||||
import {
|
||||
PluginContextProvider,
|
||||
PluginExtensionPoints,
|
||||
PluginLoadingStrategy,
|
||||
PluginMeta,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { AppPluginConfig, PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
|
|
@ -19,6 +13,7 @@ import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
|||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './registry/types';
|
||||
import { basicApp } from './test-fixtures/config.apps';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginComponents } from './usePluginComponents';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
|
|
@ -62,9 +57,10 @@ describe('usePluginComponents()', () => {
|
|||
let registries: PluginExtensionRegistries;
|
||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||
let pluginMeta: PluginMeta;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const pluginId = basicApp.id;
|
||||
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||
const originalBuildInfoEnv = config.buildInfo.env;
|
||||
let apps: AppPluginConfig[];
|
||||
|
||||
beforeEach(() => {
|
||||
config.buildInfo.env = originalBuildInfoEnv;
|
||||
|
|
@ -72,11 +68,12 @@ describe('usePluginComponents()', () => {
|
|||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
|
||||
resetLogMock(log);
|
||||
apps = [basicApp];
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(apps),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(apps),
|
||||
addedLinksRegistry: new AddedLinksRegistry(apps),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(apps),
|
||||
};
|
||||
|
||||
pluginMeta = {
|
||||
|
|
@ -115,32 +112,6 @@ describe('usePluginComponents()', () => {
|
|||
},
|
||||
};
|
||||
|
||||
config.apps[pluginId] = {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
|
|
@ -235,14 +206,14 @@ describe('usePluginComponents()', () => {
|
|||
pluginId,
|
||||
title: '1',
|
||||
description: '1',
|
||||
id: '-1921123020',
|
||||
id: '1982424218',
|
||||
type: 'component',
|
||||
});
|
||||
expect(result.current.components[1].meta).toEqual({
|
||||
pluginId,
|
||||
title: '2',
|
||||
description: '2',
|
||||
id: '-1921123019',
|
||||
id: '1982424219',
|
||||
type: 'component',
|
||||
});
|
||||
});
|
||||
|
|
@ -303,7 +274,7 @@ describe('usePluginComponents()', () => {
|
|||
// Should also render the component if it wants to change the props
|
||||
expect(() => render(<Component foo={originalFoo} override />)).not.toThrow();
|
||||
expect(log.error).toHaveBeenCalledWith(
|
||||
`Attempted to mutate object property "foo4" from extension with id myorg-extensions-app and version unknown`,
|
||||
`Attempted to mutate object property "foo4" from extension with id grafana-basic-app and version unknown`,
|
||||
{
|
||||
stack: expect.any(String),
|
||||
}
|
||||
|
|
@ -367,7 +338,7 @@ describe('usePluginComponents()', () => {
|
|||
// Should also render the component if it wants to change the props
|
||||
expect(() => render(<Component foo={originalFoo} override />)).not.toThrow();
|
||||
expect(log.warning).toHaveBeenCalledWith(
|
||||
`Attempted to mutate object property "foo4" from extension with id myorg-extensions-app and version unknown`,
|
||||
`Attempted to mutate object property "foo4" from extension with id grafana-basic-app and version unknown`,
|
||||
{
|
||||
stack: expect.any(String),
|
||||
}
|
||||
|
|
@ -506,8 +477,9 @@ describe('usePluginComponents()', () => {
|
|||
component: () => <div>Component</div>,
|
||||
};
|
||||
|
||||
// The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata (config.apps).
|
||||
config.apps[pluginId].extensions.addedComponents = [componentConfig];
|
||||
registries.addedComponentsRegistry = new AddedComponentsRegistry([
|
||||
{ ...apps[0], extensions: { ...apps[0].extensions, addedComponents: [componentConfig] } },
|
||||
]);
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import {
|
||||
PluginContextProvider,
|
||||
PluginExtensionPoints,
|
||||
PluginLoadingStrategy,
|
||||
PluginMeta,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AppPluginConfig, PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
|
|
@ -19,6 +12,7 @@ import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
|||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './registry/types';
|
||||
import { basicApp } from './test-fixtures/config.apps';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginFunctions } from './usePluginFunctions';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
|
|
@ -57,17 +51,19 @@ describe('usePluginFunctions()', () => {
|
|||
let registries: PluginExtensionRegistries;
|
||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||
let pluginMeta: PluginMeta;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const pluginId = basicApp.id;
|
||||
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||
let apps: AppPluginConfig[];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
apps = [basicApp];
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(apps),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(apps),
|
||||
addedLinksRegistry: new AddedLinksRegistry(apps),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(apps),
|
||||
};
|
||||
resetLogMock(log);
|
||||
|
||||
|
|
@ -107,32 +103,6 @@ describe('usePluginFunctions()', () => {
|
|||
},
|
||||
};
|
||||
|
||||
config.apps[pluginId] = {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
|
|
@ -328,8 +298,9 @@ describe('usePluginFunctions()', () => {
|
|||
fn: () => 'function1',
|
||||
};
|
||||
|
||||
// The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata (config.apps).
|
||||
config.apps[pluginId].extensions.addedFunctions = [functionConfig];
|
||||
registries.addedFunctionsRegistry = new AddedFunctionsRegistry([
|
||||
{ ...apps[0], extensions: { ...apps[0].extensions!, addedFunctions: [functionConfig] } },
|
||||
]);
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import {
|
||||
PluginContextProvider,
|
||||
PluginExtensionPoints,
|
||||
PluginLoadingStrategy,
|
||||
PluginMeta,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AppPluginConfig, PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
|
|
@ -19,6 +12,7 @@ import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
|||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||
import { PluginExtensionRegistries } from './registry/types';
|
||||
import { basicApp } from './test-fixtures/config.apps';
|
||||
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||
import { usePluginLinks } from './usePluginLinks';
|
||||
import { isGrafanaDevMode } from './utils';
|
||||
|
|
@ -57,17 +51,20 @@ describe('usePluginLinks()', () => {
|
|||
let registries: PluginExtensionRegistries;
|
||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||
let pluginMeta: PluginMeta;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const pluginId = basicApp.id;
|
||||
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||
let apps: AppPluginConfig[];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
apps = [basicApp];
|
||||
registries = {
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||
addedLinksRegistry: new AddedLinksRegistry(),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||
addedComponentsRegistry: new AddedComponentsRegistry(apps),
|
||||
exposedComponentsRegistry: new ExposedComponentsRegistry(apps),
|
||||
addedLinksRegistry: new AddedLinksRegistry(apps),
|
||||
addedFunctionsRegistry: new AddedFunctionsRegistry(apps),
|
||||
};
|
||||
resetLogMock(log);
|
||||
|
||||
|
|
@ -107,32 +104,6 @@ describe('usePluginLinks()', () => {
|
|||
},
|
||||
};
|
||||
|
||||
config.apps[pluginId] = {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
|
|
@ -268,8 +239,9 @@ describe('usePluginLinks()', () => {
|
|||
path: `/a/${pluginId}/2`,
|
||||
};
|
||||
|
||||
// The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps).
|
||||
config.apps[pluginId].extensions.addedLinks = [linkConfig];
|
||||
registries.addedLinksRegistry = new AddedLinksRegistry([
|
||||
{ ...apps[0], extensions: { ...apps[0].extensions!, addedLinks: [linkConfig] } },
|
||||
]);
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
import {
|
||||
AppPluginConfig,
|
||||
PluginContextType,
|
||||
PluginExtensionAddedLinkConfig,
|
||||
PluginExtensionPoints,
|
||||
PluginLoadingStrategy,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { createLogMock } from './logs/testUtils';
|
||||
import { basicApp } from './test-fixtures/config.apps';
|
||||
import {
|
||||
assertConfigureIsValid,
|
||||
assertStringProps,
|
||||
|
|
@ -23,6 +23,72 @@ import {
|
|||
isReactComponent,
|
||||
} from './validators';
|
||||
|
||||
const PLUGIN_ID = 'myorg-extensions-app';
|
||||
|
||||
function createAppPluginConfig(
|
||||
pluginId: string,
|
||||
overrides: Partial<AppPluginConfig> = {},
|
||||
extensionOverrides: Partial<AppPluginConfig['extensions']> = {}
|
||||
): AppPluginConfig {
|
||||
return {
|
||||
...basicApp,
|
||||
extensions: {
|
||||
...basicApp.extensions,
|
||||
...extensionOverrides,
|
||||
},
|
||||
id: pluginId,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createPluginContext(
|
||||
pluginId: string,
|
||||
overrides: {
|
||||
extensions?: Partial<PluginContextType['meta']['extensions']>;
|
||||
dependencies?: Partial<PluginContextType['meta']['dependencies']>;
|
||||
} = {}
|
||||
): PluginContextType {
|
||||
return {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
...overrides.extensions,
|
||||
},
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
...overrides.dependencies,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Plugin Extension Validators', () => {
|
||||
describe('assertConfigureIsValid()', () => {
|
||||
it('should NOT throw an error if the configure() function is missing', () => {
|
||||
|
|
@ -130,7 +196,7 @@ describe('Plugin Extension Validators', () => {
|
|||
expect(isReactComponent(wrapped)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return FALSE if we pass in a valid React component', () => {
|
||||
it('should return FALSE if we pass in an invalid React component', () => {
|
||||
expect(isReactComponent('Foo bar')).toBe(false);
|
||||
expect(isReactComponent(123)).toBe(false);
|
||||
expect(isReactComponent(false)).toBe(false);
|
||||
|
|
@ -209,7 +275,7 @@ describe('Plugin Extension Validators', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return FALSE true if the extension point id is set by a core plugin', () => {
|
||||
it('should return TRUE if the extension point id is set by a core plugin', () => {
|
||||
expect(
|
||||
isExtensionPointIdValid({
|
||||
extensionPointId: 'traces',
|
||||
|
|
@ -223,54 +289,17 @@ describe('Plugin Extension Validators', () => {
|
|||
});
|
||||
|
||||
describe('isAddedLinkMetaInfoMissing()', () => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
};
|
||||
const extensionConfig = {
|
||||
targets: [PluginExtensionPoints.DashboardPanelMenu],
|
||||
title: 'Link title',
|
||||
description: 'Link description',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig] })];
|
||||
|
||||
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log);
|
||||
const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
expect(log.error).toHaveBeenCalledTimes(0);
|
||||
|
|
@ -278,9 +307,9 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the app config is not found', () => {
|
||||
const log = createLogMock();
|
||||
delete config.apps[pluginId];
|
||||
const apps: AppPluginConfig[] = [];
|
||||
|
||||
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log);
|
||||
const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -289,9 +318,9 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the link has no meta-info in the plugin.json', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [] })];
|
||||
|
||||
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log);
|
||||
const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -302,15 +331,16 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the "targets" do not match', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig] })];
|
||||
|
||||
const returnValue = isAddedLinkMetaInfoMissing(
|
||||
pluginId,
|
||||
PLUGIN_ID,
|
||||
{
|
||||
...extensionConfig,
|
||||
targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction],
|
||||
},
|
||||
log
|
||||
log,
|
||||
apps
|
||||
);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
|
|
@ -322,15 +352,16 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return FALSE and log a warning if the "description" does not match', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig] })];
|
||||
|
||||
const returnValue = isAddedLinkMetaInfoMissing(
|
||||
pluginId,
|
||||
PLUGIN_ID,
|
||||
{
|
||||
...extensionConfig,
|
||||
description: 'Link description UPDATED',
|
||||
},
|
||||
log
|
||||
log,
|
||||
apps
|
||||
);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
|
|
@ -340,14 +371,13 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return FALSE with links with the same title but different targets', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedLinks.push(extensionConfig);
|
||||
const extensionConfig2 = {
|
||||
...extensionConfig,
|
||||
targets: [PluginExtensionPoints.ExploreToolbarAction],
|
||||
};
|
||||
config.apps[pluginId].extensions.addedLinks.push(extensionConfig2);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedLinks: [extensionConfig, extensionConfig2] })];
|
||||
|
||||
const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig2, log);
|
||||
const returnValue = isAddedLinkMetaInfoMissing(PLUGIN_ID, extensionConfig2, log, apps);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
expect(log.error).toHaveBeenCalledTimes(0);
|
||||
|
|
@ -355,33 +385,6 @@ describe('Plugin Extension Validators', () => {
|
|||
});
|
||||
|
||||
describe('isAddedComponentMetaInfoMissing()', () => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
};
|
||||
const extensionConfig = {
|
||||
targets: [PluginExtensionPoints.DashboardPanelMenu],
|
||||
title: 'Component title',
|
||||
|
|
@ -389,21 +392,11 @@ describe('Plugin Extension Validators', () => {
|
|||
component: () => <div>Component content</div>,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig] })];
|
||||
|
||||
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log);
|
||||
const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
expect(log.error).toHaveBeenCalledTimes(0);
|
||||
|
|
@ -411,9 +404,9 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the app config is not found', () => {
|
||||
const log = createLogMock();
|
||||
delete config.apps[pluginId];
|
||||
const apps: AppPluginConfig[] = [];
|
||||
|
||||
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log);
|
||||
const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -422,9 +415,9 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the Component has no meta-info in the plugin.json', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedComponents = [];
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [] })];
|
||||
|
||||
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log);
|
||||
const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -435,15 +428,16 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the "targets" do not match', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig] })];
|
||||
|
||||
const returnValue = isAddedComponentMetaInfoMissing(
|
||||
pluginId,
|
||||
PLUGIN_ID,
|
||||
{
|
||||
...extensionConfig,
|
||||
targets: [PluginExtensionPoints.ExploreToolbarAction],
|
||||
},
|
||||
log
|
||||
log,
|
||||
apps
|
||||
);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
|
|
@ -454,15 +448,16 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return FALSE and log a warning if the "description" does not match', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig] })];
|
||||
|
||||
const returnValue = isAddedComponentMetaInfoMissing(
|
||||
pluginId,
|
||||
PLUGIN_ID,
|
||||
{
|
||||
...extensionConfig,
|
||||
description: 'UPDATED',
|
||||
},
|
||||
log
|
||||
log,
|
||||
apps
|
||||
);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
|
|
@ -472,14 +467,13 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return FALSE with components with the same title but different targets', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.addedComponents.push(extensionConfig);
|
||||
const extensionConfig2 = {
|
||||
...extensionConfig,
|
||||
targets: [PluginExtensionPoints.ExploreToolbarAction],
|
||||
};
|
||||
config.apps[pluginId].extensions.addedComponents.push(extensionConfig2);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { addedComponents: [extensionConfig, extensionConfig2] })];
|
||||
|
||||
const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig2, log);
|
||||
const returnValue = isAddedComponentMetaInfoMissing(PLUGIN_ID, extensionConfig2, log, apps);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
expect(log.error).toHaveBeenCalledTimes(0);
|
||||
|
|
@ -487,55 +481,18 @@ describe('Plugin Extension Validators', () => {
|
|||
});
|
||||
|
||||
describe('isExposedComponentMetaInfoMissing()', () => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
};
|
||||
const exposedComponentConfig = {
|
||||
id: `${pluginId}/component/v1`,
|
||||
id: `${PLUGIN_ID}/component/v1`,
|
||||
title: 'Exposed component',
|
||||
description: 'Exposed component description',
|
||||
component: () => <div>Component content</div>,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig] })];
|
||||
|
||||
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log);
|
||||
const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
expect(log.warning).toHaveBeenCalledTimes(0);
|
||||
|
|
@ -543,9 +500,9 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the app config is not found', () => {
|
||||
const log = createLogMock();
|
||||
delete config.apps[pluginId];
|
||||
const apps: AppPluginConfig[] = [];
|
||||
|
||||
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log);
|
||||
const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -554,9 +511,9 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the exposed component has no meta-info in the plugin.json', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.exposedComponents = [];
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [] })];
|
||||
|
||||
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log);
|
||||
const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig, log, apps);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -567,15 +524,16 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return TRUE and log an error if the title does not match', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig] })];
|
||||
|
||||
const returnValue = isExposedComponentMetaInfoMissing(
|
||||
pluginId,
|
||||
PLUGIN_ID,
|
||||
{
|
||||
...exposedComponentConfig,
|
||||
title: 'UPDATED',
|
||||
},
|
||||
log
|
||||
log,
|
||||
apps
|
||||
);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
|
|
@ -587,15 +545,16 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return FALSE and log a warning if the "description" does not match', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||
const apps = [createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig] })];
|
||||
|
||||
const returnValue = isExposedComponentMetaInfoMissing(
|
||||
pluginId,
|
||||
PLUGIN_ID,
|
||||
{
|
||||
...exposedComponentConfig,
|
||||
description: 'UPDATED',
|
||||
},
|
||||
log
|
||||
log,
|
||||
apps
|
||||
);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
|
|
@ -605,14 +564,15 @@ describe('Plugin Extension Validators', () => {
|
|||
|
||||
it('should return FALSE with components with the same title but different targets', () => {
|
||||
const log = createLogMock();
|
||||
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig);
|
||||
const exposedComponentConfig2 = {
|
||||
...exposedComponentConfig,
|
||||
targets: [PluginExtensionPoints.ExploreToolbarAction],
|
||||
};
|
||||
config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig2);
|
||||
const apps = [
|
||||
createAppPluginConfig(PLUGIN_ID, {}, { exposedComponents: [exposedComponentConfig, exposedComponentConfig2] }),
|
||||
];
|
||||
|
||||
const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig2, log);
|
||||
const returnValue = isExposedComponentMetaInfoMissing(PLUGIN_ID, exposedComponentConfig2, log, apps);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
expect(log.error).toHaveBeenCalledTimes(0);
|
||||
|
|
@ -620,113 +580,54 @@ describe('Plugin Extension Validators', () => {
|
|||
});
|
||||
|
||||
describe('isExposedComponentDependencyMissing()', () => {
|
||||
let pluginContext: PluginContextType;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const exposedComponentId = `${pluginId}/component/v1`;
|
||||
|
||||
beforeEach(() => {
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const exposedComponentId = `${PLUGIN_ID}/component/v1`;
|
||||
|
||||
it('should return FALSE if the meta-info in the plugin.json is correct', () => {
|
||||
pluginContext.meta.dependencies?.extensions.exposedComponents.push(exposedComponentId);
|
||||
const pluginContext = createPluginContext(PLUGIN_ID, {
|
||||
dependencies: {
|
||||
extensions: {
|
||||
exposedComponents: [exposedComponentId],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext);
|
||||
|
||||
expect(returnValue).toBe(false);
|
||||
});
|
||||
|
||||
it('should return TRUE if the dependencies are missing', () => {
|
||||
const pluginContext = createPluginContext(PLUGIN_ID);
|
||||
delete pluginContext.meta.dependencies;
|
||||
|
||||
const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
});
|
||||
|
||||
it('should return TRUE if the exposed component id is not specified in the list of dependencies', () => {
|
||||
const pluginContext = createPluginContext(PLUGIN_ID);
|
||||
|
||||
const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExtensionPointMetaInfoMissing()', () => {
|
||||
let pluginContext: PluginContextType;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const extensionPointId = `${pluginId}/extension-point/v1`;
|
||||
const extensionPointId = `${PLUGIN_ID}/extension-point/v1`;
|
||||
const extensionPointConfig = {
|
||||
id: extensionPointId,
|
||||
title: 'Extension point title',
|
||||
description: 'Extension point description',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
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: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
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 pluginContext = createPluginContext(PLUGIN_ID, {
|
||||
extensions: {
|
||||
extensionPoints: [extensionPointConfig],
|
||||
},
|
||||
});
|
||||
|
||||
const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext);
|
||||
|
||||
|
|
@ -734,7 +635,10 @@ describe('Plugin Extension Validators', () => {
|
|||
});
|
||||
|
||||
it('should return TRUE if the extension point id is not recorded in the plugin.json', () => {
|
||||
const pluginContext = createPluginContext(PLUGIN_ID);
|
||||
|
||||
const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext);
|
||||
|
||||
expect(returnValue).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import {
|
|||
type PluginExtensionAddedFunctionConfig,
|
||||
PluginExtensionPoints,
|
||||
PluginExtensionPointPatterns,
|
||||
AppPluginConfig,
|
||||
} from '@grafana/data';
|
||||
import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal';
|
||||
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
||||
import { isPluginExtensionLink } from '@grafana/runtime';
|
||||
|
||||
import * as errors from './errors';
|
||||
import { ExtensionsLog } from './logs/log';
|
||||
|
|
@ -148,10 +149,11 @@ export const isExposedComponentDependencyMissing = (id: string, pluginContext: P
|
|||
export const isAddedLinkMetaInfoMissing = (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionAddedLinkConfig,
|
||||
log: ExtensionsLog
|
||||
log: ExtensionsLog,
|
||||
apps: AppPluginConfig[]
|
||||
) => {
|
||||
const logPrefix = 'Could not register link extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = apps.find((a) => a.id === pluginId);
|
||||
const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null;
|
||||
|
||||
if (!app) {
|
||||
|
|
@ -180,10 +182,11 @@ export const isAddedLinkMetaInfoMissing = (
|
|||
export const isAddedFunctionMetaInfoMissing = (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionAddedFunctionConfig,
|
||||
log: ExtensionsLog
|
||||
log: ExtensionsLog,
|
||||
apps: AppPluginConfig[]
|
||||
) => {
|
||||
const logPrefix = 'Could not register function extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = apps.find((a) => a.id === pluginId);
|
||||
const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null;
|
||||
|
||||
if (!app) {
|
||||
|
|
@ -212,10 +215,11 @@ export const isAddedFunctionMetaInfoMissing = (
|
|||
export const isAddedComponentMetaInfoMissing = (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionAddedComponentConfig,
|
||||
log: ExtensionsLog
|
||||
log: ExtensionsLog,
|
||||
apps: AppPluginConfig[]
|
||||
) => {
|
||||
const logPrefix = 'Could not register component extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = apps.find((a) => a.id === pluginId);
|
||||
const pluginJsonMetaInfo = app
|
||||
? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title)
|
||||
: null;
|
||||
|
|
@ -246,10 +250,11 @@ export const isAddedComponentMetaInfoMissing = (
|
|||
export const isExposedComponentMetaInfoMissing = (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionExposedComponentConfig,
|
||||
log: ExtensionsLog
|
||||
log: ExtensionsLog,
|
||||
apps: AppPluginConfig[]
|
||||
) => {
|
||||
const logPrefix = 'Could not register exposed component extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = apps.find((a) => a.id === pluginId);
|
||||
const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null;
|
||||
|
||||
if (!app) {
|
||||
|
|
|
|||
|
|
@ -10,21 +10,50 @@ import {
|
|||
PluginType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import {
|
||||
addedComponentsRegistry,
|
||||
addedFunctionsRegistry,
|
||||
addedLinksRegistry,
|
||||
exposedComponentsRegistry,
|
||||
} from '../extensions/registry/setup';
|
||||
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
|
||||
import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry';
|
||||
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
|
||||
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
|
||||
import { pluginsLogger } from '../utils';
|
||||
|
||||
import * as importPluginModule from './importPluginModule';
|
||||
import { pluginImporter, clearCaches } from './pluginImporter';
|
||||
|
||||
jest.mock('../extensions/registry/setup', () => ({
|
||||
...jest.requireActual('../extensions/registry/setup'),
|
||||
getPluginExtensionRegistries: jest.fn(),
|
||||
}));
|
||||
|
||||
const { getPluginExtensionRegistries } = jest.requireMock('../extensions/registry/setup');
|
||||
const getPluginExtensionRegistriesMock = jest.mocked(getPluginExtensionRegistries);
|
||||
|
||||
describe('pluginImporter', () => {
|
||||
let exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||
let addedComponentsRegistry: AddedComponentsRegistry;
|
||||
let addedLinksRegistry: AddedLinksRegistry;
|
||||
let addedFunctionsRegistry: AddedFunctionsRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
clearCaches();
|
||||
addedComponentsRegistry = new AddedComponentsRegistry([]);
|
||||
addedFunctionsRegistry = new AddedFunctionsRegistry([]);
|
||||
addedLinksRegistry = new AddedLinksRegistry([]);
|
||||
exposedComponentsRegistry = new ExposedComponentsRegistry([]);
|
||||
|
||||
addedComponentsRegistry.register = jest.fn();
|
||||
addedFunctionsRegistry.register = jest.fn();
|
||||
addedLinksRegistry.register = jest.fn();
|
||||
exposedComponentsRegistry.register = jest.fn();
|
||||
|
||||
const registries = {
|
||||
addedComponentsRegistry,
|
||||
addedFunctionsRegistry,
|
||||
addedLinksRegistry,
|
||||
exposedComponentsRegistry,
|
||||
};
|
||||
|
||||
getPluginExtensionRegistriesMock.mockResolvedValue(registries);
|
||||
});
|
||||
|
||||
describe('importPanel', () => {
|
||||
|
|
@ -217,12 +246,7 @@ describe('pluginImporter', () => {
|
|||
addedFunctionConfigs: [{}],
|
||||
},
|
||||
};
|
||||
const exposedComponentsRegistrySpy = jest
|
||||
.spyOn(exposedComponentsRegistry, 'register')
|
||||
.mockImplementation(() => {});
|
||||
const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {});
|
||||
const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {});
|
||||
const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {});
|
||||
|
||||
const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({ ...plugin });
|
||||
|
||||
const result = await pluginImporter.importApp({ ...appPlugin });
|
||||
|
|
@ -237,10 +261,23 @@ describe('pluginImporter', () => {
|
|||
});
|
||||
expect(init).toHaveBeenCalledWith({ ...appPlugin });
|
||||
expect(setComponentsFromLegacyExports).toHaveBeenCalledWith({ ...plugin });
|
||||
expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedComponentsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
expect(addedLinksRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
expect(addedFunctionsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
|
||||
expect(exposedComponentsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
...appPlugin,
|
||||
|
|
@ -268,12 +305,7 @@ describe('pluginImporter', () => {
|
|||
addedFunctionConfigs: [{}],
|
||||
},
|
||||
};
|
||||
const exposedComponentsRegistrySpy = jest
|
||||
.spyOn(exposedComponentsRegistry, 'register')
|
||||
.mockImplementation(() => {});
|
||||
const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {});
|
||||
const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {});
|
||||
const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {});
|
||||
|
||||
const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({ ...plugin });
|
||||
const meta = { ...appPlugin, loadingStrategy: PluginLoadingStrategy.script };
|
||||
|
||||
|
|
@ -289,10 +321,23 @@ describe('pluginImporter', () => {
|
|||
});
|
||||
expect(init).toHaveBeenCalledWith({ ...meta });
|
||||
expect(setComponentsFromLegacyExports).toHaveBeenCalledWith({ ...plugin });
|
||||
expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] });
|
||||
expect(addedComponentsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
expect(addedLinksRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
expect(addedFunctionsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
|
||||
expect(exposedComponentsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [{}],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
...appPlugin,
|
||||
|
|
@ -307,12 +352,6 @@ describe('pluginImporter', () => {
|
|||
});
|
||||
|
||||
it('should import an empty app plugin if missing exported plugin', async () => {
|
||||
const exposedComponentsRegistrySpy = jest
|
||||
.spyOn(exposedComponentsRegistry, 'register')
|
||||
.mockImplementation(() => {});
|
||||
const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {});
|
||||
const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {});
|
||||
const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {});
|
||||
const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({});
|
||||
|
||||
const result = await pluginImporter.importApp({ ...appPlugin });
|
||||
|
|
@ -325,10 +364,24 @@ describe('pluginImporter', () => {
|
|||
moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b',
|
||||
translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' },
|
||||
});
|
||||
expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] });
|
||||
expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] });
|
||||
expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] });
|
||||
expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] });
|
||||
|
||||
expect(addedComponentsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [],
|
||||
});
|
||||
expect(addedLinksRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [],
|
||||
});
|
||||
expect(addedFunctionsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [],
|
||||
});
|
||||
|
||||
expect(exposedComponentsRegistry.register).toHaveBeenCalledWith({
|
||||
pluginId: 'test-plugin',
|
||||
configs: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ...new AppPlugin(), meta: { ...appPlugin } });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ import { config } from '@grafana/runtime';
|
|||
import { GenericDataSourcePlugin } from 'app/features/datasources/types';
|
||||
import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError';
|
||||
|
||||
import {
|
||||
addedComponentsRegistry,
|
||||
addedFunctionsRegistry,
|
||||
addedLinksRegistry,
|
||||
exposedComponentsRegistry,
|
||||
} from '../extensions/registry/setup';
|
||||
import { getPluginExtensionRegistries } from '../extensions/registry/setup';
|
||||
import { pluginsLogger } from '../utils';
|
||||
|
||||
import { importPluginModule } from './importPluginModule';
|
||||
|
|
@ -98,6 +93,8 @@ const appPluginPostImport: PostImportStrategy<AppPlugin, AppPluginMeta> = async
|
|||
plugin.meta = meta;
|
||||
plugin.setComponentsFromLegacyExports(pluginExports);
|
||||
|
||||
const { exposedComponentsRegistry, addedComponentsRegistry, addedFunctionsRegistry, addedLinksRegistry } =
|
||||
await getPluginExtensionRegistries();
|
||||
exposedComponentsRegistry.register({ pluginId: meta.id, configs: plugin.exposedComponentConfigs || [] });
|
||||
addedComponentsRegistry.register({ pluginId: meta.id, configs: plugin.addedComponentConfigs || [] });
|
||||
addedLinksRegistry.register({ pluginId: meta.id, configs: plugin.addedLinkConfigs || [] });
|
||||
|
|
|
|||
Loading…
Reference in a new issue