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
pull/20081/head
David 6 years ago committed by GitHub
parent 49c44da73b
commit 781cff07af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/grafana-ui/src/types/datasource.ts
  2. 28
      public/app/features/explore/Explore.tsx
  3. 19
      public/app/features/explore/QueryRow.tsx
  4. 22
      public/app/features/explore/state/actionTypes.ts
  5. 75
      public/app/features/explore/state/actions.test.ts
  6. 53
      public/app/features/explore/state/actions.ts
  7. 58
      public/app/features/explore/state/reducers.test.ts
  8. 35
      public/app/features/explore/state/reducers.ts
  9. 83
      public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx
  10. 3
      public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx
  11. 5
      public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx
  12. 8
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  13. 24
      public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx
  14. 42
      public/app/plugins/datasource/loki/components/useLokiLabels.test.ts
  15. 17
      public/app/plugins/datasource/loki/components/useLokiLabels.ts
  16. 13
      public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts
  17. 10
      public/app/plugins/datasource/loki/components/useLokiSyntax.ts
  18. 2
      public/app/plugins/datasource/loki/datasource.ts
  19. 3
      public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx
  20. 31
      public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
  21. 1
      public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap
  22. 2
      public/app/plugins/datasource/prometheus/datasource.ts
  23. 4
      public/app/types/explore.ts

@ -295,7 +295,6 @@ export interface ExploreQueryFieldProps<
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> extends QueryEditorProps<DSType, TQuery, TOptions> {
datasourceStatus: DataSourceStatus;
history: any[];
onHint?: (action: QueryFixAction) => void;
}

@ -9,7 +9,7 @@ import memoizeOne from 'memoize-one';
// Services & Utils
import store from 'app/core/store';
// Components
import { Alert, ErrorBoundaryAlert, DataQuery, ExploreStartPageProps, DataSourceApi, PanelData } from '@grafana/ui';
import { ErrorBoundaryAlert, DataQuery, ExploreStartPageProps, DataSourceApi, PanelData } from '@grafana/ui';
import LogsContainer from './LogsContainer';
import QueryRows from './QueryRows';
import TableContainer from './TableContainer';
@ -21,7 +21,6 @@ import {
scanStart,
setQueries,
refreshExplore,
reconnectDatasource,
updateTimeRange,
toggleGraph,
} from './state/actions';
@ -46,7 +45,6 @@ import {
import { Emitter } from 'app/core/utils/emitter';
import { ExploreToolbar } from './ExploreToolbar';
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
import { getTimeZone } from '../profile/state/selectors';
import { ErrorContainer } from './ErrorContainer';
import { scanStopAction } from './state/actionTypes';
@ -65,7 +63,6 @@ const getStyles = memoizeOne(() => {
interface ExploreProps {
StartPage?: ComponentType<ExploreStartPageProps>;
changeSize: typeof changeSize;
datasourceError: string;
datasourceInstance: DataSourceApi;
datasourceMissing: boolean;
exploreId: ExploreId;
@ -73,7 +70,6 @@ interface ExploreProps {
initialized: boolean;
modifyQueries: typeof modifyQueries;
update: ExploreUpdateState;
reconnectDatasource: typeof reconnectDatasource;
refreshExplore: typeof refreshExplore;
scanning?: boolean;
scanRange?: RawTimeRange;
@ -238,18 +234,10 @@ export class Explore extends React.PureComponent<ExploreProps> {
);
};
onReconnect = (event: React.MouseEvent<HTMLButtonElement>) => {
const { exploreId, reconnectDatasource } = this.props;
event.preventDefault();
reconnectDatasource(exploreId);
};
render() {
const {
StartPage,
datasourceInstance,
datasourceError,
datasourceMissing,
exploreId,
showingStartPage,
@ -272,17 +260,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
<div className={exploreClass} ref={this.getRef}>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
{datasourceMissing ? this.renderEmptyState() : null}
<FadeIn duration={datasourceError ? 150 : 5} in={datasourceError ? true : false}>
<div className="explore-container">
<Alert
title={`Error connecting to datasource: ${datasourceError}`}
buttonText={'Reconnect'}
onButtonClick={this.onReconnect}
/>
</div>
</FadeIn>
{datasourceInstance && (
<div className="explore-container">
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
@ -357,7 +334,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
const timeZone = getTimeZone(state.user);
const {
StartPage,
datasourceError,
datasourceInstance,
datasourceMissing,
initialized,
@ -402,7 +378,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
return {
StartPage,
datasourceError,
datasourceInstance,
datasourceMissing,
initialized,
@ -431,7 +406,6 @@ const mapDispatchToProps: Partial<ExploreProps> = {
changeSize,
initializeExplore,
modifyQueries,
reconnectDatasource,
refreshExplore,
scanStart,
scanStopAction,

@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
// Types
import { StoreState } from 'app/types';
import { TimeRange, AbsoluteTimeRange, LoadingState } from '@grafana/data';
import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData } from '@grafana/ui';
import { DataQuery, DataSourceApi, QueryFixAction, PanelData } from '@grafana/ui';
import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter';
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
@ -31,7 +31,6 @@ interface QueryRowProps extends PropsFromParent {
className?: string;
exploreId: ExploreId;
datasourceInstance: DataSourceApi;
datasourceStatus: DataSourceStatus;
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
history: HistoryItem[];
query: DataQuery;
@ -121,7 +120,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
exploreEvents,
range,
absoluteRange,
datasourceStatus,
queryResponse,
latency,
mode,
@ -148,7 +146,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
//@ts-ignore
<QueryField
datasource={datasourceInstance}
datasourceStatus={datasourceStatus}
query={query}
history={history}
onRunQuery={this.onRunQuery}
@ -190,19 +187,8 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const {
datasourceInstance,
history,
queries,
range,
absoluteRange,
datasourceError,
latency,
mode,
queryResponse,
} = item;
const { datasourceInstance, history, queries, range, absoluteRange, latency, mode, queryResponse } = item;
const query = queries[index];
const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected;
return {
datasourceInstance,
@ -210,7 +196,6 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
query,
range,
absoluteRange,
datasourceStatus,
queryResponse,
latency,
mode,

@ -105,19 +105,6 @@ export interface LoadDatasourceReadyPayload {
history: HistoryItem[];
}
export interface TestDatasourcePendingPayload {
exploreId: ExploreId;
}
export interface TestDatasourceFailurePayload {
exploreId: ExploreId;
error: string;
}
export interface TestDatasourceSuccessPayload {
exploreId: ExploreId;
}
export interface ModifyQueriesPayload {
exploreId: ExploreId;
modification: QueryFixAction;
@ -391,15 +378,6 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>(
*/
export const resetExploreAction = actionCreatorFactory<ResetExplorePayload>('explore/RESET_EXPLORE').create();
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
export const testDataSourcePendingAction = actionCreatorFactory<TestDatasourcePendingPayload>(
'explore/TEST_DATASOURCE_PENDING'
).create();
export const testDataSourceSuccessAction = actionCreatorFactory<TestDatasourceSuccessPayload>(
'explore/TEST_DATASOURCE_SUCCESS'
).create();
export const testDataSourceFailureAction = actionCreatorFactory<TestDatasourceFailurePayload>(
'explore/TEST_DATASOURCE_FAILURE'
).create();
export const loadExploreDatasources = actionCreatorFactory<LoadExploreDataSourcesPayload>(
'explore/LOAD_EXPLORE_DATASOURCES'
).create();

@ -1,4 +1,4 @@
import { refreshExplore, testDatasource, loadDatasource } from './actions';
import { refreshExplore, loadDatasource } from './actions';
import { ExploreId, ExploreUrlState, ExploreUpdateState, ExploreMode } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester';
import {
@ -6,9 +6,6 @@ import {
InitializeExplorePayload,
updateUIStateAction,
setQueriesAction,
testDataSourcePendingAction,
testDataSourceSuccessAction,
testDataSourceFailureAction,
loadDatasourcePendingAction,
loadDatasourceReadyAction,
} from './actionTypes';
@ -164,72 +161,6 @@ describe('refreshExplore', () => {
});
});
describe('test datasource', () => {
describe('when testDatasource thunk is dispatched', () => {
describe('and testDatasource call on instance is successful', () => {
it('then it should dispatch testDataSourceSuccessAction', async () => {
const exploreId = ExploreId.left;
const mockDatasourceInstance = {
testDatasource: () => {
return Promise.resolve({ status: 'success' });
},
};
const dispatchedActions = await thunkTester({})
.givenThunk(testDatasource)
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
expect(dispatchedActions).toEqual([
testDataSourcePendingAction({ exploreId }),
testDataSourceSuccessAction({ exploreId }),
]);
});
});
describe('and testDatasource call on instance is not successful', () => {
it('then it should dispatch testDataSourceFailureAction', async () => {
const exploreId = ExploreId.left;
const error = 'something went wrong';
const mockDatasourceInstance = {
testDatasource: () => {
return Promise.resolve({ status: 'fail', message: error });
},
};
const dispatchedActions = await thunkTester({})
.givenThunk(testDatasource)
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
expect(dispatchedActions).toEqual([
testDataSourcePendingAction({ exploreId }),
testDataSourceFailureAction({ exploreId, error }),
]);
});
});
describe('and testDatasource call on instance throws', () => {
it('then it should dispatch testDataSourceFailureAction', async () => {
const exploreId = ExploreId.left;
const error = 'something went wrong';
const mockDatasourceInstance = {
testDatasource: () => {
throw { statusText: error };
},
};
const dispatchedActions = await thunkTester({})
.givenThunk(testDatasource)
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
expect(dispatchedActions).toEqual([
testDataSourcePendingAction({ exploreId }),
testDataSourceFailureAction({ exploreId, error }),
]);
});
});
});
});
describe('loading datasource', () => {
describe('when loadDatasource thunk is dispatched', () => {
describe('and all goes fine', () => {
@ -255,8 +186,6 @@ describe('loading datasource', () => {
exploreId,
requestedDatasourceName: mockDatasourceInstance.name,
}),
testDataSourcePendingAction({ exploreId }),
testDataSourceSuccessAction({ exploreId }),
loadDatasourceReadyAction({ exploreId, history: [] }),
]);
});
@ -285,8 +214,6 @@ describe('loading datasource', () => {
exploreId,
requestedDatasourceName: mockDatasourceInstance.name,
}),
testDataSourcePendingAction({ exploreId }),
testDataSourceSuccessAction({ exploreId }),
]);
});
});

@ -59,9 +59,6 @@ import {
ToggleGraphPayload,
ToggleTablePayload,
updateUIStateAction,
testDataSourcePendingAction,
testDataSourceSuccessAction,
testDataSourceFailureAction,
loadExploreDatasources,
changeModeAction,
scanStopAction,
@ -340,41 +337,6 @@ export function importQueries(
};
}
/**
* Tests datasource.
*/
export const testDatasource = (exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> => {
return async dispatch => {
let datasourceError = null;
dispatch(testDataSourcePendingAction({ exploreId }));
try {
const testResult = await instance.testDatasource();
datasourceError = testResult.status === 'success' ? null : testResult.message;
} catch (error) {
datasourceError = (error && error.statusText) || 'Network error';
}
if (datasourceError) {
dispatch(testDataSourceFailureAction({ exploreId, error: datasourceError }));
return;
}
dispatch(testDataSourceSuccessAction({ exploreId }));
};
};
/**
* Reconnects datasource when there is a connection failure.
*/
export const reconnectDatasource = (exploreId: ExploreId): ThunkResult<void> => {
return async (dispatch, getState) => {
const instance = getState().explore[exploreId].datasourceInstance;
dispatch(changeDatasource(exploreId, instance.name));
};
};
/**
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
*/
@ -385,13 +347,6 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi, or
// Keep ID to track selection
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
await dispatch(testDatasource(exploreId, instance));
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
return;
}
if (instance.init) {
try {
instance.init();
@ -401,7 +356,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi, or
}
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
// User already changed datasource, discard results
return;
}
@ -441,7 +396,6 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
const {
datasourceInstance,
queries,
datasourceError,
containerWidth,
isLive: live,
range,
@ -454,11 +408,6 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
showingTable,
} = exploreItemState;
if (datasourceError) {
// let's not run any queries if data source is in a faulty state
return;
}
if (!hasNonEmptyQuery(queries)) {
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave()); // Remember to save to state and update location

@ -10,9 +10,6 @@ import { ExploreId, ExploreItemState, ExploreUrlState, ExploreState, ExploreMode
import { reducerTester } from 'test/core/redux/reducerTester';
import {
scanStartAction,
testDataSourcePendingAction,
testDataSourceSuccessAction,
testDataSourceFailureAction,
updateDatasourceInstanceAction,
splitOpenAction,
splitCloseAction,
@ -29,7 +26,7 @@ import { updateLocation } from 'app/core/actions/location';
import { serializeStateToUrlParam } from 'app/core/utils/explore';
import TableModel from 'app/core/table_model';
import { DataSourceApi, DataQuery } from '@grafana/ui';
import { LogsModel, LogsDedupStrategy, dateTime, LoadingState } from '@grafana/data';
import { LogsDedupStrategy, dateTime, LoadingState } from '@grafana/data';
describe('Explore item reducer', () => {
describe('scanning', () => {
@ -65,56 +62,7 @@ describe('Explore item reducer', () => {
});
});
describe('testing datasource', () => {
describe('when testDataSourcePendingAction is dispatched', () => {
it('then it should set datasourceError', () => {
reducerTester()
.givenReducer(itemReducer, { datasourceError: {} })
.whenActionIsDispatched(testDataSourcePendingAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({ datasourceError: null });
});
});
describe('when testDataSourceSuccessAction is dispatched', () => {
it('then it should set datasourceError', () => {
reducerTester()
.givenReducer(itemReducer, { datasourceError: {} })
.whenActionIsDispatched(testDataSourceSuccessAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({ datasourceError: null });
});
});
describe('when testDataSourceFailureAction is dispatched', () => {
it('then it should set correct state', () => {
const error = 'some error';
const initalState: Partial<ExploreItemState> = {
datasourceError: null,
graphResult: [],
tableResult: {} as TableModel,
logsResult: {} as LogsModel,
update: {
datasource: true,
queries: true,
range: true,
mode: true,
ui: true,
},
};
const expectedState = {
datasourceError: error,
graphResult: undefined as any[],
tableResult: undefined as TableModel,
logsResult: undefined as LogsModel,
update: makeInitialUpdateState(),
};
reducerTester()
.givenReducer(itemReducer, initalState)
.whenActionIsDispatched(testDataSourceFailureAction({ exploreId: ExploreId.left, error }))
.thenStateShouldEqual(expectedState);
});
});
describe('changing datasource', () => {
describe('when changeDataType is dispatched', () => {
it('then it should set correct state', () => {
reducerTester()
@ -125,9 +73,7 @@ describe('Explore item reducer', () => {
});
});
});
});
describe('changing datasource', () => {
describe('when updateDatasourceInstanceAction is dispatched', () => {
describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
it('then it should set correct state', () => {

@ -15,9 +15,6 @@ import { DataQuery, DataSourceApi, PanelData, DataQueryRequest, RefreshPicker, P
import {
HigherOrderAction,
ActionTypes,
testDataSourcePendingAction,
testDataSourceSuccessAction,
testDataSourceFailureAction,
splitCloseAction,
SplitCloseActionPayload,
loadExploreDatasources,
@ -82,7 +79,6 @@ export const makeExploreItemState = (): ExploreItemState => ({
containerWidth: 0,
datasourceInstance: null,
requestedDatasourceName: null,
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
exploreDatasources: [],
@ -475,37 +471,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
};
},
})
.addMapper({
filter: testDataSourcePendingAction,
mapper: (state): ExploreItemState => {
return {
...state,
datasourceError: null,
};
},
})
.addMapper({
filter: testDataSourceSuccessAction,
mapper: (state): ExploreItemState => {
return {
...state,
datasourceError: null,
};
},
})
.addMapper({
filter: testDataSourceFailureAction,
mapper: (state, action): ExploreItemState => {
return {
...state,
datasourceError: action.payload.error,
graphResult: undefined,
tableResult: undefined,
logsResult: undefined,
update: makeInitialUpdateState(),
};
},
})
.addMapper({
filter: loadExploreDatasources,
mapper: (state, action): ExploreItemState => {

@ -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);
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,
});
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,
});
}
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>
);

@ -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={() => {}}

@ -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}

@ -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

@ -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}

@ -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();
});
});

@ -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,

@ -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);

@ -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

@ -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,
};

@ -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">

@ -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 (

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

@ -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,
};

@ -161,10 +161,6 @@ export interface ExploreItemState {
* Current data source name or null if default
*/
requestedDatasourceName: string | null;
/**
* Error to be shown when datasource loading or testing failed.
*/
datasourceError: string;
/**
* True if the datasource is loading. `null` if the loading has not started yet.
*/

Loading…
Cancel
Save