diff --git a/.betterer.results b/.betterer.results index e8cf5c8ca7e..98144b86045 100644 --- a/.betterer.results +++ b/.betterer.results @@ -179,7 +179,7 @@ exports[`no enzyme tests`] = { "public/app/features/dimensions/editors/ThresholdsEditor/ThresholdsEditor.test.tsx:4164297658": [ [0, 17, 13, "RegExp match", "2409514259"] ], - "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3420464349": [ + "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:523695501": [ [0, 17, 13, "RegExp match", "2409514259"] ], "public/app/features/folders/FolderSettingsPage.test.tsx:1109052730": [ diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index 5a52a6cd746..08566735036 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -86,7 +86,7 @@ describe('RichHistoryLocalStorage', () => { it('should save query history to localStorage', async () => { await storage.addToRichHistory(mockItem); expect(store.exists(key)).toBeTruthy(); - expect(await storage.getRichHistory(mockFilters)).toMatchObject([mockItem]); + expect((await storage.getRichHistory(mockFilters)).richHistory).toMatchObject([mockItem]); }); it('should not save duplicated query to localStorage', async () => { @@ -95,25 +95,25 @@ describe('RichHistoryLocalStorage', () => { await expect(async () => { await storage.addToRichHistory(mockItem2); }).rejects.toThrow('Entry already exists'); - expect(await storage.getRichHistory(mockFilters)).toMatchObject([mockItem2, mockItem]); + expect((await storage.getRichHistory(mockFilters)).richHistory).toMatchObject([mockItem2, mockItem]); }); it('should update starred in localStorage', async () => { await storage.addToRichHistory(mockItem); await storage.updateStarred(mockItem.id, false); - expect((await storage.getRichHistory(mockFilters))[0].starred).toEqual(false); + expect((await storage.getRichHistory(mockFilters)).richHistory[0].starred).toEqual(false); }); it('should update comment in localStorage', async () => { await storage.addToRichHistory(mockItem); await storage.updateComment(mockItem.id, 'new comment'); - expect((await storage.getRichHistory(mockFilters))[0].comment).toEqual('new comment'); + expect((await storage.getRichHistory(mockFilters)).richHistory[0].comment).toEqual('new comment'); }); it('should delete query in localStorage', async () => { await storage.addToRichHistory(mockItem); await storage.deleteRichHistory(mockItem.id); - expect(await storage.getRichHistory(mockFilters)).toEqual([]); + expect((await storage.getRichHistory(mockFilters)).richHistory).toEqual([]); expect(store.getObject(key)).toEqual([]); }); @@ -172,7 +172,7 @@ describe('RichHistoryLocalStorage', () => { queries: [{ refId: 'ref' }], }; await storage.addToRichHistory(historyNew); - const richHistory = await storage.getRichHistory({ + const { richHistory } = await storage.getRichHistory({ search: '', sortOrder: SortOrder.Descending, datasourceFilters: [], @@ -264,8 +264,9 @@ describe('RichHistoryLocalStorage', () => { ], }; - const result = await storage.getRichHistory(mockFilters); - expect(result).toStrictEqual([expectedHistoryItem]); + const { richHistory, total } = await storage.getRichHistory(mockFilters); + expect(richHistory).toStrictEqual([expectedHistoryItem]); + expect(total).toBe(1); }); it('should load when queries are json-encoded strings', async () => { @@ -299,8 +300,9 @@ describe('RichHistoryLocalStorage', () => { ], }; - const result = await storage.getRichHistory(mockFilters); - expect(result).toStrictEqual([expectedHistoryItem]); + const { richHistory, total } = await storage.getRichHistory(mockFilters); + expect(richHistory).toStrictEqual([expectedHistoryItem]); + expect(total).toBe(1); }); }); }); diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 61174bbc9d4..a73cf32e5f6 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -37,10 +37,11 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage { const allQueries = getRichHistoryDTOs().map(fromDTO); const queries = filters.starred ? allQueries.filter((q) => q.starred === true) : allQueries; - return filterAndSortQueries(queries, filters.sortOrder, filters.datasourceFilters, filters.search, [ + const richHistory = filterAndSortQueries(queries, filters.sortOrder, filters.datasourceFilters, filters.search, [ filters.from, filters.to, ]); + return { richHistory, total: richHistory.length }; } async addToRichHistory(newRichHistoryQuery: Omit) { diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts index 0471a4cfd27..6ad95e63483 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.test.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -78,6 +78,7 @@ describe('RichHistoryRemoteStorage', () => { data: { result: { queryHistory: returnedDTOs, + totalCount: returnedDTOs.length, }, }, }) @@ -91,14 +92,22 @@ describe('RichHistoryRemoteStorage', () => { const expectedLimit = 100; const expectedPage = 1; - const items = await storage.getRichHistory({ search, datasourceFilters, sortOrder, starred, to, from }); + const { richHistory, total } = await storage.getRichHistory({ + search, + datasourceFilters, + sortOrder, + starred, + to, + from, + }); expect(fetchMock).toBeCalledWith({ method: 'GET', url: `/api/query-history?datasourceUid=ds1&datasourceUid=ds2&searchString=${search}&sort=time-desc&to=now-${from}d&from=now-${to}d&limit=${expectedLimit}&page=${expectedPage}&onlyStarred=${starred}`, requestId: 'query-history-get-all', }); - expect(items).toMatchObject([richHistoryQuery]); + expect(richHistory).toMatchObject([richHistoryQuery]); + expect(total).toBe(1); }); it('read starred home tab preferences', async () => { diff --git a/public/app/core/history/RichHistoryRemoteStorage.ts b/public/app/core/history/RichHistoryRemoteStorage.ts index eafcc9eea85..514fe7bfb1f 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.ts @@ -34,6 +34,7 @@ type RichHistoryRemoteStorageMigrationPayloadDTO = { type RichHistoryRemoteStorageResultsPayloadDTO = { result: { queryHistory: RichHistoryRemoteStorageDTO[]; + totalCount: number; }; }; @@ -64,8 +65,9 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage { throw new Error('not supported yet'); } - async getRichHistory(filters: RichHistorySearchFilters): Promise { + async getRichHistory(filters: RichHistorySearchFilters) { const params = buildQueryParams(filters); + const queryHistory = await lastValueFrom( getBackendSrv().fetch({ method: 'GET', @@ -74,7 +76,12 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage { requestId: 'query-history-get-all', }) ); - return ((queryHistory.data as RichHistoryRemoteStorageResultsPayloadDTO).result.queryHistory || []).map(fromDTO); + + const data = queryHistory.data as RichHistoryRemoteStorageResultsPayloadDTO; + const richHistory = (data.result.queryHistory || []).map(fromDTO); + const total = data.result.totalCount || 0; + + return { richHistory, total }; } async getSettings(): Promise { @@ -137,7 +144,7 @@ function buildQueryParams(filters: RichHistorySearchFilters): string { params = params + `&to=${relativeFrom}`; params = params + `&from=${relativeTo}`; params = params + `&limit=100`; - params = params + `&page=1`; + params = params + `&page=${filters.page || 1}`; if (filters.starred) { params = params + `&onlyStarred=${filters.starred}`; } diff --git a/public/app/core/history/RichHistoryStorage.ts b/public/app/core/history/RichHistoryStorage.ts index 0dbc27ce4a7..6003fddd492 100644 --- a/public/app/core/history/RichHistoryStorage.ts +++ b/public/app/core/history/RichHistoryStorage.ts @@ -28,12 +28,14 @@ export type RichHistoryStorageWarningDetails = { message: string; }; +export type RichHistoryResults = { richHistory: RichHistoryQuery[]; total?: number }; + /** * @internal * @alpha */ export default interface RichHistoryStorage { - getRichHistory(filters: RichHistorySearchFilters): Promise; + getRichHistory(filters: RichHistorySearchFilters): Promise; /** * Creates new RichHistoryQuery, returns object with unique id and created date diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index fd53f88ea48..cff9ebe501e 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -184,11 +184,11 @@ describe('richHistory', () => { }); it('migrates history', async () => { - const history = [{ id: 'test' }, { id: 'test2' }]; + const history = { richHistory: [{ id: 'test' }, { id: 'test2' }], total: 2 }; richHistoryLocalStorageMock.getRichHistory.mockReturnValue(history); await migrateQueryHistoryFromLocalStorage(); - expect(richHistoryRemoteStorageMock.migrate).toBeCalledWith(history); + expect(richHistoryRemoteStorageMock.migrate).toBeCalledWith(history.richHistory); }); it('does not migrate if there are no entries', async () => { richHistoryLocalStorageMock.getRichHistory.mockReturnValue([]); diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index dc3c48d7fd8..2b86d4305fd 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -15,6 +15,7 @@ import { RichHistoryQuery } from 'app/types/explore'; import RichHistoryLocalStorage from '../history/RichHistoryLocalStorage'; import RichHistoryRemoteStorage from '../history/RichHistoryRemoteStorage'; import { + RichHistoryResults, RichHistoryServiceError, RichHistoryStorageWarning, RichHistoryStorageWarningDetails, @@ -80,7 +81,7 @@ export async function addToRichHistory( return {}; } -export async function getRichHistory(filters: RichHistorySearchFilters): Promise { +export async function getRichHistory(filters: RichHistorySearchFilters): Promise { return await getRichHistoryStorage().getRichHistory(filters); } @@ -135,7 +136,7 @@ export async function migrateQueryHistoryFromLocalStorage(): Promise) => { height: 100, activeDatasourceInstance: 'Test datasource', richHistory: [], + richHistoryTotal: 0, firstTab: Tabs.RichHistory, deleteRichHistory: jest.fn(), loadRichHistory: jest.fn(), + loadMoreRichHistory: jest.fn(), clearRichHistoryResults: jest.fn(), onClose: jest.fn(), richHistorySearchFilters: { diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index cd441bb97d0..462104a4b3b 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -28,11 +28,13 @@ export const getSortOrderOptions = () => export interface RichHistoryProps extends Themeable { richHistory: RichHistoryQuery[]; + richHistoryTotal?: number; richHistorySettings: RichHistorySettings; richHistorySearchFilters?: RichHistorySearchFilters; updateHistorySettings: (settings: RichHistorySettings) => void; updateHistorySearchFilters: (exploreId: ExploreId, filters: RichHistorySearchFilters) => void; loadRichHistory: (exploreId: ExploreId) => void; + loadMoreRichHistory: (exploreId: ExploreId) => void; clearRichHistoryResults: (exploreId: ExploreId) => void; deleteRichHistory: () => void; activeDatasourceInstance: string; @@ -59,6 +61,7 @@ class UnThemedRichHistory extends PureComponent { const filters = { ...this.props.richHistorySearchFilters!, ...filtersToUpdate, + page: 1, // always load fresh results when updating filters }; this.props.updateHistorySearchFilters(this.props.exploreId, filters); this.loadRichHistory(); @@ -96,8 +99,16 @@ class UnThemedRichHistory extends PureComponent { } render() { - const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab, activeDatasourceInstance } = - this.props; + const { + richHistory, + richHistoryTotal, + height, + exploreId, + deleteRichHistory, + onClose, + firstTab, + activeDatasourceInstance, + } = this.props; const { loading } = this.state; const QueriesTab: TabConfig = { @@ -106,9 +117,11 @@ class UnThemedRichHistory extends PureComponent { content: ( this.props.clearRichHistoryResults(this.props.exploreId)} + loadMoreRichHistory={() => this.props.loadMoreRichHistory(this.props.exploreId)} activeDatasourceInstance={activeDatasourceInstance} richHistorySettings={this.props.richHistorySettings} richHistorySearchFilters={this.props.richHistorySearchFilters} @@ -125,10 +138,12 @@ class UnThemedRichHistory extends PureComponent { content: ( this.props.clearRichHistoryResults(this.props.exploreId)} + loadMoreRichHistory={() => this.props.loadMoreRichHistory(this.props.exploreId)} richHistorySettings={this.props.richHistorySettings} richHistorySearchFilters={this.props.richHistorySearchFilters} exploreId={exploreId} diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx index 83cc100d9db..dfe279eca66 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx @@ -29,6 +29,7 @@ const setup = (propOverrides?: Partial) => { deleteRichHistory: jest.fn(), initRichHistory: jest.fn(), loadRichHistory: jest.fn(), + loadMoreRichHistory: jest.fn(), clearRichHistoryResults: jest.fn(), updateHistorySearchFilters: jest.fn(), updateHistorySettings: jest.fn(), @@ -47,6 +48,7 @@ const setup = (propOverrides?: Partial) => { activeDatasourceOnly: true, lastUsedDatasourceFilters: [], }, + richHistoryTotal: 0, }; Object.assign(props, propOverrides); diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index 9715b9b236d..099cea64304 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -13,6 +13,7 @@ import { deleteRichHistory, initRichHistory, loadRichHistory, + loadMoreRichHistory, clearRichHistoryResults, updateHistorySettings, updateHistorySearchFilters, @@ -30,9 +31,10 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI const richHistorySettings = explore.richHistorySettings; const { datasourceInstance } = item; const firstTab = richHistorySettings?.starredTabAsFirstTab ? Tabs.Starred : Tabs.RichHistory; - const { richHistory } = item; + const { richHistory, richHistoryTotal } = item; return { richHistory, + richHistoryTotal, firstTab, activeDatasourceInstance: datasourceInstance!.name, richHistorySettings, @@ -43,6 +45,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI const mapDispatchToProps = { initRichHistory, loadRichHistory, + loadMoreRichHistory, clearRichHistoryResults, updateHistorySettings, updateHistorySearchFilters, @@ -64,6 +67,7 @@ export function RichHistoryContainer(props: Props) { const { richHistory, + richHistoryTotal, width, firstTab, activeDatasourceInstance, @@ -71,6 +75,7 @@ export function RichHistoryContainer(props: Props) { deleteRichHistory, initRichHistory, loadRichHistory, + loadMoreRichHistory, clearRichHistoryResults, richHistorySettings, updateHistorySettings, @@ -96,6 +101,7 @@ export function RichHistoryContainer(props: Props) { > diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 88c11c323e1..fb35ca34ea4 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -3,7 +3,7 @@ import React, { useEffect } from 'react'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { FilterInput, MultiSelect, RangeSlider, Select, stylesFactory, useTheme } from '@grafana/ui'; +import { Button, FilterInput, MultiSelect, RangeSlider, Select, stylesFactory, useTheme } from '@grafana/ui'; import { createDatasourcesList, mapNumbertoTimeInSlider, @@ -19,10 +19,12 @@ import RichHistoryCard from './RichHistoryCard'; export interface Props { queries: RichHistoryQuery[]; + totalQueries: number; loading: boolean; activeDatasourceInstance: string; updateFilters: (filtersToUpdate?: Partial) => void; clearRichHistoryResults: () => void; + loadMoreRichHistory: () => void; richHistorySettings: RichHistorySettings; richHistorySearchFilters?: RichHistorySearchFilters; exploreId: ExploreId; @@ -121,10 +123,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { export function RichHistoryQueriesTab(props: Props) { const { queries, + totalQueries, loading, richHistorySearchFilters, updateFilters, clearRichHistoryResults, + loadMoreRichHistory, richHistorySettings, exploreId, height, @@ -166,6 +170,7 @@ export function RichHistoryQueriesTab(props: Props) { */ const mappedQueriesToHeadings = mapQueriesToHeadings(queries, richHistorySearchFilters.sortOrder); const sortOrderOptions = getSortOrderOptions(); + const partialResults = queries.length && queries.length !== totalQueries; return (
@@ -231,7 +236,11 @@ export function RichHistoryQueriesTab(props: Props) { return (
- {heading} {mappedQueriesToHeadings[heading].length} queries + {heading}{' '} + + {partialResults ? 'Displaying ' : ''} + {mappedQueriesToHeadings[heading].length} queries +
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => { const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName); @@ -248,6 +257,11 @@ export function RichHistoryQueriesTab(props: Props) {
); })} + {partialResults ? ( +
+ Showing {queries.length} of {totalQueries} +
+ ) : null}
{!config.queryHistoryEnabled ? 'The history is local to your browser and is not shared with others.' : ''}
diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx index 9d4e0e5f193..e0434713f69 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx @@ -22,8 +22,10 @@ const setup = (activeDatasourceOnly = false) => { const props: Props = { queries: [], loading: false, + totalQueries: 0, activeDatasourceInstance: {} as any, updateFilters: jest.fn(), + loadMoreRichHistory: jest.fn(), clearRichHistoryResults: jest.fn(), exploreId: ExploreId.left, richHistorySettings: { diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 9b98172420c..012a9b166ce 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -3,7 +3,7 @@ import React, { useEffect } from 'react'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { stylesFactory, useTheme, Select, MultiSelect, FilterInput } from '@grafana/ui'; +import { stylesFactory, useTheme, Select, MultiSelect, FilterInput, Button } from '@grafana/ui'; import { createDatasourcesList, SortOrder, @@ -17,10 +17,12 @@ import RichHistoryCard from './RichHistoryCard'; export interface Props { queries: RichHistoryQuery[]; + totalQueries: number; loading: boolean; activeDatasourceInstance: string; updateFilters: (filtersToUpdate: Partial) => void; clearRichHistoryResults: () => void; + loadMoreRichHistory: () => void; richHistorySearchFilters?: RichHistorySearchFilters; richHistorySettings: RichHistorySettings; exploreId: ExploreId; @@ -74,9 +76,11 @@ export function RichHistoryStarredTab(props: Props) { const { updateFilters, clearRichHistoryResults, + loadMoreRichHistory, activeDatasourceInstance, richHistorySettings, queries, + totalQueries, loading, richHistorySearchFilters, exploreId, @@ -161,6 +165,11 @@ export function RichHistoryStarredTab(props: Props) { /> ); })} + {queries.length && queries.length !== totalQueries ? ( +
+ Showing {queries.length} of {totalQueries} +
+ ) : null}
{!config.queryHistoryEnabled ? 'The history is local to your browser and is not shared with others.' : ''}
diff --git a/public/app/features/explore/spec/helper/assert.ts b/public/app/features/explore/spec/helper/assert.ts index 2650ba50575..56fbe497e42 100644 --- a/public/app/features/explore/spec/helper/assert.ts +++ b/public/app/features/explore/spec/helper/assert.ts @@ -15,7 +15,7 @@ export const assertQueryHistoryExists = async (query: string, exploreId: Explore export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId: ExploreId = ExploreId.left) => { const selector = withinExplore(exploreId); await waitFor(() => { - expect(selector.getByText(`${expectedQueryTexts.length} queries`)).toBeInTheDocument(); + expect(selector.getByText(new RegExp(`${expectedQueryTexts.length} queries`))).toBeInTheDocument(); const queryTexts = selector.getAllByLabelText('Query text'); expectedQueryTexts.forEach((expectedQueryText, queryIndex) => { expect(queryTexts[queryIndex]).toHaveTextContent(expectedQueryText); @@ -48,3 +48,15 @@ export const assertDataSourceFilterVisibility = (visible: boolean, exploreId: Ex expect(filterInput).not.toBeInTheDocument(); } }; + +export const assertQueryHistoryElementsShown = ( + shown: number, + total: number, + exploreId: ExploreId = ExploreId.left +) => { + expect(withinExplore(exploreId).queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument(); +}; + +export const assertLoadMoreQueryHistoryNotVisible = (exploreId: ExploreId = ExploreId.left) => { + expect(withinExplore(exploreId).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 0a10d3fde9f..565e868bca7 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -66,6 +66,11 @@ export const deleteQueryHistory = (queryIndex: number, exploreId: ExploreId = Ex invokeAction(queryIndex, 'Delete query', exploreId); }; +export const loadMoreQueryHistory = async (exploreId: ExploreId = ExploreId.left) => { + const button = withinExplore(exploreId).getByRole('button', { name: 'Load more' }); + await userEvent.click(button); +}; + const invokeAction = async (queryIndex: number, actionAccessibleName: string, exploreId: ExploreId) => { const selector = withinExplore(exploreId); const buttons = selector.getAllByRole('button', { name: actionAccessibleName }); diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 2133c9ca5b3..1e20d654f7e 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -12,6 +12,7 @@ import { Echo } from 'app/core/services/echo/Echo'; import { configureStore } from 'app/store/configureStore'; import { RICH_HISTORY_KEY, RichHistoryLocalStorageDTO } from '../../../../core/history/RichHistoryLocalStorage'; +import { RICH_HISTORY_SETTING_KEYS } from '../../../../core/history/richHistoryLocalStorageUtils'; import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource'; import { LokiQuery } from '../../../../plugins/datasource/loki/types'; import { ExploreId } from '../../../../types'; @@ -158,6 +159,10 @@ export const withinExplore = (exploreId: ExploreId) => { return within(container[exploreId === ExploreId.left ? 0 : 1]); }; +export const localStorageHasAlreadyBeenMigrated = () => { + window.localStorage.setItem(RICH_HISTORY_SETTING_KEYS.migrated, 'true'); +}; + export const setupLocalStorageRichHistory = (dsName: string) => { window.localStorage.setItem( RICH_HISTORY_KEY, diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index f0ec2816a60..4133ce61da5 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -9,7 +9,9 @@ import { ExploreId } from '../../../types'; import { assertDataSourceFilterVisibility, + assertLoadMoreQueryHistoryNotVisible, assertQueryHistory, + assertQueryHistoryElementsShown, assertQueryHistoryExists, assertQueryHistoryIsStarred, assertQueryHistoryTabIsSelected, @@ -18,6 +20,7 @@ import { closeQueryHistory, deleteQueryHistory, inputQuery, + loadMoreQueryHistory, openQueryHistory, runQuery, selectOnlyActiveDataSource, @@ -26,7 +29,13 @@ import { switchToQueryHistoryTab, } from './helper/interactions'; import { makeLogsQueryResponse } from './helper/query'; -import { setupExplore, setupLocalStorageRichHistory, tearDown, waitForExplore } from './helper/setup'; +import { + localStorageHasAlreadyBeenMigrated, + setupExplore, + setupLocalStorageRichHistory, + tearDown, + waitForExplore, +} from './helper/setup'; const fetchMock = jest.fn(); const postMock = jest.fn(); @@ -215,4 +224,26 @@ describe('Explore: Query History', () => { ); }); }); + + it('pagination', async () => { + config.queryHistoryEnabled = true; + localStorageHasAlreadyBeenMigrated(); + const { datasources } = setupExplore(); + (datasources.loki.query as jest.Mock).mockReturnValueOnce(makeLogsQueryResponse()); + fetchMock.mockReturnValue( + of({ + data: { result: { queryHistory: [{ datasourceUid: 'loki', queries: [{ expr: 'query' }] }], totalCount: 2 } }, + }) + ); + await waitForExplore(); + + await openQueryHistory(); + await assertQueryHistory(['{"expr":"query"}']); + assertQueryHistoryElementsShown(1, 2); + + await loadMoreQueryHistory(); + await assertQueryHistory(['{"expr":"query"}', '{"expr":"query"}']); + + assertLoadMoreQueryHistoryNotVisible(); + }); }); diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 98dac17a6a2..32a69ff9649 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -257,9 +257,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac state = historyReducer(state, action); if (richHistoryUpdatedAction.match(action)) { + const { richHistory, total } = action.payload.richHistoryResults; return { ...state, - richHistory: action.payload.richHistory, + richHistory, + richHistoryTotal: total, }; } diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index a4eb896d1a8..23aba3ee9e5 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -60,7 +60,12 @@ const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesO .map((query) => (query.id === updatedQuery?.id ? updatedQuery : query)) // or remove .filter((query) => query.id !== deletedId); - dispatch(richHistoryUpdatedAction({ richHistory: newRichHistory, exploreId })); + dispatch( + richHistoryUpdatedAction({ + richHistoryResults: { richHistory: newRichHistory, total: item.richHistoryTotal }, + exploreId, + }) + ); }); }; }; @@ -118,8 +123,12 @@ export const deleteHistoryItem = (id: string): ThunkResult => { export const deleteRichHistory = (): ThunkResult => { return async (dispatch) => { await deleteAllFromRichHistory(); - dispatch(richHistoryUpdatedAction({ richHistory: [], exploreId: ExploreId.left })); - dispatch(richHistoryUpdatedAction({ richHistory: [], exploreId: ExploreId.right })); + dispatch( + richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.left }) + ); + dispatch( + richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.right }) + ); }; }; @@ -127,8 +136,24 @@ export const loadRichHistory = (exploreId: ExploreId): ThunkResult => { return async (dispatch, getState) => { const filters = getState().explore![exploreId]?.richHistorySearchFilters; if (filters) { - const richHistory = await getRichHistory(filters); - dispatch(richHistoryUpdatedAction({ richHistory, exploreId })); + const richHistoryResults = await getRichHistory(filters); + dispatch(richHistoryUpdatedAction({ richHistoryResults, exploreId })); + } + }; +}; + +export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult => { + return async (dispatch, getState) => { + const currentFilters = getState().explore![exploreId]?.richHistorySearchFilters; + const currentRichHistory = getState().explore![exploreId]?.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 }) + ); } }; }; @@ -136,7 +161,7 @@ export const loadRichHistory = (exploreId: ExploreId): ThunkResult => { export const clearRichHistoryResults = (exploreId: ExploreId): ThunkResult => { return async (dispatch) => { dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId })); - dispatch(richHistoryUpdatedAction({ richHistory: [], exploreId })); + dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId })); }; }; diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 727588e1ade..2fcf82ad40d 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -5,8 +5,9 @@ import { ExploreUrlState, serializeStateToUrlParam, SplitOpen, UrlQueryMap } fro import { DataSourceSrv, getDataSourceSrv, locationService } from '@grafana/runtime'; import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore'; import { PanelModel } from 'app/features/dashboard/state'; -import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery } from 'app/types/explore'; +import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore'; +import { RichHistoryResults } from '../../../core/history/RichHistoryStorage'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; import { ThunkResult } from '../../../types'; import { TimeSrv } from '../../dashboard/services/TimeSrv'; @@ -23,7 +24,7 @@ export interface SyncTimesPayload { } export const syncTimesAction = createAction('explore/syncTimes'); -export const richHistoryUpdatedAction = createAction<{ richHistory: RichHistoryQuery[]; exploreId: ExploreId }>( +export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults; exploreId: ExploreId }>( 'explore/richHistoryUpdated' ); export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction'); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 34e3f58d32c..3859c12cbf2 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -166,6 +166,7 @@ export interface ExploreItemState { */ richHistory: RichHistoryQuery[]; richHistorySearchFilters?: RichHistorySearchFilters; + richHistoryTotal?: number; /** * We are using caching to store query responses of queries run from logs navigation.