Commit 66cdc187 by Andrej Ocenas Committed by GitHub

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>
parent 2c898ab1
...@@ -164,7 +164,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -164,7 +164,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
isLive, isLive,
isPaused, isPaused,
originPanelId, originPanelId,
datasourceLoading,
containerWidth, containerWidth,
onChangeTimeZone, onChangeTimeZone,
} = this.props; } = this.props;
...@@ -217,7 +216,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -217,7 +216,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
onChange={this.onChangeDatasource} onChange={this.onChangeDatasource}
datasources={getExploreDatasources()} datasources={getExploreDatasources()}
current={this.getSelectedDatasource()} current={this.getSelectedDatasource()}
showLoading={datasourceLoading === true}
hideTextValue={showSmallDataSourcePicker} hideTextValue={showSmallDataSourcePicker}
/> />
</div> </div>
...@@ -342,7 +340,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps ...@@ -342,7 +340,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
isPaused, isPaused,
originPanelId, originPanelId,
queries, queries,
datasourceLoading,
containerWidth, containerWidth,
} = exploreItem; } = exploreItem;
...@@ -362,7 +359,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps ...@@ -362,7 +359,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
originPanelId, originPanelId,
queries, queries,
syncedTimes, syncedTimes,
datasourceLoading: datasourceLoading ?? undefined,
containerWidth, containerWidth,
}; };
}; };
......
import React from 'react'; 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 Wrapper from './Wrapper';
import { configureStore } from '../../store/configureStore'; import { configureStore } from '../../store/configureStore';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
...@@ -73,6 +73,12 @@ describe('Wrapper', () => { ...@@ -73,6 +73,12 @@ describe('Wrapper', () => {
...query, ...query,
}); });
expect(store.getState().explore.richHistory[0]).toMatchObject({
datasourceId: '1',
datasourceName: 'loki',
queries: [{ expr: '{ label="value"}' }],
});
// We called the data source query method once // We called the data source query method once
expect(datasources.loki.query).toBeCalledTimes(1); expect(datasources.loki.query).toBeCalledTimes(1);
expect((datasources.loki.query as Mock).mock.calls[0][0]).toMatchObject({ expect((datasources.loki.query as Mock).mock.calls[0][0]).toMatchObject({
...@@ -107,6 +113,7 @@ describe('Wrapper', () => { ...@@ -107,6 +113,7 @@ describe('Wrapper', () => {
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the logs // Wait for rendering the logs
await screen.findByText(/custom log line/i); await screen.findByText(/custom log line/i);
await screen.findByText(`loki Editor input: { label="value"}`);
(datasources.elastic.query as Mock).mockReturnValueOnce(makeMetricsQueryResponse()); (datasources.elastic.query as Mock).mockReturnValueOnce(makeMetricsQueryResponse());
store.dispatch( store.dispatch(
...@@ -117,19 +124,25 @@ describe('Wrapper', () => { ...@@ -117,19 +124,25 @@ describe('Wrapper', () => {
); );
// Editor renders the new query // Editor renders the new query
await screen.findByText(`loki Editor input: other query`); await screen.findByText(`elastic Editor input: other query`);
// Renders graph // Renders graph
await screen.findByText(/Graph/i); await screen.findByText(/Graph/i);
}); });
it('handles changing the datasource manually', async () => { 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 // Wait for rendering the editor
await screen.findByText(/Editor/i); await screen.findByText(/Editor/i);
await changeDatasource('elastic'); await changeDatasource('elastic');
await screen.findByText('elastic Editor input:'); await screen.findByText('elastic Editor input:');
expect(datasources.elastic.query).not.toBeCalled(); 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 () => { it('opens the split pane', async () => {
...@@ -154,8 +167,10 @@ describe('Wrapper', () => { ...@@ -154,8 +167,10 @@ describe('Wrapper', () => {
(datasources.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Make sure we render the logs panel // Make sure we render the logs panel
const logsPanels = await screen.findAllByText(/^Logs$/i); await waitFor(() => {
expect(logsPanels.length).toBe(2); const logsPanels = screen.getAllByText(/^Logs$/i);
expect(logsPanels.length).toBe(2);
});
// Make sure we render the log line // Make sure we render the log line
const logsLines = await screen.findAllByText(/custom log line/i); const logsLines = await screen.findAllByText(/custom log line/i);
...@@ -195,7 +210,10 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou ...@@ -195,7 +210,10 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
window.localStorage.clear(); window.localStorage.clear();
// Create this here so any mocks are recreated on setup and don't retain state // 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; const dsSettings = options?.datasources || defaultDatasources;
...@@ -235,18 +253,18 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou ...@@ -235,18 +253,18 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
return { datasources: fromPairs(dsSettings.map(d => [d.api.name, d.api])) }; 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 = { const meta: any = {
info: { info: {
logos: { logos: {
small: '', small: '',
}, },
}, },
id: '1', id: id.toString(),
}; };
return { return {
settings: { settings: {
id: 1, id,
uid: name, uid: name,
type: 'logs', type: 'logs',
name, name,
......
...@@ -6,12 +6,14 @@ import { StoreState } from 'app/types'; ...@@ -6,12 +6,14 @@ import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui'; import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui';
import { resetExploreAction } from './state/main'; import { resetExploreAction, richHistoryUpdatedAction } from './state/main';
import Explore from './Explore'; import Explore from './Explore';
import { getRichHistory } from '../../core/utils/richHistory';
interface WrapperProps { interface WrapperProps {
split: boolean; split: boolean;
resetExploreAction: typeof resetExploreAction; resetExploreAction: typeof resetExploreAction;
richHistoryUpdatedAction: typeof richHistoryUpdatedAction;
} }
export class Wrapper extends Component<WrapperProps> { export class Wrapper extends Component<WrapperProps> {
...@@ -19,6 +21,11 @@ export class Wrapper extends Component<WrapperProps> { ...@@ -19,6 +21,11 @@ export class Wrapper extends Component<WrapperProps> {
this.props.resetExploreAction({}); this.props.resetExploreAction({});
} }
componentDidMount() {
const richHistory = getRichHistory();
this.props.richHistoryUpdatedAction({ richHistory });
}
render() { render() {
const { split } = this.props; const { split } = this.props;
...@@ -48,6 +55,7 @@ const mapStateToProps = (state: StoreState) => { ...@@ -48,6 +55,7 @@ const mapStateToProps = (state: StoreState) => {
const mapDispatchToProps = { const mapDispatchToProps = {
resetExploreAction, resetExploreAction,
richHistoryUpdatedAction,
}; };
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper)); export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
import { import { updateDatasourceInstanceAction, datasourceReducer } from './datasource';
loadDatasource,
loadDatasourcePendingAction,
loadDatasourceReadyAction,
updateDatasourceInstanceAction,
datasourceReducer,
} from './datasource';
import { ExploreId, ExploreItemState } from 'app/types'; import { ExploreId, ExploreItemState } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { DataQuery, DataSourceApi } from '@grafana/data'; import { DataQuery, DataSourceApi } from '@grafana/data';
import { createEmptyQueryResponse } from './utils'; import { createEmptyQueryResponse } from './utils';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
describe('loading datasource', () => { describe('Datasource reducer', () => {
describe('when loadDatasource thunk is dispatched', () => { it('should handle set updateDatasourceInstanceAction correctly', () => {
describe('and all goes fine', () => { const StartPage = {};
it('then it should dispatch correct actions', async () => { const datasourceInstance = {
const exploreId = ExploreId.left; meta: {
const name = 'some-datasource'; metrics: true,
const initialState = { explore: { [exploreId]: { requestedDatasourceName: name } } }; logs: true,
const mockDatasourceInstance = { },
testDatasource: () => { components: {
return Promise.resolve({ status: 'success' }); ExploreStartPage: StartPage,
}, },
name, } as DataSourceApi;
init: jest.fn(), const queries: DataQuery[] = [];
meta: { id: 'some id' }, const queryKeys: string[] = [];
}; const initialState: ExploreItemState = ({
datasourceInstance: null,
const dispatchedActions = await thunkTester(initialState) queries,
.givenThunk(loadDatasource) queryKeys,
.whenThunkIsDispatched(exploreId, mockDatasourceInstance); } as unknown) as ExploreItemState;
const expectedState: any = {
expect(dispatchedActions).toEqual([ datasourceInstance,
loadDatasourcePendingAction({ queries,
exploreId, queryKeys,
requestedDatasourceName: mockDatasourceInstance.name, graphResult: null,
}), logsResult: null,
loadDatasourceReadyAction({ exploreId, history: [] }), tableResult: null,
]); latency: 0,
}); loading: false,
}); queryResponse: createEmptyQueryResponse(),
};
describe('and user changes datasource during load', () => {
it('then it should dispatch correct actions', async () => { const result = datasourceReducer(
const exploreId = ExploreId.left; initialState,
const name = 'some-datasource'; updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance, history: [] })
const initialState = { explore: { [exploreId]: { requestedDatasourceName: 'some-other-datasource' } } }; );
const mockDatasourceInstance = { expect(result).toMatchObject(expectedState);
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);
});
});
});
}); });
}); });
// Libraries // Libraries
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit'; import { AnyAction, createAction } from '@reduxjs/toolkit';
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import { DataSourceApi, HistoryItem } from '@grafana/data'; import { DataSourceApi, HistoryItem } from '@grafana/data';
import store from 'app/core/store'; import { stopQueryState } from 'app/core/utils/explore';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { lastUsedDatasourceKeyForOrgId, stopQueryState } from 'app/core/utils/explore';
import { ExploreItemState, ThunkResult } from 'app/types'; import { ExploreItemState, ThunkResult } from 'app/types';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { getExploreDatasources } from './selectors';
import { importQueries, runQueries } from './query'; import { importQueries, runQueries } from './query';
import { changeRefreshInterval } from './time'; import { changeRefreshInterval } from './time';
import { createEmptyQueryResponse, makeInitialUpdateState } from './utils'; import { createEmptyQueryResponse, loadAndInitDatasource, makeInitialUpdateState } from './utils';
// //
// Actions and Payloads // 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 * Updates datasource instance before datasource loading has started
*/ */
export interface UpdateDatasourceInstancePayload { export interface UpdateDatasourceInstancePayload {
exploreId: ExploreId; exploreId: ExploreId;
datasourceInstance: DataSourceApi; datasourceInstance: DataSourceApi;
history: HistoryItem[];
} }
export const updateDatasourceInstanceAction = createAction<UpdateDatasourceInstancePayload>( export const updateDatasourceInstanceAction = createAction<UpdateDatasourceInstancePayload>(
'explore/updateDatasourceInstance' 'explore/updateDatasourceInstance'
...@@ -67,35 +39,28 @@ export function changeDatasource( ...@@ -67,35 +39,28 @@ export function changeDatasource(
options?: { importQueries: boolean } options?: { importQueries: boolean }
): ThunkResult<void> { ): ThunkResult<void> {
return async (dispatch, getState) => { 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 orgId = getState().user.orgId;
const { history, instance } = await loadAndInitDatasource(orgId, datasourceName);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
dispatch( dispatch(
updateDatasourceInstanceAction({ updateDatasourceInstanceAction({
exploreId, exploreId,
datasourceInstance: newDataSourceInstance, datasourceInstance: instance,
history,
}) })
); );
const queries = getState().explore[exploreId].queries;
if (options?.importQueries) { if (options?.importQueries) {
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance));
} }
if (getState().explore[exploreId].isLive) { if (getState().explore[exploreId].isLive) {
dispatch(changeRefreshInterval(exploreId, RefreshPicker.offOption.value)); 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 // Exception - we only want to run queries on data source change, if the queries were imported
if (options?.importQueries) { if (options?.importQueries) {
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
...@@ -103,72 +68,6 @@ export function changeDatasource( ...@@ -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 // Reducer
// //
...@@ -183,7 +82,7 @@ export const loadDatasource = (exploreId: ExploreId, instance: DataSourceApi, or ...@@ -183,7 +82,7 @@ export const loadDatasource = (exploreId: ExploreId, instance: DataSourceApi, or
// https://github.com/reduxjs/redux-toolkit/issues/242 // https://github.com/reduxjs/redux-toolkit/issues/242
export const datasourceReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => { export const datasourceReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => {
if (updateDatasourceInstanceAction.match(action)) { if (updateDatasourceInstanceAction.match(action)) {
const { datasourceInstance } = action.payload; const { datasourceInstance, history } = action.payload;
// Custom components // Custom components
stopQueryState(state.querySubscription); stopQueryState(state.querySubscription);
...@@ -199,32 +98,7 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E ...@@ -199,32 +98,7 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E
loading: false, loading: false,
queryKeys: [], queryKeys: [],
originPanelId: state.urlState && state.urlState.originPanelId, 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, history,
datasourceLoading: false,
datasourceMissing: false, datasourceMissing: false,
logsHighlighterExpressions: undefined, logsHighlighterExpressions: undefined,
update: makeInitialUpdateState(), update: makeInitialUpdateState(),
......
...@@ -102,8 +102,8 @@ describe('refreshExplore', () => { ...@@ -102,8 +102,8 @@ describe('refreshExplore', () => {
.givenThunk(refreshExplore) .givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId); .whenThunkIsDispatched(exploreId);
const initializeExplore = dispatchedActions[1] as PayloadAction<InitializeExplorePayload>; const initializeExplore = dispatchedActions.find(action => action.type === initializeExploreAction.type);
const { type, payload } = initializeExplore; const { type, payload } = initializeExplore as PayloadAction<InitializeExplorePayload>;
expect(type).toEqual(initializeExploreAction.type); expect(type).toEqual(initializeExploreAction.type);
expect(payload.containerWidth).toEqual(containerWidth); expect(payload.containerWidth).toEqual(containerWidth);
...@@ -144,7 +144,7 @@ describe('refreshExplore', () => { ...@@ -144,7 +144,7 @@ describe('refreshExplore', () => {
}); });
}); });
describe('Explore item reducer', () => { describe('Explore pane reducer', () => {
describe('changing dedup strategy', () => { describe('changing dedup strategy', () => {
describe('when changeDedupStrategyAction is dispatched', () => { describe('when changeDedupStrategyAction is dispatched', () => {
it('then it should set correct dedup strategy in state', () => { it('then it should set correct dedup strategy in state', () => {
......
...@@ -6,26 +6,33 @@ import { queryReducer } from './query'; ...@@ -6,26 +6,33 @@ import { queryReducer } from './query';
import { datasourceReducer } from './datasource'; import { datasourceReducer } from './datasource';
import { timeReducer } from './time'; import { timeReducer } from './time';
import { historyReducer } from './history'; import { historyReducer } from './history';
import { makeExplorePaneState, makeInitialUpdateState } from './utils'; import { makeExplorePaneState, makeInitialUpdateState, loadAndInitDatasource, createEmptyQueryResponse } from './utils';
import { createAction, PayloadAction } from '@reduxjs/toolkit'; 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 { import {
clearQueryKeys, clearQueryKeys,
ensureQueries, ensureQueries,
generateNewKeyAndAddRefIdIfMissing, generateNewKeyAndAddRefIdIfMissing,
getTimeRangeFromUrl, getTimeRangeFromUrl,
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
import { getRichHistory } from 'app/core/utils/richHistory';
// Types // Types
import { ThunkResult } from 'app/types'; import { ThunkResult } from 'app/types';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { updateLocation } from '../../../core/actions'; import { updateLocation } from '../../../core/actions';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { richHistoryUpdatedAction } from './main';
import { runQueries, setQueriesAction } from './query'; import { runQueries, setQueriesAction } from './query';
import { loadExploreDatasourcesAndSetDatasource } from './datasource';
import { updateTime } from './time'; import { updateTime } from './time';
import { toRawTimeRange } from '../utils/time'; import { toRawTimeRange } from '../utils/time';
import { getExploreDatasources } from './selectors';
// //
// Actions and Payloads // Actions and Payloads
...@@ -72,6 +79,8 @@ export interface InitializeExplorePayload { ...@@ -72,6 +79,8 @@ export interface InitializeExplorePayload {
eventBridge: EventBusExtended; eventBridge: EventBusExtended;
queries: DataQuery[]; queries: DataQuery[];
range: TimeRange; range: TimeRange;
history: HistoryItem[];
datasourceInstance?: DataSourceApi;
originPanelId?: number | null; originPanelId?: number | null;
} }
export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore'); export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
...@@ -122,7 +131,17 @@ export function initializeExplore( ...@@ -122,7 +131,17 @@ export function initializeExplore(
originPanelId?: number | null originPanelId?: number | null
): ThunkResult<void> { ): ThunkResult<void> {
return async (dispatch, getState) => { 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( dispatch(
initializeExploreAction({ initializeExploreAction({
exploreId, exploreId,
...@@ -131,11 +150,15 @@ export function initializeExplore( ...@@ -131,11 +150,15 @@ export function initializeExplore(
queries, queries,
range, range,
originPanelId, originPanelId,
datasourceInstance: instance,
history,
}) })
); );
dispatch(updateTime({ exploreId })); dispatch(updateTime({ exploreId }));
const richHistory = getRichHistory();
dispatch(richHistoryUpdatedAction({ richHistory })); if (instance) {
dispatch(runQueries(exploreId));
}
}; };
} }
...@@ -149,20 +172,9 @@ export const stateSave = (): ThunkResult<void> => { ...@@ -149,20 +172,9 @@ export const stateSave = (): ThunkResult<void> => {
const orgId = getState().user.orgId.toString(); const orgId = getState().user.orgId.toString();
const replace = left && left.urlReplaced === false; const replace = left && left.urlReplaced === false;
const urlStates: { [index: string]: string } = { orgId }; const urlStates: { [index: string]: string } = { orgId };
const leftUrlState: ExploreUrlState = { urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left), true);
datasource: left.datasourceInstance!.name,
queries: left.queries.map(clearQueryKeys),
range: toRawTimeRange(left.range),
};
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
if (split) { if (split) {
const rightUrlState: ExploreUrlState = { urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right), true);
datasource: right.datasourceInstance!.name,
queries: right.queries.map(clearQueryKeys),
range: toRawTimeRange(right.range),
};
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
} }
dispatch(updateLocation({ query: urlStates, replace })); dispatch(updateLocation({ query: urlStates, replace }));
...@@ -259,7 +271,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac ...@@ -259,7 +271,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
} }
if (initializeExploreAction.match(action)) { if (initializeExploreAction.match(action)) {
const { containerWidth, eventBridge, queries, range, originPanelId } = action.payload; const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
return { return {
...state, ...state,
containerWidth, containerWidth,
...@@ -270,6 +282,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac ...@@ -270,6 +282,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
queryKeys: getQueryKeys(queries, state.datasourceInstance), queryKeys: getQueryKeys(queries, state.datasourceInstance),
originPanelId, originPanelId,
update: makeInitialUpdateState(), update: makeInitialUpdateState(),
datasourceInstance,
history,
datasourceMissing: !datasourceInstance,
queryResponse: createEmptyQueryResponse(),
logsHighlighterExpressions: undefined,
}; };
} }
...@@ -290,3 +307,13 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac ...@@ -290,3 +307,13 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
return state; 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', () => { ...@@ -33,7 +33,7 @@ describe('running queries', () => {
const initialState = { const initialState = {
explore: { explore: {
[exploreId]: { [exploreId]: {
datasourceInstance: 'test-datasource', datasourceInstance: { name: 'testDs' },
initialized: true, initialized: true,
loading: true, loading: true,
querySubscription: unsubscribable, querySubscription: unsubscribable,
......
...@@ -342,7 +342,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -342,7 +342,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
liveStreaming: live, liveStreaming: live,
}; };
const datasourceName = exploreItemState.requestedDatasourceName; const datasourceName = datasourceInstance.name;
const timeZone = getTimeZone(getState().user); const timeZone = getTimeZone(getState().user);
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone); const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone);
......
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 { 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 = { export const DEFAULT_RANGE = {
from: 'now-6h', from: 'now-6h',
...@@ -20,8 +31,6 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({ ...@@ -20,8 +31,6 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({
export const makeExplorePaneState = (): ExploreItemState => ({ export const makeExplorePaneState = (): ExploreItemState => ({
containerWidth: 0, containerWidth: 0,
datasourceInstance: null, datasourceInstance: null,
requestedDatasourceName: null,
datasourceLoading: null,
datasourceMissing: false, datasourceMissing: false,
history: [], history: [],
queries: [], queries: [],
...@@ -57,3 +66,25 @@ export const createEmptyQueryResponse = (): PanelData => ({ ...@@ -57,3 +66,25 @@ export const createEmptyQueryResponse = (): PanelData => ({
series: [], series: [],
timeRange: DefaultTimeRange, 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 };
}
...@@ -59,14 +59,6 @@ export interface ExploreItemState { ...@@ -59,14 +59,6 @@ export interface ExploreItemState {
*/ */
datasourceInstance?: DataSourceApi | null; 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. * True if there is no datasource to be selected.
*/ */
datasourceMissing: boolean; datasourceMissing: boolean;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment