mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 17:12:11 +08:00

* feat: add a reactive extension registry Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: add hooks to work with the reactive registry Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: start using the reactive registry Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: update the "command palette" extension point to use the hook * feat: update the "alerting" extension point to use the hooks Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: update the "explore" extension point to use the hooks Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: update the "datasources config" extension point to use the hooks Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: update the "panel menu" extension point to use the hooks Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: update the "pyroscope datasource" extension point to use the hooks Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * feat: update the "user profile page" extension point to use the hooks * chore: update betterer * fix: update the hooks to not re-render unnecessarily * chore: remove the old `createPluginExtensionRegistry` impementation * chore: add "TODO" for `PanelMenuBehaviour` extension point * feat: update the return value of the hooks to contain a `{ isLoading }` param * tests: add more tests for the usePluginExtensions() hook * fix: exclude the cloud-home-app from being non-awaited * refactor: use uuidv4() for random ID generation (for the registry object) * fix: linting issue * feat: use the hooks for the new alerting extension point * feat: use `useMemo()` for `AlertInstanceAction` extension point context --------- Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
import { screen, render } from '@testing-library/react';
|
|
import React from 'react';
|
|
import { Provider } from 'react-redux';
|
|
|
|
import { PluginExtensionTypes, PluginState } from '@grafana/data';
|
|
import { setAngularLoader, setPluginExtensionsHook } from '@grafana/runtime';
|
|
import { configureStore } from 'app/store/configureStore';
|
|
|
|
import { getMockDataSource, getMockDataSourceMeta, getMockDataSourceSettingsState } from '../__mocks__';
|
|
|
|
import { missingRightsMessage } from './DataSourceMissingRightsMessage';
|
|
import { readOnlyMessage } from './DataSourceReadOnlyMessage';
|
|
import { EditDataSourceView, ViewProps } from './EditDataSource';
|
|
|
|
jest.mock('@grafana/runtime', () => {
|
|
return {
|
|
...jest.requireActual('@grafana/runtime'),
|
|
getDataSourceSrv: jest.fn(() => ({
|
|
getInstanceSettings: (uid: string) => ({
|
|
uid,
|
|
meta: getMockDataSourceMeta(),
|
|
}),
|
|
})),
|
|
};
|
|
});
|
|
|
|
const setup = (props?: Partial<ViewProps>) => {
|
|
const store = configureStore();
|
|
|
|
return render(
|
|
<Provider store={store}>
|
|
<EditDataSourceView
|
|
dataSource={getMockDataSource()}
|
|
dataSourceMeta={getMockDataSourceMeta()}
|
|
dataSourceSettings={getMockDataSourceSettingsState()}
|
|
dataSourceRights={{ readOnly: false, hasWriteRights: true, hasDeleteRights: true }}
|
|
exploreUrl={'/explore'}
|
|
onDelete={jest.fn()}
|
|
onDefaultChange={jest.fn()}
|
|
onNameChange={jest.fn()}
|
|
onOptionsChange={jest.fn()}
|
|
onTest={jest.fn()}
|
|
onUpdate={jest.fn()}
|
|
{...props}
|
|
/>
|
|
</Provider>
|
|
);
|
|
};
|
|
|
|
describe('<EditDataSource>', () => {
|
|
beforeAll(() => {
|
|
setAngularLoader({
|
|
load: () => ({
|
|
destroy: jest.fn(),
|
|
digest: jest.fn(),
|
|
getScope: () => ({ $watch: () => {} }),
|
|
}),
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
setPluginExtensionsHook(jest.fn().mockReturnValue({ extensions: [] }));
|
|
});
|
|
|
|
describe('On loading errors', () => {
|
|
it('should render a Back button', () => {
|
|
setup({
|
|
dataSource: getMockDataSource({ name: 'My Datasource' }),
|
|
dataSourceSettings: getMockDataSourceSettingsState({ loadError: 'Some weird error.' }),
|
|
});
|
|
|
|
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('My Datasource')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Back')).toBeVisible();
|
|
});
|
|
|
|
it('should render a Delete button if the user has rights delete the datasource', () => {
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({ loadError: 'Some weird error.' }),
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: true,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText('Delete')).toBeVisible();
|
|
});
|
|
|
|
it('should not render a Delete button if the user has no rights to delete the datasource', () => {
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({ loadError: 'Some weird error.' }),
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: false,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render a message if the datasource is read-only', () => {
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({ loadError: 'Some weird error.' }),
|
|
dataSourceRights: {
|
|
readOnly: true,
|
|
hasDeleteRights: false,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText(readOnlyMessage)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe('On loading', () => {
|
|
it('should render a loading indicator while the data is being fetched', () => {
|
|
setup({
|
|
dataSource: getMockDataSource({ name: 'My Datasource' }),
|
|
dataSourceSettings: getMockDataSourceSettingsState({ loading: true }),
|
|
});
|
|
|
|
expect(screen.queryByText('Loading ...')).toBeVisible();
|
|
expect(screen.queryByText('My Datasource')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render loading when data is already available', () => {
|
|
setup();
|
|
|
|
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('On editing', () => {
|
|
it('should render no messages if the user has write access and if the data-source is not read-only', () => {
|
|
setup({
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: true,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText(readOnlyMessage)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(missingRightsMessage)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render a message if the user has no write access', () => {
|
|
setup({
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: false,
|
|
hasWriteRights: false,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText(missingRightsMessage)).toBeVisible();
|
|
});
|
|
|
|
it('should render a message if the data-source is read-only', () => {
|
|
setup({
|
|
dataSourceRights: {
|
|
readOnly: true,
|
|
hasDeleteRights: false,
|
|
hasWriteRights: false,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText(readOnlyMessage)).toBeVisible();
|
|
});
|
|
|
|
it('should render a beta info message if the plugin is still in Beta state', () => {
|
|
setup({
|
|
dataSourceMeta: getMockDataSourceMeta({
|
|
state: PluginState.beta,
|
|
}),
|
|
});
|
|
|
|
expect(screen.getByTitle('This feature is close to complete but not fully tested')).toBeVisible();
|
|
});
|
|
|
|
it('should render an alpha info message if the plugin is still in Alpha state', () => {
|
|
setup({
|
|
dataSourceMeta: getMockDataSourceMeta({
|
|
state: PluginState.alpha,
|
|
}),
|
|
});
|
|
|
|
expect(
|
|
screen.getByTitle('This feature is experimental and future updates might not be backward compatible')
|
|
).toBeVisible();
|
|
});
|
|
|
|
it('should render testing errors with a detailed error message', () => {
|
|
const message = 'message';
|
|
const detailsMessage = 'detailed message';
|
|
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({
|
|
testingStatus: {
|
|
message,
|
|
status: 'error',
|
|
details: { message: detailsMessage },
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(screen.getByText(message)).toBeVisible();
|
|
expect(screen.getByText(detailsMessage)).toBeVisible();
|
|
});
|
|
|
|
it('should render testing errors with empty details', () => {
|
|
const message = 'message';
|
|
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({
|
|
testingStatus: {
|
|
message,
|
|
status: 'error',
|
|
details: {},
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(screen.getByText(message)).toBeVisible();
|
|
});
|
|
|
|
it('should render testing errors with no details', () => {
|
|
const message = 'message';
|
|
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({
|
|
testingStatus: {
|
|
message,
|
|
status: 'error',
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(screen.getByText(message)).toBeVisible();
|
|
});
|
|
|
|
it('should use the verboseMessage property in the error details whenever it is available', () => {
|
|
const message = 'message';
|
|
const detailsMessage = 'detailed message';
|
|
const detailsVerboseMessage = 'even more detailed...';
|
|
|
|
setup({
|
|
dataSourceSettings: getMockDataSourceSettingsState({
|
|
testingStatus: {
|
|
message,
|
|
status: 'error',
|
|
details: {
|
|
details: detailsMessage,
|
|
verboseMessage: detailsVerboseMessage,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(screen.queryByText(message)).toBeVisible();
|
|
expect(screen.queryByText(detailsMessage)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(detailsVerboseMessage)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('when extending the datasource config form', () => {
|
|
it('should be possible to extend the form with a "component" extension in case the plugin ID is whitelisted', () => {
|
|
const message = "I'm a UI extension component!";
|
|
|
|
setPluginExtensionsHook(
|
|
jest.fn().mockReturnValue({
|
|
extensions: [
|
|
{
|
|
id: '1',
|
|
pluginId: 'grafana-pdc-app',
|
|
type: PluginExtensionTypes.component,
|
|
title: 'Example component',
|
|
description: 'Example description',
|
|
component: () => <div>{message}</div>,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
setup({
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: true,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText(message)).toBeVisible();
|
|
});
|
|
|
|
it('should NOT be possible to extend the form with a "component" extension in case the plugin ID is NOT whitelisted', () => {
|
|
const message = "I'm a UI extension component!";
|
|
|
|
setPluginExtensionsHook(
|
|
jest.fn().mockReturnValue({
|
|
extensions: [
|
|
{
|
|
id: '1',
|
|
pluginId: 'myorg-basic-app',
|
|
type: PluginExtensionTypes.component,
|
|
title: 'Example component',
|
|
description: 'Example description',
|
|
component: () => <div>{message}</div>,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
setup({
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: true,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByText(message)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should pass a context prop to the rendered UI extension component', () => {
|
|
const message = "I'm a UI extension component!";
|
|
const component = jest.fn().mockReturnValue(<div>{message}</div>);
|
|
|
|
setPluginExtensionsHook(
|
|
jest.fn().mockReturnValue({
|
|
extensions: [
|
|
{
|
|
id: '1',
|
|
pluginId: 'grafana-pdc-app',
|
|
type: PluginExtensionTypes.component,
|
|
title: 'Example component',
|
|
description: 'Example description',
|
|
component,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
setup({
|
|
dataSourceRights: {
|
|
readOnly: false,
|
|
hasDeleteRights: true,
|
|
hasWriteRights: true,
|
|
},
|
|
});
|
|
|
|
expect(component).toHaveBeenCalled();
|
|
|
|
const props = component.mock.calls[0][0];
|
|
|
|
expect(props.context).toBeDefined();
|
|
expect(props.context.dataSource).toBeDefined();
|
|
expect(props.context.dataSourceMeta).toBeDefined();
|
|
expect(props.context.setJsonData).toBeDefined();
|
|
expect(props.context.testingStatus).toBeDefined();
|
|
});
|
|
});
|
|
});
|