mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
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>
This commit is contained in:
co-authored by
Torkel Ödegaard
parent
fe59b65f9e
commit
f8faacd54a
+9
-7
@@ -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.", "25"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
|
[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.", "27"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "29"]
|
|
||||||
],
|
],
|
||||||
"packages/grafana-data/src/types/explore.ts:5381": [
|
"packages/grafana-data/src/types/explore.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[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, "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.", "3"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
[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.", "5"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
|
||||||
],
|
],
|
||||||
"packages/grafana-runtime/src/utils/plugin.ts:5381": [
|
"packages/grafana-runtime/src/utils/plugin.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/datasources/components/DataSourceTestingStatus.tsx:5381": [
|
"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": [
|
"public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[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"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||||
],
|
],
|
||||||
"public/app/features/datasources/state/actions.ts:5381": [
|
"public/app/features/datasources/state/actions.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[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": [
|
"public/app/features/datasources/state/navModel.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[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:
|
* a TestingStatus object. Unknown errors and HTTP errors can be re-thrown and will be handled here:
|
||||||
* public/app/features/datasources/state/actions.ts
|
* public/app/features/datasources/state/actions.ts
|
||||||
*/
|
*/
|
||||||
abstract testDatasource(): Promise<any>;
|
abstract testDatasource(): Promise<TestDataSourceResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override to skip executing a query
|
* Override to skip executing a query
|
||||||
@@ -473,6 +473,13 @@ export interface DataQueryResponse {
|
|||||||
traceIds?: string[];
|
traceIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestDataSourceResponse {
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
error?: Error;
|
||||||
|
details?: { message?: string; verboseMessage?: string };
|
||||||
|
}
|
||||||
|
|
||||||
export enum DataQueryErrorType {
|
export enum DataQueryErrorType {
|
||||||
Cancelled = 'cancelled',
|
Cancelled = 'cancelled',
|
||||||
Timeout = 'timeout',
|
Timeout = 'timeout',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
@@ -342,7 +343,7 @@ class DataSourceWithBackend<
|
|||||||
* Checks the plugin health
|
* Checks the plugin health
|
||||||
* see public/app/features/datasources/state/actions.ts for what needs to be returned here
|
* 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) => {
|
return this.callHealthCheck().then((res) => {
|
||||||
if (res.status === HealthStatus.OK) {
|
if (res.status === HealthStatus.OK) {
|
||||||
return {
|
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 {
|
import {
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
MetricFindValue,
|
MetricFindValue,
|
||||||
@@ -68,7 +69,7 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
|||||||
return Promise.resolve({ data: results });
|
return Promise.resolve({ data: results });
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
let info = `${this.data.length} Series:`;
|
let info = `${this.data.length} Series:`;
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export class BackendSrv implements BackendService {
|
|||||||
return;
|
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 (
|
if (
|
||||||
config.showSuccessAlert === undefined &&
|
config.showSuccessAlert === undefined &&
|
||||||
(config.method === 'GET' || isDataQuery(config.url) || !isLocalUrl(config.url))
|
(config.method === 'GET' || isDataQuery(config.url) || !isLocalUrl(config.url))
|
||||||
|
|||||||
@@ -125,15 +125,18 @@ describe('backendSrv', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('request', () => {
|
describe('request', () => {
|
||||||
|
const testMessage = 'Datasource updated';
|
||||||
|
const errorMessage = 'UnAuthorized';
|
||||||
|
|
||||||
describe('when making a successful call and conditions for showSuccessAlert are not favorable', () => {
|
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 () => {
|
it('then it should return correct result and not emit anything', async () => {
|
||||||
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
|
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
|
||||||
data: { message: 'A message' },
|
data: { message: testMessage },
|
||||||
});
|
});
|
||||||
const url = '/api/dashboard/';
|
const url = '/api/dashboard/';
|
||||||
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: false });
|
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();
|
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
||||||
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: false });
|
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: false });
|
||||||
});
|
});
|
||||||
@@ -142,14 +145,14 @@ describe('backendSrv', () => {
|
|||||||
describe('when making a successful call and conditions for showSuccessAlert are favorable', () => {
|
describe('when making a successful call and conditions for showSuccessAlert are favorable', () => {
|
||||||
it('then it should emit correct message', async () => {
|
it('then it should emit correct message', async () => {
|
||||||
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
|
const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({
|
||||||
data: { message: 'A message' },
|
data: { message: testMessage },
|
||||||
});
|
});
|
||||||
const url = '/api/dashboard/';
|
const url = '/api/dashboard/';
|
||||||
const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: true });
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, ['A message']);
|
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, [testMessage]);
|
||||||
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: true });
|
expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -161,8 +164,8 @@ describe('backendSrv', () => {
|
|||||||
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 401,
|
status: 401,
|
||||||
statusText: 'UnAuthorized',
|
statusText: errorMessage,
|
||||||
data: { message: 'UnAuthorized' },
|
data: { message: errorMessage },
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,8 +177,8 @@ describe('backendSrv', () => {
|
|||||||
.request({ url, method: 'GET', retry: 0 })
|
.request({ url, method: 'GET', retry: 0 })
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
expect(error.status).toBe(401);
|
expect(error.status).toBe(401);
|
||||||
expect(error.statusText).toBe('UnAuthorized');
|
expect(error.statusText).toBe(errorMessage);
|
||||||
expect(error.data).toEqual({ message: 'UnAuthorized' });
|
expect(error.data).toEqual({ message: errorMessage });
|
||||||
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
||||||
expect(logoutMock).not.toHaveBeenCalled();
|
expect(logoutMock).not.toHaveBeenCalled();
|
||||||
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
|
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
|
||||||
@@ -183,9 +186,9 @@ describe('backendSrv', () => {
|
|||||||
jest.advanceTimersByTime(50);
|
jest.advanceTimersByTime(50);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
expect(error).toEqual({ message: 'UnAuthorized' });
|
expect(error).toEqual({ message: errorMessage });
|
||||||
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
|
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({
|
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 401,
|
status: 401,
|
||||||
statusText: 'UnAuthorized',
|
statusText: errorMessage,
|
||||||
data: { message: 'Token revoked', error: { id: 'ERR_TOKEN_REVOKED', maxConcurrentSessions: 3 } },
|
data: { message: 'Token revoked', error: { id: 'ERR_TOKEN_REVOKED', maxConcurrentSessions: 3 } },
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
@@ -226,8 +229,8 @@ describe('backendSrv', () => {
|
|||||||
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 401,
|
status: 401,
|
||||||
statusText: 'UnAuthorized',
|
statusText: errorMessage,
|
||||||
data: { message: 'UnAuthorized' },
|
data: { message: errorMessage },
|
||||||
});
|
});
|
||||||
|
|
||||||
backendSrv.loginPing = jest
|
backendSrv.loginPing = jest
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
DataSourcePluginMeta,
|
DataSourcePluginMeta,
|
||||||
@@ -151,7 +152,7 @@ export class PublicDashboardDataSource extends DataSourceApi<DataQuery, DataSour
|
|||||||
return { data: [toDataFrame(annotations)] };
|
return { data: [toDataFrame(annotations)] };
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource(): Promise<null> {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve(null);
|
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 getDataSourcePlugins = () => getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
|
||||||
|
|
||||||
export const updateDataSource = (dataSource: DataSourceSettings) =>
|
export const updateDataSource = (dataSource: DataSourceSettings) => {
|
||||||
getBackendSrv().put(`/api/datasources/uid/${dataSource.uid}`, dataSource);
|
// 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}`);
|
export const deleteDataSource = (uid: string) => getBackendSrv().delete(`/api/datasources/uid/${uid}`);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const setup = (propOverrides?: object) => {
|
|||||||
canSave: false,
|
canSave: false,
|
||||||
onSubmit: jest.fn(),
|
onSubmit: jest.fn(),
|
||||||
onTest: jest.fn(),
|
onTest: jest.fn(),
|
||||||
exploreUrl: '/explore',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@@ -22,7 +21,7 @@ describe('<ButtonRow>', () => {
|
|||||||
it('should render component', () => {
|
it('should render component', () => {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: 'Explore' })).toBeInTheDocument();
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('should render save & test', () => {
|
it('should render save & test', () => {
|
||||||
setup({ canSave: true });
|
setup({ canSave: true });
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Button, LinkButton } from '@grafana/ui';
|
import { Button } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
|
||||||
import { AccessControlAction } from 'app/types';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
exploreUrl: string;
|
|
||||||
canSave: boolean;
|
canSave: boolean;
|
||||||
onSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
onSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
onTest: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onTest: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonRow({ canSave, onSubmit, onTest, exploreUrl }: Props) {
|
export function ButtonRow({ canSave, onSubmit, onTest }: Props) {
|
||||||
const canExploreDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-button-row">
|
<div className="gf-form-button-row">
|
||||||
<LinkButton variant="secondary" fill="solid" href={exploreUrl} disabled={!canExploreDataSources}>
|
|
||||||
Explore
|
|
||||||
</LinkButton>
|
|
||||||
{canSave && (
|
{canSave && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!canSave}
|
disabled={!canSave}
|
||||||
onClick={(event) => onSubmit(event)}
|
onClick={onSubmit}
|
||||||
aria-label={selectors.pages.DataSource.saveAndTest}
|
aria-label={selectors.pages.DataSource.saveAndTest}
|
||||||
>
|
>
|
||||||
Save & test
|
Save & 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 { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||||
import { TestingStatus } from '@grafana/runtime';
|
import { TestingStatus, config } from '@grafana/runtime';
|
||||||
import { Alert } from '@grafana/ui';
|
import { AlertVariant, Alert, useTheme2, Link } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { contextSrv } from '../../../core/core';
|
||||||
|
import { AccessControlAction } from '../../../types';
|
||||||
|
import { trackCreateDashboardClicked } from '../tracking';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
testingStatus?: TestingStatus;
|
testingStatus?: TestingStatus;
|
||||||
|
exploreUrl: string;
|
||||||
|
dataSource: DataSourceSettingsType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DataSourceTestingStatus({ testingStatus }: Props) {
|
interface AlertMessageProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
const isError = testingStatus?.status === 'error';
|
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};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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 message = testingStatus?.message;
|
||||||
const detailsMessage = testingStatus?.details?.message;
|
const detailsMessage = testingStatus?.details?.message;
|
||||||
const detailsVerboseMessage = testingStatus?.details?.verboseMessage;
|
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) {
|
if (message) {
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-group p-t-2">
|
<div className="gf-form-group p-t-2">
|
||||||
<Alert
|
<Alert severity={severity} title={message} aria-label={e2eSelectors.pages.DataSource.alert}>
|
||||||
severity={isError ? 'error' : 'success'}
|
|
||||||
title={message}
|
|
||||||
aria-label={e2eSelectors.pages.DataSource.alert}
|
|
||||||
>
|
|
||||||
{testingStatus?.details && (
|
{testingStatus?.details && (
|
||||||
<>
|
<>
|
||||||
{detailsMessage}
|
{detailsMessage}
|
||||||
|
{severity === 'success' ? (
|
||||||
|
<AlertSuccessMessage
|
||||||
|
title={message}
|
||||||
|
exploreUrl={exploreUrl}
|
||||||
|
dataSourceId={dataSource.uid}
|
||||||
|
onDashboardLinkClicked={onDashboardLinkClicked}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{detailsVerboseMessage ? (
|
{detailsVerboseMessage ? (
|
||||||
<details style={{ whiteSpace: 'pre-wrap' }}>{String(detailsVerboseMessage)}</details>
|
<details style={{ whiteSpace: 'pre-wrap' }}>{String(detailsVerboseMessage)}</details>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -119,7 +119,11 @@ export function EditDataSourceView({
|
|||||||
|
|
||||||
const onSubmit = async (e: React.MouseEvent<HTMLButtonElement> | React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.MouseEvent<HTMLButtonElement> | React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await onUpdate({ ...dataSource });
|
try {
|
||||||
|
await onUpdate({ ...dataSource });
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onTest();
|
onTest();
|
||||||
};
|
};
|
||||||
@@ -173,9 +177,9 @@ export function EditDataSourceView({
|
|||||||
</DataSourcePluginContextProvider>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Store } from 'redux';
|
|||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
import { LayoutModes } from '@grafana/data';
|
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 { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
@@ -102,12 +102,23 @@ describe('<EditDataSourcePage>', () => {
|
|||||||
// Title
|
// Title
|
||||||
expect(screen.queryByText(name)).toBeVisible();
|
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(() => {
|
await waitFor(() => {
|
||||||
// Buttons
|
// Buttons
|
||||||
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
|
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
|
||||||
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
|
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
|
||||||
expect(screen.queryByRole('link', { name: /Build a dashboard/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({
|
get: jest.fn().mockReturnValue({
|
||||||
testDatasource: jest.fn().mockReturnValue({
|
testDatasource: jest.fn().mockReturnValue({
|
||||||
status: '',
|
status: 'success',
|
||||||
message: '',
|
message: '',
|
||||||
}),
|
}),
|
||||||
type: 'cloudwatch',
|
type: 'cloudwatch',
|
||||||
@@ -228,8 +228,9 @@ describe('testDataSource', () => {
|
|||||||
};
|
};
|
||||||
const state = {
|
const state = {
|
||||||
testingStatus: {
|
testingStatus: {
|
||||||
status: '',
|
status: 'success',
|
||||||
message: '',
|
message: '',
|
||||||
|
details: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const dispatchedActions = await thunkTester(state)
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
|||||||
@@ -57,6 +57,40 @@ export interface TestDataSourceDependencies {
|
|||||||
getBackendSrv: typeof getBackendSrv;
|
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 = (
|
export const initDataSourceSettings = (
|
||||||
uid: string,
|
uid: string,
|
||||||
dependencies: InitDataSourceSettingDependencies = {
|
dependencies: InitDataSourceSettingDependencies = {
|
||||||
@@ -112,7 +146,9 @@ export const testDataSource = (
|
|||||||
try {
|
try {
|
||||||
const result = await dsApi.testDatasource();
|
const result = await dsApi.testDatasource();
|
||||||
|
|
||||||
dispatch(testDataSourceSucceeded(result));
|
const parsedResult = parseHealthCheckSuccess({ ...result, details: { ...result.details } });
|
||||||
|
dispatch(testDataSourceSucceeded(parsedResult));
|
||||||
|
|
||||||
trackDataSourceTested({
|
trackDataSourceTested({
|
||||||
grafana_version: config.buildInfo.version,
|
grafana_version: config.buildInfo.version,
|
||||||
plugin_id: dsApi.type,
|
plugin_id: dsApi.type,
|
||||||
@@ -121,19 +157,9 @@ export const testDataSource = (
|
|||||||
path: editLink,
|
path: editLink,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message: string | undefined;
|
const formattedError = parseHealthCheckError(err);
|
||||||
let details: HealthCheckResultDetails;
|
|
||||||
|
|
||||||
if (err instanceof HealthCheckError) {
|
dispatch(testDataSourceFailed({ ...formattedError }));
|
||||||
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 }));
|
|
||||||
trackDataSourceTested({
|
trackDataSourceTested({
|
||||||
grafana_version: config.buildInfo.version,
|
grafana_version: config.buildInfo.version,
|
||||||
plugin_id: dsApi.type,
|
plugin_id: dsApi.type,
|
||||||
@@ -216,6 +242,7 @@ export function addDataSource(
|
|||||||
isDefault: isFirstDataSource,
|
isDefault: isFirstDataSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: typo in name
|
||||||
if (nameExits(dataSources, newInstance.name)) {
|
if (nameExits(dataSources, newInstance.name)) {
|
||||||
newInstance.name = findNewName(dataSources, newInstance.name);
|
newInstance.name = findNewName(dataSources, newInstance.name);
|
||||||
}
|
}
|
||||||
@@ -248,9 +275,23 @@ export function loadDataSourcePlugins(): ThunkResult<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateDataSource(dataSource: DataSourceSettings) {
|
export function updateDataSource(dataSource: DataSourceSettings) {
|
||||||
return async (dispatch: (dataSourceSettings: ThunkResult<Promise<DataSourceSettings>>) => DataSourceSettings) => {
|
return async (
|
||||||
await api.updateDataSource(dataSource);
|
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();
|
await getDatasourceSrv().reload();
|
||||||
|
|
||||||
return dispatch(loadDataSource(dataSource.uid));
|
return dispatch(loadDataSource(dataSource.uid));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -259,13 +300,18 @@ export function deleteLoadedDataSource(): ThunkResult<void> {
|
|||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const { uid } = getStore().dataSources.dataSource;
|
const { uid } = getStore().dataSources.dataSource;
|
||||||
|
|
||||||
await api.deleteDataSource(uid);
|
try {
|
||||||
await getDatasourceSrv().reload();
|
await api.deleteDataSource(uid);
|
||||||
|
await getDatasourceSrv().reload();
|
||||||
|
|
||||||
const datasourcesUrl = config.featureToggles.dataConnectionsConsole
|
const datasourcesUrl = config.featureToggles.dataConnectionsConsole
|
||||||
? CONNECTIONS_ROUTES.DataSources
|
? CONNECTIONS_ROUTES.DataSources
|
||||||
: '/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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
testingStatus: {
|
testingStatus: {
|
||||||
message: 'Testing...',
|
message: 'Testing... this could take up to a couple of minutes',
|
||||||
status: 'info',
|
status: 'info',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,3 +14,5 @@ export type DataSourcesRoutes = {
|
|||||||
List: string;
|
List: string;
|
||||||
Dashboards: string;
|
Dashboards: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DataSourceTestStatus = 'success' | 'warning' | 'error';
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { GetDataSourceListFilters, setDataSourceSrv } from '@grafana/runtime';
|
import { GetDataSourceListFilters, setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
import { CloudWatchDatasource } from '../datasource';
|
|
||||||
import { CloudWatchLogsQueryStatus } from '../types';
|
import { CloudWatchLogsQueryStatus } from '../types';
|
||||||
|
|
||||||
import { meta, setupMockedDataSource } from './CloudWatchDataSource';
|
import { meta, setupMockedDataSource } from './CloudWatchDataSource';
|
||||||
@@ -62,7 +62,7 @@ export function setupForLogs() {
|
|||||||
): Observable<DataQueryResponse> | Promise<DataQueryResponse> {
|
): Observable<DataQueryResponse> | Promise<DataQueryResponse> {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
testDatasource: function (): Promise<CloudWatchDatasource> {
|
testDatasource: function (): Promise<TestDataSourceResponse> {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
meta: meta,
|
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';
|
import { DashboardQuery } from './types';
|
||||||
|
|
||||||
@@ -18,7 +24,7 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
|||||||
return Promise.reject('This should not be called directly');
|
return Promise.reject('This should not be called directly');
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({ message: '', status: '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
|
TestDataSourceResponse,
|
||||||
isValidLiveChannelAddress,
|
isValidLiveChannelAddress,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
parseLiveChannelAddress,
|
parseLiveChannelAddress,
|
||||||
@@ -252,8 +253,8 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
|||||||
return { data: [toDataFrame(annotations)] };
|
return { data: [toDataFrame(annotations)] };
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve();
|
return Promise.resolve({ message: '', status: '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@@ -94,8 +95,8 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
|
|||||||
return forkJoin(runningQueries).pipe(flattenResponses(), map(this.finalizeResponses), mergeAll());
|
return forkJoin(runningQueries).pipe(flattenResponses(), map(this.finalizeResponses), mergeAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({ message: '', status: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
private isQueryable(query: BatchedQueries): boolean {
|
private isQueryable(query: BatchedQueries): boolean {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
|
|||||||
import {
|
import {
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
|
TestDataSourceResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
DataSourcePluginMeta,
|
DataSourcePluginMeta,
|
||||||
@@ -57,8 +58,8 @@ export class MockDataSourceApi extends DataSourceApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve();
|
return Promise.resolve({ message: '', status: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
setupMixed(value: boolean) {
|
setupMixed(value: boolean) {
|
||||||
@@ -99,7 +100,7 @@ export class MockObservableDataSourceApi extends DataSourceApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve();
|
return Promise.resolve({ message: '', status: '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user