Datasource: Overhaul plugin error handling and action buttons (#67014)

* - initial work on data source config page

* - add links to test status box
- add tracking function

* - add test for the DataSourceConfigAlert component

* - fix flicker of the alert box

* - fix the build

* - small improvements

* - fix failing build

* - fix failing unit tests

* - prettier and betterer fixes

* - fix failing e2e tests

* - fix build again

* - rewrite solution according to the PR comments

* - cleanup

* - fix failing e2e

* - use absolute path in link

* Minor fixes

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
pull/67552/head
Kuba Siemiatkowski 2 years ago committed by GitHub
parent fe59b65f9e
commit f8faacd54a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      .betterer.results
  2. 9
      packages/grafana-data/src/types/datasource.ts
  3. 9
      packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
  4. 3
      plugins-bundled/internal/input-datasource/src/InputDatasource.ts
  5. 2
      public/app/core/services/backend_srv.ts
  6. 31
      public/app/core/specs/backend_srv.test.ts
  7. 5
      public/app/features/dashboard/services/PublicDashboardDataSource.ts
  8. 10
      public/app/features/datasources/api.ts
  9. 3
      public/app/features/datasources/components/ButtonRow.test.tsx
  10. 14
      public/app/features/datasources/components/ButtonRow.tsx
  11. 98
      public/app/features/datasources/components/DataSourceTestingStatus.tsx
  12. 10
      public/app/features/datasources/components/EditDataSource.tsx
  13. 15
      public/app/features/datasources/pages/EditDataSourcePage.test.tsx
  14. 5
      public/app/features/datasources/state/actions.test.ts
  15. 90
      public/app/features/datasources/state/actions.ts
  16. 2
      public/app/features/datasources/state/reducers.ts
  17. 2
      public/app/features/datasources/types.ts
  18. 4
      public/app/plugins/datasource/cloudwatch/__mocks__/logsTestContext.ts
  19. 12
      public/app/plugins/datasource/dashboard/datasource.ts
  20. 5
      public/app/plugins/datasource/grafana/datasource.ts
  21. 5
      public/app/plugins/datasource/mixed/MixedDataSource.ts
  22. 9
      public/test/mocks/datasource_srv.ts

@ -380,8 +380,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"]
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
],
"packages/grafana-data/src/types/explore.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -841,8 +840,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"packages/grafana-runtime/src/utils/plugin.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@ -2604,7 +2602,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/datasources/components/DataSourceTestingStatus.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
],
"public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
@ -2618,8 +2617,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/features/datasources/state/actions.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/features/datasources/state/navModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

@ -260,7 +260,7 @@ abstract class DataSourceApi<
* a TestingStatus object. Unknown errors and HTTP errors can be re-thrown and will be handled here:
* public/app/features/datasources/state/actions.ts
*/
abstract testDatasource(): Promise<any>;
abstract testDatasource(): Promise<TestDataSourceResponse>;
/**
* Override to skip executing a query
@ -473,6 +473,13 @@ export interface DataQueryResponse {
traceIds?: string[];
}
export interface TestDataSourceResponse {
status: string;
message: string;
error?: Error;
details?: { message?: string; verboseMessage?: string };
}
export enum DataQueryErrorType {
Cancelled = 'cancelled',
Timeout = 'timeout',

@ -7,6 +7,7 @@ import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
@ -342,7 +343,7 @@ class DataSourceWithBackend<
* Checks the plugin health
* see public/app/features/datasources/state/actions.ts for what needs to be returned here
*/
async testDatasource(): Promise<any> {
async testDatasource(): Promise<TestDataSourceResponse> {
return this.callHealthCheck().then((res) => {
if (res.status === HealthStatus.OK) {
return {
@ -351,7 +352,11 @@ class DataSourceWithBackend<
};
}
throw new HealthCheckError(res.message, res.details);
return Promise.reject({
status: 'error',
message: res.message,
error: new HealthCheckError(res.message, res.details),
});
});
}
}

@ -2,6 +2,7 @@
import {
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
DataSourceApi,
DataSourceInstanceSettings,
MetricFindValue,
@ -68,7 +69,7 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
return Promise.resolve({ data: results });
}
testDatasource() {
testDatasource(): Promise<TestDataSourceResponse> {
return new Promise((resolve, reject) => {
let rowCount = 0;
let info = `${this.data.length} Series:`;

@ -301,7 +301,7 @@ export class BackendSrv implements BackendService {
return;
}
// is showSuccessAlert is undefined we only show alerts non GET request, non data query and local api requests
// if showSuccessAlert is undefined we only show alerts non GET request, non data query and local api requests
if (
config.showSuccessAlert === undefined &&
(config.method === 'GET' || isDataQuery(config.url) || !isLocalUrl(config.url))

@ -125,15 +125,18 @@ describe('backendSrv', () => {
});
describe('request', () => {
const testMessage = 'Datasource updated';
const errorMessage = 'UnAuthorized';
describe('when making a successful call and conditions for showSuccessAlert are not favorable', () => {
it('then it should return correct result and not emit anything', async () => {
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
data: { message: 'A message' },
data: { message: testMessage },
});
const url = '/api/dashboard/';
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: false });
expect(result).toEqual({ message: 'A message' });
expect(result).toEqual({ message: testMessage });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: false });
});
@ -142,14 +145,14 @@ describe('backendSrv', () => {
describe('when making a successful call and conditions for showSuccessAlert are favorable', () => {
it('then it should emit correct message', async () => {
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
data: { message: 'A message' },
data: { message: testMessage },
});
const url = '/api/dashboard/';
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: true });
expect(result).toEqual({ message: 'A message' });
expect(result).toEqual({ message: testMessage });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, ['A message']);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, [testMessage]);
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: true });
});
});
@ -161,8 +164,8 @@ describe('backendSrv', () => {
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
statusText: errorMessage,
data: { message: errorMessage },
url,
});
@ -174,8 +177,8 @@ describe('backendSrv', () => {
.request({ url, method: 'GET', retry: 0 })
.catch((error) => {
expect(error.status).toBe(401);
expect(error.statusText).toBe('UnAuthorized');
expect(error.data).toEqual({ message: 'UnAuthorized' });
expect(error.statusText).toBe(errorMessage);
expect(error.data).toEqual({ message: errorMessage });
expect(appEventsMock.emit).not.toHaveBeenCalled();
expect(logoutMock).not.toHaveBeenCalled();
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
@ -183,9 +186,9 @@ describe('backendSrv', () => {
jest.advanceTimersByTime(50);
})
.catch((error) => {
expect(error).toEqual({ message: 'UnAuthorized' });
expect(error).toEqual({ message: errorMessage });
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, ['UnAuthorized', '']);
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, [errorMessage, '']);
});
});
});
@ -196,7 +199,7 @@ describe('backendSrv', () => {
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
statusText: errorMessage,
data: { message: 'Token revoked', error: { id: 'ERR_TOKEN_REVOKED', maxConcurrentSessions: 3 } },
url,
});
@ -226,8 +229,8 @@ describe('backendSrv', () => {
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
ok: false,
status: 401,
statusText: 'UnAuthorized',
data: { message: 'UnAuthorized' },
statusText: errorMessage,
data: { message: errorMessage },
});
backendSrv.loginPing = jest

@ -5,6 +5,7 @@ import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
DataSourceApi,
DataSourceJsonData,
DataSourcePluginMeta,
@ -151,7 +152,7 @@ export class PublicDashboardDataSource extends DataSourceApi<DataQuery, DataSour
return { data: [toDataFrame(annotations)] };
}
testDatasource(): Promise<null> {
return Promise.resolve(null);
testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ message: '', status: '' });
}
}

@ -68,7 +68,13 @@ export const createDataSource = (dataSource: Partial<DataSourceSettings>) =>
export const getDataSourcePlugins = () => getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
export const updateDataSource = (dataSource: DataSourceSettings) =>
getBackendSrv().put(`/api/datasources/uid/${dataSource.uid}`, dataSource);
export const updateDataSource = (dataSource: DataSourceSettings) => {
// we're setting showErrorAlert and showSuccessAlert to false to suppress the popover notifications. Request result will now be
// handled by the data source config page
return getBackendSrv().put(`/api/datasources/uid/${dataSource.uid}`, dataSource, {
showErrorAlert: false,
showSuccessAlert: false,
});
};
export const deleteDataSource = (uid: string) => getBackendSrv().delete(`/api/datasources/uid/${uid}`);

@ -10,7 +10,6 @@ const setup = (propOverrides?: object) => {
canSave: false,
onSubmit: jest.fn(),
onTest: jest.fn(),
exploreUrl: '/explore',
};
Object.assign(props, propOverrides);
@ -22,7 +21,7 @@ describe('<ButtonRow>', () => {
it('should render component', () => {
setup();
expect(screen.getByRole('link', { name: 'Explore' })).toBeInTheDocument();
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('should render save & test', () => {
setup({ canSave: true });

@ -1,31 +1,23 @@
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button, LinkButton } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types';
import { Button } from '@grafana/ui';
export interface Props {
exploreUrl: string;
canSave: boolean;
onSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void;
onTest: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
export function ButtonRow({ canSave, onSubmit, onTest, exploreUrl }: Props) {
const canExploreDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
export function ButtonRow({ canSave, onSubmit, onTest }: Props) {
return (
<div className="gf-form-button-row">
<LinkButton variant="secondary" fill="solid" href={exploreUrl} disabled={!canExploreDataSources}>
Explore
</LinkButton>
{canSave && (
<Button
type="submit"
variant="primary"
disabled={!canSave}
onClick={(event) => onSubmit(event)}
onClick={onSubmit}
aria-label={selectors.pages.DataSource.saveAndTest}
>
Save &amp; test

@ -1,30 +1,108 @@
import React from 'react';
import { css, cx } from '@emotion/css';
import React, { HTMLAttributes } from 'react';
import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { TestingStatus } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import { TestingStatus, config } from '@grafana/runtime';
import { AlertVariant, Alert, useTheme2, Link } from '@grafana/ui';
import { contextSrv } from '../../../core/core';
import { AccessControlAction } from '../../../types';
import { trackCreateDashboardClicked } from '../tracking';
export type Props = {
testingStatus?: TestingStatus;
exploreUrl: string;
dataSource: DataSourceSettingsType;
};
interface AlertMessageProps extends HTMLAttributes<HTMLDivElement> {
title: string;
severity?: AlertVariant;
exploreUrl: string;
dataSourceId: string;
onDashboardLinkClicked: () => void;
}
const getStyles = (theme: GrafanaTheme2, hasTitle: boolean) => {
return {
content: css`
color: ${theme.colors.text.secondary};
padding-top: ${hasTitle ? theme.spacing(1) : 0};
max-height: 50vh;
overflow-y: auto;
`,
disabled: css`
pointer-events: none;
color: ${theme.colors.text.secondary};
`,
};
};
export function DataSourceTestingStatus({ testingStatus }: Props) {
const isError = testingStatus?.status === 'error';
const AlertSuccessMessage = ({ title, exploreUrl, dataSourceId, onDashboardLinkClicked }: AlertMessageProps) => {
const theme = useTheme2();
const hasTitle = Boolean(title);
const styles = getStyles(theme, hasTitle);
const canExploreDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
return (
<div className={styles.content}>
Next, you can start to visualize data by{' '}
<Link
aria-label={`Create a dashboard`}
href={`/dashboard/new-with-ds/${dataSourceId}`}
className="external-link"
onClick={onDashboardLinkClicked}
>
building a dashboard
</Link>
, or by querying data in the{' '}
<Link
aria-label={`Explore data`}
className={cx('external-link', {
[`${styles.disabled}`]: !canExploreDataSources,
'test-disabled': !canExploreDataSources,
})}
href={exploreUrl}
>
Explore view
</Link>
.
</div>
);
};
AlertSuccessMessage.displayName = 'AlertSuccessMessage';
export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource }: Props) {
const severity = testingStatus?.status ? (testingStatus?.status as AlertVariant) : 'error';
const message = testingStatus?.message;
const detailsMessage = testingStatus?.details?.message;
const detailsVerboseMessage = testingStatus?.details?.verboseMessage;
const onDashboardLinkClicked = () => {
trackCreateDashboardClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
};
if (message) {
return (
<div className="gf-form-group p-t-2">
<Alert
severity={isError ? 'error' : 'success'}
title={message}
aria-label={e2eSelectors.pages.DataSource.alert}
>
<Alert severity={severity} title={message} aria-label={e2eSelectors.pages.DataSource.alert}>
{testingStatus?.details && (
<>
{detailsMessage}
{severity === 'success' ? (
<AlertSuccessMessage
title={message}
exploreUrl={exploreUrl}
dataSourceId={dataSource.uid}
onDashboardLinkClicked={onDashboardLinkClicked}
/>
) : null}
{detailsVerboseMessage ? (
<details style={{ whiteSpace: 'pre-wrap' }}>{String(detailsVerboseMessage)}</details>
) : null}

@ -119,7 +119,11 @@ export function EditDataSourceView({
const onSubmit = async (e: React.MouseEvent<HTMLButtonElement> | React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await onUpdate({ ...dataSource });
try {
await onUpdate({ ...dataSource });
} catch (err) {
return;
}
onTest();
};
@ -173,9 +177,9 @@ export function EditDataSourceView({
</DataSourcePluginContextProvider>
)}
<DataSourceTestingStatus testingStatus={testingStatus} />
<DataSourceTestingStatus testingStatus={testingStatus} exploreUrl={exploreUrl} dataSource={dataSource} />
<ButtonRow onSubmit={onSubmit} onTest={onTest} exploreUrl={exploreUrl} canSave={!readOnly && hasWriteRights} />
<ButtonRow onSubmit={onSubmit} onTest={onTest} canSave={!readOnly && hasWriteRights} />
</form>
);
}

@ -4,7 +4,7 @@ import { Store } from 'redux';
import { TestProvider } from 'test/helpers/TestProvider';
import { LayoutModes } from '@grafana/data';
import { setAngularLoader } from '@grafana/runtime';
import { setAngularLoader, config } from '@grafana/runtime';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { configureStore } from 'app/store/configureStore';
@ -102,12 +102,23 @@ describe('<EditDataSourcePage>', () => {
// Title
expect(screen.queryByText(name)).toBeVisible();
// Buttons
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
// wait for the rest of the async processes to finish
expect(await screen.findByText(name)).toBeVisible();
});
it('should show updated action buttons when topnav is on', async () => {
config.featureToggles.topnav = true;
setup(uid, store);
await waitFor(() => {
// Buttons
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
expect(screen.queryByRole('link', { name: /Build a dashboard/i })).toBeVisible();
expect(screen.queryAllByRole('link', { name: /Explore/i })).toHaveLength(2);
expect(screen.queryAllByRole('link', { name: /Explore/i })).toHaveLength(1);
});
});
});

@ -217,7 +217,7 @@ describe('testDataSource', () => {
({
get: jest.fn().mockReturnValue({
testDatasource: jest.fn().mockReturnValue({
status: '',
status: 'success',
message: '',
}),
type: 'cloudwatch',
@ -228,8 +228,9 @@ describe('testDataSource', () => {
};
const state = {
testingStatus: {
status: '',
status: 'success',
message: '',
details: {},
},
};
const dispatchedActions = await thunkTester(state)

@ -57,6 +57,40 @@ export interface TestDataSourceDependencies {
getBackendSrv: typeof getBackendSrv;
}
type parseDataSourceSaveResponse = {
message?: string | undefined;
status?: string;
details?: HealthCheckResultDetails | { message?: string; verboseMessage?: string };
};
const parseHealthCheckError = (errorResponse: any): parseDataSourceSaveResponse => {
let message: string | undefined;
let details: HealthCheckResultDetails;
if (errorResponse.error && errorResponse.error instanceof HealthCheckError) {
message = errorResponse.error.message;
details = errorResponse.error.details;
} else if (isFetchError(errorResponse)) {
message = errorResponse.data.message ?? `HTTP error ${errorResponse.statusText}`;
} else if (errorResponse instanceof Error) {
message = errorResponse.message;
}
return { message, details };
};
const parseHealthCheckSuccess = (response: any): parseDataSourceSaveResponse => {
let message: string | undefined;
let status: string;
let details: { message?: string; verboseMessage?: string };
status = response.status;
message = response.message;
details = response.details;
return { status, message, details };
};
export const initDataSourceSettings = (
uid: string,
dependencies: InitDataSourceSettingDependencies = {
@ -112,7 +146,9 @@ export const testDataSource = (
try {
const result = await dsApi.testDatasource();
dispatch(testDataSourceSucceeded(result));
const parsedResult = parseHealthCheckSuccess({ ...result, details: { ...result.details } });
dispatch(testDataSourceSucceeded(parsedResult));
trackDataSourceTested({
grafana_version: config.buildInfo.version,
plugin_id: dsApi.type,
@ -121,19 +157,9 @@ export const testDataSource = (
path: editLink,
});
} catch (err) {
let message: string | undefined;
let details: HealthCheckResultDetails;
if (err instanceof HealthCheckError) {
message = err.message;
details = err.details;
} else if (isFetchError(err)) {
message = err.data.message ?? `HTTP error ${err.statusText}`;
} else if (err instanceof Error) {
message = err.message;
}
dispatch(testDataSourceFailed({ message, details }));
const formattedError = parseHealthCheckError(err);
dispatch(testDataSourceFailed({ ...formattedError }));
trackDataSourceTested({
grafana_version: config.buildInfo.version,
plugin_id: dsApi.type,
@ -216,6 +242,7 @@ export function addDataSource(
isDefault: isFirstDataSource,
};
// TODO: typo in name
if (nameExits(dataSources, newInstance.name)) {
newInstance.name = findNewName(dataSources, newInstance.name);
}
@ -248,9 +275,23 @@ export function loadDataSourcePlugins(): ThunkResult<void> {
}
export function updateDataSource(dataSource: DataSourceSettings) {
return async (dispatch: (dataSourceSettings: ThunkResult<Promise<DataSourceSettings>>) => DataSourceSettings) => {
await api.updateDataSource(dataSource);
return async (
dispatch: (
dataSourceSettings: ThunkResult<Promise<DataSourceSettings>> | { payload: unknown; type: string }
) => DataSourceSettings
) => {
try {
await api.updateDataSource(dataSource);
} catch (err: any) {
const formattedError = parseHealthCheckError(err);
dispatch(testDataSourceFailed(formattedError));
return Promise.reject(dataSource);
}
await getDatasourceSrv().reload();
return dispatch(loadDataSource(dataSource.uid));
};
}
@ -259,13 +300,18 @@ export function deleteLoadedDataSource(): ThunkResult<void> {
return async (dispatch, getStore) => {
const { uid } = getStore().dataSources.dataSource;
await api.deleteDataSource(uid);
await getDatasourceSrv().reload();
try {
await api.deleteDataSource(uid);
await getDatasourceSrv().reload();
const datasourcesUrl = config.featureToggles.dataConnectionsConsole
? CONNECTIONS_ROUTES.DataSources
: '/datasources';
const datasourcesUrl = config.featureToggles.dataConnectionsConsole
? CONNECTIONS_ROUTES.DataSources
: '/datasources';
locationService.push(datasourcesUrl);
locationService.push(datasourcesUrl);
} catch (err) {
const formattedError = parseHealthCheckError(err);
dispatch(testDataSourceFailed(formattedError));
}
};
}

@ -146,7 +146,7 @@ export const dataSourceSettingsReducer = (
return {
...state,
testingStatus: {
message: 'Testing...',
message: 'Testing... this could take up to a couple of minutes',
status: 'info',
},
};

@ -14,3 +14,5 @@ export type DataSourcesRoutes = {
List: string;
Dashboards: string;
};
export type DataSourceTestStatus = 'success' | 'warning' | 'error';

@ -12,10 +12,10 @@ import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
} from '@grafana/data';
import { GetDataSourceListFilters, setDataSourceSrv } from '@grafana/runtime';
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchLogsQueryStatus } from '../types';
import { meta, setupMockedDataSource } from './CloudWatchDataSource';
@ -62,7 +62,7 @@ export function setupForLogs() {
): Observable<DataQueryResponse> | Promise<DataQueryResponse> {
throw new Error('Function not implemented.');
},
testDatasource: function (): Promise<CloudWatchDatasource> {
testDatasource: function (): Promise<TestDataSourceResponse> {
throw new Error('Function not implemented.');
},
meta: meta,

@ -1,4 +1,10 @@
import { DataSourceApi, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data';
import {
DataSourceApi,
DataQueryRequest,
DataQueryResponse,
DataSourceInstanceSettings,
TestDataSourceResponse,
} from '@grafana/data';
import { DashboardQuery } from './types';
@ -18,7 +24,7 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
return Promise.reject('This should not be called directly');
}
testDatasource() {
return Promise.resolve({});
testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ message: '', status: '' });
}
}

@ -9,6 +9,7 @@ import {
DataQueryRequest,
DataQueryResponse,
DataSourceInstanceSettings,
TestDataSourceResponse,
isValidLiveChannelAddress,
MutableDataFrame,
parseLiveChannelAddress,
@ -252,8 +253,8 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
return { data: [toDataFrame(annotations)] };
}
testDatasource() {
return Promise.resolve();
testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ message: '', status: '' });
}
}

@ -6,6 +6,7 @@ import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
DataSourceApi,
DataSourceInstanceSettings,
LoadingState,
@ -94,8 +95,8 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
return forkJoin(runningQueries).pipe(flattenResponses(), map(this.finalizeResponses), mergeAll());
}
testDatasource() {
return Promise.resolve({});
testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ message: '', status: '' });
}
private isQueryable(query: BatchedQueries): boolean {

@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
import {
DataQueryRequest,
DataQueryResponse,
TestDataSourceResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourcePluginMeta,
@ -57,8 +58,8 @@ export class MockDataSourceApi extends DataSourceApi {
});
}
testDatasource() {
return Promise.resolve();
testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ message: '', status: '' });
}
setupMixed(value: boolean) {
@ -99,7 +100,7 @@ export class MockObservableDataSourceApi extends DataSourceApi {
});
}
testDatasource() {
return Promise.resolve();
testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ message: '', status: '' });
}
}

Loading…
Cancel
Save