Explore: Remove datasource testing on selector (#19910)

* Explore: Remove datasource testing on selector

- datasource testing gets in the way of fast query iteration: switching
between datasources can take seconds
- it should not be explore's duty to test datasources in the first place
- removed the concept of datasourceError in Explore, should not be its
concern
- datasource erorrs will express themselves in query errors just fine
- connection errors are still bubbled up
- removed reconnection logic from explore, should not be its concern
- missing labels in loki are still "visible" via an empty label selector
- Loki and Prometheus treated connection errors differently than other
datasources, making sure to pass through the original error message

* Show datasource error in query field for prom/loki/influx

* Removed connection test case, fixed disabled state
This commit is contained in:
David
2019-10-29 10:37:36 +00:00
committed by GitHub
parent 49c44da73b
commit 781cff07af
23 changed files with 87 additions and 450 deletions

View File

@@ -17,6 +17,13 @@ export interface State {
measurements: CascaderOption[];
measurement: string;
field: string;
error: string;
}
interface ChooserOptions {
measurement: string;
field: string;
error: string;
}
// Helper function for determining if a collection of pairs are valid
@@ -32,37 +39,54 @@ export function pairsAreValid(pairs: KeyValuePair[]) {
);
}
function getChooserText({ measurement, field, error }: ChooserOptions): string {
if (error) {
return '(No measurement found)';
}
if (measurement) {
return `Measurements (${measurement}/${field})`;
}
return 'Measurements';
}
export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
templateSrv: TemplateSrv = new TemplateSrv();
state: State = { measurements: [], measurement: null, field: null };
state: State = { measurements: [], measurement: null, field: null, error: null };
async componentDidMount() {
const { datasource } = this.props;
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, datasource.database);
const measureMentsQuery = queryBuilder.buildExploreQuery('MEASUREMENTS');
const influxMeasurements = await datasource.metricFindQuery(measureMentsQuery);
try {
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, datasource.database);
const measureMentsQuery = queryBuilder.buildExploreQuery('MEASUREMENTS');
const influxMeasurements = await datasource.metricFindQuery(measureMentsQuery);
const measurements = [];
for (let index = 0; index < influxMeasurements.length; index++) {
const measurementObj = influxMeasurements[index];
const queryBuilder = new InfluxQueryBuilder({ measurement: measurementObj.text, tags: [] }, datasource.database);
const fieldsQuery = queryBuilder.buildExploreQuery('FIELDS');
const influxFields = await datasource.metricFindQuery(fieldsQuery);
const fields: any[] = influxFields.map(
(field: any): any => ({
label: field.text,
value: field.text,
children: [],
})
);
measurements.push({
label: measurementObj.text,
value: measurementObj.text,
children: fields,
});
const measurements = [];
for (let index = 0; index < influxMeasurements.length; index++) {
const measurementObj = influxMeasurements[index];
const queryBuilder = new InfluxQueryBuilder(
{ measurement: measurementObj.text, tags: [] },
datasource.database
);
const fieldsQuery = queryBuilder.buildExploreQuery('FIELDS');
const influxFields = await datasource.metricFindQuery(fieldsQuery);
const fields: any[] = influxFields.map(
(field: any): any => ({
label: field.text,
value: field.text,
children: [],
})
);
measurements.push({
label: measurementObj.text,
value: measurementObj.text,
children: fields,
});
}
this.setState({ measurements });
} catch (error) {
const message = error && error.message ? error.message : error;
this.setState({ error: message });
}
this.setState({ measurements });
}
componentDidUpdate(prevProps: Props) {
@@ -107,8 +131,8 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
render() {
const { datasource } = this.props;
const { measurements, measurement, field } = this.state;
const cascadeText = measurement ? `Measurements (${measurement}/${field})` : 'Measurements';
const { measurements, measurement, field, error } = this.state;
const cascadeText = getChooserText({ measurement, field, error });
return (
<div className="gf-form-inline gf-form-inline--nowrap">
@@ -119,7 +143,7 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
onChange={this.onMeasurementsChange}
expandIcon={null}
>
<button className="gf-form-label gf-form-label--btn">
<button className="gf-form-label gf-form-label--btn" disabled={!measurement}>
{cascadeText} <i className="fa fa-caret-down" />
</button>
</Cascader>
@@ -132,6 +156,9 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
extendedOptions={{ measurement }}
/>
)}
{error ? (
<span className="gf-form-label gf-form-label--transparent gf-form-label--error m-l-2">{error}</span>
) : null}
</div>
</div>
);

View File

@@ -2,7 +2,6 @@
import React, { memo } from 'react';
// Types
import { DataSourceStatus } from '@grafana/ui';
import { LokiQuery } from '../types';
import { useLokiSyntax } from './useLokiSyntax';
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
@@ -25,7 +24,6 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
datasource.languageProvider,
DataSourceStatus.Connected,
absolute
);
@@ -38,7 +36,6 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
<div className="gf-form-group">
<LokiQueryFieldForm
datasource={datasource}
datasourceStatus={DataSourceStatus.Connected}
query={query}
onChange={(query: LokiQuery) => onChange(query.expr)}
onRunQuery={() => {}}

View File

@@ -3,7 +3,7 @@ import React, { memo } from 'react';
// Types
import { AbsoluteTimeRange } from '@grafana/data';
import { QueryEditorProps, DataSourceStatus } from '@grafana/ui';
import { QueryEditorProps } from '@grafana/ui';
import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types';
import { LokiQueryField } from './LokiQueryField';
@@ -30,8 +30,6 @@ export const LokiQueryEditor = memo(function LokiQueryEditor(props: Props) {
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
datasource.languageProvider,
// TODO maybe use real status
DataSourceStatus.Connected,
absolute
);
@@ -39,7 +37,6 @@ export const LokiQueryEditor = memo(function LokiQueryEditor(props: Props) {
<div>
<LokiQueryField
datasource={datasource}
datasourceStatus={DataSourceStatus.Connected}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}

View File

@@ -3,21 +3,15 @@ import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldFor
import { useLokiSyntax } from './useLokiSyntax';
import LokiLanguageProvider from '../language_provider';
export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
datasource,
datasourceStatus,
...otherProps
}) => {
export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ datasource, ...otherProps }) => {
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
datasource.languageProvider as LokiLanguageProvider,
datasourceStatus,
otherProps.absoluteRange
);
return (
<LokiQueryFieldForm
datasource={datasource}
datasourceStatus={datasourceStatus}
syntaxLoaded={isSyntaxReady}
/**
* setActiveOption name is intentional. Because of the way rc-cascader requests additional data

View File

@@ -15,17 +15,14 @@ import { Plugin, Node } from 'slate';
// Types
import { LokiQuery } from '../types';
import { TypeaheadOutput } from 'app/types/explore';
import { ExploreQueryFieldProps, DataSourceStatus, DOMUtil } from '@grafana/ui';
import { ExploreQueryFieldProps, DOMUtil } from '@grafana/ui';
import { AbsoluteTimeRange } from '@grafana/data';
import { Grammar } from 'prismjs';
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
import LokiDatasource from '../datasource';
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
if (datasourceStatus === DataSourceStatus.Disconnected) {
return '(Disconnected)';
}
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) {
if (!hasSyntax) {
return 'Loading labels...';
}
@@ -144,21 +141,12 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
};
render() {
const {
data,
query,
syntaxLoaded,
logLabelOptions,
onLoadOptions,
onLabelsRefresh,
datasource,
datasourceStatus,
} = this.props;
const { data, query, syntaxLoaded, logLabelOptions, onLoadOptions, onLabelsRefresh, datasource } = this.props;
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
const buttonDisabled = !(syntaxLoaded && hasLogLabels);
const showError = data && data.error && data.error.refId === query.refId;
return (
@@ -166,7 +154,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
<div className="gf-form-inline">
<div className="gf-form">
<Cascader
options={logLabelOptions}
options={logLabelOptions || []}
onChange={this.onChangeLogLabels}
loadData={onLoadOptions}
expandIcon={null}

View File

@@ -1,7 +1,6 @@
import { renderHook, act } from 'react-hooks-testing-library';
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiLabels } from './useLokiLabels';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import { makeMockLokiDatasource } from '../mocks';
@@ -20,49 +19,10 @@ describe('useLokiLabels hook', () => {
return Promise.resolve();
};
const { result, waitForNextUpdate } = renderHook(() =>
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Connected)
);
const { result, waitForNextUpdate } = renderHook(() => useLokiLabels(languageProvider, true, [], rangeMock));
act(() => result.current.refreshLabels());
expect(result.current.logLabelOptions).toEqual([]);
await waitForNextUpdate();
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock);
});
it('should force refresh labels after a disconnect', () => {
const datasource = makeMockLokiDatasource({});
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560153109000,
};
const languageProvider = new LanguageProvider(datasource);
languageProvider.refreshLogLabels = jest.fn();
renderHook(() =>
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Disconnected)
);
expect(languageProvider.refreshLogLabels).toBeCalledTimes(1);
expect(languageProvider.refreshLogLabels).toBeCalledWith(rangeMock, true);
});
it('should not force refresh labels after a connect', () => {
const datasource = makeMockLokiDatasource({});
const rangeMock: AbsoluteTimeRange = {
from: 1560153109000,
to: 1560153109000,
};
const languageProvider = new LanguageProvider(datasource);
languageProvider.refreshLogLabels = jest.fn();
renderHook(() =>
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Disconnected, DataSourceStatus.Connected)
);
expect(languageProvider.refreshLogLabels).not.toBeCalled();
});
});

View File

@@ -1,5 +1,4 @@
import { useState, useEffect } from 'react';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
@@ -18,18 +17,13 @@ export const useLokiLabels = (
languageProvider: LokiLanguageProvider,
languageProviderInitialised: boolean,
activeOption: CascaderOption[],
absoluteRange: AbsoluteTimeRange,
datasourceStatus: DataSourceStatus,
initialDatasourceStatus?: DataSourceStatus // used for test purposes
absoluteRange: AbsoluteTimeRange
) => {
const mounted = useRefMounted();
// State
const [logLabelOptions, setLogLabelOptions] = useState([]);
const [shouldTryRefreshLabels, setRefreshLabels] = useState(false);
const [prevDatasourceStatus, setPrevDatasourceStatus] = useState(
initialDatasourceStatus || DataSourceStatus.Connected
);
const [shouldForceRefreshLabels, setForceRefreshLabels] = useState(false);
// Async
@@ -83,15 +77,6 @@ export const useLokiLabels = (
}
}, [shouldTryRefreshLabels, shouldForceRefreshLabels]);
// This effect is performed on datasourceStatus state change only.
// We want to make sure to only force refresh AFTER a disconnected state thats why we store the previous datasourceStatus in state
useEffect(() => {
if (datasourceStatus === DataSourceStatus.Connected && prevDatasourceStatus === DataSourceStatus.Disconnected) {
setForceRefreshLabels(true);
}
setPrevDatasourceStatus(datasourceStatus);
}, [datasourceStatus]);
return {
logLabelOptions,
setLogLabelOptions,

View File

@@ -1,5 +1,4 @@
import { renderHook, act } from 'react-hooks-testing-library';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
@@ -36,9 +35,7 @@ describe('useLokiSyntax hook', () => {
};
it('should provide Loki syntax when used', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
);
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
expect(result.current.syntax).toEqual(null);
await waitForNextUpdate();
@@ -47,9 +44,7 @@ describe('useLokiSyntax hook', () => {
});
it('should fetch labels on first call', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
);
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
expect(result.current.isSyntaxReady).toBeFalsy();
expect(result.current.logLabelOptions).toEqual([]);
@@ -60,9 +55,7 @@ describe('useLokiSyntax hook', () => {
});
it('should try to fetch missing options when active option changes', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
);
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
await waitForNextUpdate();
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);

View File

@@ -1,6 +1,5 @@
import { useState, useEffect } from 'react';
import Prism from 'prismjs';
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
import { AbsoluteTimeRange } from '@grafana/data';
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
@@ -14,11 +13,7 @@ const PRISM_SYNTAX = 'promql';
* @param languageProvider
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values
*/
export const useLokiSyntax = (
languageProvider: LokiLanguageProvider,
datasourceStatus: DataSourceStatus,
absoluteRange: AbsoluteTimeRange
) => {
export const useLokiSyntax = (languageProvider: LokiLanguageProvider, absoluteRange: AbsoluteTimeRange) => {
const mounted = useRefMounted();
// State
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
@@ -35,8 +30,7 @@ export const useLokiSyntax = (
languageProvider,
languageProviderInitialized,
activeOption,
absoluteRange,
datasourceStatus
absoluteRange
);
// Async

View File

@@ -121,7 +121,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
processError = (err: any, target: any): DataQueryError => {
const error: DataQueryError = {
message: 'Unknown error during query transaction. Please check JS console logs.',
message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.',
refId: target.refId,
};

View File

@@ -2,7 +2,7 @@ import _ from 'lodash';
import React, { PureComponent } from 'react';
// Types
import { FormLabel, Select, Switch, QueryEditorProps, DataSourceStatus } from '@grafana/ui';
import { FormLabel, Select, Switch, QueryEditorProps } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { PrometheusDatasource } from '../datasource';
@@ -104,7 +104,6 @@ export class PromQueryEditor extends PureComponent<Props, State> {
onChange={this.onFieldChange}
history={[]}
data={data}
datasourceStatus={DataSourceStatus.Connected} // TODO: replace with real DataSourceStatus
/>
<div className="gf-form-inline">

View File

@@ -13,7 +13,7 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import QueryField, { TypeaheadInput } from 'app/features/explore/QueryField';
import { PromQuery, PromContext, PromOptions } from '../types';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, DOMUtil } from '@grafana/ui';
import { ExploreQueryFieldProps, QueryHint, DOMUtil } from '@grafana/ui';
import { isDataFrame, toLegacyResponseData } from '@grafana/data';
import { PrometheusDatasource } from '../datasource';
import PromQlLanguageProvider from '../language_provider';
@@ -24,13 +24,13 @@ const METRIC_MARK = 'metric';
const PRISM_SYNTAX = 'promql';
export const RECORDING_RULES_GROUP = '__recording_rules__';
function getChooserText(hasSyntax: boolean, datasourceStatus: DataSourceStatus) {
if (datasourceStatus === DataSourceStatus.Disconnected) {
return '(Disconnected)';
}
function getChooserText(hasSyntax: boolean, metrics: string[]) {
if (!hasSyntax) {
return 'Loading metrics...';
}
if (metrics && metrics.length === 0) {
return '(No metrics found)';
}
return 'Metrics';
}
@@ -159,21 +159,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
if (data && prevProps.data && prevProps.data.series !== data.series) {
this.refreshHint();
}
const reconnected =
prevProps.datasourceStatus === DataSourceStatus.Disconnected &&
this.props.datasourceStatus === DataSourceStatus.Connected;
if (!reconnected) {
return;
}
if (this.languageProviderInitializationPromise) {
this.languageProviderInitializationPromise.cancel();
}
if (this.languageProvider) {
this.refreshMetrics(makePromiseCancelable(this.languageProvider.fetchMetrics()));
}
}
refreshHint = () => {
@@ -291,11 +276,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
};
render() {
const { data, query, datasourceStatus } = this.props;
const { data, query } = this.props;
const { metricsOptions, syntaxLoaded, hint } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = getChooserText(syntaxLoaded, datasourceStatus);
const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
const chooserText = getChooserText(syntaxLoaded, metricsOptions);
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0);
const showError = data && data.error && data.error.refId === query.refId;
return (

View File

@@ -9,7 +9,6 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
"getPrometheusTime": [MockFunction],
}
}
datasourceStatus={0}
history={Array []}
onChange={[Function]}
onRunQuery={[Function]}

View File

@@ -447,7 +447,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
handleErrors = (err: any, target: PromQuery) => {
const error: DataQueryError = {
message: 'Unknown error during query transaction. Please check JS console logs.',
message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.',
refId: target.refId,
};