mirror of https://github.com/grafana/grafana
Plugins: Extend panel menu with commands from plugins (#63802)
* feat(plugins): introduce dashboard panel menu placement for adding menu items
* test: add test for getPanelMenu()
* added an unique identifier for each extension.
* added context to getPluginExtensions.
* wip
* Wip
* wiwip
* Wip
* feat: WWWIIIIPPPP 🧨
* Wip
* Renamed some of the types to align a bit better.
* added limit to how many extensions a plugin can register per placement.
* decreased number of items to 2
* will trim the lenght of titles to max 25 chars.
* wrapping configure function with error handling.
* added error handling for all scenarios.
* moved extension menu items to the bottom of the more sub menu.
* added tests for configuring the title.
* minor refactorings.
* changed so you need to specify the full path in package.json.
* wip
* removed unused type.
* big refactor to make things simpler and to centralize all configure error/validation handling.
* added missing import.
* fixed failing tests.
* fixed tests.
* revert(extensions): remove static extensions config in favour of registering via AppPlugin APIs
* removed the compose that didn't work for some reason.
* added tests just to verify that validation and error handling is tied together in configuration function.
* adding some more values to the context.
* draft validation.
* added missing tests for getPanelMenu.
* added more tests.
* refactor(extensions): move logic for validating extension link config to function
* Fixed ts errors.
* Started to add structure for supporting commands.
* fixed tests.
* adding commands to the registry
* tests: group test cases in describe blocks
* tests: add a little bit more refactoring to the tests
* tests: add a test case for checking correct placements
* feat: first version of the command handler
* feat: register panel menu items with commands
* refactor: make the 'configure' function not optional on `PluginExtensionRegistryItem`
* Wip
* Wip
* Wip
* added test to verify the default configure function.
* added some more tests to verify that commands have the proper error handling for its configure function.
* tests: fix TS errors in tests
* tests: add auxiliary functions
* refactor: small refactoring in tests
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* refactor: refactoring tests for registryFactory
* draft of wrapping command handler with error handling.
* refactor: refactoring tests for registryFactory
* added test for edge case.
* replaced the registry item with a configure function.
* renamed the configure function type.
* refactoring of the registryfactory.
* added tests for handler error handling.
* fixed issue with assert function.
* added comment about the limited type.
* Update public/app/features/plugins/extensions/errorHandling.test.ts
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
* Update public/app/features/plugins/extensions/errorHandling.test.ts
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
* Update public/app/features/plugins/extensions/errorHandling.test.ts
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
* added missing tests.
---------
Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
pull/64029/head
parent
7aca818aae
commit
b63c56903d
@ -1,79 +1,122 @@ |
||||
import { AppConfigureExtension, AppPluginExtensionLink } from '@grafana/data'; |
||||
import { AppPluginExtensionLink } from '@grafana/data'; |
||||
|
||||
import { handleErrorsInConfigure, handleErrorsInHandler } from './errorHandling'; |
||||
import type { CommandHandlerFunc, ConfigureFunc } from './types'; |
||||
|
||||
describe('error handling for extensions', () => { |
||||
describe('error handling for configure', () => { |
||||
const pluginId = 'grafana-basic-app'; |
||||
const errorHandler = handleErrorsInConfigure<AppPluginExtensionLink>({ |
||||
pluginId: pluginId, |
||||
title: 'Go to page one', |
||||
logger: jest.fn(), |
||||
}); |
||||
|
||||
import { createErrorHandling } from './errorHandling'; |
||||
const context = {}; |
||||
const extension: AppPluginExtensionLink = { |
||||
title: 'Go to page one', |
||||
description: 'Will navigate the user to page one', |
||||
path: `/a/${pluginId}/one`, |
||||
}; |
||||
|
||||
describe('extension error handling', () => { |
||||
const pluginId = 'grafana-basic-app'; |
||||
const errorHandler = createErrorHandling<AppPluginExtensionLink>({ |
||||
pluginId: pluginId, |
||||
title: 'Go to page one', |
||||
logger: jest.fn(), |
||||
}); |
||||
it('should return configured link if configure is successful', () => { |
||||
const configureWithErrorHandling = errorHandler(() => { |
||||
return { |
||||
title: 'This is a new title', |
||||
}; |
||||
}); |
||||
|
||||
const context = {}; |
||||
const extension: AppPluginExtensionLink = { |
||||
title: 'Go to page one', |
||||
description: 'Will navigate the user to page one', |
||||
path: `/a/${pluginId}/one`, |
||||
}; |
||||
const configured = configureWithErrorHandling(extension, context); |
||||
|
||||
it('should return configured link if configure is successful', () => { |
||||
const configureWithErrorHandling = errorHandler(() => { |
||||
return { |
||||
expect(configured).toEqual({ |
||||
title: 'This is a new title', |
||||
}; |
||||
}); |
||||
}); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
it('should return undefined if configure throws error', () => { |
||||
const configureWithErrorHandling = errorHandler(() => { |
||||
throw new Error(); |
||||
}); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
|
||||
expect(configured).toEqual({ |
||||
title: 'This is a new title', |
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
}); |
||||
|
||||
it('should return undefined if configure throws error', () => { |
||||
const configureWithErrorHandling = errorHandler(() => { |
||||
throw new Error(); |
||||
it('should return undefined if configure is promise/async-based', () => { |
||||
const promisebased = (async () => {}) as ConfigureFunc<AppPluginExtensionLink>; |
||||
const configureWithErrorHandling = errorHandler(promisebased); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
it('should return undefined if configure is not a function', () => { |
||||
const objectbased = {} as ConfigureFunc<AppPluginExtensionLink>; |
||||
const configureWithErrorHandling = errorHandler(objectbased); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
const configured = configureWithErrorHandling(extension, context); |
||||
|
||||
it('should return undefined if configure is promise/async-based', () => { |
||||
const promisebased = (async () => {}) as AppConfigureExtension<AppPluginExtensionLink>; |
||||
const configureWithErrorHandling = errorHandler(promisebased); |
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
it('should return undefined if configure returns other than an object', () => { |
||||
const returnString = (() => '') as ConfigureFunc<AppPluginExtensionLink>; |
||||
const configureWithErrorHandling = errorHandler(returnString); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
const configured = configureWithErrorHandling(extension, context); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
|
||||
it('should return undefined if configure is not a function', () => { |
||||
const objectbased = {} as AppConfigureExtension<AppPluginExtensionLink>; |
||||
const configureWithErrorHandling = errorHandler(objectbased); |
||||
it('should return undefined if configure returns undefined', () => { |
||||
const returnUndefined = () => undefined; |
||||
const configureWithErrorHandling = errorHandler(returnUndefined); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
const configured = configureWithErrorHandling(extension, context); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
}); |
||||
|
||||
it('should return undefined if configure returns other than an object', () => { |
||||
const returnString = (() => '') as AppConfigureExtension<AppPluginExtensionLink>; |
||||
const configureWithErrorHandling = errorHandler(returnString); |
||||
describe('error handling for command handler', () => { |
||||
const pluginId = 'grafana-basic-app'; |
||||
const errorHandler = handleErrorsInHandler({ |
||||
pluginId: pluginId, |
||||
title: 'open modal', |
||||
logger: jest.fn(), |
||||
}); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
it('should be called successfully when handler is a normal synchronous function', () => { |
||||
const handler = jest.fn(); |
||||
const handlerWithErrorHandling = errorHandler(handler); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
}); |
||||
handlerWithErrorHandling(); |
||||
|
||||
it('should return undefined if configure returns undefined', () => { |
||||
const returnUndefined = () => undefined; |
||||
const configureWithErrorHandling = errorHandler(returnUndefined); |
||||
expect(handler).toBeCalled(); |
||||
}); |
||||
|
||||
it('should not error out even if the handler throws an error', () => { |
||||
const handlerWithErrorHandling = errorHandler(() => { |
||||
throw new Error(); |
||||
}); |
||||
|
||||
const configured = configureWithErrorHandling(extension, context); |
||||
expect(handlerWithErrorHandling).not.toThrowError(); |
||||
}); |
||||
|
||||
expect(configured).toBeUndefined(); |
||||
it('should be called successfully when handler is an async function / promise', () => { |
||||
const promisebased = (async () => {}) as CommandHandlerFunc; |
||||
const configureWithErrorHandling = errorHandler(promisebased); |
||||
|
||||
expect(configureWithErrorHandling).not.toThrowError(); |
||||
}); |
||||
|
||||
it('should be called successfully when handler is not a function', () => { |
||||
const objectbased = {} as CommandHandlerFunc; |
||||
const configureWithErrorHandling = errorHandler(objectbased); |
||||
|
||||
expect(configureWithErrorHandling).not.toThrowError(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
@ -0,0 +1,15 @@ |
||||
export class PlacementsPerPlugin { |
||||
private counter: Record<string, number> = {}; |
||||
private limit = 2; |
||||
|
||||
allowedToAdd(placement: string): boolean { |
||||
const count = this.counter[placement] ?? 0; |
||||
|
||||
if (count >= this.limit) { |
||||
return false; |
||||
} |
||||
|
||||
this.counter[placement] = count + 1; |
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,4 @@ |
||||
import type { AppPluginExtensionCommandConfig } from '@grafana/data'; |
||||
|
||||
export type CommandHandlerFunc = AppPluginExtensionCommandConfig['handler']; |
||||
export type ConfigureFunc<T> = (extension: T, context?: object) => Partial<T> | undefined; |
||||
Loading…
Reference in new issue