DataSourceSettings: use details from HealthCheckResult (#32759)

* add custom HealthCheckError

* allow details from HealthCheckResult to be passed in the error

* pass in details.message from testing status into Alert component

* add chance

* add aria label to read only message

* update tests and add error message tests

* extract HealthCheckResultDetails type out and add comment

* extract TestingStatus interface out

* remove chance from test

* remove chance
This commit is contained in:
Vicky Lee 2021-04-08 13:32:12 +01:00 committed by GitHub
parent fe67680c42
commit 59a33d98ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 165 additions and 528 deletions

View File

@ -14,6 +14,7 @@ export const Pages = {
DataSource: {
name: 'Data source settings page name input field',
delete: 'Data source settings page Delete button',
readOnly: 'Data source settings page read only message',
saveAndTest: 'Data source settings page Save and Test button',
alert: 'Data source settings page Alert',
},

View File

@ -15,6 +15,16 @@ import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse'
const ExpressionDatasourceID = '__expr__';
class HealthCheckError extends Error {
details: HealthCheckResultDetails;
constructor(message: string, details: HealthCheckResultDetails) {
super(message);
this.details = details;
this.name = 'HealthCheckError';
}
}
/**
* Describes the current health status of a data source plugin.
*
@ -26,6 +36,16 @@ export enum HealthStatus {
Error = 'ERROR',
}
/**
* Describes the details in the payload returned when checking the health of a data source
* plugin.
*
* If the 'message' key exists, this will be displayed in the error message in DataSourceSettingsPage
*
* @public
*/
export type HealthCheckResultDetails = Record<string, any> | undefined;
/**
* Describes the payload returned when checking the health of a data source
* plugin.
@ -35,7 +55,7 @@ export enum HealthStatus {
export interface HealthCheckResult {
status: HealthStatus;
message: string;
details?: Record<string, any>;
details: HealthCheckResultDetails;
}
/**
@ -183,7 +203,8 @@ class DataSourceWithBackend<
message: res.message,
};
}
throw new Error(res.message);
throw new HealthCheckError(res.message, res.details);
});
}
}

View File

@ -1,85 +1,142 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DataSourceSettingsPage, Props } from './DataSourceSettingsPage';
import { DataSourceConstructor, DataSourcePlugin, DataSourceSettings, NavModel } from '@grafana/data';
import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { cleanUpAction } from 'app/core/actions/cleanUp';
import { screen, render } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { PluginState } from '@grafana/data';
const pluginMock = new DataSourcePlugin({} as DataSourceConstructor<any>);
jest.mock('app/features/plugins/plugin_loader', () => {
return {
importDataSourcePlugin: () => Promise.resolve(pluginMock),
};
const getMockNode = () => ({
text: 'text',
subTitle: 'subtitle',
icon: 'icon',
});
const setup = (propOverrides?: object) => {
const props: Props = {
...getRouteComponentProps(),
navModel: {} as NavModel,
dataSource: getMockDataSource(),
dataSourceMeta: getMockPlugin(),
dataSourceId: 1,
deleteDataSource: jest.fn(),
loadDataSource: jest.fn(),
setDataSourceName,
updateDataSource: jest.fn(),
initDataSourceSettings: jest.fn(),
testDataSource: jest.fn(),
setIsDefault,
dataSourceLoaded,
cleanUpAction,
page: null,
plugin: null,
loadError: null,
testingStatus: {},
...propOverrides,
};
return shallow(<DataSourceSettingsPage {...props} />);
};
const getProps = (): Props => ({
...getRouteComponentProps(),
navModel: {
node: getMockNode(),
main: getMockNode(),
},
dataSource: getMockDataSource(),
dataSourceMeta: getMockPlugin(),
dataSourceId: 1,
deleteDataSource: jest.fn(),
loadDataSource: jest.fn(),
setDataSourceName,
updateDataSource: jest.fn(),
initDataSourceSettings: jest.fn(),
testDataSource: jest.fn(),
setIsDefault,
dataSourceLoaded,
cleanUpAction,
page: null,
plugin: null,
loadError: null,
testingStatus: {},
});
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
it('should not render loading when props are ready', () => {
render(<DataSourceSettingsPage {...getProps()} />);
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
});
it('should render loader', () => {
const wrapper = setup({
dataSource: {} as DataSourceSettings,
plugin: pluginMock,
});
it('should render loading if datasource is not ready', () => {
const mockProps = getProps();
mockProps.dataSource.id = 0;
expect(wrapper).toMatchSnapshot();
render(<DataSourceSettingsPage {...mockProps} />);
expect(screen.getByText('Loading ...')).toBeInTheDocument();
});
it('should render beta info text', () => {
const wrapper = setup({
dataSourceMeta: { ...getMockPlugin(), state: 'beta' },
});
it('should render beta info text if plugin state is beta', () => {
const mockProps = getProps();
mockProps.dataSourceMeta.state = PluginState.beta;
expect(wrapper).toMatchSnapshot();
render(<DataSourceSettingsPage {...mockProps} />);
expect(
screen.getByTitle('Beta Plugin: There could be bugs and minor breaking changes to this plugin')
).toBeInTheDocument();
});
it('should render alpha info text', () => {
const wrapper = setup({
dataSourceMeta: { ...getMockPlugin(), state: 'alpha' },
plugin: pluginMock,
});
it('should render alpha info text if plugin state is alpha', () => {
const mockProps = getProps();
mockProps.dataSourceMeta.state = PluginState.alpha;
expect(wrapper).toMatchSnapshot();
render(<DataSourceSettingsPage {...mockProps} />);
expect(
screen.getByTitle('Alpha Plugin: This plugin is a work in progress and updates may include breaking changes')
).toBeInTheDocument();
});
it('should render is ready only message', () => {
const wrapper = setup({
dataSource: { ...getMockDataSource(), readOnly: true },
plugin: pluginMock,
});
it('should not render is ready only message is readOnly is false', () => {
const mockProps = getProps();
mockProps.dataSource.readOnly = false;
expect(wrapper).toMatchSnapshot();
render(<DataSourceSettingsPage {...mockProps} />);
expect(screen.queryByLabelText(selectors.pages.DataSource.readOnly)).not.toBeInTheDocument();
});
it('should render is ready only message is readOnly is true', () => {
const mockProps = getProps();
mockProps.dataSource.readOnly = true;
render(<DataSourceSettingsPage {...mockProps} />);
expect(screen.getByLabelText(selectors.pages.DataSource.readOnly)).toBeInTheDocument();
});
it('should render error message with detailed message', () => {
const mockProps = {
...getProps(),
testingStatus: {
message: 'message',
status: 'error',
details: { message: 'detailed message' },
},
};
render(<DataSourceSettingsPage {...mockProps} />);
expect(screen.getByText(mockProps.testingStatus.message)).toBeInTheDocument();
expect(screen.getByText(mockProps.testingStatus.details.message)).toBeInTheDocument();
});
it('should render error message with empty details', () => {
const mockProps = {
...getProps(),
testingStatus: {
message: 'message',
status: 'error',
details: {},
},
};
render(<DataSourceSettingsPage {...mockProps} />);
expect(screen.getByText(mockProps.testingStatus.message)).toBeInTheDocument();
});
it('should render error message without details', () => {
const mockProps = {
...getProps(),
testingStatus: {
message: 'message',
status: 'error',
},
};
render(<DataSourceSettingsPage {...mockProps} />);
expect(screen.getByText(mockProps.testingStatus.message)).toBeInTheDocument();
});
});

View File

@ -127,7 +127,7 @@ export class DataSourceSettingsPage extends PureComponent<Props> {
renderIsReadOnlyMessage() {
return (
<InfoBox severity="info">
<InfoBox aria-label={selectors.pages.DataSource.readOnly} severity="info">
This data source was added by config and cannot be modified using the UI. Please contact your server admin to
update this data source.
</InfoBox>
@ -234,12 +234,14 @@ export class DataSourceSettingsPage extends PureComponent<Props> {
)}
<div className="gf-form-group">
{testingStatus && testingStatus.message && (
{testingStatus?.message && (
<Alert
severity={testingStatus.status === 'error' ? 'error' : 'success'}
title={testingStatus.message}
aria-label={selectors.pages.DataSource.alert}
/>
>
{testingStatus.details?.message ?? null}
</Alert>
)}
</div>

View File

@ -1,439 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render alpha info text 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div>
<form
onSubmit={[Function]}
>
<div
className="gf-form"
>
<label
className="gf-form-label width-10"
>
Plugin state
</label>
<label
className="gf-form-label gf-form-label--transparent"
>
<PluginStateinfo
state="alpha"
/>
</label>
</div>
<CloudInfoBox
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": false,
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
}
}
/>
<BasicSettings
dataSourceName="gdev-cloudwatch"
isDefault={false}
onDefaultChange={[Function]}
onNameChange={[Function]}
/>
<PluginSettings
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": false,
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
}
}
dataSourceMeta={
Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "1",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
Object {
"name": "project",
"url": "one link",
},
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": Array [
Object {
"name": "test",
"path": "screenshot",
},
],
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1",
"module": "path/to/module",
"name": "pretty cool plugin 1",
"pinned": false,
"state": "alpha",
"type": "panel",
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
"meta": Object {},
}
}
/>
<div
className="gf-form-group"
/>
<ButtonRow
isReadOnly={false}
onDelete={[Function]}
onSubmit={[Function]}
onTest={[Function]}
/>
</form>
</div>
</PageContents>
</Page>
`;
exports[`Render should render beta info text 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div>
<form
onSubmit={[Function]}
>
<div
className="gf-form"
>
<label
className="gf-form-label width-10"
>
Plugin state
</label>
<label
className="gf-form-label gf-form-label--transparent"
>
<PluginStateinfo
state="beta"
/>
</label>
</div>
<CloudInfoBox
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": false,
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
}
}
/>
<BasicSettings
dataSourceName="gdev-cloudwatch"
isDefault={false}
onDefaultChange={[Function]}
onNameChange={[Function]}
/>
<div
className="gf-form-group"
/>
<ButtonRow
isReadOnly={false}
onDelete={[Function]}
onSubmit={[Function]}
onTest={[Function]}
/>
</form>
</div>
</PageContents>
</Page>
`;
exports[`Render should render component 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div>
<form
onSubmit={[Function]}
>
<CloudInfoBox
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": false,
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
}
}
/>
<BasicSettings
dataSourceName="gdev-cloudwatch"
isDefault={false}
onDefaultChange={[Function]}
onNameChange={[Function]}
/>
<div
className="gf-form-group"
/>
<ButtonRow
isReadOnly={false}
onDelete={[Function]}
onSubmit={[Function]}
onTest={[Function]}
/>
</form>
</div>
</PageContents>
</Page>
`;
exports[`Render should render is ready only message 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div>
<form
onSubmit={[Function]}
>
<InfoBox
severity="info"
>
This data source was added by config and cannot be modified using the UI. Please contact your server admin to update this data source.
</InfoBox>
<CloudInfoBox
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": true,
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
}
}
/>
<BasicSettings
dataSourceName="gdev-cloudwatch"
isDefault={false}
onDefaultChange={[Function]}
onNameChange={[Function]}
/>
<PluginSettings
dataSource={
Object {
"access": "",
"basicAuth": false,
"basicAuthPassword": "",
"basicAuthUser": "",
"database": "",
"id": 13,
"isDefault": false,
"jsonData": Object {
"authType": "credentials",
"defaultRegion": "eu-west-2",
},
"name": "gdev-cloudwatch",
"orgId": 1,
"password": "",
"readOnly": true,
"secureJsonFields": Object {},
"type": "cloudwatch",
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
"typeName": "Cloudwatch",
"url": "",
"user": "",
"withCredentials": false,
}
}
dataSourceMeta={
Object {
"baseUrl": "path/to/plugin",
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "1",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
Object {
"name": "project",
"url": "one link",
},
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": Array [
Object {
"name": "test",
"path": "screenshot",
},
],
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1",
"module": "path/to/module",
"name": "pretty cool plugin 1",
"pinned": false,
"type": "panel",
}
}
onModelChange={[Function]}
plugin={
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
"meta": Object {},
}
}
/>
<div
className="gf-form-group"
/>
<ButtonRow
isReadOnly={true}
onDelete={[Function]}
onSubmit={[Function]}
onTest={[Function]}
/>
</form>
</div>
</PageContents>
</Page>
`;
exports[`Render should render loader 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={true}
/>
</Page>
`;

View File

@ -95,15 +95,10 @@ export const testDataSource = (
dispatch(testDataSourceSucceeded(result));
} catch (err) {
let message = '';
const { statusText, message: errMessage, details } = err;
const message = statusText ? 'HTTP error ' + statusText : errMessage;
if (err.statusText) {
message = 'HTTP error ' + err.statusText;
} else {
message = err.message;
}
dispatch(testDataSourceFailed({ message }));
dispatch(testDataSourceFailed({ message, details }));
}
});
};

View File

@ -1,7 +1,7 @@
import { AnyAction, createAction } from '@reduxjs/toolkit';
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
import { DataSourcesState, DataSourceSettingsState } from 'app/types';
import { DataSourcesState, DataSourceSettingsState, TestingStatus } from 'app/types';
import { LayoutMode, LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
import { DataSourceTypesLoadedPayload } from './actions';
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
@ -96,10 +96,7 @@ export const dataSourcesReducer = (state: DataSourcesState = initialState, actio
};
export const initialDataSourceSettingsState: DataSourceSettingsState = {
testingStatus: {
status: null,
message: null,
},
testingStatus: {},
loadError: null,
plugin: null,
};
@ -112,12 +109,9 @@ export const initDataSourceSettingsFailed = createAction<Error>('dataSourceSetti
export const testDataSourceStarting = createAction<undefined>('dataSourceSettings/testDataSourceStarting');
export const testDataSourceSucceeded = createAction<{
status: string;
message: string;
}>('dataSourceSettings/testDataSourceSucceeded');
export const testDataSourceSucceeded = createAction<TestingStatus>('dataSourceSettings/testDataSourceSucceeded');
export const testDataSourceFailed = createAction<{ message: string }>('dataSourceSettings/testDataSourceFailed');
export const testDataSourceFailed = createAction<TestingStatus>('dataSourceSettings/testDataSourceFailed');
export const dataSourceSettingsReducer = (
state: DataSourceSettingsState = initialDataSourceSettingsState,
@ -145,8 +139,9 @@ export const dataSourceSettingsReducer = (
return {
...state,
testingStatus: {
status: action.payload.status,
message: action.payload.message,
status: action.payload?.status,
message: action.payload?.message,
details: action.payload?.details,
},
};
}
@ -156,7 +151,8 @@ export const dataSourceSettingsReducer = (
...state,
testingStatus: {
status: 'error',
message: action.payload.message,
message: action.payload?.message,
details: action.payload?.details,
},
};
}

View File

@ -1,6 +1,7 @@
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
import { GenericDataSourcePlugin } from 'app/features/datasources/settings/PluginSettings';
import { HealthCheckResultDetails } from '@grafana/runtime/src/utils/DataSourceWithBackend';
export interface DataSourcesState {
dataSources: DataSourceSettings[];
@ -16,12 +17,15 @@ export interface DataSourcesState {
categories: DataSourcePluginCategory[];
}
export interface TestingStatus {
message?: string | null;
status?: string | null;
details?: HealthCheckResultDetails;
}
export interface DataSourceSettingsState {
plugin?: GenericDataSourcePlugin | null;
testingStatus?: {
message?: string | null;
status?: string | null;
};
testingStatus?: TestingStatus;
loadError?: string | null;
}