- {!richHistorySettings.activeDatasourceOnly && (
+ {!richHistorySettings.activeDatasourcesOnly && (
{
@@ -159,14 +185,14 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) {
/>
- {loading && (
+ {loading && loadingDs && (
Loading results...
)}
- {!loading &&
+ {!(loading && loadingDs) &&
queries.map((q) => {
- return
;
+ return
;
})}
{queries.length && queries.length !== totalQueries ? (
diff --git a/public/app/features/explore/spec/helper/assert.ts b/public/app/features/explore/spec/helper/assert.ts
index 123db21a233..6e8f97d15e3 100644
--- a/public/app/features/explore/spec/helper/assert.ts
+++ b/public/app/features/explore/spec/helper/assert.ts
@@ -1,26 +1,18 @@
import { waitFor } from '@testing-library/react';
-import { getAllByRoleInQueryHistoryTab, withinExplore } from './setup';
+import { withinQueryHistory } from './setup';
-export const assertQueryHistoryExists = async (query: string, exploreId = 'left') => {
- const selector = withinExplore(exploreId);
+export const assertQueryHistoryExists = async (query: string) => {
+ const selector = withinQueryHistory();
expect(await selector.findByText('1 queries')).toBeInTheDocument();
const queryItem = selector.getByLabelText('Query text');
expect(queryItem).toHaveTextContent(query);
};
-export const assertQueryHistoryContains = async (query: string, exploreId = 'left') => {
- const selector = withinExplore(exploreId);
+export const assertQueryHistory = async (expectedQueryTexts: string[]) => {
+ const selector = withinQueryHistory();
- await waitFor(() => {
- const containsQuery = selector.getAllByLabelText('Query text').map((e) => (e.textContent || '').includes(query));
- expect(containsQuery).toContain(true);
- });
-};
-
-export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId = 'left') => {
- const selector = withinExplore(exploreId);
await waitFor(() => {
expect(selector.getByText(new RegExp(`${expectedQueryTexts.length} queries`))).toBeInTheDocument();
const queryTexts = selector.getAllByLabelText('Query text');
@@ -30,15 +22,15 @@ export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId
});
};
-export const assertQueryHistoryIsEmpty = async (exploreId = 'left') => {
- const selector = withinExplore(exploreId);
+export const assertQueryHistoryIsEmpty = async () => {
+ const selector = withinQueryHistory();
const queryTexts = selector.queryAllByLabelText('Query text');
expect(await queryTexts).toHaveLength(0);
};
-export const assertQueryHistoryComment = async (expectedQueryComments: string[], exploreId = 'left') => {
- const selector = withinExplore(exploreId);
+export const assertQueryHistoryComment = async (expectedQueryComments: string[]) => {
+ const selector = withinQueryHistory();
await waitFor(() => {
expect(selector.getByText(new RegExp(`${expectedQueryComments.length} queries`))).toBeInTheDocument();
const queryComments = selector.getAllByLabelText('Query comment');
@@ -48,25 +40,12 @@ export const assertQueryHistoryComment = async (expectedQueryComments: string[],
});
};
-export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], exploreId = 'left') => {
- const starButtons = getAllByRoleInQueryHistoryTab(exploreId, 'button', /Star query|Unstar query/);
-
- await waitFor(() =>
- expectedStars.forEach((starred, queryIndex) => {
- expect(starButtons[queryIndex]).toHaveAccessibleName(starred ? 'Unstar query' : 'Star query');
- })
- );
+export const assertQueryHistoryTabIsSelected = (tabName: 'Query history' | 'Starred' | 'Settings') => {
+ expect(withinQueryHistory().getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
};
-export const assertQueryHistoryTabIsSelected = (
- tabName: 'Query history' | 'Starred' | 'Settings',
- exploreId = 'left'
-) => {
- expect(withinExplore(exploreId).getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
-};
-
-export const assertDataSourceFilterVisibility = (visible: boolean, exploreId = 'left') => {
- const filterInput = withinExplore(exploreId).queryByLabelText('Filter queries for data sources(s)');
+export const assertDataSourceFilterVisibility = (visible: boolean) => {
+ const filterInput = withinQueryHistory().queryByLabelText('Filter queries for data sources(s)');
if (visible) {
expect(filterInput).toBeInTheDocument();
} else {
@@ -74,10 +53,10 @@ export const assertDataSourceFilterVisibility = (visible: boolean, exploreId = '
}
};
-export const assertQueryHistoryElementsShown = (shown: number, total: number, exploreId = 'left') => {
- expect(withinExplore(exploreId).queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument();
+export const assertQueryHistoryElementsShown = (shown: number, total: number) => {
+ expect(withinQueryHistory().queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument();
};
-export const assertLoadMoreQueryHistoryNotVisible = (exploreId = 'left') => {
- expect(withinExplore(exploreId).queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
+export const assertLoadMoreQueryHistoryNotVisible = () => {
+ expect(withinQueryHistory().queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
};
diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts
index 3afd11fef18..5304115ead0 100644
--- a/public/app/features/explore/spec/helper/interactions.ts
+++ b/public/app/features/explore/spec/helper/interactions.ts
@@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event';
import { selectors } from '@grafana/e2e-selectors';
-import { getAllByRoleInQueryHistoryTab, withinExplore } from './setup';
+import { getAllByRoleInQueryHistoryTab, withinExplore, withinQueryHistory } from './setup';
export const changeDatasource = async (name: string) => {
const datasourcePicker = (await screen.findByTestId(selectors.components.DataSourcePicker.container)).children[0];
@@ -25,56 +25,57 @@ export const runQuery = async (exploreId = 'left') => {
await userEvent.click(button);
};
-export const openQueryHistory = async (exploreId = 'left') => {
- const selector = withinExplore(exploreId);
- const button = selector.getByRole('button', { name: 'Query history' });
+export const openQueryHistory = async () => {
+ const explore = withinExplore('left');
+ const button = explore.getByRole('button', { name: 'Query history' });
await userEvent.click(button);
- expect(await selector.findByPlaceholderText('Search queries')).toBeInTheDocument();
+ expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument();
};
-export const closeQueryHistory = async (exploreId = 'left') => {
- const closeButton = withinExplore(exploreId).getByRole('button', { name: 'Close query history' });
+export const closeQueryHistory = async () => {
+ const selector = withinQueryHistory();
+ const closeButton = selector.getByRole('button', { name: 'Close query history' });
await userEvent.click(closeButton);
};
-export const switchToQueryHistoryTab = async (name: 'Settings' | 'Query History', exploreId = 'left') => {
- await userEvent.click(withinExplore(exploreId).getByRole('tab', { name: `Tab ${name}` }));
+export const switchToQueryHistoryTab = async (name: 'Settings' | 'Query History') => {
+ await userEvent.click(withinQueryHistory().getByRole('tab', { name: `Tab ${name}` }));
};
-export const selectStarredTabFirst = async (exploreId = 'left') => {
- const checkbox = withinExplore(exploreId).getByRole('checkbox', {
+export const selectStarredTabFirst = async () => {
+ const checkbox = withinQueryHistory().getByRole('checkbox', {
name: /Change the default active tab from “Query history” to “Starred”/,
});
await userEvent.click(checkbox);
};
-export const selectOnlyActiveDataSource = async (exploreId = 'left') => {
- const checkbox = withinExplore(exploreId).getByLabelText(/Only show queries for data source currently active.*/);
+export const selectOnlyActiveDataSource = async () => {
+ const checkbox = withinQueryHistory().getByLabelText(/Only show queries for data source currently active.*/);
await userEvent.click(checkbox);
};
-export const starQueryHistory = async (queryIndex: number, exploreId = 'left') => {
- await invokeAction(queryIndex, 'Star query', exploreId);
+export const starQueryHistory = async (queryIndex: number) => {
+ await invokeAction(queryIndex, 'Star query');
};
-export const commentQueryHistory = async (queryIndex: number, comment: string, exploreId = 'left') => {
- await invokeAction(queryIndex, 'Add comment', exploreId);
- const input = withinExplore(exploreId).getByPlaceholderText('An optional description of what the query does.');
+export const commentQueryHistory = async (queryIndex: number, comment: string) => {
+ await invokeAction(queryIndex, 'Add comment');
+ const input = withinQueryHistory().getByPlaceholderText('An optional description of what the query does.');
await userEvent.clear(input);
await userEvent.type(input, comment);
- await invokeAction(queryIndex, 'Save comment', exploreId);
+ await invokeAction(queryIndex, 'Save comment');
};
-export const deleteQueryHistory = async (queryIndex: number, exploreId = 'left') => {
- await invokeAction(queryIndex, 'Delete query', exploreId);
+export const deleteQueryHistory = async (queryIndex: number) => {
+ await invokeAction(queryIndex, 'Delete query');
};
-export const loadMoreQueryHistory = async (exploreId = 'left') => {
- const button = withinExplore(exploreId).getByRole('button', { name: 'Load more' });
+export const loadMoreQueryHistory = async () => {
+ const button = withinQueryHistory().getByRole('button', { name: 'Load more' });
await userEvent.click(button);
};
-const invokeAction = async (queryIndex: number, actionAccessibleName: string | RegExp, exploreId: string) => {
- const buttons = getAllByRoleInQueryHistoryTab(exploreId, 'button', actionAccessibleName);
+const invokeAction = async (queryIndex: number, actionAccessibleName: string | RegExp) => {
+ const buttons = getAllByRoleInQueryHistoryTab('button', actionAccessibleName);
await userEvent.click(buttons[queryIndex]);
};
diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx
index db5dfa966f5..6630602fa53 100644
--- a/public/app/features/explore/spec/helper/setup.tsx
+++ b/public/app/features/explore/spec/helper/setup.tsx
@@ -282,6 +282,11 @@ export const withinExplore = (exploreId: string) => {
return within(container[exploreId === 'left' ? 0 : 1]);
};
+export const withinQueryHistory = () => {
+ const container = screen.getByTestId('data-testid QueryHistory');
+ return within(container);
+};
+
const exploreTestsHelper: { setupExplore: typeof setupExplore; tearDownExplore?: (options?: TearDownOptions) => void } =
{
setupExplore,
@@ -291,8 +296,8 @@ const exploreTestsHelper: { setupExplore: typeof setupExplore; tearDownExplore?:
/**
* Optimized version of getAllByRole to avoid timeouts in tests. Please check #70158, #59116 and #47635, #78236.
*/
-export const getAllByRoleInQueryHistoryTab = (exploreId: string, role: ByRoleMatcher, name: string | RegExp) => {
- const selector = withinExplore(exploreId);
+export const getAllByRoleInQueryHistoryTab = (role: ByRoleMatcher, name: string | RegExp) => {
+ const selector = withinQueryHistory();
// Test ID is used to avoid test timeouts reported in
const queriesContainer = selector.getByTestId('query-history-queries-tab');
return within(queriesContainer).getAllByRole(role, { name });
diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx
index 7aed7e67ba3..66eefeea309 100644
--- a/public/app/features/explore/spec/queryHistory.test.tsx
+++ b/public/app/features/explore/spec/queryHistory.test.tsx
@@ -12,11 +12,9 @@ import {
assertLoadMoreQueryHistoryNotVisible,
assertQueryHistory,
assertQueryHistoryComment,
- assertQueryHistoryContains,
assertQueryHistoryElementsShown,
assertQueryHistoryExists,
assertQueryHistoryIsEmpty,
- assertQueryHistoryIsStarred,
assertQueryHistoryTabIsSelected,
} from './helper/assert';
import {
@@ -29,7 +27,6 @@ import {
runQuery,
selectOnlyActiveDataSource,
selectStarredTabFirst,
- starQueryHistory,
switchToQueryHistoryTab,
} from './helper/interactions';
import { makeLogsQueryResponse } from './helper/query';
@@ -141,58 +138,6 @@ describe('Explore: Query History', () => {
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}']);
});
- describe('updates the state in both Explore panes', () => {
- beforeEach(async () => {
- const urlParams = {
- left: serializeStateToUrlParam({
- datasource: 'loki',
- queries: [{ refId: 'A', expr: 'query #1' }],
- range: { from: 'now-1h', to: 'now' },
- }),
- right: serializeStateToUrlParam({
- datasource: 'loki',
- queries: [{ refId: 'A', expr: 'query #2' }],
- range: { from: 'now-1h', to: 'now' },
- }),
- };
-
- const { datasources } = setupExplore({ urlParams });
- jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
- await waitForExplore();
- await waitForExplore('right');
-
- await openQueryHistory('left');
- await openQueryHistory('right');
- });
-
- it('initial state is in sync', async () => {
- await assertQueryHistoryContains('{"expr":"query #1"}', 'left');
- await assertQueryHistoryContains('{"expr":"query #2"}', 'left');
- await assertQueryHistoryContains('{"expr":"query #1"}', 'right');
- await assertQueryHistoryContains('{"expr":"query #2"}', 'right');
- });
-
- it('starred queries are synced', async () => {
- // star one one query
- await starQueryHistory(1, 'left');
- await assertQueryHistoryIsStarred([false, true], 'left');
- await assertQueryHistoryIsStarred([false, true], 'right');
- expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_starred', {
- queryHistoryEnabled: false,
- newValue: true,
- });
- });
-
- it('deleted queries are synced', async () => {
- await deleteQueryHistory(0, 'left');
- await assertQueryHistory(['{"expr":"query #1"}'], 'left');
- await assertQueryHistory(['{"expr":"query #1"}'], 'right');
- expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_deleted', {
- queryHistoryEnabled: false,
- });
- });
- });
-
it('add comments to query history', async () => {
const urlParams = {
left: serializeStateToUrlParam({
@@ -206,9 +151,9 @@ describe('Explore: Query History', () => {
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
await waitForExplore();
await openQueryHistory();
- await assertQueryHistory(['{"expr":"query #1"}'], 'left');
+ await assertQueryHistory(['{"expr":"query #1"}']);
await commentQueryHistory(0, 'test comment');
- await assertQueryHistoryComment(['test comment'], 'left');
+ await assertQueryHistoryComment(['test comment']);
});
it('removes the query item from the history panel when user deletes a regular query', async () => {
@@ -227,13 +172,13 @@ describe('Explore: Query History', () => {
await openQueryHistory();
// queries in history
- await assertQueryHistory(['{"expr":"query #1"}'], 'left');
+ await assertQueryHistory(['{"expr":"query #1"}']);
// delete query
- await deleteQueryHistory(0, 'left');
+ await deleteQueryHistory(0);
// there was only one query in history so assert that query history is empty
- await assertQueryHistoryIsEmpty('left');
+ await assertQueryHistoryIsEmpty();
});
it('updates query history settings', async () => {
diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts
index e6feeda2995..b83eb938b3b 100644
--- a/public/app/features/explore/state/explorePane.ts
+++ b/public/app/features/explore/state/explorePane.ts
@@ -20,7 +20,6 @@ import { createAsyncThunk, ThunkResult } from 'app/types';
import { ExploreItemState } from 'app/types/explore';
import { datasourceReducer } from './datasource';
-import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction } from './main';
import { queryReducer, runQueries } from './query';
import { timeReducer, updateTime } from './time';
import {
@@ -214,23 +213,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
state = datasourceReducer(state, action);
state = timeReducer(state, action);
- if (richHistoryUpdatedAction.match(action)) {
- const { richHistory, total } = action.payload.richHistoryResults;
- return {
- ...state,
- richHistory,
- richHistoryTotal: total,
- };
- }
-
- if (richHistorySearchFiltersUpdatedAction.match(action)) {
- const richHistorySearchFilters = action.payload.filters;
- return {
- ...state,
- richHistorySearchFilters,
- };
- }
-
if (changeSizeAction.match(action)) {
const containerWidth = action.payload.width;
return { ...state, containerWidth };
diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts
index fa8348fd8dc..d582ef42878 100644
--- a/public/app/features/explore/state/history.ts
+++ b/public/app/features/explore/state/history.ts
@@ -1,3 +1,6 @@
+import { createAction } from '@reduxjs/toolkit';
+
+import { HistoryItem } from '@grafana/data';
import { DataQuery } from '@grafana/schema';
import {
addToRichHistory,
@@ -9,7 +12,7 @@ import {
updateRichHistorySettings,
updateStarredInRichHistory,
} from 'app/core/utils/richHistory';
-import { ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types';
+import { RichHistoryQuery, ThunkResult } from 'app/types';
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
@@ -21,7 +24,15 @@ import {
richHistoryStorageFullAction,
richHistoryUpdatedAction,
} from './main';
-import { selectPanesEntries } from './selectors';
+
+//
+// Actions and Payloads
+//
+
+export interface HistoryUpdatedPayload {
+ history: HistoryItem[];
+}
+export const historyUpdatedAction = createAction('explore/historyUpdated');
//
// Action creators
@@ -37,27 +48,23 @@ type SyncHistoryUpdatesOptions = {
*/
const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesOptions): ThunkResult => {
return async (dispatch, getState) => {
- forEachExplorePane(getState().explore, (item, exploreId) => {
- const newRichHistory = item.richHistory
- // update
- .map((query) => (query.id === updatedQuery?.id ? updatedQuery : query))
- // or remove
- .filter((query) => query.id !== deletedId);
- const deletedItems = item.richHistory.length - newRichHistory.length;
- dispatch(
- richHistoryUpdatedAction({
- richHistoryResults: { richHistory: newRichHistory, total: item.richHistoryTotal! - deletedItems },
- exploreId,
- })
- );
- });
- };
-};
+ const richHistory = getState().explore.richHistory;
-const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: string) => void) => {
- Object.entries(state.panes).forEach(([exploreId, item]) => {
- item && callback(item, exploreId);
- });
+ // update or remove entries
+ const newRichHistory = richHistory
+ .map((query) => (query.id === updatedQuery?.id ? updatedQuery : query))
+ .filter((query) => query.id !== deletedId);
+
+ const deletedItems = richHistory.length - newRichHistory.length;
+ dispatch(
+ richHistoryUpdatedAction({
+ richHistoryResults: {
+ richHistory: newRichHistory,
+ total: getState().explore.richHistoryTotal! - deletedItems,
+ },
+ })
+ );
+ };
};
export const addHistoryItem = (
@@ -114,45 +121,41 @@ export const deleteHistoryItem = (id: string): ThunkResult => {
};
export const deleteRichHistory = (): ThunkResult => {
- return async (dispatch, getState) => {
+ return async (dispatch) => {
await deleteAllFromRichHistory();
- selectPanesEntries(getState()).forEach(([exploreId]) => {
- dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
- dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
- });
+ dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 } }));
+ dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 } }));
};
};
-export const loadRichHistory = (exploreId: string): ThunkResult => {
+export const loadRichHistory = (): ThunkResult => {
return async (dispatch, getState) => {
- const filters = getState().explore.panes[exploreId]!.richHistorySearchFilters;
+ const filters = getState().explore.richHistorySearchFilters;
if (filters) {
const richHistoryResults = await getRichHistory(filters);
- dispatch(richHistoryUpdatedAction({ richHistoryResults, exploreId }));
+ dispatch(richHistoryUpdatedAction({ richHistoryResults }));
}
};
};
-export const loadMoreRichHistory = (exploreId: string): ThunkResult => {
+export const loadMoreRichHistory = (): ThunkResult => {
return async (dispatch, getState) => {
- const currentFilters = getState().explore.panes[exploreId]?.richHistorySearchFilters;
- const currentRichHistory = getState().explore.panes[exploreId]?.richHistory;
+ const currentFilters = getState().explore.richHistorySearchFilters;
+ const currentRichHistory = getState().explore.richHistory;
if (currentFilters && currentRichHistory) {
const nextFilters = { ...currentFilters, page: (currentFilters?.page || 1) + 1 };
const moreRichHistory = await getRichHistory(nextFilters);
const richHistory = [...currentRichHistory, ...moreRichHistory.richHistory];
- dispatch(richHistorySearchFiltersUpdatedAction({ filters: nextFilters, exploreId }));
- dispatch(
- richHistoryUpdatedAction({ richHistoryResults: { richHistory, total: moreRichHistory.total }, exploreId })
- );
+ dispatch(richHistorySearchFiltersUpdatedAction({ filters: nextFilters }));
+ dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory, total: moreRichHistory.total } }));
}
};
};
-export const clearRichHistoryResults = (exploreId: string): ThunkResult => {
+export const clearRichHistoryResults = (): ThunkResult => {
return async (dispatch) => {
- dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId }));
- dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
+ dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined }));
+ dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 } }));
};
};
@@ -180,9 +183,9 @@ export const updateHistorySettings = (settings: RichHistorySettings): ThunkResul
/**
* Assumed this can be called only when settings and filters are initialised
*/
-export const updateHistorySearchFilters = (exploreId: string, filters: RichHistorySearchFilters): ThunkResult => {
+export const updateHistorySearchFilters = (filters: RichHistorySearchFilters): ThunkResult => {
return async (dispatch, getState) => {
- await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } }));
+ await dispatch(richHistorySearchFiltersUpdatedAction({ filters: { ...filters } }));
const currentSettings = getState().explore.richHistorySettings!;
if (supportedFeatures().lastUsedDataSourcesAvailable) {
await dispatch(
diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts
index 7fb7f02622f..07c234fa634 100644
--- a/public/app/features/explore/state/main.ts
+++ b/public/app/features/explore/state/main.ts
@@ -26,7 +26,7 @@ export interface SyncTimesPayload {
}
export const syncTimesAction = createAction('explore/syncTimes');
-export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults; exploreId: string }>(
+export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults }>(
'explore/richHistoryUpdated'
);
export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
@@ -34,7 +34,6 @@ export const richHistoryLimitExceededAction = createAction('explore/richHistoryL
export const richHistorySettingsUpdatedAction = createAction('explore/richHistorySettingsUpdated');
export const richHistorySearchFiltersUpdatedAction = createAction<{
- exploreId: string;
filters?: RichHistorySearchFilters;
}>('explore/richHistorySearchFiltersUpdatedAction');
@@ -125,6 +124,8 @@ export const changeCorrelationEditorDetails = createAction('explore/changeShowQueryHistory');
+
export interface NavigateToExploreDependencies {
timeRange: TimeRange;
getExploreUrl: (args: GetExploreUrlArguments) => Promise;
@@ -168,6 +169,8 @@ export const initialExploreState: ExploreState = {
largerExploreId: undefined,
maxedExploreId: undefined,
evenSplitPanes: true,
+ showQueryHistory: false,
+ richHistory: [],
};
/**
@@ -243,6 +246,23 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
+ if (richHistoryUpdatedAction.match(action)) {
+ const { richHistory, total } = action.payload.richHistoryResults;
+ return {
+ ...state,
+ richHistory,
+ richHistoryTotal: total,
+ };
+ }
+
+ if (richHistorySearchFiltersUpdatedAction.match(action)) {
+ const richHistorySearchFilters = action.payload.filters;
+ return {
+ ...state,
+ richHistorySearchFilters,
+ };
+ }
+
if (createNewSplitOpenPane.pending.match(action)) {
return {
...state,
@@ -303,6 +323,13 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
+ if (changeShowQueryHistory.match(action)) {
+ return {
+ ...state,
+ showQueryHistory: action.payload,
+ };
+ }
+
const exploreId: string | undefined = action.payload?.exploreId;
if (typeof exploreId === 'string') {
return {
diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts
index 320f1b47108..d6e2d017512 100644
--- a/public/app/features/explore/state/query.ts
+++ b/public/app/features/explore/state/query.ts
@@ -494,10 +494,7 @@ async function handleHistory(
// Because filtering happens in the backend we cannot add a new entry without checking if it matches currently
// used filters. Instead, we refresh the query history list.
- // TODO: run only if Query History list is opened (#47252)
- for (const exploreId in state.panes) {
- await dispatch(loadRichHistory(exploreId));
- }
+ await dispatch(loadRichHistory());
}
interface RunQueriesOptions {
diff --git a/public/app/features/explore/state/selectors.test.ts b/public/app/features/explore/state/selectors.test.ts
new file mode 100644
index 00000000000..8fda241937b
--- /dev/null
+++ b/public/app/features/explore/state/selectors.test.ts
@@ -0,0 +1,123 @@
+import { DataSourceApi, DataSourceJsonData } from '@grafana/data';
+import { DataQuery } from '@grafana/schema/dist/esm/index';
+import { configureStore } from 'app/store/configureStore';
+import { StoreState, ThunkDispatch } from 'app/types';
+
+import { createDefaultInitialState } from './helpers';
+import { selectExploreDSMaps } from './selectors';
+
+const { defaultInitialState } = createDefaultInitialState();
+
+const datasources: DataSourceApi[] = [
+ {
+ name: 'testDs',
+ type: 'postgres',
+ uid: 'ds1',
+ getRef: () => {
+ return { type: 'postgres', uid: 'ds1' };
+ },
+ } as DataSourceApi,
+ {
+ name: 'testDs2',
+ type: 'mysql',
+ uid: 'ds2',
+ getRef: () => {
+ return { type: 'mysql', uid: 'ds2' };
+ },
+ } as DataSourceApi,
+];
+
+describe('selectExploreDSMaps', () => {
+ it('returns datasource information as empty with empty state', () => {
+ const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore();
+
+ const dsMaps = selectExploreDSMaps(store.getState());
+ expect(dsMaps.dsToExplore).toEqual([]);
+ expect(dsMaps.exploreToDS).toEqual([]);
+ });
+
+ it('returns root datasources from 2 panes with empty queries', () => {
+ const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
+ ...defaultInitialState,
+ explore: {
+ panes: {
+ left: {
+ ...defaultInitialState.explore.panes.left,
+ datasourceInstance: datasources[0],
+ queries: [],
+ },
+ right: {
+ ...defaultInitialState.explore.panes.left,
+ datasourceInstance: datasources[1],
+ queries: [],
+ },
+ },
+ },
+ } as unknown as Partial);
+
+ const dsMaps = selectExploreDSMaps(store.getState());
+ expect(dsMaps.dsToExplore.length).toEqual(2);
+
+ // ds 1
+ expect(dsMaps.dsToExplore[0].datasource.uid).toEqual('ds1');
+ expect(dsMaps.dsToExplore[0].exploreIds.length).toEqual(1);
+ expect(dsMaps.dsToExplore[0].exploreIds[0]).toEqual('left');
+ // ds 2
+ expect(dsMaps.dsToExplore[1].datasource.uid).toEqual('ds2');
+ expect(dsMaps.dsToExplore[1].exploreIds.length).toEqual(1);
+ expect(dsMaps.dsToExplore[1].exploreIds[0]).toEqual('right');
+
+ expect(dsMaps.exploreToDS.length).toEqual(2);
+ // pane 1
+ expect(dsMaps.exploreToDS[0].exploreId).toEqual('left');
+ expect(dsMaps.exploreToDS[0].datasources.length).toEqual(1);
+ expect(dsMaps.exploreToDS[0].datasources[0].uid).toEqual('ds1');
+ //pane 2
+ expect(dsMaps.exploreToDS[1].exploreId).toEqual('right');
+ expect(dsMaps.exploreToDS[1].datasources.length).toEqual(1);
+ expect(dsMaps.exploreToDS[1].datasources[0].uid).toEqual('ds2');
+ });
+
+ it('returns all datasources from 2 panes with queries', () => {
+ const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
+ ...defaultInitialState,
+ explore: {
+ panes: {
+ different: {
+ ...defaultInitialState.explore.panes.left,
+ datasourceInstance: datasources[0],
+ queries: [{ datasource: datasources[1] }],
+ },
+ match: {
+ ...defaultInitialState.explore.panes.left,
+ datasourceInstance: datasources[1],
+ queries: [{ datasource: datasources[1] }],
+ },
+ },
+ },
+ } as unknown as Partial);
+
+ const dsMaps = selectExploreDSMaps(store.getState());
+ expect(dsMaps.dsToExplore.length).toEqual(2);
+ // ds 1
+ expect(dsMaps.dsToExplore[0].datasource.uid).toEqual('ds1');
+ expect(dsMaps.dsToExplore[0].exploreIds.length).toEqual(1);
+ expect(dsMaps.dsToExplore[0].exploreIds[0]).toEqual('different');
+ // ds2
+ expect(dsMaps.dsToExplore[1].datasource.uid).toEqual('ds2');
+ expect(dsMaps.dsToExplore[1].exploreIds.length).toEqual(2);
+ expect(dsMaps.dsToExplore[1].exploreIds[0]).toEqual('different');
+ expect(dsMaps.dsToExplore[1].exploreIds[1]).toEqual('match');
+
+ expect(dsMaps.exploreToDS.length).toEqual(2);
+ // pane 1
+ expect(dsMaps.exploreToDS[0].exploreId).toEqual('different');
+ expect(dsMaps.exploreToDS[0].datasources.length).toEqual(2);
+ expect(dsMaps.exploreToDS[0].datasources[0].uid).toEqual('ds1');
+ expect(dsMaps.exploreToDS[0].datasources[1].uid).toEqual('ds2');
+ // pane 2
+ expect(dsMaps.exploreToDS[1].exploreId).toEqual('match');
+ expect(dsMaps.exploreToDS[1].datasources.length).toEqual(1);
+ expect(dsMaps.exploreToDS[1].datasources[0].uid).toEqual('ds2');
+ });
+});
diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts
index a836e174270..a475e8b5b04 100644
--- a/public/app/features/explore/state/selectors.ts
+++ b/public/app/features/explore/state/selectors.ts
@@ -1,5 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
+import { flatten, uniqBy } from 'lodash';
+import { DataSourceRef } from '@grafana/schema';
import { ExploreItemState, StoreState } from 'app/types';
export const selectPanes = (state: Pick) => state.explore.panes;
@@ -23,3 +25,43 @@ export const isLeftPaneSelector = (exploreId: string) =>
export const getExploreItemSelector = (exploreId: string) => createSelector(selectPanes, (panes) => panes[exploreId]);
export const selectCorrelationDetails = createSelector(selectExploreRoot, (state) => state.correlationEditorDetails);
+
+export const selectShowQueryHistory = createSelector(selectExploreRoot, (state) => state.showQueryHistory);
+
+export const selectExploreDSMaps = createSelector(selectPanesEntries, (panes) => {
+ const exploreDSMap = panes
+ .map(([exploreId, pane]) => {
+ const rootDatasource = [pane?.datasourceInstance?.getRef()];
+ const queryDatasources = pane?.queries.map((q) => q.datasource) || [];
+ const datasources = [...rootDatasource, ...queryDatasources].filter(
+ (datasource): datasource is DataSourceRef => !!datasource
+ );
+
+ if (datasources === undefined || datasources.length === 0) {
+ return undefined;
+ } else {
+ return {
+ exploreId,
+ datasources: uniqBy(datasources, (ds) => ds.uid),
+ };
+ }
+ })
+ .filter((pane): pane is { exploreId: string; datasources: DataSourceRef[] } => !!pane);
+
+ const uniqueDataSources = uniqBy(flatten(exploreDSMap.map((pane) => pane.datasources)), (ds) => ds.uid);
+
+ const dsToExploreMap = uniqueDataSources.map((ds) => {
+ let exploreIds: string[] = [];
+ exploreDSMap.forEach((eds) => {
+ if (eds.datasources.some((edsDs) => edsDs.uid === ds.uid)) {
+ exploreIds.push(eds.exploreId);
+ }
+ });
+ return {
+ datasource: ds,
+ exploreIds: exploreIds,
+ };
+ });
+
+ return { exploreToDS: exploreDSMap, dsToExplore: dsToExploreMap };
+});
diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts
index 0918049c03e..9ee49059123 100644
--- a/public/app/features/explore/state/utils.ts
+++ b/public/app/features/explore/state/utils.ts
@@ -75,7 +75,6 @@ export const makeExplorePaneState = (overrides?: Partial): Exp
rawPrometheusResult: null,
eventBridge: null as unknown as EventBusExtended,
cache: [],
- richHistory: [],
supplementaryQueries: loadSupplementaryQueries(),
panelsState: {},
correlations: undefined,
diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts
index e0b5afaa0df..3f91e26c6d4 100644
--- a/public/app/types/explore.ts
+++ b/public/app/types/explore.ts
@@ -63,6 +63,18 @@ export interface ExploreState {
panes: Record;
+ /**
+ * Is the drawer for query history showing
+ */
+ showQueryHistory: boolean;
+
+ /**
+ * History of all queries
+ */
+ richHistory: RichHistoryQuery[];
+ richHistorySearchFilters?: RichHistorySearchFilters;
+ richHistoryTotal?: number;
+
/**
* Settings for rich history (note: filters are stored per each pane separately)
*/
@@ -206,13 +218,6 @@ export interface ExploreItemState {
showFlameGraph?: boolean;
showCustom?: boolean;
- /**
- * History of all queries
- */
- richHistory: RichHistoryQuery[];
- richHistorySearchFilters?: RichHistorySearchFilters;
- richHistoryTotal?: number;
-
/**
* We are using caching to store query responses of queries run from logs navigation.
* In logs navigation, we do pagination and we don't want our users to unnecessarily run the same queries that they've run just moments before.
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index b037d9e8b35..dc683b42f36 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -446,10 +446,11 @@
"delete-query-tooltip": "Delete query",
"delete-starred-query-confirmation-text": "Are you sure you want to permanently delete your starred query?",
"edit-comment-tooltip": "Edit comment",
- "loading-text": "loading...",
+ "left-pane": "Left pane",
"optional-description": "An optional description of what the query does.",
"query-comment-label": "Query comment",
"query-text-label": "Query text",
+ "right-pane": "Right pane",
"run-query-button": "Run query",
"save-comment": "Save comment",
"star-query-tooltip": "Star query",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index fe9435e5c32..c6df2a9ed4b 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -446,10 +446,11 @@
"delete-query-tooltip": "Đęľęŧę qūęřy",
"delete-starred-query-confirmation-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő pęřmäʼnęʼnŧľy đęľęŧę yőūř şŧäřřęđ qūęřy?",
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
- "loading-text": "ľőäđįʼnģ...",
+ "left-pane": "Ŀęƒŧ päʼnę",
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
"query-comment-label": "Qūęřy čőmmęʼnŧ",
"query-text-label": "Qūęřy ŧęχŧ",
+ "right-pane": "Ŗįģĥŧ päʼnę",
"run-query-button": "Ŗūʼn qūęřy",
"save-comment": "Ŝävę čőmmęʼnŧ",
"star-query-tooltip": "Ŝŧäř qūęřy",