mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Refactor/Explore: Inline datasource actions into initialisation (#28953)
* Inline datasource actions into initialisation * Fix datasource change * Fix rich history * Fix test * Move rich history init to Wrapper * Remove console log * TS fixes Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
parent
2c898ab1bc
commit
66cdc18705
@ -164,7 +164,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
isLive,
|
||||
isPaused,
|
||||
originPanelId,
|
||||
datasourceLoading,
|
||||
containerWidth,
|
||||
onChangeTimeZone,
|
||||
} = this.props;
|
||||
@ -217,7 +216,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
onChange={this.onChangeDatasource}
|
||||
datasources={getExploreDatasources()}
|
||||
current={this.getSelectedDatasource()}
|
||||
showLoading={datasourceLoading === true}
|
||||
hideTextValue={showSmallDataSourcePicker}
|
||||
/>
|
||||
</div>
|
||||
@ -342,7 +340,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
||||
isPaused,
|
||||
originPanelId,
|
||||
queries,
|
||||
datasourceLoading,
|
||||
containerWidth,
|
||||
} = exploreItem;
|
||||
|
||||
@ -362,7 +359,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
||||
originPanelId,
|
||||
queries,
|
||||
syncedTimes,
|
||||
datasourceLoading: datasourceLoading ?? undefined,
|
||||
containerWidth,
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import Wrapper from './Wrapper';
|
||||
import { configureStore } from '../../store/configureStore';
|
||||
import { Provider } from 'react-redux';
|
||||
@ -73,6 +73,12 @@ describe('Wrapper', () => {
|
||||
...query,
|
||||
});
|
||||
|
||||
expect(store.getState().explore.richHistory[0]).toMatchObject({
|
||||
datasourceId: '1',
|
||||
datasourceName: 'loki',
|
||||
queries: [{ expr: '{ label="value"}' }],
|
||||
});
|
||||
|
||||
// We called the data source query method once
|
||||
expect(datasources.loki.query).toBeCalledTimes(1);
|
||||
expect((datasources.loki.query as Mock).mock.calls[0][0]).toMatchObject({
|
||||
@ -107,6 +113,7 @@ describe('Wrapper', () => {
|
||||
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
// Wait for rendering the logs
|
||||
await screen.findByText(/custom log line/i);
|
||||
await screen.findByText(`loki Editor input: { label="value"}`);
|
||||
|
||||
(datasources.elastic.query as Mock).mockReturnValueOnce(makeMetricsQueryResponse());
|
||||
store.dispatch(
|
||||
@ -117,19 +124,25 @@ describe('Wrapper', () => {
|
||||
);
|
||||
|
||||
// Editor renders the new query
|
||||
await screen.findByText(`loki Editor input: other query`);
|
||||
await screen.findByText(`elastic Editor input: other query`);
|
||||
// Renders graph
|
||||
await screen.findByText(/Graph/i);
|
||||
});
|
||||
|
||||
it('handles changing the datasource manually', async () => {
|
||||
const { datasources } = setup();
|
||||
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
|
||||
const { datasources } = setup({ query });
|
||||
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
// Wait for rendering the editor
|
||||
await screen.findByText(/Editor/i);
|
||||
await changeDatasource('elastic');
|
||||
|
||||
await screen.findByText('elastic Editor input:');
|
||||
expect(datasources.elastic.query).not.toBeCalled();
|
||||
expect(store.getState().location.query).toEqual({
|
||||
orgId: '1',
|
||||
left: JSON.stringify(['now-1h', 'now', 'elastic', {}]),
|
||||
});
|
||||
});
|
||||
|
||||
it('opens the split pane', async () => {
|
||||
@ -154,8 +167,10 @@ describe('Wrapper', () => {
|
||||
(datasources.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
|
||||
// Make sure we render the logs panel
|
||||
const logsPanels = await screen.findAllByText(/^Logs$/i);
|
||||
expect(logsPanels.length).toBe(2);
|
||||
await waitFor(() => {
|
||||
const logsPanels = screen.getAllByText(/^Logs$/i);
|
||||
expect(logsPanels.length).toBe(2);
|
||||
});
|
||||
|
||||
// Make sure we render the log line
|
||||
const logsLines = await screen.findAllByText(/custom log line/i);
|
||||
@ -195,7 +210,10 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
|
||||
window.localStorage.clear();
|
||||
|
||||
// Create this here so any mocks are recreated on setup and don't retain state
|
||||
const defaultDatasources: DatasourceSetup[] = [makeDatasourceSetup(), makeDatasourceSetup({ name: 'elastic' })];
|
||||
const defaultDatasources: DatasourceSetup[] = [
|
||||
makeDatasourceSetup(),
|
||||
makeDatasourceSetup({ name: 'elastic', id: 2 }),
|
||||
];
|
||||
|
||||
const dsSettings = options?.datasources || defaultDatasources;
|
||||
|
||||
@ -235,18 +253,18 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
|
||||
return { datasources: fromPairs(dsSettings.map(d => [d.api.name, d.api])) };
|
||||
}
|
||||
|
||||
function makeDatasourceSetup({ name = 'loki' }: { name?: string } = {}): DatasourceSetup {
|
||||
function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: number } = {}): DatasourceSetup {
|
||||
const meta: any = {
|
||||
info: {
|
||||
logos: {
|
||||
small: '',
|
||||
},
|
||||
},
|
||||
id: '1',
|
||||
id: id.toString(),
|
||||
};
|
||||
return {
|
||||
settings: {
|
||||
id: 1,
|
||||
id,
|
||||
uid: name,
|
||||
type: 'logs',
|
||||
name,
|
||||
|
@ -6,12 +6,14 @@ import { StoreState } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui';
|
||||
import { resetExploreAction } from './state/main';
|
||||
import { resetExploreAction, richHistoryUpdatedAction } from './state/main';
|
||||
import Explore from './Explore';
|
||||
import { getRichHistory } from '../../core/utils/richHistory';
|
||||
|
||||
interface WrapperProps {
|
||||
split: boolean;
|
||||
resetExploreAction: typeof resetExploreAction;
|
||||
richHistoryUpdatedAction: typeof richHistoryUpdatedAction;
|
||||
}
|
||||
|
||||
export class Wrapper extends Component<WrapperProps> {
|
||||
@ -19,6 +21,11 @@ export class Wrapper extends Component<WrapperProps> {
|
||||
this.props.resetExploreAction({});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const richHistory = getRichHistory();
|
||||
this.props.richHistoryUpdatedAction({ richHistory });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { split } = this.props;
|
||||
|
||||
@ -48,6 +55,7 @@ const mapStateToProps = (state: StoreState) => {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
resetExploreAction,
|
||||
richHistoryUpdatedAction,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
|
||||
|
@ -1,115 +1,43 @@
|
||||
import {
|
||||
loadDatasource,
|
||||
loadDatasourcePendingAction,
|
||||
loadDatasourceReadyAction,
|
||||
updateDatasourceInstanceAction,
|
||||
datasourceReducer,
|
||||
} from './datasource';
|
||||
import { updateDatasourceInstanceAction, datasourceReducer } from './datasource';
|
||||
import { ExploreId, ExploreItemState } from 'app/types';
|
||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||
import { DataQuery, DataSourceApi } from '@grafana/data';
|
||||
import { createEmptyQueryResponse } from './utils';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
|
||||
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' },
|
||||
};
|
||||
describe('Datasource reducer', () => {
|
||||
it('should handle set updateDatasourceInstanceAction correctly', () => {
|
||||
const StartPage = {};
|
||||
const datasourceInstance = {
|
||||
meta: {
|
||||
metrics: true,
|
||||
logs: true,
|
||||
},
|
||||
components: {
|
||||
ExploreStartPage: StartPage,
|
||||
},
|
||||
} as DataSourceApi;
|
||||
const queries: DataQuery[] = [];
|
||||
const queryKeys: string[] = [];
|
||||
const initialState: ExploreItemState = ({
|
||||
datasourceInstance: null,
|
||||
queries,
|
||||
queryKeys,
|
||||
} as unknown) as ExploreItemState;
|
||||
const expectedState: any = {
|
||||
datasourceInstance,
|
||||
queries,
|
||||
queryKeys,
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
latency: 0,
|
||||
loading: false,
|
||||
queryResponse: createEmptyQueryResponse(),
|
||||
};
|
||||
|
||||
const dispatchedActions = await thunkTester(initialState)
|
||||
.givenThunk(loadDatasource)
|
||||
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
loadDatasourcePendingAction({
|
||||
exploreId,
|
||||
requestedDatasourceName: mockDatasourceInstance.name,
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Explore item reducer', () => {
|
||||
describe('changing datasource', () => {
|
||||
describe('when updateDatasourceInstanceAction is dispatched', () => {
|
||||
describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
|
||||
it('then it should set correct state', () => {
|
||||
const StartPage = {};
|
||||
const datasourceInstance = {
|
||||
meta: {
|
||||
metrics: true,
|
||||
logs: true,
|
||||
},
|
||||
components: {
|
||||
ExploreStartPage: StartPage,
|
||||
},
|
||||
} as DataSourceApi;
|
||||
const queries: DataQuery[] = [];
|
||||
const queryKeys: string[] = [];
|
||||
const initialState: ExploreItemState = ({
|
||||
datasourceInstance: null,
|
||||
queries,
|
||||
queryKeys,
|
||||
} as unknown) as ExploreItemState;
|
||||
const expectedState: any = {
|
||||
datasourceInstance,
|
||||
queries,
|
||||
queryKeys,
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
latency: 0,
|
||||
loading: false,
|
||||
queryResponse: createEmptyQueryResponse(),
|
||||
};
|
||||
|
||||
reducerTester<ExploreItemState>()
|
||||
.givenReducer(datasourceReducer, initialState)
|
||||
.whenActionIsDispatched(updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance }))
|
||||
.thenStateShouldEqual(expectedState);
|
||||
});
|
||||
});
|
||||
});
|
||||
const result = datasourceReducer(
|
||||
initialState,
|
||||
updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance, history: [] })
|
||||
);
|
||||
expect(result).toMatchObject(expectedState);
|
||||
});
|
||||
});
|
||||
|
@ -1,54 +1,26 @@
|
||||
// Libraries
|
||||
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import { DataSourceApi, HistoryItem } from '@grafana/data';
|
||||
import store from 'app/core/store';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { lastUsedDatasourceKeyForOrgId, stopQueryState } from 'app/core/utils/explore';
|
||||
import { stopQueryState } from 'app/core/utils/explore';
|
||||
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
import { getExploreDatasources } from './selectors';
|
||||
import { importQueries, runQueries } from './query';
|
||||
import { changeRefreshInterval } from './time';
|
||||
import { createEmptyQueryResponse, makeInitialUpdateState } from './utils';
|
||||
import { createEmptyQueryResponse, loadAndInitDatasource, makeInitialUpdateState } from './utils';
|
||||
|
||||
//
|
||||
// Actions and Payloads
|
||||
//
|
||||
|
||||
/**
|
||||
* Display an error when no datasources have been configured
|
||||
*/
|
||||
export interface LoadDatasourceMissingPayload {
|
||||
exploreId: ExploreId;
|
||||
}
|
||||
export const loadDatasourceMissingAction = createAction<LoadDatasourceMissingPayload>('explore/loadDatasourceMissing');
|
||||
|
||||
/**
|
||||
* Start the async process of loading a datasource to display a loading indicator
|
||||
*/
|
||||
export interface LoadDatasourcePendingPayload {
|
||||
exploreId: ExploreId;
|
||||
requestedDatasourceName: string;
|
||||
}
|
||||
export const loadDatasourcePendingAction = createAction<LoadDatasourcePendingPayload>('explore/loadDatasourcePending');
|
||||
|
||||
/**
|
||||
* Datasource loading was completed.
|
||||
*/
|
||||
export interface LoadDatasourceReadyPayload {
|
||||
exploreId: ExploreId;
|
||||
history: HistoryItem[];
|
||||
}
|
||||
export const loadDatasourceReadyAction = createAction<LoadDatasourceReadyPayload>('explore/loadDatasourceReady');
|
||||
|
||||
/**
|
||||
* Updates datasource instance before datasource loading has started
|
||||
*/
|
||||
export interface UpdateDatasourceInstancePayload {
|
||||
exploreId: ExploreId;
|
||||
datasourceInstance: DataSourceApi;
|
||||
history: HistoryItem[];
|
||||
}
|
||||
export const updateDatasourceInstanceAction = createAction<UpdateDatasourceInstancePayload>(
|
||||
'explore/updateDatasourceInstance'
|
||||
@ -67,35 +39,28 @@ export function changeDatasource(
|
||||
options?: { importQueries: boolean }
|
||||
): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
let newDataSourceInstance: DataSourceApi;
|
||||
|
||||
if (!datasourceName) {
|
||||
newDataSourceInstance = await getDatasourceSrv().get();
|
||||
} else {
|
||||
newDataSourceInstance = await getDatasourceSrv().get(datasourceName);
|
||||
}
|
||||
|
||||
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
||||
const queries = getState().explore[exploreId].queries;
|
||||
const orgId = getState().user.orgId;
|
||||
const { history, instance } = await loadAndInitDatasource(orgId, datasourceName);
|
||||
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
||||
|
||||
dispatch(
|
||||
updateDatasourceInstanceAction({
|
||||
exploreId,
|
||||
datasourceInstance: newDataSourceInstance,
|
||||
datasourceInstance: instance,
|
||||
history,
|
||||
})
|
||||
);
|
||||
|
||||
const queries = getState().explore[exploreId].queries;
|
||||
|
||||
if (options?.importQueries) {
|
||||
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
|
||||
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance));
|
||||
}
|
||||
|
||||
if (getState().explore[exploreId].isLive) {
|
||||
dispatch(changeRefreshInterval(exploreId, RefreshPicker.offOption.value));
|
||||
}
|
||||
|
||||
await dispatch(loadDatasource(exploreId, newDataSourceInstance, orgId));
|
||||
|
||||
// Exception - we only want to run queries on data source change, if the queries were imported
|
||||
if (options?.importQueries) {
|
||||
dispatch(runQueries(exploreId));
|
||||
@ -103,72 +68,6 @@ export function changeDatasource(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 async dispatch => {
|
||||
const exploreDatasources = getExploreDatasources();
|
||||
|
||||
if (exploreDatasources.length >= 1) {
|
||||
await dispatch(changeDatasource(exploreId, datasourceName, { importQueries: true }));
|
||||
} else {
|
||||
dispatch(loadDatasourceMissingAction({ exploreId }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Datasource loading was successfully completed.
|
||||
*/
|
||||
export const loadDatasourceReady = (
|
||||
exploreId: ExploreId,
|
||||
instance: DataSourceApi,
|
||||
orgId: number
|
||||
): PayloadAction<LoadDatasourceReadyPayload> => {
|
||||
const historyKey = `grafana.explore.history.${instance.meta?.id}`;
|
||||
const history = store.getObject(historyKey, []);
|
||||
// Save last-used datasource
|
||||
|
||||
store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.name);
|
||||
|
||||
return loadDatasourceReadyAction({
|
||||
exploreId,
|
||||
history,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
|
||||
*/
|
||||
export const loadDatasource = (exploreId: ExploreId, instance: DataSourceApi, orgId: number): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const datasourceName = instance.name;
|
||||
|
||||
// Keep ID to track selection
|
||||
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
|
||||
|
||||
if (instance.init) {
|
||||
try {
|
||||
instance.init();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
||||
// User already changed datasource, discard results
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(loadDatasourceReady(exploreId, instance, orgId));
|
||||
};
|
||||
};
|
||||
|
||||
//
|
||||
// Reducer
|
||||
//
|
||||
@ -183,7 +82,7 @@ export const loadDatasource = (exploreId: ExploreId, instance: DataSourceApi, or
|
||||
// https://github.com/reduxjs/redux-toolkit/issues/242
|
||||
export const datasourceReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => {
|
||||
if (updateDatasourceInstanceAction.match(action)) {
|
||||
const { datasourceInstance } = action.payload;
|
||||
const { datasourceInstance, history } = action.payload;
|
||||
|
||||
// Custom components
|
||||
stopQueryState(state.querySubscription);
|
||||
@ -199,32 +98,7 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E
|
||||
loading: false,
|
||||
queryKeys: [],
|
||||
originPanelId: state.urlState && state.urlState.originPanelId,
|
||||
};
|
||||
}
|
||||
|
||||
if (loadDatasourceMissingAction.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
datasourceMissing: true,
|
||||
datasourceLoading: false,
|
||||
update: makeInitialUpdateState(),
|
||||
};
|
||||
}
|
||||
|
||||
if (loadDatasourcePendingAction.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
datasourceLoading: true,
|
||||
requestedDatasourceName: action.payload.requestedDatasourceName,
|
||||
};
|
||||
}
|
||||
|
||||
if (loadDatasourceReadyAction.match(action)) {
|
||||
const { history } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
history,
|
||||
datasourceLoading: false,
|
||||
datasourceMissing: false,
|
||||
logsHighlighterExpressions: undefined,
|
||||
update: makeInitialUpdateState(),
|
||||
|
@ -102,8 +102,8 @@ describe('refreshExplore', () => {
|
||||
.givenThunk(refreshExplore)
|
||||
.whenThunkIsDispatched(exploreId);
|
||||
|
||||
const initializeExplore = dispatchedActions[1] as PayloadAction<InitializeExplorePayload>;
|
||||
const { type, payload } = initializeExplore;
|
||||
const initializeExplore = dispatchedActions.find(action => action.type === initializeExploreAction.type);
|
||||
const { type, payload } = initializeExplore as PayloadAction<InitializeExplorePayload>;
|
||||
|
||||
expect(type).toEqual(initializeExploreAction.type);
|
||||
expect(payload.containerWidth).toEqual(containerWidth);
|
||||
@ -144,7 +144,7 @@ describe('refreshExplore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Explore item reducer', () => {
|
||||
describe('Explore pane reducer', () => {
|
||||
describe('changing dedup strategy', () => {
|
||||
describe('when changeDedupStrategyAction is dispatched', () => {
|
||||
it('then it should set correct dedup strategy in state', () => {
|
||||
|
@ -6,26 +6,33 @@ import { queryReducer } from './query';
|
||||
import { datasourceReducer } from './datasource';
|
||||
import { timeReducer } from './time';
|
||||
import { historyReducer } from './history';
|
||||
import { makeExplorePaneState, makeInitialUpdateState } from './utils';
|
||||
import { makeExplorePaneState, makeInitialUpdateState, loadAndInitDatasource, createEmptyQueryResponse } from './utils';
|
||||
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { EventBusExtended, DataQuery, ExploreUrlState, LogLevel, LogsDedupStrategy, TimeRange } from '@grafana/data';
|
||||
import {
|
||||
EventBusExtended,
|
||||
DataQuery,
|
||||
ExploreUrlState,
|
||||
LogLevel,
|
||||
LogsDedupStrategy,
|
||||
TimeRange,
|
||||
HistoryItem,
|
||||
DataSourceApi,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
clearQueryKeys,
|
||||
ensureQueries,
|
||||
generateNewKeyAndAddRefIdIfMissing,
|
||||
getTimeRangeFromUrl,
|
||||
} from 'app/core/utils/explore';
|
||||
import { getRichHistory } from 'app/core/utils/richHistory';
|
||||
// Types
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||
import { richHistoryUpdatedAction } from './main';
|
||||
import { runQueries, setQueriesAction } from './query';
|
||||
import { loadExploreDatasourcesAndSetDatasource } from './datasource';
|
||||
import { updateTime } from './time';
|
||||
import { toRawTimeRange } from '../utils/time';
|
||||
import { getExploreDatasources } from './selectors';
|
||||
|
||||
//
|
||||
// Actions and Payloads
|
||||
@ -72,6 +79,8 @@ export interface InitializeExplorePayload {
|
||||
eventBridge: EventBusExtended;
|
||||
queries: DataQuery[];
|
||||
range: TimeRange;
|
||||
history: HistoryItem[];
|
||||
datasourceInstance?: DataSourceApi;
|
||||
originPanelId?: number | null;
|
||||
}
|
||||
export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
|
||||
@ -122,7 +131,17 @@ export function initializeExplore(
|
||||
originPanelId?: number | null
|
||||
): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
|
||||
const exploreDatasources = getExploreDatasources();
|
||||
let instance = undefined;
|
||||
let history: HistoryItem[] = [];
|
||||
|
||||
if (exploreDatasources.length >= 1) {
|
||||
const orgId = getState().user.orgId;
|
||||
const loadResult = await loadAndInitDatasource(orgId, datasourceName);
|
||||
instance = loadResult.instance;
|
||||
history = loadResult.history;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
initializeExploreAction({
|
||||
exploreId,
|
||||
@ -131,11 +150,15 @@ export function initializeExplore(
|
||||
queries,
|
||||
range,
|
||||
originPanelId,
|
||||
datasourceInstance: instance,
|
||||
history,
|
||||
})
|
||||
);
|
||||
dispatch(updateTime({ exploreId }));
|
||||
const richHistory = getRichHistory();
|
||||
dispatch(richHistoryUpdatedAction({ richHistory }));
|
||||
|
||||
if (instance) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -149,20 +172,9 @@ export const stateSave = (): ThunkResult<void> => {
|
||||
const orgId = getState().user.orgId.toString();
|
||||
const replace = left && left.urlReplaced === false;
|
||||
const urlStates: { [index: string]: string } = { orgId };
|
||||
const leftUrlState: ExploreUrlState = {
|
||||
datasource: left.datasourceInstance!.name,
|
||||
queries: left.queries.map(clearQueryKeys),
|
||||
range: toRawTimeRange(left.range),
|
||||
};
|
||||
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
||||
urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left), true);
|
||||
if (split) {
|
||||
const rightUrlState: ExploreUrlState = {
|
||||
datasource: right.datasourceInstance!.name,
|
||||
queries: right.queries.map(clearQueryKeys),
|
||||
range: toRawTimeRange(right.range),
|
||||
};
|
||||
|
||||
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
||||
urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right), true);
|
||||
}
|
||||
|
||||
dispatch(updateLocation({ query: urlStates, replace }));
|
||||
@ -259,7 +271,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
||||
}
|
||||
|
||||
if (initializeExploreAction.match(action)) {
|
||||
const { containerWidth, eventBridge, queries, range, originPanelId } = action.payload;
|
||||
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
containerWidth,
|
||||
@ -270,6 +282,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||
originPanelId,
|
||||
update: makeInitialUpdateState(),
|
||||
datasourceInstance,
|
||||
history,
|
||||
datasourceMissing: !datasourceInstance,
|
||||
queryResponse: createEmptyQueryResponse(),
|
||||
logsHighlighterExpressions: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -290,3 +307,13 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState {
|
||||
return {
|
||||
// It can happen that if we are in a split and initial load also runs queries we can be here before the second pane
|
||||
// is initialized so datasourceInstance will be still undefined.
|
||||
datasource: pane.datasourceInstance?.name || pane.urlState!.datasource,
|
||||
queries: pane.queries.map(clearQueryKeys),
|
||||
range: toRawTimeRange(pane.range),
|
||||
};
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ describe('running queries', () => {
|
||||
const initialState = {
|
||||
explore: {
|
||||
[exploreId]: {
|
||||
datasourceInstance: 'test-datasource',
|
||||
datasourceInstance: { name: 'testDs' },
|
||||
initialized: true,
|
||||
loading: true,
|
||||
querySubscription: unsubscribable,
|
||||
|
@ -342,7 +342,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
||||
liveStreaming: live,
|
||||
};
|
||||
|
||||
const datasourceName = exploreItemState.requestedDatasourceName;
|
||||
const datasourceName = datasourceInstance.name;
|
||||
const timeZone = getTimeZone(getState().user);
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone);
|
||||
|
||||
|
@ -1,6 +1,17 @@
|
||||
import { EventBusExtended, DefaultTimeRange, LoadingState, LogsDedupStrategy, PanelData } from '@grafana/data';
|
||||
import {
|
||||
EventBusExtended,
|
||||
DefaultTimeRange,
|
||||
LoadingState,
|
||||
LogsDedupStrategy,
|
||||
PanelData,
|
||||
DataSourceApi,
|
||||
HistoryItem,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { ExploreItemState, ExploreUpdateState } from 'app/types/explore';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import store from '../../../core/store';
|
||||
import { lastUsedDatasourceKeyForOrgId } from '../../../core/utils/explore';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
@ -20,8 +31,6 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({
|
||||
export const makeExplorePaneState = (): ExploreItemState => ({
|
||||
containerWidth: 0,
|
||||
datasourceInstance: null,
|
||||
requestedDatasourceName: null,
|
||||
datasourceLoading: null,
|
||||
datasourceMissing: false,
|
||||
history: [],
|
||||
queries: [],
|
||||
@ -57,3 +66,25 @@ export const createEmptyQueryResponse = (): PanelData => ({
|
||||
series: [],
|
||||
timeRange: DefaultTimeRange,
|
||||
});
|
||||
|
||||
export async function loadAndInitDatasource(
|
||||
orgId: number,
|
||||
datasourceName?: string
|
||||
): Promise<{ history: HistoryItem[]; instance: DataSourceApi }> {
|
||||
const instance = await getDatasourceSrv().get(datasourceName);
|
||||
if (instance.init) {
|
||||
try {
|
||||
instance.init();
|
||||
} catch (err) {
|
||||
// TODO: should probably be handled better
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const historyKey = `grafana.explore.history.${instance.meta?.id}`;
|
||||
const history = store.getObject(historyKey, []);
|
||||
// Save last-used datasource
|
||||
|
||||
store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.name);
|
||||
return { history, instance };
|
||||
}
|
||||
|
@ -58,14 +58,6 @@ export interface ExploreItemState {
|
||||
* Datasource instance that has been selected. Datasource-specific logic can be run on this object.
|
||||
*/
|
||||
datasourceInstance?: DataSourceApi | null;
|
||||
/**
|
||||
* Current data source name or null if default
|
||||
*/
|
||||
requestedDatasourceName: string | null;
|
||||
/**
|
||||
* True if the datasource is loading. `null` if the loading has not started yet.
|
||||
*/
|
||||
datasourceLoading: boolean | null;
|
||||
/**
|
||||
* True if there is no datasource to be selected.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user