diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 05f7f8ecd56..2250c6de878 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -164,7 +164,6 @@ export class UnConnectedExploreToolbar extends PureComponent { isLive, isPaused, originPanelId, - datasourceLoading, containerWidth, onChangeTimeZone, } = this.props; @@ -217,7 +216,6 @@ export class UnConnectedExploreToolbar extends PureComponent { onChange={this.onChangeDatasource} datasources={getExploreDatasources()} current={this.getSelectedDatasource()} - showLoading={datasourceLoading === true} hideTextValue={showSmallDataSourcePicker} /> @@ -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, }; }; diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 34acb92352e..34a26ec7dfd 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -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, diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index f54e73149e2..469f92c5a29 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -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 { @@ -19,6 +21,11 @@ export class Wrapper extends Component { 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)); diff --git a/public/app/features/explore/state/datasource.test.ts b/public/app/features/explore/state/datasource.test.ts index 93070fda05b..e742c74a3b2 100644 --- a/public/app/features/explore/state/datasource.test.ts +++ b/public/app/features/explore/state/datasource.test.ts @@ -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' }, - }; - - 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() - .givenReducer(datasourceReducer, initialState) - .whenActionIsDispatched(updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance })) - .thenStateShouldEqual(expectedState); - }); - }); - }); +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 result = datasourceReducer( + initialState, + updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance, history: [] }) + ); + expect(result).toMatchObject(expectedState); }); }); diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index e93842a4a89..f48024768ba 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -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('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('explore/loadDatasourcePending'); - -/** - * Datasource loading was completed. - */ -export interface LoadDatasourceReadyPayload { - exploreId: ExploreId; - history: HistoryItem[]; -} -export const loadDatasourceReadyAction = createAction('explore/loadDatasourceReady'); - /** * Updates datasource instance before datasource loading has started */ export interface UpdateDatasourceInstancePayload { exploreId: ExploreId; datasourceInstance: DataSourceApi; + history: HistoryItem[]; } export const updateDatasourceInstanceAction = createAction( 'explore/updateDatasourceInstance' @@ -67,35 +39,28 @@ export function changeDatasource( options?: { importQueries: boolean } ): ThunkResult { 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 { - 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 => { - 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 => { - 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(), diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index d015eb097d4..c3b99c513bf 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -102,8 +102,8 @@ describe('refreshExplore', () => { .givenThunk(refreshExplore) .whenThunkIsDispatched(exploreId); - const initializeExplore = dispatchedActions[1] as PayloadAction; - const { type, payload } = initializeExplore; + const initializeExplore = dispatchedActions.find(action => action.type === initializeExploreAction.type); + const { type, payload } = initializeExplore as PayloadAction; 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', () => { diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 27d01446fae..17e2f5dafa0 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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('explore/initializeExplore'); @@ -122,7 +131,17 @@ export function initializeExplore( originPanelId?: number | null ): ThunkResult { 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 => { 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), + }; +} diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 5bf6f5875fb..d668a5a8c79 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -33,7 +33,7 @@ describe('running queries', () => { const initialState = { explore: { [exploreId]: { - datasourceInstance: 'test-datasource', + datasourceInstance: { name: 'testDs' }, initialized: true, loading: true, querySubscription: unsubscribable, diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index e975b72fc87..896c8edb716 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -342,7 +342,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult => { liveStreaming: live, }; - const datasourceName = exploreItemState.requestedDatasourceName; + const datasourceName = datasourceInstance.name; const timeZone = getTimeZone(getState().user); const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone); diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 995d2e01665..8d0aa093a8c 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -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 }; +} diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 995733025b0..2d918b5d2a0 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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. */