The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/plugins/extensions/utils.test.tsx

973 lines
27 KiB

import { render, screen } from '@testing-library/react';
import { type Unsubscribable } from 'rxjs';
import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data';
import { config } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import { ShowModalReactEvent } from 'app/types/events';
PluginExtension: Added debug log (#94146) * wip * add simple scenes object with logs panel * return hardcoded log message from runtime ds * simplify log entry * use log in links registry * wired the log together. * wip * Connected the extensions log to the runtime datasource to steam logs * wired the other registies. * implemented child function. * set right field type on labels * set meta type * using the logger in various places. * added type of onclick. * removed time picker. * removed imports. * passing log to functions where they are needed. * moved scene into admin page. * minor improvement to the message. * added possibility to update query with values based on the data. * added filter suppoert. * wip * wip * fixed so extension points are displayed. * use log level from grafana data * fixed bugs with the filtering. * Fixed some logs. * only register extensions page in development mode. * fixed filtering. * added on click debug log. * PluginExtensions: Add debug log to Grafana (Rewrite to scenes-react) (#93954) * refactoring. * simplify it even more. * Update public/app/features/plugins/extensions/logs/LogViewer.tsx Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> * used VizGridLayout instead of VizGrid component. * Fixed feedback and fixed bug in filtering logic. * fixed another nit. * empty string instead of title. * Added tests and fixed error. * added test file. * regenerated yarn.lock * Update public/app/features/plugins/extensions/logs/filterTransformation.test.ts Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com> * fixed nit. * more nits. * added more test cases. * simplified filtering logic. * removed unused dep. * defined broadcast channel in jest setup. * added tests for datasource. * fixed failed tests. * fixed tests. * fixing go lint issue. * silent go lint. * fixed lint issue. --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
8 months ago
import { log } from './logs/log';
import {
deepFreeze,
handleErrorsInFn,
getReadOnlyProxy,
createOpenModalFunction,
wrapWithPluginContext,
getExtensionPointPluginDependencies,
getExposedComponentPluginDependencies,
getAppPluginConfigs,
getAppPluginIdFromExposedComponentId,
getAppPluginDependencies,
} from './utils';
jest.mock('app/features/plugins/pluginSettings', () => ({
...jest.requireActual('app/features/plugins/pluginSettings'),
getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' } }),
}));
describe('Plugin Extensions / Utils', () => {
describe('deepFreeze()', () => {
test('should not fail when called with primitive values', () => {
// Although the type system doesn't allow to call it with primitive values, it can happen that the plugin just ignores these errors.
// In these cases, we would like to make sure that the function doesn't fail.
// @ts-ignore
expect(deepFreeze(1)).toBe(1);
// @ts-ignore
expect(deepFreeze('foo')).toBe('foo');
// @ts-ignore
expect(deepFreeze(true)).toBe(true);
// @ts-ignore
expect(deepFreeze(false)).toBe(false);
// @ts-ignore
expect(deepFreeze(undefined)).toBe(undefined);
// @ts-ignore
expect(deepFreeze(null)).toBe(null);
});
test('should freeze an object so it cannot be overriden', () => {
const obj = {
a: 1,
b: '2',
c: true,
};
const frozen = deepFreeze(obj);
expect(Object.isFrozen(frozen)).toBe(true);
expect(() => {
frozen.a = 234;
}).toThrow(TypeError);
});
test('should freeze the primitive properties of an object', () => {
const obj = {
a: 1,
b: '2',
c: true,
};
const frozen = deepFreeze(obj);
expect(Object.isFrozen(frozen)).toBe(true);
expect(() => {
frozen.a = 2;
frozen.b = '3';
frozen.c = false;
}).toThrow(TypeError);
});
test('should return the same object (but frozen)', () => {
const obj = {
a: 1,
b: '2',
c: true,
d: {
e: {
f: 'foo',
},
},
};
const frozen = deepFreeze(obj);
expect(Object.isFrozen(frozen)).toBe(true);
expect(frozen).toEqual(obj);
});
test('should freeze the nested object properties', () => {
const obj = {
a: 1,
b: {
c: {
d: 2,
e: {
f: 3,
},
},
},
};
const frozen = deepFreeze(obj);
// Check if the object is frozen
expect(Object.isFrozen(frozen)).toBe(true);
// Trying to override a primitive property -> should fail
expect(() => {
frozen.a = 2;
}).toThrow(TypeError);
// Trying to override an underlying object -> should fail
expect(Object.isFrozen(frozen.b)).toBe(true);
expect(() => {
// @ts-ignore
frozen.b = {};
}).toThrow(TypeError);
// Trying to override deeply nested properties -> should fail
expect(() => {
frozen.b.c.e.f = 12345;
}).toThrow(TypeError);
});
test('should not mutate the original object', () => {
const obj = {
a: 1,
b: {
c: {
d: 2,
e: {
f: 3,
},
},
},
};
deepFreeze(obj);
// We should still be able to override the original object's properties
expect(Object.isFrozen(obj)).toBe(false);
expect(() => {
obj.b.c.d = 12345;
expect(obj.b.c.d).toBe(12345);
}).not.toThrow();
});
test('should work with nested arrays as well', () => {
const obj = {
a: 1,
b: {
c: {
d: [{ e: { f: 1 } }],
},
},
};
const frozen = deepFreeze(obj);
// Should be still possible to override the original object
expect(() => {
obj.b.c.d[0].e.f = 12345;
expect(obj.b.c.d[0].e.f).toBe(12345);
}).not.toThrow();
// Trying to override the frozen object throws a TypeError
expect(() => {
frozen.b.c.d[0].e.f = 6789;
}).toThrow();
// The original object should not be mutated
expect(obj.b.c.d[0].e.f).toBe(12345);
expect(frozen.b.c.d).toHaveLength(1);
expect(frozen.b.c.d[0].e.f).toBe(1);
});
test('should not blow up when called with an object that contains cycles', () => {
const obj = {
a: 1,
b: {
c: 123,
},
};
// @ts-ignore
obj.b.d = obj;
let frozen: typeof obj;
// Check if it does not throw due to the cycle in the object
expect(() => {
frozen = deepFreeze(obj);
}).not.toThrow();
// Check if it did freeze the object
// @ts-ignore
expect(Object.isFrozen(frozen)).toBe(true);
// @ts-ignore
expect(Object.isFrozen(frozen.b)).toBe(true);
// @ts-ignore
expect(Object.isFrozen(frozen.b.d)).toBe(true);
});
});
describe('handleErrorsInFn()', () => {
test('should catch errors thrown by the provided function and print them as console warnings', () => {
global.console.warn = jest.fn();
expect(() => {
const fn = handleErrorsInFn((foo: string) => {
throw new Error('Error: ' + foo);
});
fn('TEST');
// Logs the errors
expect(console.warn).toHaveBeenCalledWith('Error: TEST');
}).not.toThrow();
});
});
describe('getReadOnlyProxy()', () => {
it('should not be possible to modify values in proxied object', () => {
const proxy = getReadOnlyProxy({ a: 'a' });
expect(() => {
proxy.a = 'b';
}).toThrowError(TypeError);
});
it('should not be possible to modify values in proxied array', () => {
const proxy = getReadOnlyProxy([1, 2, 3]);
expect(() => {
proxy[0] = 2;
}).toThrowError(TypeError);
});
it('should not be possible to modify nested objects in proxied object', () => {
const proxy = getReadOnlyProxy({
a: {
c: 'c',
},
b: 'b',
});
expect(() => {
proxy.a.c = 'testing';
}).toThrowError(TypeError);
});
it('should not be possible to modify nested arrays in proxied object', () => {
const proxy = getReadOnlyProxy({
a: {
c: ['c', 'd'],
},
b: 'b',
});
expect(() => {
proxy.a.c[0] = 'testing';
}).toThrowError(TypeError);
});
it('should be possible to modify source object', () => {
const source = { a: 'b' };
getReadOnlyProxy(source);
source.a = 'c';
expect(source.a).toBe('c');
});
it('should be possible to modify source array', () => {
const source = ['a', 'b'];
getReadOnlyProxy(source);
source[0] = 'c';
expect(source[0]).toBe('c');
});
it('should be possible to modify nedsted objects in source object', () => {
const source = { a: { b: 'c' } };
getReadOnlyProxy(source);
source.a.b = 'd';
expect(source.a.b).toBe('d');
});
it('should be possible to modify nedsted arrays in source object', () => {
const source = { a: { b: ['c', 'd'] } };
getReadOnlyProxy(source);
source.a.b[0] = 'd';
expect(source.a.b[0]).toBe('d');
});
it('should be possible to call functions in proxied object', () => {
const proxy = getReadOnlyProxy({
a: () => 'testing',
});
expect(proxy.a()).toBe('testing');
});
it('should return a clone of moment/datetime in context', () => {
const source = dateTime('2023-10-26T18:25:01Z');
const proxy = getReadOnlyProxy({
a: source,
});
expect(source.isSame(proxy.a)).toBe(true);
expect(source).not.toBe(proxy.a);
});
});
describe('createOpenModalFunction()', () => {
let renderModalSubscription: Unsubscribable | undefined;
beforeAll(() => {
renderModalSubscription = appEvents.subscribe(ShowModalReactEvent, (event) => {
const { payload } = event;
const Modal = payload.component;
render(<Modal />);
});
});
afterAll(() => {
renderModalSubscription?.unsubscribe();
});
it('should open modal with provided title and body', async () => {
const pluginId = 'grafana-worldmap-panel';
const openModal = createOpenModalFunction(pluginId);
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
});
expect(await screen.findByRole('dialog')).toBeVisible();
expect(screen.getByRole('heading')).toHaveTextContent('Title in modal');
expect(screen.getByText('Text in body')).toBeVisible();
});
it('should open modal with default width if not specified', async () => {
const pluginId = 'grafana-worldmap-panel';
const openModal = createOpenModalFunction(pluginId);
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
});
const modal = await screen.findByRole('dialog');
const style = window.getComputedStyle(modal);
expect(style.width).toBe('750px');
expect(style.height).toBe('');
});
it('should open modal with specified width', async () => {
const pluginId = 'grafana-worldmap-panel';
const openModal = createOpenModalFunction(pluginId);
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
width: '70%',
});
const modal = await screen.findByRole('dialog');
const style = window.getComputedStyle(modal);
expect(style.width).toBe('70%');
});
it('should open modal with specified height', async () => {
const pluginId = 'grafana-worldmap-panel';
const openModal = createOpenModalFunction(pluginId);
openModal({
title: 'Title in modal',
body: () => <div>Text in body</div>,
height: 600,
});
const modal = await screen.findByRole('dialog');
const style = window.getComputedStyle(modal);
expect(style.height).toBe('600px');
});
it('should open modal with the plugin context being available', async () => {
const pluginId = 'grafana-worldmap-panel';
const openModal = createOpenModalFunction(pluginId);
const ModalContent = () => {
const context = usePluginContext();
Plugin Extensions: Require meta-data to be defined in `plugin.json` during development mode (#93429) * feat: add extensions to the backend plugin model * feat: update the frontend plugin types * feat(pluginContext): return a `null` if there is no context found This will be necessary to understand if a certain hook is running inside a plugin context or not. * feat: add utility functions for checking extension configs * tests: fix failing tests due to the type updates * feat(AddedComponentsRegistry): validate plugin meta-info * feat(AddedLinksRegistry): validate meta-info * feat(ExposedComponentsRegistry): validate meta-info * feat(usePluginComponent): add meta-info validation * feat(usePluginComponents): add meta-info validation * feat(usePluginLinks): add meta-info validation * fix: only validate meta-info in registries if dev mode is enabled * tests: add unit tests for the restrictions functionality * tests: fix Go tests * fix(tests): revert accidental changes * fix: run goimports * fix: api tests * add nested app so that meta data can bested e2e tested * refactor(types): extract the ExtensionInfo into a separate type * refactor(extensions/utils): use Array.prototype.some() instead of .find() * refactor(usePluginLinks): update warning message * feat(usePluginExtensions()): validate plugin meta-info * Wip * fix(e2e): E2E tests for extensions * fix(extensions): allow multiple "/" slashes in the extension point id * fix(extensions/validators): stop validating the plugin id pattern --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
8 months ago
return <div>Version: {context!.meta.info.version}</div>;
};
openModal({
title: 'Title in modal',
body: ModalContent,
});
const modal = await screen.findByRole('dialog');
expect(modal).toHaveTextContent('Version: 1.0.0');
});
});
describe('wrapExtensionComponentWithContext()', () => {
type ExampleComponentProps = {
audience?: string;
};
const ExampleComponent = (props: ExampleComponentProps) => {
Plugin Extensions: Require meta-data to be defined in `plugin.json` during development mode (#93429) * feat: add extensions to the backend plugin model * feat: update the frontend plugin types * feat(pluginContext): return a `null` if there is no context found This will be necessary to understand if a certain hook is running inside a plugin context or not. * feat: add utility functions for checking extension configs * tests: fix failing tests due to the type updates * feat(AddedComponentsRegistry): validate plugin meta-info * feat(AddedLinksRegistry): validate meta-info * feat(ExposedComponentsRegistry): validate meta-info * feat(usePluginComponent): add meta-info validation * feat(usePluginComponents): add meta-info validation * feat(usePluginLinks): add meta-info validation * fix: only validate meta-info in registries if dev mode is enabled * tests: add unit tests for the restrictions functionality * tests: fix Go tests * fix(tests): revert accidental changes * fix: run goimports * fix: api tests * add nested app so that meta data can bested e2e tested * refactor(types): extract the ExtensionInfo into a separate type * refactor(extensions/utils): use Array.prototype.some() instead of .find() * refactor(usePluginLinks): update warning message * feat(usePluginExtensions()): validate plugin meta-info * Wip * fix(e2e): E2E tests for extensions * fix(extensions): allow multiple "/" slashes in the extension point id * fix(extensions/validators): stop validating the plugin id pattern --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
8 months ago
const pluginContext = usePluginContext();
const audience = props.audience || 'Grafana';
return (
<div>
Plugin Extensions: Require meta-data to be defined in `plugin.json` during development mode (#93429) * feat: add extensions to the backend plugin model * feat: update the frontend plugin types * feat(pluginContext): return a `null` if there is no context found This will be necessary to understand if a certain hook is running inside a plugin context or not. * feat: add utility functions for checking extension configs * tests: fix failing tests due to the type updates * feat(AddedComponentsRegistry): validate plugin meta-info * feat(AddedLinksRegistry): validate meta-info * feat(ExposedComponentsRegistry): validate meta-info * feat(usePluginComponent): add meta-info validation * feat(usePluginComponents): add meta-info validation * feat(usePluginLinks): add meta-info validation * fix: only validate meta-info in registries if dev mode is enabled * tests: add unit tests for the restrictions functionality * tests: fix Go tests * fix(tests): revert accidental changes * fix: run goimports * fix: api tests * add nested app so that meta data can bested e2e tested * refactor(types): extract the ExtensionInfo into a separate type * refactor(extensions/utils): use Array.prototype.some() instead of .find() * refactor(usePluginLinks): update warning message * feat(usePluginExtensions()): validate plugin meta-info * Wip * fix(e2e): E2E tests for extensions * fix(extensions): allow multiple "/" slashes in the extension point id * fix(extensions/validators): stop validating the plugin id pattern --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
8 months ago
<h1>Hello {audience}!</h1> Version: {pluginContext!.meta.info.version}
</div>
);
};
it('should make the plugin context available for the wrapped component', async () => {
const pluginId = 'grafana-worldmap-panel';
PluginExtension: Added debug log (#94146) * wip * add simple scenes object with logs panel * return hardcoded log message from runtime ds * simplify log entry * use log in links registry * wired the log together. * wip * Connected the extensions log to the runtime datasource to steam logs * wired the other registies. * implemented child function. * set right field type on labels * set meta type * using the logger in various places. * added type of onclick. * removed time picker. * removed imports. * passing log to functions where they are needed. * moved scene into admin page. * minor improvement to the message. * added possibility to update query with values based on the data. * added filter suppoert. * wip * wip * fixed so extension points are displayed. * use log level from grafana data * fixed bugs with the filtering. * Fixed some logs. * only register extensions page in development mode. * fixed filtering. * added on click debug log. * PluginExtensions: Add debug log to Grafana (Rewrite to scenes-react) (#93954) * refactoring. * simplify it even more. * Update public/app/features/plugins/extensions/logs/LogViewer.tsx Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> * used VizGridLayout instead of VizGrid component. * Fixed feedback and fixed bug in filtering logic. * fixed another nit. * empty string instead of title. * Added tests and fixed error. * added test file. * regenerated yarn.lock * Update public/app/features/plugins/extensions/logs/filterTransformation.test.ts Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com> * fixed nit. * more nits. * added more test cases. * simplified filtering logic. * removed unused dep. * defined broadcast channel in jest setup. * added tests for datasource. * fixed failed tests. * fixed tests. * fixing go lint issue. * silent go lint. * fixed lint issue. --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
8 months ago
const Component = wrapWithPluginContext(pluginId, ExampleComponent, log);
render(<Component />);
expect(await screen.findByText('Hello Grafana!')).toBeVisible();
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
});
it('should pass the properties into the wrapped component', async () => {
const pluginId = 'grafana-worldmap-panel';
PluginExtension: Added debug log (#94146) * wip * add simple scenes object with logs panel * return hardcoded log message from runtime ds * simplify log entry * use log in links registry * wired the log together. * wip * Connected the extensions log to the runtime datasource to steam logs * wired the other registies. * implemented child function. * set right field type on labels * set meta type * using the logger in various places. * added type of onclick. * removed time picker. * removed imports. * passing log to functions where they are needed. * moved scene into admin page. * minor improvement to the message. * added possibility to update query with values based on the data. * added filter suppoert. * wip * wip * fixed so extension points are displayed. * use log level from grafana data * fixed bugs with the filtering. * Fixed some logs. * only register extensions page in development mode. * fixed filtering. * added on click debug log. * PluginExtensions: Add debug log to Grafana (Rewrite to scenes-react) (#93954) * refactoring. * simplify it even more. * Update public/app/features/plugins/extensions/logs/LogViewer.tsx Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> * used VizGridLayout instead of VizGrid component. * Fixed feedback and fixed bug in filtering logic. * fixed another nit. * empty string instead of title. * Added tests and fixed error. * added test file. * regenerated yarn.lock * Update public/app/features/plugins/extensions/logs/filterTransformation.test.ts Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com> * fixed nit. * more nits. * added more test cases. * simplified filtering logic. * removed unused dep. * defined broadcast channel in jest setup. * added tests for datasource. * fixed failed tests. * fixed tests. * fixing go lint issue. * silent go lint. * fixed lint issue. --------- Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
8 months ago
const Component = wrapWithPluginContext(pluginId, ExampleComponent, log);
render(<Component audience="folks" />);
expect(await screen.findByText('Hello folks!')).toBeVisible();
expect(screen.getByText('Version: 1.0.0')).toBeVisible();
});
});
describe('getAppPluginConfigs()', () => {
const originalApps = config.apps;
const genereicAppPluginConfig = {
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: [],
},
};
afterEach(() => {
config.apps = originalApps;
});
test('should return the app plugin configs based on the provided plugin ids', () => {
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
};
expect(getAppPluginConfigs(['myorg-first-app', 'myorg-third-app'])).toEqual([
config.apps['myorg-first-app'],
config.apps['myorg-third-app'],
]);
});
test('should simply ignore the app plugin ids that do not belong to a config', () => {
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
};
expect(getAppPluginConfigs(['myorg-first-app', 'unknown-app-id'])).toEqual([config.apps['myorg-first-app']]);
});
});
describe('getAppPluginIdFromExposedComponentId()', () => {
test('should return the app plugin id from an extension point id', () => {
expect(getAppPluginIdFromExposedComponentId('myorg-extensions-app/component/v1')).toBe('myorg-extensions-app');
});
});
describe('getExtensionPointPluginDependencies()', () => {
const originalApps = config.apps;
const genereicAppPluginConfig = {
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: [],
},
};
afterEach(() => {
config.apps = originalApps;
});
test('should return the app plugin ids that register extensions to a link extension point', () => {
const extensionPointId = 'myorg-first-app/link/v1';
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
// This plugin is registering a link extension to the extension point
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
extensions: {
addedLinks: [
{
targets: [extensionPointId],
title: 'Link title',
},
],
addedComponents: [],
exposedComponents: [],
extensionPoints: [],
},
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
};
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
expect(appPluginIds).toEqual(['myorg-second-app']);
});
test('should return the app plugin ids that register extensions to a component extension point', () => {
const extensionPointId = 'myorg-first-app/component/v1';
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
},
// This plugin is registering a component extension to the extension point
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
extensions: {
addedLinks: [],
addedComponents: [
{
targets: [extensionPointId],
title: 'Component title',
},
],
exposedComponents: [],
extensionPoints: [],
},
},
};
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
expect(appPluginIds).toEqual(['myorg-third-app']);
});
test('should return an empty array if there are no apps that that extend the extension point', () => {
const extensionPointId = 'myorg-first-app/component/v1';
// None of the apps are extending the extension point
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
};
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
expect(appPluginIds).toEqual([]);
});
test('should also return (recursively) the app plugin ids that the apps which extend the extension-point depend on', () => {
const extensionPointId = 'myorg-first-app/component/v1';
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
// This plugin is registering a component extension to the extension point.
// It is also depending on the 'myorg-fourth-app' plugin.
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
extensions: {
addedLinks: [],
addedComponents: [
{
targets: [extensionPointId],
title: 'Component title',
},
],
exposedComponents: [],
extensionPoints: [],
},
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
exposedComponents: ['myorg-fourth-app/component/v1'],
},
},
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
// This plugin exposes a component, but is also depending on the 'myorg-fifth-app'.
'myorg-fourth-app': {
...genereicAppPluginConfig,
id: 'myorg-fourth-app',
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [
{
id: 'myorg-fourth-app/component/v1',
title: 'Exposed component',
},
],
extensionPoints: [],
},
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
exposedComponents: ['myorg-fifth-app/component/v1'],
},
},
},
'myorg-fifth-app': {
...genereicAppPluginConfig,
id: 'myorg-fifth-app',
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [
{
id: 'myorg-fifth-app/component/v1',
title: 'Exposed component',
},
],
extensionPoints: [],
},
},
'myorg-sixth-app': {
...genereicAppPluginConfig,
id: 'myorg-sixth-app',
},
};
const appPluginIds = getExtensionPointPluginDependencies(extensionPointId);
expect(appPluginIds).toEqual(['myorg-second-app', 'myorg-fourth-app', 'myorg-fifth-app']);
});
});
describe('getExposedComponentPluginDependencies()', () => {
const originalApps = config.apps;
const genereicAppPluginConfig = {
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: [],
},
};
afterEach(() => {
config.apps = originalApps;
});
test('should only return the app plugin id that exposes the component, if that component does not depend on anything', () => {
const exposedComponentId = 'myorg-second-app/component/v1';
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [
{
id: exposedComponentId,
title: 'Component title',
},
],
extensionPoints: [],
},
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
};
const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId);
expect(appPluginIds).toEqual(['myorg-second-app']);
});
test('should also return the list of app plugin ids that the plugin - which exposes the component - is depending on', () => {
const exposedComponentId = 'myorg-second-app/component/v1';
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [
{
id: exposedComponentId,
title: 'Component title',
},
],
extensionPoints: [],
},
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
exposedComponents: ['myorg-fourth-app/component/v1'],
},
},
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
},
'myorg-fourth-app': {
...genereicAppPluginConfig,
id: 'myorg-fourth-app',
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [
{
id: 'myorg-fourth-app/component/v1',
title: 'Component title',
},
],
extensionPoints: [],
},
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
exposedComponents: ['myorg-fifth-app/component/v1'],
},
},
},
'myorg-fifth-app': {
...genereicAppPluginConfig,
id: 'myorg-fifth-app',
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [
{
id: 'myorg-fifth-app/component/v1',
title: 'Component title',
},
],
extensionPoints: [],
},
},
};
const appPluginIds = getExposedComponentPluginDependencies(exposedComponentId);
expect(appPluginIds).toEqual(['myorg-second-app', 'myorg-fourth-app', 'myorg-fifth-app']);
});
});
describe('getAppPluginDependencies()', () => {
const originalApps = config.apps;
const genereicAppPluginConfig = {
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: [],
},
};
afterEach(() => {
config.apps = originalApps;
});
test('should not end up in an infinite loop if there are circular dependencies', () => {
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
exposedComponents: ['myorg-third-app/link/v1'],
},
},
},
'myorg-third-app': {
...genereicAppPluginConfig,
id: 'myorg-third-app',
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
exposedComponents: ['myorg-second-app/link/v1'],
},
},
},
};
const appPluginIds = getAppPluginDependencies('myorg-second-app');
expect(appPluginIds).toEqual(['myorg-third-app']);
});
test('should not end up in an infinite loop if a plugin depends on itself', () => {
config.apps = {
'myorg-first-app': {
...genereicAppPluginConfig,
id: 'myorg-first-app',
},
'myorg-second-app': {
...genereicAppPluginConfig,
id: 'myorg-second-app',
dependencies: {
...genereicAppPluginConfig.dependencies,
extensions: {
// Not a valid scenario!
// (As this is sometimes happening out in the wild, we thought it's better to also cover it with a test-case.)
exposedComponents: ['myorg-second-app/link/v1'],
},
},
},
};
const appPluginIds = getAppPluginDependencies('myorg-second-app');
expect(appPluginIds).toEqual([]);
});
});
});