mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Feat: Adds reconnect for failing datasource in Explore (#16226)
* Style: made outlined buttons and used it in Alert component * Refactor: clean up state on load data source failure * Refactor: test data source thunk created * Refactor: move logic to changeDatasource and call that from intialize * Refactor: move load explore datasources to own thunk * Refactor: move logic to updateDatasourceInstanceAction * Tests: reducer tests * Test(Explore): Added tests and made thunkTester async * Fix(Explore): Fixed so that we do not render StartPage if there is no StartPage * Fix(Explore): Missed type in merge * Refactor: Thunktester did not fail tests on async failures and prevented queires from running on datasource failures * Feat: Fadein error alert to prevent flickering * Feat: Refresh labels after reconnect * Refactor: Move useLokiForceLabels into useLokiLabels from PR comments * Feat: adds refresh metrics to Prometheus languageprovider * Style: removes padding for connected datasources * Chore: remove implicit anys
This commit is contained in:
parent
e69039d8d1
commit
988b7c4dc3
@ -54,8 +54,14 @@ export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends D
|
|||||||
onChange: (value: TQuery) => void;
|
onChange: (value: TQuery) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DatasourceStatus {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||||
datasource: DSType;
|
datasource: DSType;
|
||||||
|
datasourceStatus: DatasourceStatus;
|
||||||
query: TQuery;
|
query: TQuery;
|
||||||
error?: string | JSX.Element;
|
error?: string | JSX.Element;
|
||||||
hint?: QueryHint;
|
hint?: QueryHint;
|
||||||
|
@ -2,12 +2,16 @@ import React, { FC } from 'react';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: any;
|
message: any;
|
||||||
|
button?: {
|
||||||
|
text: string;
|
||||||
|
onClick: (event: React.MouseEvent) => void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Alert: FC<Props> = props => {
|
export const Alert: FC<Props> = props => {
|
||||||
const { message } = props;
|
const { message, button } = props;
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-group section">
|
<div className="alert-container">
|
||||||
<div className="alert-error alert">
|
<div className="alert-error alert">
|
||||||
<div className="alert-icon">
|
<div className="alert-icon">
|
||||||
<i className="fa fa-exclamation-triangle" />
|
<i className="fa fa-exclamation-triangle" />
|
||||||
@ -15,6 +19,13 @@ export const Alert: FC<Props> = props => {
|
|||||||
<div className="alert-body">
|
<div className="alert-body">
|
||||||
<div className="alert-title">{message}</div>
|
<div className="alert-title">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{button && (
|
||||||
|
<div className="alert-button">
|
||||||
|
<button className="btn btn-outline-danger" onClick={button.onClick}>
|
||||||
|
{button.text}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
scanStart,
|
scanStart,
|
||||||
setQueries,
|
setQueries,
|
||||||
refreshExplore,
|
refreshExplore,
|
||||||
|
reconnectDatasource,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@ -39,6 +40,7 @@ import { Emitter } from 'app/core/utils/emitter';
|
|||||||
import { ExploreToolbar } from './ExploreToolbar';
|
import { ExploreToolbar } from './ExploreToolbar';
|
||||||
import { scanStopAction } from './state/actionTypes';
|
import { scanStopAction } from './state/actionTypes';
|
||||||
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
||||||
|
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||||
|
|
||||||
interface ExploreProps {
|
interface ExploreProps {
|
||||||
StartPage?: ComponentClass<ExploreStartPageProps>;
|
StartPage?: ComponentClass<ExploreStartPageProps>;
|
||||||
@ -54,6 +56,7 @@ interface ExploreProps {
|
|||||||
modifyQueries: typeof modifyQueries;
|
modifyQueries: typeof modifyQueries;
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
update: ExploreUpdateState;
|
update: ExploreUpdateState;
|
||||||
|
reconnectDatasource: typeof reconnectDatasource;
|
||||||
refreshExplore: typeof refreshExplore;
|
refreshExplore: typeof refreshExplore;
|
||||||
scanner?: RangeScanner;
|
scanner?: RangeScanner;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
@ -201,6 +204,13 @@ 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,
|
||||||
@ -224,13 +234,16 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
||||||
{datasourceMissing ? this.renderEmptyState() : null}
|
{datasourceMissing ? this.renderEmptyState() : null}
|
||||||
|
|
||||||
{datasourceError && (
|
<FadeIn duration={datasourceError ? 150 : 5} in={datasourceError ? true : false}>
|
||||||
<div className="explore-container">
|
<div className="explore-container">
|
||||||
<Alert message={`Error connecting to datasource: ${datasourceError}`} />
|
<Alert
|
||||||
|
message={`Error connecting to datasource: ${datasourceError}`}
|
||||||
|
button={{ text: 'Reconnect', onClick: this.onReconnect }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</FadeIn>
|
||||||
|
|
||||||
{datasourceInstance && !datasourceError && (
|
{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} />
|
||||||
<AutoSizer onResize={this.onResize} disableHeight>
|
<AutoSizer onResize={this.onResize} disableHeight>
|
||||||
@ -315,6 +328,7 @@ const mapDispatchToProps = {
|
|||||||
changeTime,
|
changeTime,
|
||||||
initializeExplore,
|
initializeExplore,
|
||||||
modifyQueries,
|
modifyQueries,
|
||||||
|
reconnectDatasource,
|
||||||
refreshExplore,
|
refreshExplore,
|
||||||
scanStart,
|
scanStart,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
|
// @ts-ignore
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@ -13,7 +15,14 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui';
|
import {
|
||||||
|
RawTimeRange,
|
||||||
|
DataQuery,
|
||||||
|
ExploreDataSourceApi,
|
||||||
|
QueryHint,
|
||||||
|
QueryFixAction,
|
||||||
|
DatasourceStatus,
|
||||||
|
} from '@grafana/ui';
|
||||||
import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
|
import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } 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';
|
||||||
@ -32,6 +41,7 @@ interface QueryRowProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
datasourceInstance: ExploreDataSourceApi;
|
datasourceInstance: ExploreDataSourceApi;
|
||||||
|
datasourceStatus: DatasourceStatus;
|
||||||
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
|
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
index: number;
|
index: number;
|
||||||
@ -95,7 +105,16 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { datasourceInstance, history, index, query, queryTransactions, exploreEvents, range } = this.props;
|
const {
|
||||||
|
datasourceInstance,
|
||||||
|
history,
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
queryTransactions,
|
||||||
|
exploreEvents,
|
||||||
|
range,
|
||||||
|
datasourceStatus,
|
||||||
|
} = this.props;
|
||||||
const transactions = queryTransactions.filter(t => t.rowIndex === index);
|
const transactions = queryTransactions.filter(t => t.rowIndex === index);
|
||||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||||
const hint = getFirstHintFromTransactions(transactions);
|
const hint = getFirstHintFromTransactions(transactions);
|
||||||
@ -110,6 +129,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
{QueryField ? (
|
{QueryField ? (
|
||||||
<QueryField
|
<QueryField
|
||||||
datasource={datasourceInstance}
|
datasource={datasourceInstance}
|
||||||
|
datasourceStatus={datasourceStatus}
|
||||||
query={query}
|
query={query}
|
||||||
error={queryError}
|
error={queryError}
|
||||||
hint={hint}
|
hint={hint}
|
||||||
@ -152,12 +172,19 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState, { exploreId, index }) {
|
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 { datasourceInstance, history, queries, queryTransactions, range } = item;
|
const { datasourceInstance, history, queries, queryTransactions, range, datasourceError } = item;
|
||||||
const query = queries[index];
|
const query = queries[index];
|
||||||
return { datasourceInstance, history, query, queryTransactions, range };
|
return {
|
||||||
|
datasourceInstance,
|
||||||
|
history,
|
||||||
|
query,
|
||||||
|
queryTransactions,
|
||||||
|
range,
|
||||||
|
datasourceStatus: datasourceError ? DatasourceStatus.Disconnected : DatasourceStatus.Connected,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -79,7 +79,6 @@ export interface InitializeExplorePayload {
|
|||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
containerWidth: number;
|
containerWidth: number;
|
||||||
eventBridge: Emitter;
|
eventBridge: Emitter;
|
||||||
exploreDatasources: DataSourceSelectItem[];
|
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
ui: ExploreUIState;
|
ui: ExploreUIState;
|
||||||
@ -99,16 +98,22 @@ export interface LoadDatasourcePendingPayload {
|
|||||||
requestedDatasourceName: string;
|
requestedDatasourceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadDatasourceSuccessPayload {
|
export interface LoadDatasourceReadyPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
StartPage?: any;
|
|
||||||
datasourceInstance: any;
|
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
logsHighlighterExpressions?: any[];
|
}
|
||||||
showingStartPage: boolean;
|
|
||||||
supportsGraph: boolean;
|
export interface TestDatasourcePendingPayload {
|
||||||
supportsLogs: boolean;
|
exploreId: ExploreId;
|
||||||
supportsTable: boolean;
|
}
|
||||||
|
|
||||||
|
export interface TestDatasourceFailurePayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestDatasourceSuccessPayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModifyQueriesPayload {
|
export interface ModifyQueriesPayload {
|
||||||
@ -199,6 +204,11 @@ export interface QueriesImportedPayload {
|
|||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadExploreDataSourcesPayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
exploreDatasources: DataSourceSelectItem[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a query row after the row with the given index.
|
* Adds a query row after the row with the given index.
|
||||||
*/
|
*/
|
||||||
@ -246,13 +256,6 @@ export const initializeExploreAction = actionCreatorFactory<InitializeExplorePay
|
|||||||
'explore/INITIALIZE_EXPLORE'
|
'explore/INITIALIZE_EXPLORE'
|
||||||
).create();
|
).create();
|
||||||
|
|
||||||
/**
|
|
||||||
* Display an error that happened during the selection of a datasource
|
|
||||||
*/
|
|
||||||
export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>(
|
|
||||||
'explore/LOAD_DATASOURCE_FAILURE'
|
|
||||||
).create();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an error when no datasources have been configured
|
* Display an error when no datasources have been configured
|
||||||
*/
|
*/
|
||||||
@ -268,12 +271,10 @@ export const loadDatasourcePendingAction = actionCreatorFactory<LoadDatasourcePe
|
|||||||
).create();
|
).create();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
|
* Datasource loading was completed.
|
||||||
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
|
||||||
* e.g., Prometheus -> Loki queries.
|
|
||||||
*/
|
*/
|
||||||
export const loadDatasourceSuccessAction = actionCreatorFactory<LoadDatasourceSuccessPayload>(
|
export const loadDatasourceReadyAction = actionCreatorFactory<LoadDatasourceReadyPayload>(
|
||||||
'explore/LOAD_DATASOURCE_SUCCESS'
|
'explore/LOAD_DATASOURCE_READY'
|
||||||
).create();
|
).create();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -391,37 +392,21 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>(
|
|||||||
*/
|
*/
|
||||||
export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
|
export const resetExploreAction = noPayloadActionCreatorFactory('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>(
|
||||||
|
'explore/LOAD_EXPLORE_DATASOURCES'
|
||||||
|
).create();
|
||||||
|
|
||||||
export type HigherOrderAction =
|
export type HigherOrderAction =
|
||||||
| ActionOf<SplitCloseActionPayload>
|
| ActionOf<SplitCloseActionPayload>
|
||||||
| SplitOpenAction
|
| SplitOpenAction
|
||||||
| ResetExploreAction
|
| ResetExploreAction
|
||||||
| ActionOf<any>;
|
| ActionOf<any>;
|
||||||
|
|
||||||
export type Action =
|
|
||||||
| ActionOf<AddQueryRowPayload>
|
|
||||||
| ActionOf<ChangeQueryPayload>
|
|
||||||
| ActionOf<ChangeSizePayload>
|
|
||||||
| ActionOf<ChangeTimePayload>
|
|
||||||
| ActionOf<ClearQueriesPayload>
|
|
||||||
| ActionOf<HighlightLogsExpressionPayload>
|
|
||||||
| ActionOf<InitializeExplorePayload>
|
|
||||||
| ActionOf<LoadDatasourceFailurePayload>
|
|
||||||
| ActionOf<LoadDatasourceMissingPayload>
|
|
||||||
| ActionOf<LoadDatasourcePendingPayload>
|
|
||||||
| ActionOf<LoadDatasourceSuccessPayload>
|
|
||||||
| ActionOf<ModifyQueriesPayload>
|
|
||||||
| ActionOf<QueryTransactionFailurePayload>
|
|
||||||
| ActionOf<QueryTransactionStartPayload>
|
|
||||||
| ActionOf<QueryTransactionSuccessPayload>
|
|
||||||
| ActionOf<RemoveQueryRowPayload>
|
|
||||||
| ActionOf<ScanStartPayload>
|
|
||||||
| ActionOf<ScanRangePayload>
|
|
||||||
| ActionOf<SetQueriesPayload>
|
|
||||||
| ActionOf<SplitOpenPayload>
|
|
||||||
| ActionOf<ToggleTablePayload>
|
|
||||||
| ActionOf<ToggleGraphPayload>
|
|
||||||
| ActionOf<ToggleLogsPayload>
|
|
||||||
| ActionOf<UpdateDatasourceInstancePayload>
|
|
||||||
| ActionOf<QueriesImportedPayload>
|
|
||||||
| ActionOf<ToggleLogLevelPayload>;
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { refreshExplore } from './actions';
|
import { refreshExplore, testDatasource, loadDatasource } from './actions';
|
||||||
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
|
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
|
||||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||||
@ -8,10 +8,16 @@ import {
|
|||||||
changeTimeAction,
|
changeTimeAction,
|
||||||
updateUIStateAction,
|
updateUIStateAction,
|
||||||
setQueriesAction,
|
setQueriesAction,
|
||||||
|
testDataSourcePendingAction,
|
||||||
|
testDataSourceSuccessAction,
|
||||||
|
testDataSourceFailureAction,
|
||||||
|
loadDatasourcePendingAction,
|
||||||
|
loadDatasourceReadyAction,
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { Emitter } from 'app/core/core';
|
import { Emitter } from 'app/core/core';
|
||||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { makeInitialUpdateState } from './reducers';
|
import { makeInitialUpdateState } from './reducers';
|
||||||
|
import { DataQuery } from '@grafana/ui/src/types/datasource';
|
||||||
|
|
||||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||||
getDatasourceSrv: () => ({
|
getDatasourceSrv: () => ({
|
||||||
@ -41,7 +47,7 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
|||||||
eventBridge,
|
eventBridge,
|
||||||
update,
|
update,
|
||||||
datasourceInstance: { name: 'some-datasource' },
|
datasourceInstance: { name: 'some-datasource' },
|
||||||
queries: [],
|
queries: [] as DataQuery[],
|
||||||
range,
|
range,
|
||||||
ui,
|
ui,
|
||||||
},
|
},
|
||||||
@ -61,87 +67,204 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
|||||||
describe('refreshExplore', () => {
|
describe('refreshExplore', () => {
|
||||||
describe('when explore is initialized', () => {
|
describe('when explore is initialized', () => {
|
||||||
describe('and update datasource is set', () => {
|
describe('and update datasource is set', () => {
|
||||||
it('then it should dispatch initializeExplore', () => {
|
it('then it should dispatch initializeExplore', async () => {
|
||||||
const { exploreId, ui, range, initialState, containerWidth, eventBridge } = setup({ datasource: true });
|
const { exploreId, ui, range, initialState, containerWidth, eventBridge } = setup({ datasource: true });
|
||||||
|
|
||||||
thunkTester(initialState)
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
.givenThunk(refreshExplore)
|
.givenThunk(refreshExplore)
|
||||||
.whenThunkIsDispatched(exploreId)
|
.whenThunkIsDispatched(exploreId);
|
||||||
.thenDispatchedActionsAreEqual(dispatchedActions => {
|
|
||||||
const initializeExplore = dispatchedActions[0] as ActionOf<InitializeExplorePayload>;
|
|
||||||
const { type, payload } = initializeExplore;
|
|
||||||
|
|
||||||
expect(type).toEqual(initializeExploreAction.type);
|
const initializeExplore = dispatchedActions[2] as ActionOf<InitializeExplorePayload>;
|
||||||
expect(payload.containerWidth).toEqual(containerWidth);
|
const { type, payload } = initializeExplore;
|
||||||
expect(payload.eventBridge).toEqual(eventBridge);
|
|
||||||
expect(payload.exploreDatasources).toEqual([]);
|
|
||||||
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
|
|
||||||
expect(payload.range).toEqual(range);
|
|
||||||
expect(payload.ui).toEqual(ui);
|
|
||||||
|
|
||||||
return true;
|
expect(type).toEqual(initializeExploreAction.type);
|
||||||
});
|
expect(payload.containerWidth).toEqual(containerWidth);
|
||||||
|
expect(payload.eventBridge).toEqual(eventBridge);
|
||||||
|
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
|
||||||
|
expect(payload.range).toEqual(range);
|
||||||
|
expect(payload.ui).toEqual(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and update range is set', () => {
|
describe('and update range is set', () => {
|
||||||
it('then it should dispatch changeTimeAction', () => {
|
it('then it should dispatch changeTimeAction', async () => {
|
||||||
const { exploreId, range, initialState } = setup({ range: true });
|
const { exploreId, range, initialState } = setup({ range: true });
|
||||||
|
|
||||||
thunkTester(initialState)
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
.givenThunk(refreshExplore)
|
.givenThunk(refreshExplore)
|
||||||
.whenThunkIsDispatched(exploreId)
|
.whenThunkIsDispatched(exploreId);
|
||||||
.thenDispatchedActionsAreEqual(dispatchedActions => {
|
|
||||||
expect(dispatchedActions[0].type).toEqual(changeTimeAction.type);
|
|
||||||
expect(dispatchedActions[0].payload).toEqual({ exploreId, range });
|
|
||||||
|
|
||||||
return true;
|
expect(dispatchedActions[0].type).toEqual(changeTimeAction.type);
|
||||||
});
|
expect(dispatchedActions[0].payload).toEqual({ exploreId, range });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and update ui is set', () => {
|
describe('and update ui is set', () => {
|
||||||
it('then it should dispatch updateUIStateAction', () => {
|
it('then it should dispatch updateUIStateAction', async () => {
|
||||||
const { exploreId, initialState, ui } = setup({ ui: true });
|
const { exploreId, initialState, ui } = setup({ ui: true });
|
||||||
|
|
||||||
thunkTester(initialState)
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
.givenThunk(refreshExplore)
|
.givenThunk(refreshExplore)
|
||||||
.whenThunkIsDispatched(exploreId)
|
.whenThunkIsDispatched(exploreId);
|
||||||
.thenDispatchedActionsAreEqual(dispatchedActions => {
|
|
||||||
expect(dispatchedActions[0].type).toEqual(updateUIStateAction.type);
|
|
||||||
expect(dispatchedActions[0].payload).toEqual({ ...ui, exploreId });
|
|
||||||
|
|
||||||
return true;
|
expect(dispatchedActions[0].type).toEqual(updateUIStateAction.type);
|
||||||
});
|
expect(dispatchedActions[0].payload).toEqual({ ...ui, exploreId });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and update queries is set', () => {
|
describe('and update queries is set', () => {
|
||||||
it('then it should dispatch setQueriesAction', () => {
|
it('then it should dispatch setQueriesAction', async () => {
|
||||||
const { exploreId, initialState } = setup({ queries: true });
|
const { exploreId, initialState } = setup({ queries: true });
|
||||||
|
|
||||||
thunkTester(initialState)
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
.givenThunk(refreshExplore)
|
.givenThunk(refreshExplore)
|
||||||
.whenThunkIsDispatched(exploreId)
|
.whenThunkIsDispatched(exploreId);
|
||||||
.thenDispatchedActionsAreEqual(dispatchedActions => {
|
|
||||||
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
|
|
||||||
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
|
|
||||||
|
|
||||||
return true;
|
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
|
||||||
});
|
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when update is not initialized', () => {
|
describe('when update is not initialized', () => {
|
||||||
it('then it should not dispatch any actions', () => {
|
it('then it should not dispatch any actions', async () => {
|
||||||
const exploreId = ExploreId.left;
|
const exploreId = ExploreId.left;
|
||||||
const initialState = { explore: { [exploreId]: { initialized: false } } };
|
const initialState = { explore: { [exploreId]: { initialized: false } } };
|
||||||
|
|
||||||
thunkTester(initialState)
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
.givenThunk(refreshExplore)
|
.givenThunk(refreshExplore)
|
||||||
.whenThunkIsDispatched(exploreId)
|
.whenThunkIsDispatched(exploreId);
|
||||||
.thenThereAreNoDispatchedActions();
|
|
||||||
|
expect(dispatchedActions).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
it('then it should dispatch correct actions', async () => {
|
||||||
|
const exploreId = ExploreId.left;
|
||||||
|
const name = 'some-datasource';
|
||||||
|
const initialState = { explore: { [exploreId]: { requestedDatasourceName: name } } };
|
||||||
|
const mockDatasourceInstance = {
|
||||||
|
testDatasource: () => {
|
||||||
|
return Promise.resolve({ status: 'success' });
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
init: jest.fn(),
|
||||||
|
meta: { id: 'some id' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
|
.givenThunk(loadDatasource)
|
||||||
|
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([
|
||||||
|
loadDatasourcePendingAction({
|
||||||
|
exploreId,
|
||||||
|
requestedDatasourceName: mockDatasourceInstance.name,
|
||||||
|
}),
|
||||||
|
testDataSourcePendingAction({ exploreId }),
|
||||||
|
testDataSourceSuccessAction({ exploreId }),
|
||||||
|
loadDatasourceReadyAction({ exploreId, history: [] }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and user changes datasource during load', () => {
|
||||||
|
it('then it should dispatch correct actions', async () => {
|
||||||
|
const exploreId = ExploreId.left;
|
||||||
|
const name = 'some-datasource';
|
||||||
|
const initialState = { explore: { [exploreId]: { requestedDatasourceName: 'some-other-datasource' } } };
|
||||||
|
const mockDatasourceInstance = {
|
||||||
|
testDatasource: () => {
|
||||||
|
return Promise.resolve({ status: 'success' });
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
init: jest.fn(),
|
||||||
|
meta: { id: 'some id' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchedActions = await thunkTester(initialState)
|
||||||
|
.givenThunk(loadDatasource)
|
||||||
|
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([
|
||||||
|
loadDatasourcePendingAction({
|
||||||
|
exploreId,
|
||||||
|
requestedDatasourceName: mockDatasourceInstance.name,
|
||||||
|
}),
|
||||||
|
testDataSourcePendingAction({ exploreId }),
|
||||||
|
testDataSourceSuccessAction({ exploreId }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
@ -22,6 +23,7 @@ import {
|
|||||||
import { updateLocation } from 'app/core/actions';
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
import { ThunkResult } from 'app/types';
|
||||||
import {
|
import {
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
@ -31,7 +33,15 @@ import {
|
|||||||
QueryHint,
|
QueryHint,
|
||||||
QueryFixAction,
|
QueryFixAction,
|
||||||
} from '@grafana/ui/src/types';
|
} from '@grafana/ui/src/types';
|
||||||
import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions, ExploreUIState } from 'app/types/explore';
|
import {
|
||||||
|
ExploreId,
|
||||||
|
ExploreUrlState,
|
||||||
|
RangeScanner,
|
||||||
|
ResultType,
|
||||||
|
QueryOptions,
|
||||||
|
ExploreUIState,
|
||||||
|
QueryTransaction,
|
||||||
|
} from 'app/types/explore';
|
||||||
import {
|
import {
|
||||||
updateDatasourceInstanceAction,
|
updateDatasourceInstanceAction,
|
||||||
changeQueryAction,
|
changeQueryAction,
|
||||||
@ -42,11 +52,10 @@ import {
|
|||||||
clearQueriesAction,
|
clearQueriesAction,
|
||||||
initializeExploreAction,
|
initializeExploreAction,
|
||||||
loadDatasourceMissingAction,
|
loadDatasourceMissingAction,
|
||||||
loadDatasourceFailureAction,
|
|
||||||
loadDatasourcePendingAction,
|
loadDatasourcePendingAction,
|
||||||
queriesImportedAction,
|
queriesImportedAction,
|
||||||
LoadDatasourceSuccessPayload,
|
LoadDatasourceReadyPayload,
|
||||||
loadDatasourceSuccessAction,
|
loadDatasourceReadyAction,
|
||||||
modifyQueriesAction,
|
modifyQueriesAction,
|
||||||
queryTransactionFailureAction,
|
queryTransactionFailureAction,
|
||||||
queryTransactionStartAction,
|
queryTransactionStartAction,
|
||||||
@ -65,16 +74,19 @@ import {
|
|||||||
ToggleTablePayload,
|
ToggleTablePayload,
|
||||||
updateUIStateAction,
|
updateUIStateAction,
|
||||||
runQueriesAction,
|
runQueriesAction,
|
||||||
|
testDataSourcePendingAction,
|
||||||
|
testDataSourceSuccessAction,
|
||||||
|
testDataSourceFailureAction,
|
||||||
|
loadExploreDatasources,
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||||
import { ThunkResult } from 'app/types';
|
|
||||||
import { parseTime } from '../TimePicker';
|
import { parseTime } from '../TimePicker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates UI state and save it to the URL
|
* Updates UI state and save it to the URL
|
||||||
*/
|
*/
|
||||||
const updateExploreUIState = (exploreId, uiStateFragment: Partial<ExploreUIState>) => {
|
const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial<ExploreUIState>): ThunkResult<void> => {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch(updateUIStateAction({ exploreId, ...uiStateFragment }));
|
dispatch(updateUIStateAction({ exploreId, ...uiStateFragment }));
|
||||||
dispatch(stateSave());
|
dispatch(stateSave());
|
||||||
@ -97,7 +109,14 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
|
|||||||
*/
|
*/
|
||||||
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
|
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const newDataSourceInstance = await getDatasourceSrv().get(datasource);
|
let newDataSourceInstance: DataSourceApi = null;
|
||||||
|
|
||||||
|
if (!datasource) {
|
||||||
|
newDataSourceInstance = await getDatasourceSrv().get();
|
||||||
|
} else {
|
||||||
|
newDataSourceInstance = await getDatasourceSrv().get(datasource);
|
||||||
|
}
|
||||||
|
|
||||||
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
||||||
const queries = getState().explore[exploreId].queries;
|
const queries = getState().explore[exploreId].queries;
|
||||||
|
|
||||||
@ -105,12 +124,7 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
|
|||||||
|
|
||||||
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
|
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
|
||||||
|
|
||||||
try {
|
await dispatch(loadDatasource(exploreId, newDataSourceInstance));
|
||||||
await dispatch(loadDatasource(exploreId, newDataSourceInstance));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(runQueries(exploreId));
|
dispatch(runQueries(exploreId));
|
||||||
};
|
};
|
||||||
@ -171,6 +185,33 @@ export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all explore data sources and sets the chosen datasource.
|
||||||
|
* If there are no datasources a missing datasource action is dispatched.
|
||||||
|
*/
|
||||||
|
export function loadExploreDatasourcesAndSetDatasource(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
datasourceName: string
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||||
|
.getExternal()
|
||||||
|
.map((ds: any) => ({
|
||||||
|
value: ds.name,
|
||||||
|
name: ds.name,
|
||||||
|
meta: ds.meta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch(loadExploreDatasources({ exploreId, exploreDatasources }));
|
||||||
|
|
||||||
|
if (exploreDatasources.length >= 1) {
|
||||||
|
dispatch(changeDatasource(exploreId, datasourceName));
|
||||||
|
} else {
|
||||||
|
dispatch(loadDatasourceMissingAction({ exploreId }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Explore state with state from the URL and the React component.
|
* Initialize Explore state with state from the URL and the React component.
|
||||||
* Call this only on components for with the Explore state has not been initialized.
|
* Call this only on components for with the Explore state has not been initialized.
|
||||||
@ -185,83 +226,35 @@ export function initializeExplore(
|
|||||||
ui: ExploreUIState
|
ui: ExploreUIState
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
|
||||||
.getExternal()
|
|
||||||
.map(ds => ({
|
|
||||||
value: ds.name,
|
|
||||||
name: ds.name,
|
|
||||||
meta: ds.meta,
|
|
||||||
}));
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
initializeExploreAction({
|
initializeExploreAction({
|
||||||
exploreId,
|
exploreId,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
eventBridge,
|
eventBridge,
|
||||||
exploreDatasources,
|
|
||||||
queries,
|
queries,
|
||||||
range,
|
range,
|
||||||
ui,
|
ui,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (exploreDatasources.length >= 1) {
|
|
||||||
let instance;
|
|
||||||
|
|
||||||
if (datasourceName) {
|
|
||||||
try {
|
|
||||||
instance = await getDatasourceSrv().get(datasourceName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Checking on instance here because requested datasource could be deleted already
|
|
||||||
if (!instance) {
|
|
||||||
instance = await getDatasourceSrv().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dispatch(loadDatasource(exploreId, instance));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(runQueries(exploreId, true));
|
|
||||||
} else {
|
|
||||||
dispatch(loadDatasourceMissingAction({ exploreId }));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
|
* Datasource loading was successfully completed.
|
||||||
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
|
||||||
* e.g., Prometheus -> Loki queries.
|
|
||||||
*/
|
*/
|
||||||
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf<LoadDatasourceSuccessPayload> => {
|
export const loadDatasourceReady = (
|
||||||
// Capabilities
|
exploreId: ExploreId,
|
||||||
const supportsGraph = instance.meta.metrics;
|
instance: DataSourceApi
|
||||||
const supportsLogs = instance.meta.logs;
|
): ActionOf<LoadDatasourceReadyPayload> => {
|
||||||
const supportsTable = instance.meta.tables;
|
|
||||||
// Custom components
|
|
||||||
const StartPage = instance.pluginExports.ExploreStartPage;
|
|
||||||
|
|
||||||
const historyKey = `grafana.explore.history.${instance.meta.id}`;
|
const historyKey = `grafana.explore.history.${instance.meta.id}`;
|
||||||
const history = store.getObject(historyKey, []);
|
const history = store.getObject(historyKey, []);
|
||||||
// Save last-used datasource
|
// Save last-used datasource
|
||||||
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
|
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
|
||||||
|
|
||||||
return loadDatasourceSuccessAction({
|
return loadDatasourceReadyAction({
|
||||||
exploreId,
|
exploreId,
|
||||||
StartPage,
|
|
||||||
datasourceInstance: instance,
|
|
||||||
history,
|
history,
|
||||||
showingStartPage: Boolean(StartPage),
|
|
||||||
supportsGraph,
|
|
||||||
supportsLogs,
|
|
||||||
supportsTable,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -270,8 +263,14 @@ export function importQueries(
|
|||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
sourceDataSource: DataSourceApi,
|
sourceDataSource: DataSourceApi,
|
||||||
targetDataSource: DataSourceApi
|
targetDataSource: DataSourceApi
|
||||||
) {
|
): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
|
if (!sourceDataSource) {
|
||||||
|
// explore not initialized
|
||||||
|
dispatch(queriesImportedAction({ exploreId, queries }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let importedQueries = queries;
|
let importedQueries = queries;
|
||||||
// Check if queries can be imported from previously selected datasource
|
// Check if queries can be imported from previously selected datasource
|
||||||
if (sourceDataSource.meta.id === targetDataSource.meta.id) {
|
if (sourceDataSource.meta.id === targetDataSource.meta.id) {
|
||||||
@ -295,16 +294,14 @@ export function importQueries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
|
* Tests datasource.
|
||||||
*/
|
*/
|
||||||
export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> {
|
export const testDatasource = (exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> => {
|
||||||
return async (dispatch, getState) => {
|
return async dispatch => {
|
||||||
const datasourceName = instance.name;
|
|
||||||
|
|
||||||
// Keep ID to track selection
|
|
||||||
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
|
|
||||||
let datasourceError = null;
|
let datasourceError = null;
|
||||||
|
|
||||||
|
dispatch(testDataSourcePendingAction({ exploreId }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const testResult = await instance.testDatasource();
|
const testResult = await instance.testDatasource();
|
||||||
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||||
@ -313,10 +310,36 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (datasourceError) {
|
if (datasourceError) {
|
||||||
dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
|
dispatch(testDataSourceFailureAction({ exploreId, error: datasourceError }));
|
||||||
return Promise.reject(`${datasourceName} loading failed`);
|
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.
|
||||||
|
*/
|
||||||
|
export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const datasourceName = instance.name;
|
||||||
|
|
||||||
|
// Keep ID to track selection
|
||||||
|
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
|
||||||
|
|
||||||
|
await dispatch(testDatasource(exploreId, instance));
|
||||||
|
|
||||||
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
||||||
// User already changed datasource again, discard results
|
// User already changed datasource again, discard results
|
||||||
return;
|
return;
|
||||||
@ -331,8 +354,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(loadDatasourceSuccess(exploreId, instance));
|
dispatch(loadDatasourceReady(exploreId, instance));
|
||||||
return Promise.resolve();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,7 +524,7 @@ export function queryTransactionSuccess(
|
|||||||
/**
|
/**
|
||||||
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
||||||
*/
|
*/
|
||||||
export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const {
|
const {
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
@ -513,8 +535,14 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
|||||||
supportsGraph,
|
supportsGraph,
|
||||||
supportsLogs,
|
supportsLogs,
|
||||||
supportsTable,
|
supportsTable,
|
||||||
|
datasourceError,
|
||||||
} = getState().explore[exploreId];
|
} = getState().explore[exploreId];
|
||||||
|
|
||||||
|
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 saves to state and update location
|
dispatch(stateSave()); // Remember to saves to state and update location
|
||||||
@ -538,7 +566,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
|||||||
instant: true,
|
instant: true,
|
||||||
valueWithRefId: true,
|
valueWithRefId: true,
|
||||||
},
|
},
|
||||||
data => data[0]
|
(data: any) => data[0]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -576,7 +604,7 @@ function runQueriesForType(
|
|||||||
resultType: ResultType,
|
resultType: ResultType,
|
||||||
queryOptions: QueryOptions,
|
queryOptions: QueryOptions,
|
||||||
resultGetter?: any
|
resultGetter?: any
|
||||||
) {
|
): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
|
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
|
||||||
const datasourceId = datasourceInstance.meta.id;
|
const datasourceId = datasourceInstance.meta.id;
|
||||||
@ -659,9 +687,10 @@ export function splitOpen(): ThunkResult<void> {
|
|||||||
const leftState = getState().explore[ExploreId.left];
|
const leftState = getState().explore[ExploreId.left];
|
||||||
const queryState = getState().location.query[ExploreId.left] as string;
|
const queryState = getState().location.query[ExploreId.left] as string;
|
||||||
const urlState = parseUrlState(queryState);
|
const urlState = parseUrlState(queryState);
|
||||||
|
const queryTransactions: QueryTransaction[] = [];
|
||||||
const itemState = {
|
const itemState = {
|
||||||
...leftState,
|
...leftState,
|
||||||
queryTransactions: [],
|
queryTransactions,
|
||||||
queries: leftState.queries.slice(),
|
queries: leftState.queries.slice(),
|
||||||
exploreId: ExploreId.right,
|
exploreId: ExploreId.right,
|
||||||
urlState,
|
urlState,
|
||||||
@ -675,7 +704,7 @@ export function splitOpen(): ThunkResult<void> {
|
|||||||
* Saves Explore state to URL using the `left` and `right` parameters.
|
* Saves Explore state to URL using the `left` and `right` parameters.
|
||||||
* If split view is not active, `right` will not be set.
|
* If split view is not active, `right` will not be set.
|
||||||
*/
|
*/
|
||||||
export function stateSave() {
|
export function stateSave(): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { left, right, split } = getState().explore;
|
const { left, right, split } = getState().explore;
|
||||||
const urlStates: { [index: string]: string } = {};
|
const urlStates: { [index: string]: string } = {};
|
||||||
@ -720,7 +749,7 @@ const togglePanelActionCreator = (
|
|||||||
| ActionCreator<ToggleGraphPayload>
|
| ActionCreator<ToggleGraphPayload>
|
||||||
| ActionCreator<ToggleLogsPayload>
|
| ActionCreator<ToggleLogsPayload>
|
||||||
| ActionCreator<ToggleTablePayload>
|
| ActionCreator<ToggleTablePayload>
|
||||||
) => (exploreId: ExploreId, isPanelVisible: boolean) => {
|
) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
let uiFragmentStateUpdate: Partial<ExploreUIState>;
|
let uiFragmentStateUpdate: Partial<ExploreUIState>;
|
||||||
const shouldRunQueries = !isPanelVisible;
|
const shouldRunQueries = !isPanelVisible;
|
||||||
@ -764,7 +793,7 @@ export const toggleTable = togglePanelActionCreator(toggleTableAction);
|
|||||||
/**
|
/**
|
||||||
* Change logs deduplication strategy and update URL.
|
* Change logs deduplication strategy and update URL.
|
||||||
*/
|
*/
|
||||||
export const changeDedupStrategy = (exploreId, dedupStrategy: LogsDedupStrategy) => {
|
export const changeDedupStrategy = (exploreId: ExploreId, dedupStrategy: LogsDedupStrategy): ThunkResult<void> => {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
|
dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
|
||||||
};
|
};
|
||||||
|
@ -5,14 +5,32 @@ import {
|
|||||||
makeInitialUpdateState,
|
makeInitialUpdateState,
|
||||||
initialExploreState,
|
initialExploreState,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { ExploreId, ExploreItemState, ExploreUrlState, ExploreState } from 'app/types/explore';
|
import {
|
||||||
|
ExploreId,
|
||||||
|
ExploreItemState,
|
||||||
|
ExploreUrlState,
|
||||||
|
ExploreState,
|
||||||
|
QueryTransaction,
|
||||||
|
RangeScanner,
|
||||||
|
} from 'app/types/explore';
|
||||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
import { scanStartAction, scanStopAction, splitOpenAction, splitCloseAction } from './actionTypes';
|
import {
|
||||||
|
scanStartAction,
|
||||||
|
scanStopAction,
|
||||||
|
testDataSourcePendingAction,
|
||||||
|
testDataSourceSuccessAction,
|
||||||
|
testDataSourceFailureAction,
|
||||||
|
updateDatasourceInstanceAction,
|
||||||
|
splitOpenAction,
|
||||||
|
splitCloseAction,
|
||||||
|
} from './actionTypes';
|
||||||
import { Reducer } from 'redux';
|
import { Reducer } from 'redux';
|
||||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { updateLocation } from 'app/core/actions/location';
|
import { updateLocation } from 'app/core/actions/location';
|
||||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
import { LogsDedupStrategy, LogsModel } from 'app/core/logs_model';
|
||||||
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||||
|
import TableModel from 'app/core/table_model';
|
||||||
|
import { DataSourceApi, DataQuery } from '@grafana/ui';
|
||||||
|
|
||||||
describe('Explore item reducer', () => {
|
describe('Explore item reducer', () => {
|
||||||
describe('scanning', () => {
|
describe('scanning', () => {
|
||||||
@ -21,7 +39,7 @@ describe('Explore item reducer', () => {
|
|||||||
const initalState = {
|
const initalState = {
|
||||||
...makeExploreItemState(),
|
...makeExploreItemState(),
|
||||||
scanning: false,
|
scanning: false,
|
||||||
scanner: undefined,
|
scanner: undefined as RangeScanner,
|
||||||
};
|
};
|
||||||
|
|
||||||
reducerTester()
|
reducerTester()
|
||||||
@ -53,6 +71,106 @@ 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 queryTransactions: QueryTransaction[] = [];
|
||||||
|
const initalState: Partial<ExploreItemState> = {
|
||||||
|
datasourceError: null,
|
||||||
|
queryTransactions: [{} as QueryTransaction],
|
||||||
|
graphResult: [],
|
||||||
|
tableResult: {} as TableModel,
|
||||||
|
logsResult: {} as LogsModel,
|
||||||
|
update: {
|
||||||
|
datasource: true,
|
||||||
|
queries: true,
|
||||||
|
range: true,
|
||||||
|
ui: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedState = {
|
||||||
|
datasourceError: error,
|
||||||
|
queryTransactions,
|
||||||
|
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 updateDatasourceInstanceAction is dispatched', () => {
|
||||||
|
describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
|
||||||
|
it('then it should set correct state', () => {
|
||||||
|
const StartPage = {};
|
||||||
|
const datasourceInstance = {
|
||||||
|
meta: {
|
||||||
|
metrics: {},
|
||||||
|
logs: {},
|
||||||
|
tables: {},
|
||||||
|
},
|
||||||
|
pluginExports: {
|
||||||
|
ExploreStartPage: StartPage,
|
||||||
|
},
|
||||||
|
} as DataSourceApi;
|
||||||
|
const queries: DataQuery[] = [];
|
||||||
|
const queryKeys: string[] = [];
|
||||||
|
const initalState: Partial<ExploreItemState> = {
|
||||||
|
datasourceInstance: null,
|
||||||
|
supportsGraph: false,
|
||||||
|
supportsLogs: false,
|
||||||
|
supportsTable: false,
|
||||||
|
StartPage: null,
|
||||||
|
showingStartPage: false,
|
||||||
|
queries,
|
||||||
|
queryKeys,
|
||||||
|
};
|
||||||
|
const expectedState = {
|
||||||
|
datasourceInstance,
|
||||||
|
supportsGraph: true,
|
||||||
|
supportsLogs: true,
|
||||||
|
supportsTable: true,
|
||||||
|
StartPage,
|
||||||
|
showingStartPage: true,
|
||||||
|
queries,
|
||||||
|
queryKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(itemReducer, initalState)
|
||||||
|
.whenActionIsDispatched(updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance }))
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setup = (urlStateOverrides?: any) => {
|
export const setup = (urlStateOverrides?: any) => {
|
||||||
@ -201,7 +319,8 @@ describe('Explore reducer', () => {
|
|||||||
describe('but urlState is not set in state', () => {
|
describe('but urlState is not set in state', () => {
|
||||||
it('then it should just add urlState and update in state', () => {
|
it('then it should just add urlState and update in state', () => {
|
||||||
const { initalState, serializedUrlState } = setup();
|
const { initalState, serializedUrlState } = setup();
|
||||||
const stateWithoutUrlState = { ...initalState, left: { urlState: null } };
|
const urlState: ExploreUrlState = null;
|
||||||
|
const stateWithoutUrlState = { ...initalState, left: { urlState } };
|
||||||
const expectedState = { ...initalState };
|
const expectedState = { ...initalState };
|
||||||
|
|
||||||
reducerTester()
|
reducerTester()
|
||||||
|
@ -11,8 +11,16 @@ import {
|
|||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore';
|
import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore';
|
||||||
import { DataQuery } from '@grafana/ui/src/types';
|
import { DataQuery } from '@grafana/ui/src/types';
|
||||||
|
import {
|
||||||
import { HigherOrderAction, ActionTypes, SplitCloseActionPayload, splitCloseAction } from './actionTypes';
|
HigherOrderAction,
|
||||||
|
ActionTypes,
|
||||||
|
testDataSourcePendingAction,
|
||||||
|
testDataSourceSuccessAction,
|
||||||
|
testDataSourceFailureAction,
|
||||||
|
splitCloseAction,
|
||||||
|
SplitCloseActionPayload,
|
||||||
|
loadExploreDatasources,
|
||||||
|
} from './actionTypes';
|
||||||
import { reducerFactory } from 'app/core/redux';
|
import { reducerFactory } from 'app/core/redux';
|
||||||
import {
|
import {
|
||||||
addQueryRowAction,
|
addQueryRowAction,
|
||||||
@ -23,10 +31,9 @@ import {
|
|||||||
highlightLogsExpressionAction,
|
highlightLogsExpressionAction,
|
||||||
initializeExploreAction,
|
initializeExploreAction,
|
||||||
updateDatasourceInstanceAction,
|
updateDatasourceInstanceAction,
|
||||||
loadDatasourceFailureAction,
|
|
||||||
loadDatasourceMissingAction,
|
loadDatasourceMissingAction,
|
||||||
loadDatasourcePendingAction,
|
loadDatasourcePendingAction,
|
||||||
loadDatasourceSuccessAction,
|
loadDatasourceReadyAction,
|
||||||
modifyQueriesAction,
|
modifyQueriesAction,
|
||||||
queryTransactionFailureAction,
|
queryTransactionFailureAction,
|
||||||
queryTransactionStartAction,
|
queryTransactionStartAction,
|
||||||
@ -197,12 +204,11 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
.addMapper({
|
.addMapper({
|
||||||
filter: initializeExploreAction,
|
filter: initializeExploreAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
|
const { containerWidth, eventBridge, queries, range, ui } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
eventBridge,
|
eventBridge,
|
||||||
exploreDatasources,
|
|
||||||
range,
|
range,
|
||||||
queries,
|
queries,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
@ -216,17 +222,22 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
filter: updateDatasourceInstanceAction,
|
filter: updateDatasourceInstanceAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { datasourceInstance } = action.payload;
|
const { datasourceInstance } = action.payload;
|
||||||
return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
|
// Capabilities
|
||||||
},
|
const supportsGraph = datasourceInstance.meta.metrics;
|
||||||
})
|
const supportsLogs = datasourceInstance.meta.logs;
|
||||||
.addMapper({
|
const supportsTable = datasourceInstance.meta.tables;
|
||||||
filter: loadDatasourceFailureAction,
|
// Custom components
|
||||||
mapper: (state, action): ExploreItemState => {
|
const StartPage = datasourceInstance.pluginExports.ExploreStartPage;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
datasourceError: action.payload.error,
|
datasourceInstance,
|
||||||
datasourceLoading: false,
|
supportsGraph,
|
||||||
update: makeInitialUpdateState(),
|
supportsLogs,
|
||||||
|
supportsTable,
|
||||||
|
StartPage,
|
||||||
|
showingStartPage: Boolean(StartPage),
|
||||||
|
queryKeys: getQueryKeys(state.queries, datasourceInstance),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -244,37 +255,26 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
.addMapper({
|
.addMapper({
|
||||||
filter: loadDatasourcePendingAction,
|
filter: loadDatasourcePendingAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
|
return {
|
||||||
|
...state,
|
||||||
|
datasourceLoading: true,
|
||||||
|
requestedDatasourceName: action.payload.requestedDatasourceName,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: loadDatasourceSuccessAction,
|
filter: loadDatasourceReadyAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { containerWidth, range } = state;
|
const { containerWidth, range, datasourceInstance } = state;
|
||||||
const {
|
const { history } = action.payload;
|
||||||
StartPage,
|
|
||||||
datasourceInstance,
|
|
||||||
history,
|
|
||||||
showingStartPage,
|
|
||||||
supportsGraph,
|
|
||||||
supportsLogs,
|
|
||||||
supportsTable,
|
|
||||||
} = action.payload;
|
|
||||||
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
|
const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
queryIntervals,
|
queryIntervals,
|
||||||
StartPage,
|
|
||||||
datasourceInstance,
|
|
||||||
history,
|
history,
|
||||||
showingStartPage,
|
|
||||||
supportsGraph,
|
|
||||||
supportsLogs,
|
|
||||||
supportsTable,
|
|
||||||
datasourceLoading: false,
|
datasourceLoading: false,
|
||||||
datasourceMissing: false,
|
datasourceMissing: false,
|
||||||
datasourceError: null,
|
|
||||||
logsHighlighterExpressions: undefined,
|
logsHighlighterExpressions: undefined,
|
||||||
queryTransactions: [],
|
queryTransactions: [],
|
||||||
update: makeInitialUpdateState(),
|
update: makeInitialUpdateState(),
|
||||||
@ -517,6 +517,47 @@ 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,
|
||||||
|
queryTransactions: [],
|
||||||
|
graphResult: undefined,
|
||||||
|
tableResult: undefined,
|
||||||
|
logsResult: undefined,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: loadExploreDatasources,
|
||||||
|
mapper: (state, action): ExploreItemState => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
exploreDatasources: action.payload.exploreDatasources,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
export const updateChildRefreshState = (
|
export const updateChildRefreshState = (
|
||||||
|
@ -2,12 +2,20 @@ import React, { FunctionComponent } from 'react';
|
|||||||
import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldForm';
|
import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldForm';
|
||||||
import { useLokiSyntax } from './useLokiSyntax';
|
import { useLokiSyntax } from './useLokiSyntax';
|
||||||
|
|
||||||
const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({ datasource, ...otherProps }) => {
|
const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
|
||||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(datasource.languageProvider);
|
datasource,
|
||||||
|
datasourceStatus,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||||
|
datasource.languageProvider,
|
||||||
|
datasourceStatus
|
||||||
|
);
|
||||||
|
|
||||||
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
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
import Cascader from 'rc-cascader';
|
import Cascader from 'rc-cascader';
|
||||||
|
// @ts-ignore
|
||||||
import PluginPrism from 'slate-prism';
|
import PluginPrism from 'slate-prism';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@ -15,10 +17,13 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
|||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||||
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
|
import { ExploreDataSourceApi, ExploreQueryFieldProps, DatasourceStatus } from '@grafana/ui';
|
||||||
|
|
||||||
function getChooserText(hasSytax, hasLogLabels) {
|
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DatasourceStatus) {
|
||||||
if (!hasSytax) {
|
if (datasourceStatus === DatasourceStatus.Disconnected) {
|
||||||
|
return '(Disconnected)';
|
||||||
|
}
|
||||||
|
if (!hasSyntax) {
|
||||||
return 'Loading labels...';
|
return 'Loading labels...';
|
||||||
}
|
}
|
||||||
if (!hasLogLabels) {
|
if (!hasLogLabels) {
|
||||||
@ -76,15 +81,15 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
modifiedSearch: string;
|
modifiedSearch: string;
|
||||||
modifiedQuery: string;
|
modifiedQuery: string;
|
||||||
|
|
||||||
constructor(props: LokiQueryFieldFormProps, context) {
|
constructor(props: LokiQueryFieldFormProps, context: React.Context<any>) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.plugins = [
|
this.plugins = [
|
||||||
BracesPlugin(),
|
BracesPlugin(),
|
||||||
RunnerPlugin({ handler: props.onExecuteQuery }),
|
RunnerPlugin({ handler: props.onExecuteQuery }),
|
||||||
PluginPrism({
|
PluginPrism({
|
||||||
onlyIn: node => node.type === 'code_block',
|
onlyIn: (node: any) => node.type === 'code_block',
|
||||||
getSyntax: node => 'promql',
|
getSyntax: (node: any) => 'promql',
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -159,10 +164,12 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
onLoadOptions,
|
onLoadOptions,
|
||||||
onLabelsRefresh,
|
onLabelsRefresh,
|
||||||
datasource,
|
datasource,
|
||||||
|
datasourceStatus,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
|
const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
|
||||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
||||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
|
const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
|
||||||
|
const buttonDisabled = !syntaxLoaded || datasourceStatus === DatasourceStatus.Disconnected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -172,13 +179,13 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
options={logLabelOptions}
|
options={logLabelOptions}
|
||||||
onChange={this.onChangeLogLabels}
|
onChange={this.onChangeLogLabels}
|
||||||
loadData={onLoadOptions}
|
loadData={onLoadOptions}
|
||||||
onPopupVisibleChange={isVisible => {
|
onPopupVisibleChange={(isVisible: boolean) => {
|
||||||
if (isVisible && onLabelsRefresh) {
|
if (isVisible && onLabelsRefresh) {
|
||||||
onLabelsRefresh();
|
onLabelsRefresh();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
|
<button className="gf-form-label gf-form-label--btn" disabled={buttonDisabled}>
|
||||||
{chooserText} <i className="fa fa-caret-down" />
|
{chooserText} <i className="fa fa-caret-down" />
|
||||||
</button>
|
</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
|
@ -1,24 +1,56 @@
|
|||||||
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/plugin';
|
||||||
|
|
||||||
describe('useLokiLabels hook', () => {
|
describe('useLokiLabels hook', () => {
|
||||||
const datasource = {
|
|
||||||
metadataRequest: () => ({ data: { data: [] } }),
|
|
||||||
};
|
|
||||||
const languageProvider = new LanguageProvider(datasource);
|
|
||||||
const logLabelOptionsMock = ['Holy mock!'];
|
|
||||||
|
|
||||||
languageProvider.refreshLogLabels = () => {
|
|
||||||
languageProvider.logLabelOptions = logLabelOptionsMock;
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should refresh labels', async () => {
|
it('should refresh labels', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useLokiLabels(languageProvider, true, []));
|
const datasource = {
|
||||||
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
|
};
|
||||||
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
|
const logLabelOptionsMock = ['Holy mock!'];
|
||||||
|
|
||||||
|
languageProvider.refreshLogLabels = () => {
|
||||||
|
languageProvider.logLabelOptions = logLabelOptionsMock;
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useLokiLabels(languageProvider, true, [], 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 = {
|
||||||
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
|
};
|
||||||
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
|
languageProvider.refreshLogLabels = jest.fn();
|
||||||
|
|
||||||
|
renderHook(() =>
|
||||||
|
useLokiLabels(languageProvider, true, [], DatasourceStatus.Connected, DatasourceStatus.Disconnected)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(languageProvider.refreshLogLabels).toBeCalledTimes(1);
|
||||||
|
expect(languageProvider.refreshLogLabels).toBeCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not force refresh labels after a connect', () => {
|
||||||
|
const datasource = {
|
||||||
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
|
};
|
||||||
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
|
languageProvider.refreshLogLabels = jest.fn();
|
||||||
|
|
||||||
|
renderHook(() =>
|
||||||
|
useLokiLabels(languageProvider, true, [], DatasourceStatus.Disconnected, DatasourceStatus.Connected)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(languageProvider.refreshLogLabels).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
|
||||||
|
|
||||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||||
import { useRefMounted } from 'app/core/hooks/useRefMounted';
|
import { useRefMounted } from 'app/core/hooks/useRefMounted';
|
||||||
@ -14,16 +16,22 @@ import { useRefMounted } from 'app/core/hooks/useRefMounted';
|
|||||||
export const useLokiLabels = (
|
export const useLokiLabels = (
|
||||||
languageProvider: LokiLanguageProvider,
|
languageProvider: LokiLanguageProvider,
|
||||||
languageProviderInitialised: boolean,
|
languageProviderInitialised: boolean,
|
||||||
activeOption: CascaderOption[]
|
activeOption: CascaderOption[],
|
||||||
|
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);
|
||||||
|
|
||||||
// Async
|
// Async
|
||||||
const fetchOptionValues = async option => {
|
const fetchOptionValues = async (option: string) => {
|
||||||
await languageProvider.fetchLabelValues(option);
|
await languageProvider.fetchLabelValues(option);
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
setLogLabelOptions(languageProvider.logLabelOptions);
|
setLogLabelOptions(languageProvider.logLabelOptions);
|
||||||
@ -31,9 +39,11 @@ export const useLokiLabels = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tryLabelsRefresh = async () => {
|
const tryLabelsRefresh = async () => {
|
||||||
await languageProvider.refreshLogLabels();
|
await languageProvider.refreshLogLabels(shouldForceRefreshLabels);
|
||||||
|
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
setRefreshLabels(false);
|
setRefreshLabels(false);
|
||||||
|
setForceRefreshLabels(false);
|
||||||
setLogLabelOptions(languageProvider.logLabelOptions);
|
setLogLabelOptions(languageProvider.logLabelOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -62,14 +72,23 @@ export const useLokiLabels = (
|
|||||||
}
|
}
|
||||||
}, [activeOption]);
|
}, [activeOption]);
|
||||||
|
|
||||||
// This effect is performed on shouldTryRefreshLabels state change only.
|
// This effect is performed on shouldTryRefreshLabels or shouldForceRefreshLabels state change only.
|
||||||
// Since shouldTryRefreshLabels is reset AFTER the labels are refreshed we are secured in case of trying to refresh
|
// Since shouldTryRefreshLabels is reset AFTER the labels are refreshed we are secured in case of trying to refresh
|
||||||
// when previous refresh hasn't finished yet
|
// when previous refresh hasn't finished yet
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldTryRefreshLabels) {
|
if (shouldTryRefreshLabels || shouldForceRefreshLabels) {
|
||||||
tryLabelsRefresh();
|
tryLabelsRefresh();
|
||||||
}
|
}
|
||||||
}, [shouldTryRefreshLabels]);
|
}, [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,
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { renderHook, act } from 'react-hooks-testing-library';
|
import { renderHook, act } from 'react-hooks-testing-library';
|
||||||
|
import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
|
||||||
|
|
||||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { useLokiSyntax } from './useLokiSyntax';
|
import { useLokiSyntax } from './useLokiSyntax';
|
||||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||||
|
|
||||||
describe('useLokiSyntax hook', () => {
|
describe('useLokiSyntax hook', () => {
|
||||||
const datasource = {
|
const datasource = {
|
||||||
metadataRequest: () => ({ data: { data: [] } }),
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
};
|
};
|
||||||
const languageProvider = new LanguageProvider(datasource);
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
const logLabelOptionsMock = ['Holy mock!'];
|
const logLabelOptionsMock = ['Holy mock!'];
|
||||||
@ -28,7 +30,7 @@ describe('useLokiSyntax hook', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should provide Loki syntax when used', async () => {
|
it('should provide Loki syntax when used', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider));
|
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DatasourceStatus.Connected));
|
||||||
expect(result.current.syntax).toEqual(null);
|
expect(result.current.syntax).toEqual(null);
|
||||||
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
@ -37,7 +39,7 @@ describe('useLokiSyntax hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch labels on first call', async () => {
|
it('should fetch labels on first call', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider));
|
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DatasourceStatus.Connected));
|
||||||
expect(result.current.isSyntaxReady).toBeFalsy();
|
expect(result.current.isSyntaxReady).toBeFalsy();
|
||||||
expect(result.current.logLabelOptions).toEqual([]);
|
expect(result.current.logLabelOptions).toEqual([]);
|
||||||
|
|
||||||
@ -48,7 +50,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(() => useLokiSyntax(languageProvider));
|
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DatasourceStatus.Connected));
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
// @ts-ignore
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
|
import { DatasourceStatus } from '@grafana/ui/src/types/plugin';
|
||||||
|
|
||||||
|
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';
|
||||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||||
import { useRefMounted } from 'app/core/hooks/useRefMounted';
|
import { useRefMounted } from 'app/core/hooks/useRefMounted';
|
||||||
@ -12,7 +15,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 = (languageProvider: LokiLanguageProvider) => {
|
export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasourceStatus: DatasourceStatus) => {
|
||||||
const mounted = useRefMounted();
|
const mounted = useRefMounted();
|
||||||
// State
|
// State
|
||||||
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
||||||
@ -28,7 +31,8 @@ export const useLokiSyntax = (languageProvider: LokiLanguageProvider) => {
|
|||||||
const { logLabelOptions, setLogLabelOptions, refreshLabels } = useLokiLabels(
|
const { logLabelOptions, setLogLabelOptions, refreshLabels } = useLokiLabels(
|
||||||
languageProvider,
|
languageProvider,
|
||||||
languageProviderInitialized,
|
languageProviderInitialized,
|
||||||
activeOption
|
activeOption,
|
||||||
|
datasourceStatus
|
||||||
);
|
);
|
||||||
|
|
||||||
// Async
|
// Async
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
@ -60,13 +61,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
Object.assign(this, initialValues);
|
Object.assign(this, initialValues);
|
||||||
}
|
}
|
||||||
// Strip syntax chars
|
// Strip syntax chars
|
||||||
cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||||
|
|
||||||
getSyntax() {
|
getSyntax() {
|
||||||
return syntax;
|
return syntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
request = url => {
|
request = (url: string) => {
|
||||||
return this.datasource.metadataRequest(url);
|
return this.datasource.metadataRequest(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,12 +101,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
|
|
||||||
if (history && history.length > 0) {
|
if (history && history.length > 0) {
|
||||||
const historyItems = _.chain(history)
|
const historyItems = _.chain(history)
|
||||||
.map(h => h.query.expr)
|
.map((h: any) => h.query.expr)
|
||||||
.filter()
|
.filter()
|
||||||
.uniq()
|
.uniq()
|
||||||
.take(HISTORY_ITEM_COUNT)
|
.take(HISTORY_ITEM_COUNT)
|
||||||
.map(wrapLabel)
|
.map(wrapLabel)
|
||||||
.map(item => addHistoryMetadata(item, history))
|
.map((item: CompletionItem) => addHistoryMetadata(item, history))
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
suggestions.push({
|
suggestions.push({
|
||||||
@ -191,7 +192,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
const selectorMatch = query.match(selectorRegexp);
|
const selectorMatch = query.match(selectorRegexp);
|
||||||
if (selectorMatch) {
|
if (selectorMatch) {
|
||||||
const selector = selectorMatch[0];
|
const selector = selectorMatch[0];
|
||||||
const labels = {};
|
const labels: { [key: string]: { value: any; operator: any } } = {};
|
||||||
selector.replace(labelRegexp, (_, key, operator, value) => {
|
selector.replace(labelRegexp, (_, key, operator, value) => {
|
||||||
labels[key] = { value, operator };
|
labels[key] = { value, operator };
|
||||||
return '';
|
return '';
|
||||||
@ -200,7 +201,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
// Keep only labels that exist on origin and target datasource
|
// Keep only labels that exist on origin and target datasource
|
||||||
await this.start(); // fetches all existing label keys
|
await this.start(); // fetches all existing label keys
|
||||||
const existingKeys = this.labelKeys[EMPTY_SELECTOR];
|
const existingKeys = this.labelKeys[EMPTY_SELECTOR];
|
||||||
let labelsToKeep = {};
|
let labelsToKeep: { [key: string]: { value: any; operator: any } } = {};
|
||||||
if (existingKeys && existingKeys.length > 0) {
|
if (existingKeys && existingKeys.length > 0) {
|
||||||
// Check for common labels
|
// Check for common labels
|
||||||
for (const key in labels) {
|
for (const key in labels) {
|
||||||
@ -225,7 +226,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchLogLabels() {
|
async fetchLogLabels(): Promise<any> {
|
||||||
const url = '/api/prom/label';
|
const url = '/api/prom/label';
|
||||||
try {
|
try {
|
||||||
this.logLabelFetchTs = Date.now();
|
this.logLabelFetchTs = Date.now();
|
||||||
@ -236,11 +237,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
...this.labelKeys,
|
...this.labelKeys,
|
||||||
[EMPTY_SELECTOR]: labelKeys,
|
[EMPTY_SELECTOR]: labelKeys,
|
||||||
};
|
};
|
||||||
this.logLabelOptions = labelKeys.map(key => ({ label: key, value: key, isLeaf: false }));
|
this.logLabelOptions = labelKeys.map((key: string) => ({ label: key, value: key, isLeaf: false }));
|
||||||
|
|
||||||
// Pre-load values for default labels
|
// Pre-load values for default labels
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
labelKeys.filter(key => DEFAULT_KEYS.indexOf(key) > -1).map(key => this.fetchLabelValues(key))
|
labelKeys
|
||||||
|
.filter((key: string) => DEFAULT_KEYS.indexOf(key) > -1)
|
||||||
|
.map((key: string) => this.fetchLabelValues(key))
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -248,8 +251,8 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshLogLabels() {
|
async refreshLogLabels(forceRefresh?: boolean) {
|
||||||
if (this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) {
|
if ((this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) {
|
||||||
await this.fetchLogLabels();
|
await this.fetchLogLabels();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,7 +269,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
if (keyOption.value === key) {
|
if (keyOption.value === key) {
|
||||||
return {
|
return {
|
||||||
...keyOption,
|
...keyOption,
|
||||||
children: values.map(value => ({ label: value, value })),
|
children: values.map((value: string) => ({ label: value, value })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return keyOption;
|
return keyOption;
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
import Cascader from 'rc-cascader';
|
import Cascader from 'rc-cascader';
|
||||||
|
// @ts-ignore
|
||||||
import PluginPrism from 'slate-prism';
|
import PluginPrism from 'slate-prism';
|
||||||
|
// @ts-ignore
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
|
|
||||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||||
@ -13,13 +17,23 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
|||||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||||
import { PromQuery } from '../types';
|
import { PromQuery } from '../types';
|
||||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
||||||
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
|
import { ExploreDataSourceApi, ExploreQueryFieldProps, DatasourceStatus } from '@grafana/ui';
|
||||||
|
|
||||||
const HISTOGRAM_GROUP = '__histograms__';
|
const HISTOGRAM_GROUP = '__histograms__';
|
||||||
const METRIC_MARK = 'metric';
|
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) {
|
||||||
|
if (datasourceStatus === DatasourceStatus.Disconnected) {
|
||||||
|
return '(Disconnected)';
|
||||||
|
}
|
||||||
|
if (!hasSyntax) {
|
||||||
|
return 'Loading metrics...';
|
||||||
|
}
|
||||||
|
return 'Metrics';
|
||||||
|
}
|
||||||
|
|
||||||
export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
|
export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
|
||||||
// Filter out recording rules and insert as first option
|
// Filter out recording rules and insert as first option
|
||||||
const ruleRegex = /:\w+:/;
|
const ruleRegex = /:\w+:/;
|
||||||
@ -36,8 +50,8 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
|
|||||||
const options = ruleNames.length > 0 ? [rulesOption] : [];
|
const options = ruleNames.length > 0 ? [rulesOption] : [];
|
||||||
|
|
||||||
const metricsOptions = _.chain(metrics)
|
const metricsOptions = _.chain(metrics)
|
||||||
.filter(metric => !ruleRegex.test(metric))
|
.filter((metric: string) => !ruleRegex.test(metric))
|
||||||
.groupBy(metric => metric.split(delimiter)[0])
|
.groupBy((metric: string) => metric.split(delimiter)[0])
|
||||||
.map(
|
.map(
|
||||||
(metricsForPrefix: string[], prefix: string): CascaderOption => {
|
(metricsForPrefix: string[], prefix: string): CascaderOption => {
|
||||||
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
||||||
@ -103,7 +117,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
languageProvider: any;
|
languageProvider: any;
|
||||||
languageProviderInitializationPromise: CancelablePromise<any>;
|
languageProviderInitializationPromise: CancelablePromise<any>;
|
||||||
|
|
||||||
constructor(props: PromQueryFieldProps, context) {
|
constructor(props: PromQueryFieldProps, context: React.Context<any>) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
if (props.datasource.languageProvider) {
|
if (props.datasource.languageProvider) {
|
||||||
@ -114,8 +128,8 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
BracesPlugin(),
|
BracesPlugin(),
|
||||||
RunnerPlugin({ handler: props.onExecuteQuery }),
|
RunnerPlugin({ handler: props.onExecuteQuery }),
|
||||||
PluginPrism({
|
PluginPrism({
|
||||||
onlyIn: node => node.type === 'code_block',
|
onlyIn: (node: any) => node.type === 'code_block',
|
||||||
getSyntax: node => 'promql',
|
getSyntax: (node: any) => 'promql',
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -127,17 +141,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.languageProvider) {
|
if (this.languageProvider) {
|
||||||
this.languageProviderInitializationPromise = makePromiseCancelable(this.languageProvider.start());
|
this.refreshMetrics(makePromiseCancelable(this.languageProvider.start()));
|
||||||
this.languageProviderInitializationPromise.promise
|
|
||||||
.then(remaining => {
|
|
||||||
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
|
|
||||||
})
|
|
||||||
.then(() => this.onUpdateLanguage())
|
|
||||||
.catch(({ isCanceled }) => {
|
|
||||||
if (isCanceled) {
|
|
||||||
console.warn('PromQueryField has unmounted, language provider intialization was canceled');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +151,37 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: PromQueryFieldProps) {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMetrics = (cancelablePromise: CancelablePromise<any>) => {
|
||||||
|
this.languageProviderInitializationPromise = cancelablePromise;
|
||||||
|
this.languageProviderInitializationPromise.promise
|
||||||
|
.then(remaining => {
|
||||||
|
remaining.map((task: Promise<any>) => task.then(this.onUpdateLanguage).catch(() => {}));
|
||||||
|
})
|
||||||
|
.then(() => this.onUpdateLanguage())
|
||||||
|
.catch(({ isCanceled }) => {
|
||||||
|
if (isCanceled) {
|
||||||
|
console.warn('PromQueryField has unmounted, language provider intialization was canceled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||||
let query;
|
let query;
|
||||||
if (selectedOptions.length === 1) {
|
if (selectedOptions.length === 1) {
|
||||||
@ -202,7 +237,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
|
|
||||||
// Build metrics tree
|
// Build metrics tree
|
||||||
const metricsByPrefix = groupMetricsByPrefix(metrics);
|
const metricsByPrefix = groupMetricsByPrefix(metrics);
|
||||||
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm }));
|
||||||
const metricsOptions =
|
const metricsOptions =
|
||||||
histogramMetrics.length > 0
|
histogramMetrics.length > 0
|
||||||
? [
|
? [
|
||||||
@ -239,17 +274,18 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { error, hint, query } = this.props;
|
const { error, hint, query, datasourceStatus } = this.props;
|
||||||
const { metricsOptions, syntaxLoaded } = this.state;
|
const { metricsOptions, syntaxLoaded } = this.state;
|
||||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||||
const chooserText = syntaxLoaded ? 'Metrics' : 'Loading metrics...';
|
const chooserText = getChooserText(syntaxLoaded, datasourceStatus);
|
||||||
|
const buttonDisabled = !syntaxLoaded || datasourceStatus === DatasourceStatus.Disconnected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline gf-form-inline--nowrap">
|
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||||
<div className="gf-form flex-shrink-0">
|
<div className="gf-form flex-shrink-0">
|
||||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||||
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
|
<button className="gf-form-label gf-form-label--btn" disabled={buttonDisabled}>
|
||||||
{chooserText} <i className="fa fa-caret-down" />
|
{chooserText} <i className="fa fa-caret-down" />
|
||||||
</button>
|
</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
@ -60,23 +61,27 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
Object.assign(this, initialValues);
|
Object.assign(this, initialValues);
|
||||||
}
|
}
|
||||||
// Strip syntax chars
|
// Strip syntax chars
|
||||||
cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||||
|
|
||||||
getSyntax() {
|
getSyntax() {
|
||||||
return PromqlSyntax;
|
return PromqlSyntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
request = url => {
|
request = (url: string) => {
|
||||||
return this.datasource.metadataRequest(url);
|
return this.datasource.metadataRequest(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
start = () => {
|
start = () => {
|
||||||
if (!this.startTask) {
|
if (!this.startTask) {
|
||||||
this.startTask = this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
|
this.startTask = this.fetchMetrics();
|
||||||
}
|
}
|
||||||
return this.startTask;
|
return this.startTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchMetrics = async () => {
|
||||||
|
return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
|
||||||
|
};
|
||||||
|
|
||||||
// Keep this DOM-free for testing
|
// Keep this DOM-free for testing
|
||||||
provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
|
provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
|
||||||
// Local text properties
|
// Local text properties
|
||||||
@ -125,12 +130,12 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
|
|
||||||
if (history && history.length > 0) {
|
if (history && history.length > 0) {
|
||||||
const historyItems = _.chain(history)
|
const historyItems = _.chain(history)
|
||||||
.map(h => h.query.expr)
|
.map((h: any) => h.query.expr)
|
||||||
.filter()
|
.filter()
|
||||||
.uniq()
|
.uniq()
|
||||||
.take(HISTORY_ITEM_COUNT)
|
.take(HISTORY_ITEM_COUNT)
|
||||||
.map(wrapLabel)
|
.map(wrapLabel)
|
||||||
.map(item => addHistoryMetadata(item, history))
|
.map((item: CompletionItem) => addHistoryMetadata(item, history))
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
suggestions.push({
|
suggestions.push({
|
||||||
@ -184,7 +189,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
|
|
||||||
// Stitch all query lines together to support multi-line queries
|
// Stitch all query lines together to support multi-line queries
|
||||||
let queryOffset;
|
let queryOffset;
|
||||||
const queryText = value.document.getBlocks().reduce((text, block) => {
|
const queryText = value.document.getBlocks().reduce((text: string, block: any) => {
|
||||||
const blockText = block.getText();
|
const blockText = block.getText();
|
||||||
if (value.anchorBlock.key === block.key) {
|
if (value.anchorBlock.key === block.key) {
|
||||||
// Newline characters are not accounted for but this is irrelevant
|
// Newline characters are not accounted for but this is irrelevant
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
@include button-outline-variant($btn-inverse-bg);
|
@include button-outline-variant($btn-inverse-bg);
|
||||||
}
|
}
|
||||||
.btn-outline-danger {
|
.btn-outline-danger {
|
||||||
@include button-outline-variant(green);
|
@include button-outline-variant($btn-danger-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-disabled {
|
.btn-outline-disabled {
|
||||||
|
@ -27,35 +27,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin button-outline-variant($color) {
|
@mixin button-outline-variant($color) {
|
||||||
color: $color;
|
color: $white;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: $color;
|
border: 1px solid $white;
|
||||||
|
|
||||||
@include hover {
|
@include hover {
|
||||||
color: #fff;
|
color: $white;
|
||||||
background-color: $color;
|
background-color: $color;
|
||||||
border-color: $color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
color: #fff;
|
color: $white;
|
||||||
background-color: $color;
|
background-color: $color;
|
||||||
border-color: $color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&.active,
|
&.active,
|
||||||
.open > &.dropdown-toggle {
|
.open > &.dropdown-toggle {
|
||||||
color: #fff;
|
color: $white;
|
||||||
background-color: $color;
|
background-color: $color;
|
||||||
border-color: $color;
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
color: #fff;
|
color: $white;
|
||||||
background-color: darken($color, 17%);
|
background-color: darken($color, 17%);
|
||||||
border-color: darken($color, 25%);
|
border-color: darken($color, 25%);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
@ -9,18 +10,13 @@ export interface ThunkGiven {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ThunkWhen {
|
export interface ThunkWhen {
|
||||||
whenThunkIsDispatched: (...args: any) => ThunkThen;
|
whenThunkIsDispatched: (...args: any) => Promise<Array<ActionOf<any>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThunkThen {
|
export const thunkTester = (initialState: any, debug?: boolean): ThunkGiven => {
|
||||||
thenDispatchedActionsEqual: (actions: Array<ActionOf<any>>) => ThunkWhen;
|
|
||||||
thenDispatchedActionsAreEqual: (callback: (actions: Array<ActionOf<any>>) => boolean) => ThunkWhen;
|
|
||||||
thenThereAreNoDispatchedActions: () => ThunkWhen;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const thunkTester = (initialState: any): ThunkGiven => {
|
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
let thunkUnderTest = null;
|
let thunkUnderTest: any = null;
|
||||||
|
let dispatchedActions: Array<ActionOf<any>> = [];
|
||||||
|
|
||||||
const givenThunk = (thunkFunction: any): ThunkWhen => {
|
const givenThunk = (thunkFunction: any): ThunkWhen => {
|
||||||
thunkUnderTest = thunkFunction;
|
thunkUnderTest = thunkFunction;
|
||||||
@ -28,36 +24,20 @@ export const thunkTester = (initialState: any): ThunkGiven => {
|
|||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
function whenThunkIsDispatched(...args: any): ThunkThen {
|
const whenThunkIsDispatched = async (...args: any): Promise<Array<ActionOf<any>>> => {
|
||||||
store.dispatch(thunkUnderTest(...arguments));
|
await store.dispatch(thunkUnderTest(...args));
|
||||||
|
|
||||||
return instance;
|
dispatchedActions = store.getActions();
|
||||||
}
|
if (debug) {
|
||||||
|
console.log('resultingActions:', dispatchedActions);
|
||||||
|
}
|
||||||
|
|
||||||
const thenDispatchedActionsEqual = (actions: Array<ActionOf<any>>): ThunkWhen => {
|
return dispatchedActions;
|
||||||
const resultingActions = store.getActions();
|
|
||||||
expect(resultingActions).toEqual(actions);
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
const thenDispatchedActionsAreEqual = (callback: (dispathedActions: Array<ActionOf<any>>) => boolean): ThunkWhen => {
|
|
||||||
const resultingActions = store.getActions();
|
|
||||||
expect(callback(resultingActions)).toBe(true);
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
const thenThereAreNoDispatchedActions = () => {
|
|
||||||
return thenDispatchedActionsEqual([]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const instance = {
|
const instance = {
|
||||||
givenThunk,
|
givenThunk,
|
||||||
whenThunkIsDispatched,
|
whenThunkIsDispatched,
|
||||||
thenDispatchedActionsEqual,
|
|
||||||
thenDispatchedActionsAreEqual,
|
|
||||||
thenThereAreNoDispatchedActions,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
Loading…
Reference in New Issue
Block a user