mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
49c44da73b
commit
781cff07af
@ -295,7 +295,6 @@ export interface ExploreQueryFieldProps<
|
|||||||
TQuery extends DataQuery = DataQuery,
|
TQuery extends DataQuery = DataQuery,
|
||||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||||
> extends QueryEditorProps<DSType, TQuery, TOptions> {
|
> extends QueryEditorProps<DSType, TQuery, TOptions> {
|
||||||
datasourceStatus: DataSourceStatus;
|
|
||||||
history: any[];
|
history: any[];
|
||||||
onHint?: (action: QueryFixAction) => void;
|
onHint?: (action: QueryFixAction) => void;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import memoizeOne from 'memoize-one';
|
|||||||
// Services & Utils
|
// Services & Utils
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
// Components
|
// 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 LogsContainer from './LogsContainer';
|
||||||
import QueryRows from './QueryRows';
|
import QueryRows from './QueryRows';
|
||||||
import TableContainer from './TableContainer';
|
import TableContainer from './TableContainer';
|
||||||
@ -21,7 +21,6 @@ import {
|
|||||||
scanStart,
|
scanStart,
|
||||||
setQueries,
|
setQueries,
|
||||||
refreshExplore,
|
refreshExplore,
|
||||||
reconnectDatasource,
|
|
||||||
updateTimeRange,
|
updateTimeRange,
|
||||||
toggleGraph,
|
toggleGraph,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
@ -46,7 +45,6 @@ import {
|
|||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { ExploreToolbar } from './ExploreToolbar';
|
import { ExploreToolbar } from './ExploreToolbar';
|
||||||
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
||||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
import { ErrorContainer } from './ErrorContainer';
|
import { ErrorContainer } from './ErrorContainer';
|
||||||
import { scanStopAction } from './state/actionTypes';
|
import { scanStopAction } from './state/actionTypes';
|
||||||
@ -65,7 +63,6 @@ const getStyles = memoizeOne(() => {
|
|||||||
interface ExploreProps {
|
interface ExploreProps {
|
||||||
StartPage?: ComponentType<ExploreStartPageProps>;
|
StartPage?: ComponentType<ExploreStartPageProps>;
|
||||||
changeSize: typeof changeSize;
|
changeSize: typeof changeSize;
|
||||||
datasourceError: string;
|
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
datasourceMissing: boolean;
|
datasourceMissing: boolean;
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
@ -73,7 +70,6 @@ interface ExploreProps {
|
|||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
modifyQueries: typeof modifyQueries;
|
modifyQueries: typeof modifyQueries;
|
||||||
update: ExploreUpdateState;
|
update: ExploreUpdateState;
|
||||||
reconnectDatasource: typeof reconnectDatasource;
|
|
||||||
refreshExplore: typeof refreshExplore;
|
refreshExplore: typeof refreshExplore;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
StartPage,
|
StartPage,
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
datasourceError,
|
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
exploreId,
|
exploreId,
|
||||||
showingStartPage,
|
showingStartPage,
|
||||||
@ -272,17 +260,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
<div className={exploreClass} ref={this.getRef}>
|
<div className={exploreClass} ref={this.getRef}>
|
||||||
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
||||||
{datasourceMissing ? this.renderEmptyState() : null}
|
{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 && (
|
{datasourceInstance && (
|
||||||
<div className="explore-container">
|
<div className="explore-container">
|
||||||
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
|
<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 timeZone = getTimeZone(state.user);
|
||||||
const {
|
const {
|
||||||
StartPage,
|
StartPage,
|
||||||
datasourceError,
|
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
initialized,
|
initialized,
|
||||||
@ -402,7 +378,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
StartPage,
|
StartPage,
|
||||||
datasourceError,
|
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
initialized,
|
initialized,
|
||||||
@ -431,7 +406,6 @@ const mapDispatchToProps: Partial<ExploreProps> = {
|
|||||||
changeSize,
|
changeSize,
|
||||||
initializeExplore,
|
initializeExplore,
|
||||||
modifyQueries,
|
modifyQueries,
|
||||||
reconnectDatasource,
|
|
||||||
refreshExplore,
|
refreshExplore,
|
||||||
scanStart,
|
scanStart,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
|
@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
|
|||||||
// Types
|
// Types
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { TimeRange, AbsoluteTimeRange, LoadingState } from '@grafana/data';
|
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 { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
|
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
|
||||||
@ -31,7 +31,6 @@ interface QueryRowProps extends PropsFromParent {
|
|||||||
className?: string;
|
className?: string;
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
datasourceStatus: DataSourceStatus;
|
|
||||||
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
|
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
@ -121,7 +120,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
|||||||
exploreEvents,
|
exploreEvents,
|
||||||
range,
|
range,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
datasourceStatus,
|
|
||||||
queryResponse,
|
queryResponse,
|
||||||
latency,
|
latency,
|
||||||
mode,
|
mode,
|
||||||
@ -148,7 +146,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
<QueryField
|
<QueryField
|
||||||
datasource={datasourceInstance}
|
datasource={datasourceInstance}
|
||||||
datasourceStatus={datasourceStatus}
|
|
||||||
query={query}
|
query={query}
|
||||||
history={history}
|
history={history}
|
||||||
onRunQuery={this.onRunQuery}
|
onRunQuery={this.onRunQuery}
|
||||||
@ -190,19 +187,8 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
|||||||
function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
|
function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
|
||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const item: ExploreItemState = explore[exploreId];
|
const item: ExploreItemState = explore[exploreId];
|
||||||
const {
|
const { datasourceInstance, history, queries, range, absoluteRange, latency, mode, queryResponse } = item;
|
||||||
datasourceInstance,
|
|
||||||
history,
|
|
||||||
queries,
|
|
||||||
range,
|
|
||||||
absoluteRange,
|
|
||||||
datasourceError,
|
|
||||||
latency,
|
|
||||||
mode,
|
|
||||||
queryResponse,
|
|
||||||
} = item;
|
|
||||||
const query = queries[index];
|
const query = queries[index];
|
||||||
const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
@ -210,7 +196,6 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
|
|||||||
query,
|
query,
|
||||||
range,
|
range,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
datasourceStatus,
|
|
||||||
queryResponse,
|
queryResponse,
|
||||||
latency,
|
latency,
|
||||||
mode,
|
mode,
|
||||||
|
@ -105,19 +105,6 @@ export interface LoadDatasourceReadyPayload {
|
|||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestDatasourcePendingPayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestDatasourceFailurePayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestDatasourceSuccessPayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModifyQueriesPayload {
|
export interface ModifyQueriesPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
modification: QueryFixAction;
|
modification: QueryFixAction;
|
||||||
@ -391,15 +378,6 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>(
|
|||||||
*/
|
*/
|
||||||
export const resetExploreAction = actionCreatorFactory<ResetExplorePayload>('explore/RESET_EXPLORE').create();
|
export const resetExploreAction = actionCreatorFactory<ResetExplorePayload>('explore/RESET_EXPLORE').create();
|
||||||
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').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>(
|
export const loadExploreDatasources = actionCreatorFactory<LoadExploreDataSourcesPayload>(
|
||||||
'explore/LOAD_EXPLORE_DATASOURCES'
|
'explore/LOAD_EXPLORE_DATASOURCES'
|
||||||
).create();
|
).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 { ExploreId, ExploreUrlState, ExploreUpdateState, ExploreMode } from 'app/types';
|
||||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
import {
|
import {
|
||||||
@ -6,9 +6,6 @@ import {
|
|||||||
InitializeExplorePayload,
|
InitializeExplorePayload,
|
||||||
updateUIStateAction,
|
updateUIStateAction,
|
||||||
setQueriesAction,
|
setQueriesAction,
|
||||||
testDataSourcePendingAction,
|
|
||||||
testDataSourceSuccessAction,
|
|
||||||
testDataSourceFailureAction,
|
|
||||||
loadDatasourcePendingAction,
|
loadDatasourcePendingAction,
|
||||||
loadDatasourceReadyAction,
|
loadDatasourceReadyAction,
|
||||||
} from './actionTypes';
|
} 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('loading datasource', () => {
|
||||||
describe('when loadDatasource thunk is dispatched', () => {
|
describe('when loadDatasource thunk is dispatched', () => {
|
||||||
describe('and all goes fine', () => {
|
describe('and all goes fine', () => {
|
||||||
@ -255,8 +186,6 @@ describe('loading datasource', () => {
|
|||||||
exploreId,
|
exploreId,
|
||||||
requestedDatasourceName: mockDatasourceInstance.name,
|
requestedDatasourceName: mockDatasourceInstance.name,
|
||||||
}),
|
}),
|
||||||
testDataSourcePendingAction({ exploreId }),
|
|
||||||
testDataSourceSuccessAction({ exploreId }),
|
|
||||||
loadDatasourceReadyAction({ exploreId, history: [] }),
|
loadDatasourceReadyAction({ exploreId, history: [] }),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -285,8 +214,6 @@ describe('loading datasource', () => {
|
|||||||
exploreId,
|
exploreId,
|
||||||
requestedDatasourceName: mockDatasourceInstance.name,
|
requestedDatasourceName: mockDatasourceInstance.name,
|
||||||
}),
|
}),
|
||||||
testDataSourcePendingAction({ exploreId }),
|
|
||||||
testDataSourceSuccessAction({ exploreId }),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -59,9 +59,6 @@ import {
|
|||||||
ToggleGraphPayload,
|
ToggleGraphPayload,
|
||||||
ToggleTablePayload,
|
ToggleTablePayload,
|
||||||
updateUIStateAction,
|
updateUIStateAction,
|
||||||
testDataSourcePendingAction,
|
|
||||||
testDataSourceSuccessAction,
|
|
||||||
testDataSourceFailureAction,
|
|
||||||
loadExploreDatasources,
|
loadExploreDatasources,
|
||||||
changeModeAction,
|
changeModeAction,
|
||||||
scanStopAction,
|
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.
|
* 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
|
// Keep ID to track selection
|
||||||
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
|
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) {
|
if (instance.init) {
|
||||||
try {
|
try {
|
||||||
instance.init();
|
instance.init();
|
||||||
@ -401,7 +356,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi, or
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
||||||
// User already changed datasource again, discard results
|
// User already changed datasource, discard results
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,7 +396,6 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
const {
|
const {
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
queries,
|
queries,
|
||||||
datasourceError,
|
|
||||||
containerWidth,
|
containerWidth,
|
||||||
isLive: live,
|
isLive: live,
|
||||||
range,
|
range,
|
||||||
@ -454,11 +408,6 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
showingTable,
|
showingTable,
|
||||||
} = exploreItemState;
|
} = exploreItemState;
|
||||||
|
|
||||||
if (datasourceError) {
|
|
||||||
// let's not run any queries if data source is in a faulty state
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasNonEmptyQuery(queries)) {
|
if (!hasNonEmptyQuery(queries)) {
|
||||||
dispatch(clearQueriesAction({ exploreId }));
|
dispatch(clearQueriesAction({ exploreId }));
|
||||||
dispatch(stateSave()); // Remember to save to state and update location
|
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 { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
import {
|
import {
|
||||||
scanStartAction,
|
scanStartAction,
|
||||||
testDataSourcePendingAction,
|
|
||||||
testDataSourceSuccessAction,
|
|
||||||
testDataSourceFailureAction,
|
|
||||||
updateDatasourceInstanceAction,
|
updateDatasourceInstanceAction,
|
||||||
splitOpenAction,
|
splitOpenAction,
|
||||||
splitCloseAction,
|
splitCloseAction,
|
||||||
@ -29,7 +26,7 @@ import { updateLocation } from 'app/core/actions/location';
|
|||||||
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { DataSourceApi, DataQuery } from '@grafana/ui';
|
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('Explore item reducer', () => {
|
||||||
describe('scanning', () => {
|
describe('scanning', () => {
|
||||||
@ -65,56 +62,7 @@ describe('Explore item reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('testing datasource', () => {
|
describe('changing 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('when changeDataType is dispatched', () => {
|
describe('when changeDataType is dispatched', () => {
|
||||||
it('then it should set correct state', () => {
|
it('then it should set correct state', () => {
|
||||||
reducerTester()
|
reducerTester()
|
||||||
@ -125,9 +73,7 @@ describe('Explore item reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('changing datasource', () => {
|
|
||||||
describe('when updateDatasourceInstanceAction is dispatched', () => {
|
describe('when updateDatasourceInstanceAction is dispatched', () => {
|
||||||
describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
|
describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
|
||||||
it('then it should set correct state', () => {
|
it('then it should set correct state', () => {
|
||||||
|
@ -15,9 +15,6 @@ import { DataQuery, DataSourceApi, PanelData, DataQueryRequest, RefreshPicker, P
|
|||||||
import {
|
import {
|
||||||
HigherOrderAction,
|
HigherOrderAction,
|
||||||
ActionTypes,
|
ActionTypes,
|
||||||
testDataSourcePendingAction,
|
|
||||||
testDataSourceSuccessAction,
|
|
||||||
testDataSourceFailureAction,
|
|
||||||
splitCloseAction,
|
splitCloseAction,
|
||||||
SplitCloseActionPayload,
|
SplitCloseActionPayload,
|
||||||
loadExploreDatasources,
|
loadExploreDatasources,
|
||||||
@ -82,7 +79,6 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
|||||||
containerWidth: 0,
|
containerWidth: 0,
|
||||||
datasourceInstance: null,
|
datasourceInstance: null,
|
||||||
requestedDatasourceName: null,
|
requestedDatasourceName: null,
|
||||||
datasourceError: null,
|
|
||||||
datasourceLoading: null,
|
datasourceLoading: null,
|
||||||
datasourceMissing: false,
|
datasourceMissing: false,
|
||||||
exploreDatasources: [],
|
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({
|
.addMapper({
|
||||||
filter: loadExploreDatasources,
|
filter: loadExploreDatasources,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
|
@ -17,6 +17,13 @@ export interface State {
|
|||||||
measurements: CascaderOption[];
|
measurements: CascaderOption[];
|
||||||
measurement: string;
|
measurement: string;
|
||||||
field: string;
|
field: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChooserOptions {
|
||||||
|
measurement: string;
|
||||||
|
field: string;
|
||||||
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for determining if a collection of pairs are valid
|
// 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> {
|
export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
||||||
templateSrv: TemplateSrv = new TemplateSrv();
|
templateSrv: TemplateSrv = new TemplateSrv();
|
||||||
state: State = { measurements: [], measurement: null, field: null };
|
state: State = { measurements: [], measurement: null, field: null, error: null };
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { datasource } = this.props;
|
const { datasource } = this.props;
|
||||||
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, datasource.database);
|
try {
|
||||||
const measureMentsQuery = queryBuilder.buildExploreQuery('MEASUREMENTS');
|
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, datasource.database);
|
||||||
const influxMeasurements = await datasource.metricFindQuery(measureMentsQuery);
|
const measureMentsQuery = queryBuilder.buildExploreQuery('MEASUREMENTS');
|
||||||
|
const influxMeasurements = await datasource.metricFindQuery(measureMentsQuery);
|
||||||
|
|
||||||
const measurements = [];
|
const measurements = [];
|
||||||
for (let index = 0; index < influxMeasurements.length; index++) {
|
for (let index = 0; index < influxMeasurements.length; index++) {
|
||||||
const measurementObj = influxMeasurements[index];
|
const measurementObj = influxMeasurements[index];
|
||||||
const queryBuilder = new InfluxQueryBuilder({ measurement: measurementObj.text, tags: [] }, datasource.database);
|
const queryBuilder = new InfluxQueryBuilder(
|
||||||
const fieldsQuery = queryBuilder.buildExploreQuery('FIELDS');
|
{ measurement: measurementObj.text, tags: [] },
|
||||||
const influxFields = await datasource.metricFindQuery(fieldsQuery);
|
datasource.database
|
||||||
const fields: any[] = influxFields.map(
|
);
|
||||||
(field: any): any => ({
|
const fieldsQuery = queryBuilder.buildExploreQuery('FIELDS');
|
||||||
label: field.text,
|
const influxFields = await datasource.metricFindQuery(fieldsQuery);
|
||||||
value: field.text,
|
const fields: any[] = influxFields.map(
|
||||||
children: [],
|
(field: any): any => ({
|
||||||
})
|
label: field.text,
|
||||||
);
|
value: field.text,
|
||||||
measurements.push({
|
children: [],
|
||||||
label: measurementObj.text,
|
})
|
||||||
value: measurementObj.text,
|
);
|
||||||
children: fields,
|
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) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
@ -107,8 +131,8 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { datasource } = this.props;
|
const { datasource } = this.props;
|
||||||
const { measurements, measurement, field } = this.state;
|
const { measurements, measurement, field, error } = this.state;
|
||||||
const cascadeText = measurement ? `Measurements (${measurement}/${field})` : 'Measurements';
|
const cascadeText = getChooserText({ measurement, field, error });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-inline gf-form-inline--nowrap">
|
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||||
@ -119,7 +143,7 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
|||||||
onChange={this.onMeasurementsChange}
|
onChange={this.onMeasurementsChange}
|
||||||
expandIcon={null}
|
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" />
|
{cascadeText} <i className="fa fa-caret-down" />
|
||||||
</button>
|
</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
@ -132,6 +156,9 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
|||||||
extendedOptions={{ measurement }}
|
extendedOptions={{ measurement }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{error ? (
|
||||||
|
<span className="gf-form-label gf-form-label--transparent gf-form-label--error m-l-2">{error}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataSourceStatus } from '@grafana/ui';
|
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { useLokiSyntax } from './useLokiSyntax';
|
import { useLokiSyntax } from './useLokiSyntax';
|
||||||
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
|
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
|
||||||
@ -25,7 +24,6 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
|
|||||||
|
|
||||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||||
datasource.languageProvider,
|
datasource.languageProvider,
|
||||||
DataSourceStatus.Connected,
|
|
||||||
absolute
|
absolute
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -38,7 +36,6 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
|
|||||||
<div className="gf-form-group">
|
<div className="gf-form-group">
|
||||||
<LokiQueryFieldForm
|
<LokiQueryFieldForm
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
datasourceStatus={DataSourceStatus.Connected}
|
|
||||||
query={query}
|
query={query}
|
||||||
onChange={(query: LokiQuery) => onChange(query.expr)}
|
onChange={(query: LokiQuery) => onChange(query.expr)}
|
||||||
onRunQuery={() => {}}
|
onRunQuery={() => {}}
|
||||||
|
@ -3,7 +3,7 @@ import React, { memo } from 'react';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { AbsoluteTimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { QueryEditorProps, DataSourceStatus } from '@grafana/ui';
|
import { QueryEditorProps } from '@grafana/ui';
|
||||||
import { LokiDatasource } from '../datasource';
|
import { LokiDatasource } from '../datasource';
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { LokiQueryField } from './LokiQueryField';
|
import { LokiQueryField } from './LokiQueryField';
|
||||||
@ -30,8 +30,6 @@ export const LokiQueryEditor = memo(function LokiQueryEditor(props: Props) {
|
|||||||
|
|
||||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||||
datasource.languageProvider,
|
datasource.languageProvider,
|
||||||
// TODO maybe use real status
|
|
||||||
DataSourceStatus.Connected,
|
|
||||||
absolute
|
absolute
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,7 +37,6 @@ export const LokiQueryEditor = memo(function LokiQueryEditor(props: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<LokiQueryField
|
<LokiQueryField
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
datasourceStatus={DataSourceStatus.Connected}
|
|
||||||
query={query}
|
query={query}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
|
@ -3,21 +3,15 @@ import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldFor
|
|||||||
import { useLokiSyntax } from './useLokiSyntax';
|
import { useLokiSyntax } from './useLokiSyntax';
|
||||||
import LokiLanguageProvider from '../language_provider';
|
import LokiLanguageProvider from '../language_provider';
|
||||||
|
|
||||||
export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
|
export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ datasource, ...otherProps }) => {
|
||||||
datasource,
|
|
||||||
datasourceStatus,
|
|
||||||
...otherProps
|
|
||||||
}) => {
|
|
||||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||||
datasource.languageProvider as LokiLanguageProvider,
|
datasource.languageProvider as LokiLanguageProvider,
|
||||||
datasourceStatus,
|
|
||||||
otherProps.absoluteRange
|
otherProps.absoluteRange
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LokiQueryFieldForm
|
<LokiQueryFieldForm
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
datasourceStatus={datasourceStatus}
|
|
||||||
syntaxLoaded={isSyntaxReady}
|
syntaxLoaded={isSyntaxReady}
|
||||||
/**
|
/**
|
||||||
* setActiveOption name is intentional. Because of the way rc-cascader requests additional data
|
* setActiveOption name is intentional. Because of the way rc-cascader requests additional data
|
||||||
|
@ -15,17 +15,14 @@ import { Plugin, Node } from 'slate';
|
|||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { TypeaheadOutput } from 'app/types/explore';
|
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 { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { Grammar } from 'prismjs';
|
import { Grammar } from 'prismjs';
|
||||||
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
||||||
import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
|
import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
|
||||||
import LokiDatasource from '../datasource';
|
import LokiDatasource from '../datasource';
|
||||||
|
|
||||||
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
|
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) {
|
||||||
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
|
||||||
return '(Disconnected)';
|
|
||||||
}
|
|
||||||
if (!hasSyntax) {
|
if (!hasSyntax) {
|
||||||
return 'Loading labels...';
|
return 'Loading labels...';
|
||||||
}
|
}
|
||||||
@ -144,21 +141,12 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { data, query, syntaxLoaded, logLabelOptions, onLoadOptions, onLabelsRefresh, datasource } = this.props;
|
||||||
data,
|
|
||||||
query,
|
|
||||||
syntaxLoaded,
|
|
||||||
logLabelOptions,
|
|
||||||
onLoadOptions,
|
|
||||||
onLabelsRefresh,
|
|
||||||
datasource,
|
|
||||||
datasourceStatus,
|
|
||||||
} = this.props;
|
|
||||||
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
|
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
|
||||||
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
|
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
|
||||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
||||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
|
const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
|
||||||
const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
|
const buttonDisabled = !(syntaxLoaded && hasLogLabels);
|
||||||
const showError = data && data.error && data.error.refId === query.refId;
|
const showError = data && data.error && data.error.refId === query.refId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -166,7 +154,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<Cascader
|
<Cascader
|
||||||
options={logLabelOptions}
|
options={logLabelOptions || []}
|
||||||
onChange={this.onChangeLogLabels}
|
onChange={this.onChangeLogLabels}
|
||||||
loadData={onLoadOptions}
|
loadData={onLoadOptions}
|
||||||
expandIcon={null}
|
expandIcon={null}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { renderHook, act } from 'react-hooks-testing-library';
|
import { renderHook, act } from 'react-hooks-testing-library';
|
||||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { useLokiLabels } from './useLokiLabels';
|
import { useLokiLabels } from './useLokiLabels';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
|
||||||
import { AbsoluteTimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { makeMockLokiDatasource } from '../mocks';
|
import { makeMockLokiDatasource } from '../mocks';
|
||||||
|
|
||||||
@ -20,49 +19,10 @@ describe('useLokiLabels hook', () => {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() => useLokiLabels(languageProvider, true, [], rangeMock));
|
||||||
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Connected)
|
|
||||||
);
|
|
||||||
act(() => result.current.refreshLabels());
|
act(() => result.current.refreshLabels());
|
||||||
expect(result.current.logLabelOptions).toEqual([]);
|
expect(result.current.logLabelOptions).toEqual([]);
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock);
|
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 { useState, useEffect } from 'react';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
|
||||||
import { AbsoluteTimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
@ -18,18 +17,13 @@ export const useLokiLabels = (
|
|||||||
languageProvider: LokiLanguageProvider,
|
languageProvider: LokiLanguageProvider,
|
||||||
languageProviderInitialised: boolean,
|
languageProviderInitialised: boolean,
|
||||||
activeOption: CascaderOption[],
|
activeOption: CascaderOption[],
|
||||||
absoluteRange: AbsoluteTimeRange,
|
absoluteRange: AbsoluteTimeRange
|
||||||
datasourceStatus: DataSourceStatus,
|
|
||||||
initialDatasourceStatus?: DataSourceStatus // used for test purposes
|
|
||||||
) => {
|
) => {
|
||||||
const mounted = useRefMounted();
|
const mounted = useRefMounted();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [logLabelOptions, setLogLabelOptions] = useState([]);
|
const [logLabelOptions, setLogLabelOptions] = useState([]);
|
||||||
const [shouldTryRefreshLabels, setRefreshLabels] = useState(false);
|
const [shouldTryRefreshLabels, setRefreshLabels] = useState(false);
|
||||||
const [prevDatasourceStatus, setPrevDatasourceStatus] = useState(
|
|
||||||
initialDatasourceStatus || DataSourceStatus.Connected
|
|
||||||
);
|
|
||||||
const [shouldForceRefreshLabels, setForceRefreshLabels] = useState(false);
|
const [shouldForceRefreshLabels, setForceRefreshLabels] = useState(false);
|
||||||
|
|
||||||
// Async
|
// Async
|
||||||
@ -83,15 +77,6 @@ export const useLokiLabels = (
|
|||||||
}
|
}
|
||||||
}, [shouldTryRefreshLabels, shouldForceRefreshLabels]);
|
}, [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 {
|
return {
|
||||||
logLabelOptions,
|
logLabelOptions,
|
||||||
setLogLabelOptions,
|
setLogLabelOptions,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { renderHook, act } from 'react-hooks-testing-library';
|
import { renderHook, act } from 'react-hooks-testing-library';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
|
||||||
import { AbsoluteTimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
@ -36,9 +35,7 @@ describe('useLokiSyntax hook', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should provide Loki syntax when used', async () => {
|
it('should provide Loki syntax when used', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
|
||||||
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
|
|
||||||
);
|
|
||||||
expect(result.current.syntax).toEqual(null);
|
expect(result.current.syntax).toEqual(null);
|
||||||
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
@ -47,9 +44,7 @@ describe('useLokiSyntax hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch labels on first call', async () => {
|
it('should fetch labels on first call', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
|
||||||
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
|
|
||||||
);
|
|
||||||
expect(result.current.isSyntaxReady).toBeFalsy();
|
expect(result.current.isSyntaxReady).toBeFalsy();
|
||||||
expect(result.current.logLabelOptions).toEqual([]);
|
expect(result.current.logLabelOptions).toEqual([]);
|
||||||
|
|
||||||
@ -60,9 +55,7 @@ describe('useLokiSyntax hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should try to fetch missing options when active option changes', async () => {
|
it('should try to fetch missing options when active option changes', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, rangeMock));
|
||||||
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
|
|
||||||
);
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
|
||||||
import { AbsoluteTimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
|
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
|
||||||
@ -14,11 +13,7 @@ const PRISM_SYNTAX = 'promql';
|
|||||||
* @param languageProvider
|
* @param languageProvider
|
||||||
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values
|
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values
|
||||||
*/
|
*/
|
||||||
export const useLokiSyntax = (
|
export const useLokiSyntax = (languageProvider: LokiLanguageProvider, absoluteRange: AbsoluteTimeRange) => {
|
||||||
languageProvider: LokiLanguageProvider,
|
|
||||||
datasourceStatus: DataSourceStatus,
|
|
||||||
absoluteRange: AbsoluteTimeRange
|
|
||||||
) => {
|
|
||||||
const mounted = useRefMounted();
|
const mounted = useRefMounted();
|
||||||
// State
|
// State
|
||||||
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
||||||
@ -35,8 +30,7 @@ export const useLokiSyntax = (
|
|||||||
languageProvider,
|
languageProvider,
|
||||||
languageProviderInitialized,
|
languageProviderInitialized,
|
||||||
activeOption,
|
activeOption,
|
||||||
absoluteRange,
|
absoluteRange
|
||||||
datasourceStatus
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Async
|
// Async
|
||||||
|
@ -121,7 +121,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
|
|
||||||
processError = (err: any, target: any): DataQueryError => {
|
processError = (err: any, target: any): DataQueryError => {
|
||||||
const error: 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,
|
refId: target.refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { FormLabel, Select, Switch, QueryEditorProps, DataSourceStatus } from '@grafana/ui';
|
import { FormLabel, Select, Switch, QueryEditorProps } from '@grafana/ui';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PrometheusDatasource } from '../datasource';
|
||||||
@ -104,7 +104,6 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
|||||||
onChange={this.onFieldChange}
|
onChange={this.onFieldChange}
|
||||||
history={[]}
|
history={[]}
|
||||||
data={data}
|
data={data}
|
||||||
datasourceStatus={DataSourceStatus.Connected} // TODO: replace with real DataSourceStatus
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="gf-form-inline">
|
<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 QueryField, { TypeaheadInput } from 'app/features/explore/QueryField';
|
||||||
import { PromQuery, PromContext, PromOptions } from '../types';
|
import { PromQuery, PromContext, PromOptions } from '../types';
|
||||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
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 { isDataFrame, toLegacyResponseData } from '@grafana/data';
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PrometheusDatasource } from '../datasource';
|
||||||
import PromQlLanguageProvider from '../language_provider';
|
import PromQlLanguageProvider from '../language_provider';
|
||||||
@ -24,13 +24,13 @@ const METRIC_MARK = 'metric';
|
|||||||
const PRISM_SYNTAX = 'promql';
|
const PRISM_SYNTAX = 'promql';
|
||||||
export const RECORDING_RULES_GROUP = '__recording_rules__';
|
export const RECORDING_RULES_GROUP = '__recording_rules__';
|
||||||
|
|
||||||
function getChooserText(hasSyntax: boolean, datasourceStatus: DataSourceStatus) {
|
function getChooserText(hasSyntax: boolean, metrics: string[]) {
|
||||||
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
|
||||||
return '(Disconnected)';
|
|
||||||
}
|
|
||||||
if (!hasSyntax) {
|
if (!hasSyntax) {
|
||||||
return 'Loading metrics...';
|
return 'Loading metrics...';
|
||||||
}
|
}
|
||||||
|
if (metrics && metrics.length === 0) {
|
||||||
|
return '(No metrics found)';
|
||||||
|
}
|
||||||
return 'Metrics';
|
return 'Metrics';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,21 +159,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
if (data && prevProps.data && prevProps.data.series !== data.series) {
|
if (data && prevProps.data && prevProps.data.series !== data.series) {
|
||||||
this.refreshHint();
|
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 = () => {
|
refreshHint = () => {
|
||||||
@ -291,11 +276,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, query, datasourceStatus } = this.props;
|
const { data, query } = this.props;
|
||||||
const { metricsOptions, syntaxLoaded, hint } = this.state;
|
const { metricsOptions, syntaxLoaded, hint } = this.state;
|
||||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||||
const chooserText = getChooserText(syntaxLoaded, datasourceStatus);
|
const chooserText = getChooserText(syntaxLoaded, metricsOptions);
|
||||||
const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
|
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0);
|
||||||
const showError = data && data.error && data.error.refId === query.refId;
|
const showError = data && data.error && data.error.refId === query.refId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -9,7 +9,6 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
|||||||
"getPrometheusTime": [MockFunction],
|
"getPrometheusTime": [MockFunction],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
datasourceStatus={0}
|
|
||||||
history={Array []}
|
history={Array []}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onRunQuery={[Function]}
|
onRunQuery={[Function]}
|
||||||
|
@ -447,7 +447,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
|
|
||||||
handleErrors = (err: any, target: PromQuery) => {
|
handleErrors = (err: any, target: PromQuery) => {
|
||||||
const error: 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,
|
refId: target.refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,10 +161,6 @@ export interface ExploreItemState {
|
|||||||
* Current data source name or null if default
|
* Current data source name or null if default
|
||||||
*/
|
*/
|
||||||
requestedDatasourceName: string | null;
|
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.
|
* True if the datasource is loading. `null` if the loading has not started yet.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user