diff --git a/.betterer.results b/.betterer.results index 186b3a43f37..ab7564f8543 100644 --- a/.betterer.results +++ b/.betterer.results @@ -230,7 +230,7 @@ exports[`no enzyme tests`] = { "public/app/features/explore/LiveLogs.test.tsx:156663779": [ [0, 17, 13, "RegExp match", "2409514259"] ], - "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3328200031": [ + "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3933225580": [ [0, 17, 13, "RegExp match", "2409514259"] ], "public/app/features/explore/RunButton.test.tsx:4267530266": [ diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index c587c07a927..4a39f9d7871 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -4,7 +4,7 @@ import store from 'app/core/store'; import { afterEach, beforeEach } from '../../../test/lib/common'; import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; -import { RichHistorySettings } from '../utils/richHistoryTypes'; +import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; import RichHistoryLocalStorage, { MAX_HISTORY_ITEMS } from './RichHistoryLocalStorage'; import { RichHistoryStorageWarning } from './RichHistoryStorage'; @@ -12,7 +12,7 @@ import { RichHistoryStorageWarning } from './RichHistoryStorage'; const key = 'grafana.explore.richHistory'; jest.mock('@grafana/runtime', () => ({ - ...(jest.requireActual('@grafana/runtime') as unknown as object), + ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, getDataSourceSrv: () => { return { @@ -30,6 +30,15 @@ interface MockQuery extends DataQuery { query: string; } +const mockFilters: RichHistorySearchFilters = { + search: '', + sortOrder: SortOrder.Descending, + datasourceFilters: [], + from: 0, + to: 7, + starred: false, +}; + const mockItem: RichHistoryQuery = { id: '2', createdAt: 2, @@ -53,26 +62,28 @@ const mockItem2: RichHistoryQuery = { describe('RichHistoryLocalStorage', () => { let storage: RichHistoryLocalStorage; + let now: Date; + let old: Date; + beforeEach(async () => { + now = new Date(1970, 0, 1); + old = new Date(1969, 0, 1); + + jest.useFakeTimers('modern'); + jest.setSystemTime(now); storage = new RichHistoryLocalStorage(); await storage.deleteAll(); }); + afterEach(() => { + jest.useRealTimers(); + }); + describe('basic api', () => { - let dateSpy: jest.SpyInstance; - - beforeEach(() => { - dateSpy = jest.spyOn(Date, 'now').mockImplementation(() => 2); - }); - - afterEach(() => { - dateSpy.mockRestore(); - }); - it('should save query history to localStorage', async () => { await storage.addToRichHistory(mockItem); expect(store.exists(key)).toBeTruthy(); - expect(await storage.getRichHistory()).toMatchObject([mockItem]); + expect(await storage.getRichHistory(mockFilters)).toMatchObject([mockItem]); }); it('should not save duplicated query to localStorage', async () => { @@ -81,25 +92,25 @@ describe('RichHistoryLocalStorage', () => { await expect(async () => { await storage.addToRichHistory(mockItem2); }).rejects.toThrow('Entry already exists'); - expect(await storage.getRichHistory()).toMatchObject([mockItem2, mockItem]); + expect(await storage.getRichHistory(mockFilters)).toMatchObject([mockItem2, mockItem]); }); it('should update starred in localStorage', async () => { await storage.addToRichHistory(mockItem); await storage.updateStarred(mockItem.id, false); - expect((await storage.getRichHistory())[0].starred).toEqual(false); + expect((await storage.getRichHistory(mockFilters))[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())[0].comment).toEqual('new comment'); + expect((await storage.getRichHistory(mockFilters))[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()).toEqual([]); + expect(await storage.getRichHistory(mockFilters)).toEqual([]); expect(store.getObject(key)).toEqual([]); }); @@ -108,7 +119,7 @@ describe('RichHistoryLocalStorage', () => { retentionPeriod: 2, starredTabAsFirstTab: true, activeDatasourceOnly: true, - lastUsedDatasourceFilters: [{ value: 'foobar' }], + lastUsedDatasourceFilters: ['foobar'], }; await storage.updateSettings(settings); const storageSettings = storage.getSettings(); @@ -119,23 +130,35 @@ describe('RichHistoryLocalStorage', () => { describe('retention policy and max limits', () => { it('should clear old not-starred items', async () => { - const now = Date.now(); - const history = [ - { starred: true, ts: 0, queries: [] }, - { starred: true, ts: now, queries: [] }, - { starred: false, ts: 0, queries: [] }, - { starred: false, ts: now, queries: [] }, - ]; + const historyStarredOld = { starred: true, ts: old.getTime(), queries: [], comment: 'old starred' }; + const historyNotStarredOld = { starred: false, ts: old.getTime(), queries: [], comment: 'new not starred' }; + const historyStarredNew = { starred: true, ts: now.getTime(), queries: [], comment: 'new starred' }; + const historyNotStarredNew = { starred: false, ts: now.getTime(), queries: [], comment: 'new not starred' }; + const history = [historyNotStarredNew, historyStarredNew, historyStarredOld, historyNotStarredOld]; store.setObject(key, history); - await storage.addToRichHistory(mockItem); - const richHistory = await storage.getRichHistory(); + const historyNew = { + starred: true, + datasourceUid: 'dev-test-uid', + datasourceName: 'dev-test', + comment: 'recently added', + queries: [{ refId: 'ref' }], + }; + await storage.addToRichHistory(historyNew); + const richHistory = await storage.getRichHistory({ + search: '', + sortOrder: SortOrder.Descending, + datasourceFilters: [], + from: 0, + to: 1000, // 1000 days: use a filter that is beyond retention policy to check old items were removed correctly + starred: false, + }); expect(richHistory).toMatchObject([ - mockItem, - { starred: true, createdAt: 0, queries: [] }, - { starred: true, createdAt: now, queries: [] }, - { starred: false, createdAt: now, queries: [] }, + expect.objectContaining({ comment: 'recently added' }), + expect.objectContaining({ comment: 'new not starred' }), + expect.objectContaining({ comment: 'new starred' }), + expect.objectContaining({ comment: 'old starred' }), ]); }); @@ -214,7 +237,7 @@ describe('RichHistoryLocalStorage', () => { ], }; - const result = await storage.getRichHistory(); + const result = await storage.getRichHistory(mockFilters); expect(result).toStrictEqual([expectedHistoryItem]); }); @@ -249,7 +272,7 @@ describe('RichHistoryLocalStorage', () => { ], }; - const result = await storage.getRichHistory(); + const result = await storage.getRichHistory(mockFilters); expect(result).toStrictEqual([expectedHistoryItem]); }); }); diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 44e943e6468..61174bbc9d4 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -1,14 +1,18 @@ import { find, isEqual, omit } from 'lodash'; -import { DataQuery } from '@grafana/data'; +import { DataQuery, SelectableValue } from '@grafana/data'; +import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; import { RichHistoryQuery } from '../../types'; import store from '../store'; -import { RichHistorySettings } from '../utils/richHistoryTypes'; import RichHistoryStorage, { RichHistoryServiceError, RichHistoryStorageWarning } from './RichHistoryStorage'; import { fromDTO, toDTO } from './localStorageConverter'; -import { createRetentionPeriodBoundary, RICH_HISTORY_SETTING_KEYS } from './richHistoryLocalStorageUtils'; +import { + createRetentionPeriodBoundary, + filterAndSortQueries, + RICH_HISTORY_SETTING_KEYS, +} from './richHistoryLocalStorageUtils'; export const RICH_HISTORY_KEY = 'grafana.explore.richHistory'; export const MAX_HISTORY_ITEMS = 10000; @@ -27,10 +31,16 @@ export type RichHistoryLocalStorageDTO = { */ export default class RichHistoryLocalStorage implements RichHistoryStorage { /** - * Return all history entries, perform migration and clean up entries not matching retention policy. + * Return history entries based on provided filters, perform migration and clean up entries not matching retention policy. */ - async getRichHistory() { - return getRichHistoryDTOs().map(fromDTO); + async getRichHistory(filters: RichHistorySearchFilters) { + 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, [ + filters.from, + filters.to, + ]); } async addToRichHistory(newRichHistoryQuery: Omit) { @@ -111,7 +121,9 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage { activeDatasourceOnly: store.getObject(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, false), retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7), starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false), - lastUsedDatasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, []), + lastUsedDatasourceFilters: store + .getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, []) + .map((selectableValue: SelectableValue) => selectableValue.value), }; } @@ -119,7 +131,12 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage { store.set(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, settings.activeDatasourceOnly); store.set(RICH_HISTORY_SETTING_KEYS.retentionPeriod, settings.retentionPeriod); store.set(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, settings.starredTabAsFirstTab); - store.setObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, settings.lastUsedDatasourceFilters); + store.setObject( + RICH_HISTORY_SETTING_KEYS.datasourceFilters, + (settings.lastUsedDatasourceFilters || []).map((datasourceName: string) => { + return { value: datasourceName }; + }) + ); } } diff --git a/public/app/core/history/RichHistoryStorage.ts b/public/app/core/history/RichHistoryStorage.ts index 5e833c73f44..0dbc27ce4a7 100644 --- a/public/app/core/history/RichHistoryStorage.ts +++ b/public/app/core/history/RichHistoryStorage.ts @@ -1,5 +1,6 @@ +import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; + import { RichHistoryQuery } from '../../types'; -import { RichHistorySettings } from '../utils/richHistoryTypes'; /** * Errors are used when the operation on Rich History was not successful. @@ -32,7 +33,7 @@ export type RichHistoryStorageWarningDetails = { * @alpha */ export default interface RichHistoryStorage { - getRichHistory(): Promise; + getRichHistory(filters: RichHistorySearchFilters): Promise; /** * Creates new RichHistoryQuery, returns object with unique id and created date diff --git a/public/app/core/history/localStorageConverter.test.ts b/public/app/core/history/localStorageConverter.test.ts index 9a7e7ff4cd4..960ebbe9e5a 100644 --- a/public/app/core/history/localStorageConverter.test.ts +++ b/public/app/core/history/localStorageConverter.test.ts @@ -5,7 +5,7 @@ import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; import { fromDTO, toDTO } from './localStorageConverter'; jest.mock('@grafana/runtime', () => ({ - ...(jest.requireActual('@grafana/runtime') as unknown as object), + ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, getDataSourceSrv: () => { return { diff --git a/public/app/core/history/richHistoryLocalStorageUtils.test.ts b/public/app/core/history/richHistoryLocalStorageUtils.test.ts new file mode 100644 index 00000000000..7687f3f1bb5 --- /dev/null +++ b/public/app/core/history/richHistoryLocalStorageUtils.test.ts @@ -0,0 +1,69 @@ +import { DataQuery } from '@grafana/data'; +import { SortOrder } from 'app/core/utils/richHistory'; + +import { RichHistoryQuery } from '../../types'; + +import { filterAndSortQueries } from './richHistoryLocalStorageUtils'; + +interface MockQuery extends DataQuery { + expr: string; + maxLines?: number | null; +} + +const storedHistory: Array> = [ + { + id: '1', + createdAt: 1, + comment: '', + datasourceUid: 'datasource uid', + datasourceName: 'datasource history name', + queries: [ + { expr: 'query1', maxLines: null, refId: '1' }, + { expr: 'query2', refId: '2' }, + ], + starred: true, + }, + { + id: '2', + createdAt: 2, + comment: 'comment 2', + datasourceUid: 'datasource uid 2', + datasourceName: 'datasource history name 2', + queries: [ + { expr: 'query3', maxLines: null, refId: '1' }, + { expr: 'query4', refId: '2' }, + ], + starred: true, + }, +]; + +describe('filterQueries', () => { + it('should include all entries for empty filters', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, [], ''); + expect(filteredQueries).toMatchObject([expect.objectContaining({ id: '1' }), expect.objectContaining({ id: '2' })]); + }); + it('should sort entries based on the filter', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Descending, [], ''); + expect(filteredQueries).toMatchObject([expect.objectContaining({ id: '2' }), expect.objectContaining({ id: '1' })]); + }); + it('should filter out queries based on data source filter', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, ['not provided data source'], ''); + expect(filteredQueries).toHaveLength(0); + }); + it('should keep queries based on data source filter', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, ['datasource history name'], ''); + expect(filteredQueries).toMatchObject([expect.objectContaining({ id: '1' })]); + }); + it('should filter out all queries based on search filter', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, [], 'i do not exist in query'); + expect(filteredQueries).toHaveLength(0); + }); + it('should include queries based on search filter', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, [], 'query1'); + expect(filteredQueries).toMatchObject([expect.objectContaining({ id: '1' })]); + }); + it('should include queries based on comments', () => { + const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, [], 'comment 2'); + expect(filteredQueries).toMatchObject([expect.objectContaining({ id: '2' })]); + }); +}); diff --git a/public/app/core/history/richHistoryLocalStorageUtils.ts b/public/app/core/history/richHistoryLocalStorageUtils.ts index 096b366da50..201e5d952b4 100644 --- a/public/app/core/history/richHistoryLocalStorageUtils.ts +++ b/public/app/core/history/richHistoryLocalStorageUtils.ts @@ -9,6 +9,22 @@ import { SortOrder } from '../utils/richHistoryTypes'; * Should be migrated to RichHistoryLocalStorage.ts */ +export function filterAndSortQueries( + queries: RichHistoryQuery[], + sortOrder: SortOrder, + listOfDatasourceFilters: string[], + searchFilter: string, + timeFilter?: [number, number] +) { + const filteredQueriesByDs = filterQueriesByDataSource(queries, listOfDatasourceFilters); + const filteredQueriesByDsAndSearchFilter = filterQueriesBySearchFilter(filteredQueriesByDs, searchFilter); + const filteredQueriesToBeSorted = timeFilter + ? filterQueriesByTime(filteredQueriesByDsAndSearchFilter, timeFilter) + : filteredQueriesByDsAndSearchFilter; + + return sortQueries(filteredQueriesToBeSorted, sortOrder); +} + export const createRetentionPeriodBoundary = (days: number, isLastTs: boolean) => { const today = new Date(); const date = new Date(today.setDate(today.getDate() - days)); @@ -21,19 +37,19 @@ export const createRetentionPeriodBoundary = (days: number, isLastTs: boolean) = return boundary; }; -export function filterQueriesByTime(queries: RichHistoryQuery[], timeFilter: [number, number]) { +function filterQueriesByTime(queries: RichHistoryQuery[], timeFilter: [number, number]) { const filter1 = createRetentionPeriodBoundary(timeFilter[0], true); // probably the vars should have a different name const filter2 = createRetentionPeriodBoundary(timeFilter[1], false); return queries.filter((q) => q.createdAt < filter1 && q.createdAt > filter2); } -export function filterQueriesByDataSource(queries: RichHistoryQuery[], listOfDatasourceFilters: string[]) { +function filterQueriesByDataSource(queries: RichHistoryQuery[], listOfDatasourceFilters: string[]) { return listOfDatasourceFilters.length > 0 ? queries.filter((q) => listOfDatasourceFilters.includes(q.datasourceName)) : queries; } -export function filterQueriesBySearchFilter(queries: RichHistoryQuery[], searchFilter: string) { +function filterQueriesBySearchFilter(queries: RichHistoryQuery[], searchFilter: string) { return queries.filter((query) => { if (query.comment.includes(searchFilter)) { return true; diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index 446362f9a72..404b0ae9977 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -13,7 +13,6 @@ import { createQueryHeading, deleteAllFromRichHistory, deleteQueryInRichHistory, - filterAndSortQueries, SortOrder, } from './richHistory'; @@ -178,30 +177,6 @@ describe('richHistory', () => { }); }); - describe('filterQueries', () => { - it('should filter out queries based on data source filter', () => { - const filteredQueries = filterAndSortQueries( - storedHistory, - SortOrder.Ascending, - ['not provided data source'], - '' - ); - expect(filteredQueries).toHaveLength(0); - }); - it('should keep queries based on data source filter', () => { - const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, ['datasource history name'], ''); - expect(filteredQueries).toHaveLength(1); - }); - it('should filter out all queries based on search filter', () => { - const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, [], 'i do not exist in query'); - expect(filteredQueries).toHaveLength(0); - }); - it('should include queries based on search filter', () => { - const filteredQueries = filterAndSortQueries(storedHistory, SortOrder.Ascending, [], 'query1'); - expect(filteredQueries).toHaveLength(1); - }); - }); - describe('createQueryHeading', () => { it('should correctly create heading for queries when sort order is ascending ', () => { // Have to offset the timezone of a 1 microsecond epoch, and then reverse the changes diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index 2cb8efe0365..99575fd6ca8 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -5,12 +5,6 @@ import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; import { getDataSourceSrv } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification, createWarningNotification } from 'app/core/copy/appNotification'; -import { - filterQueriesByDataSource, - filterQueriesBySearchFilter, - filterQueriesByTime, - sortQueries, -} from 'app/core/history/richHistoryLocalStorageUtils'; import { dispatch } from 'app/store/store'; import { RichHistoryQuery } from 'app/types/explore'; @@ -21,9 +15,9 @@ import { } from '../history/RichHistoryStorage'; import { getRichHistoryStorage } from '../history/richHistoryStorageProvider'; -import { RichHistorySettings, SortOrder } from './richHistoryTypes'; +import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from './richHistoryTypes'; -export { SortOrder }; +export { RichHistorySearchFilters, RichHistorySettings, SortOrder }; /* * Add queries to rich history. Save only queries within the retention period, or that are starred. @@ -80,8 +74,8 @@ export async function addToRichHistory( return {}; } -export async function getRichHistory(): Promise { - return await getRichHistoryStorage().getRichHistory(); +export async function getRichHistory(filters: RichHistorySearchFilters): Promise { + return await getRichHistoryStorage().getRichHistory(filters); } export async function updateRichHistorySettings(settings: RichHistorySettings): Promise { @@ -124,22 +118,6 @@ export async function deleteQueryInRichHistory(id: string) { } } -export function filterAndSortQueries( - queries: RichHistoryQuery[], - sortOrder: SortOrder, - listOfDatasourceFilters: string[], - searchFilter: string, - timeFilter?: [number, number] -) { - const filteredQueriesByDs = filterQueriesByDataSource(queries, listOfDatasourceFilters); - const filteredQueriesByDsAndSearchFilter = filterQueriesBySearchFilter(filteredQueriesByDs, searchFilter); - const filteredQueriesToBeSorted = timeFilter - ? filterQueriesByTime(filteredQueriesByDsAndSearchFilter, timeFilter) - : filteredQueriesByDsAndSearchFilter; - - return sortQueries(filteredQueriesToBeSorted, sortOrder); -} - export const createUrlFromRichHistory = (query: RichHistoryQuery) => { const exploreState: ExploreUrlState = { /* Default range, as we are not saving timerange in rich history */ @@ -229,31 +207,19 @@ export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortO return mappedQueriesToHeadings; } -/* Create datasource list with images. If specific datasource retrieved from Rich history is not part of - * exploreDatasources add generic datasource image and add property isRemoved = true. +/* + * Create a list of all available data sources */ -export function createDatasourcesList(queriesDatasources: string[]) { - const datasources: Array<{ label: string; value: string; imgUrl: string; isRemoved: boolean }> = []; - - queriesDatasources.forEach((dsName) => { - const dsSettings = getDataSourceSrv().getInstanceSettings(dsName); - if (dsSettings) { - datasources.push({ - label: dsSettings.name, - value: dsSettings.name, +export function createDatasourcesList() { + return getDataSourceSrv() + .getList() + .map((dsSettings) => { + return { + name: dsSettings.name, + uid: dsSettings.uid, imgUrl: dsSettings.meta.info.logos.small, - isRemoved: false, - }); - } else { - datasources.push({ - label: dsName, - value: dsName, - imgUrl: 'public/img/icn-datasource.svg', - isRemoved: true, - }); - } - }); - return datasources; + }; + }); } export function notEmptyQuery(query: DataQuery) { diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index 0f2030069ac..4903fd40a93 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -1,5 +1,3 @@ -import { SelectableValue } from '@grafana/data'; - export enum SortOrder { Descending = 'Descending', Ascending = 'Ascending', @@ -11,13 +9,14 @@ export interface RichHistorySettings { retentionPeriod: number; starredTabAsFirstTab: boolean; activeDatasourceOnly: boolean; - lastUsedDatasourceFilters: SelectableValue[]; + lastUsedDatasourceFilters: string[]; } export type RichHistorySearchFilters = { search: string; sortOrder: SortOrder; - datasourceFilters: SelectableValue[]; + datasourceFilters: string[]; from: number; to: number; + starred: boolean; }; diff --git a/public/app/features/explore/RichHistory/RichHistory.test.tsx b/public/app/features/explore/RichHistory/RichHistory.test.tsx index 7ca79656a8c..b890b010ef2 100644 --- a/public/app/features/explore/RichHistory/RichHistory.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.test.tsx @@ -2,14 +2,25 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { GrafanaTheme } from '@grafana/data'; +import { SortOrder } from 'app/core/utils/richHistory'; -import { SortOrder } from '../../../core/utils/richHistoryTypes'; import { ExploreId } from '../../../types/explore'; import { RichHistory, RichHistoryProps, Tabs } from './RichHistory'; jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() })); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => { + return { + getList: () => { + return []; + }, + }; + }, +})); + const setup = (propOverrides?: Partial) => { const props: RichHistoryProps = { theme: {} as GrafanaTheme, @@ -19,6 +30,8 @@ const setup = (propOverrides?: Partial) => { richHistory: [], firstTab: Tabs.RichHistory, deleteRichHistory: jest.fn(), + loadRichHistory: jest.fn(), + clearRichHistoryResults: jest.fn(), onClose: jest.fn(), richHistorySearchFilters: { search: '', @@ -26,6 +39,7 @@ const setup = (propOverrides?: Partial) => { datasourceFilters: [], from: 0, to: 7, + starred: false, }, richHistorySettings: { retentionPeriod: 0, diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 3a72d1401bc..2dad677500e 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -1,12 +1,11 @@ +import { debounce } from 'lodash'; import React, { PureComponent } from 'react'; import { SelectableValue } from '@grafana/data'; import { Themeable, withTheme, TabbedContainer, TabConfig } from '@grafana/ui'; -import { SortOrder } from 'app/core/utils/richHistory'; +import { SortOrder, RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; import { RichHistoryQuery, ExploreId } from 'app/types/explore'; -import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; - import { RichHistoryQueriesTab } from './RichHistoryQueriesTab'; import { RichHistorySettingsTab } from './RichHistorySettingsTab'; import { RichHistoryStarredTab } from './RichHistoryStarredTab'; @@ -27,9 +26,11 @@ export const sortOrderOptions = [ export interface RichHistoryProps extends Themeable { richHistory: RichHistoryQuery[]; richHistorySettings: RichHistorySettings; - richHistorySearchFilters: RichHistorySearchFilters; + richHistorySearchFilters?: RichHistorySearchFilters; updateHistorySettings: (settings: RichHistorySettings) => void; updateHistorySearchFilters: (exploreId: ExploreId, filters: RichHistorySearchFilters) => void; + loadRichHistory: (exploreId: ExploreId) => void; + clearRichHistoryResults: (exploreId: ExploreId) => void; deleteRichHistory: () => void; activeDatasourceInstance?: string; firstTab: Tabs; @@ -43,13 +44,23 @@ class UnThemedRichHistory extends PureComponent { this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate }); }; - updateFilters = (filtersToUpdate: Partial) => { - this.props.updateHistorySearchFilters(this.props.exploreId, { - ...this.props.richHistorySearchFilters, + updateFilters = (filtersToUpdate?: Partial) => { + const filters = { + ...this.props.richHistorySearchFilters!, ...filtersToUpdate, - }); + }; + this.props.updateHistorySearchFilters(this.props.exploreId, filters); + this.loadRichHistory(); }; + clearResults = () => { + this.props.clearRichHistoryResults(this.props.exploreId); + }; + + loadRichHistory = debounce(() => { + this.props.loadRichHistory(this.props.exploreId); + }, 300); + onChangeRetentionPeriod = (retentionPeriod: SelectableValue) => { if (retentionPeriod.value !== undefined) { this.updateSettings({ retentionPeriod: retentionPeriod.value }); @@ -62,39 +73,9 @@ class UnThemedRichHistory extends PureComponent { toggleActiveDatasourceOnly = () => this.updateSettings({ activeDatasourceOnly: !this.props.richHistorySettings.activeDatasourceOnly }); - onSelectDatasourceFilters = (datasourceFilters: SelectableValue[]) => this.updateFilters({ datasourceFilters }); - - onChangeSortOrder = (sortOrder: SortOrder) => this.updateFilters({ sortOrder }); - - /* If user selects activeDatasourceOnly === true, set datasource filter to currently active datasource. - * Filtering based on datasource won't be available. Otherwise set to null, as filtering will be - * available for user. - */ - initFilters() { - if (this.props.richHistorySettings.activeDatasourceOnly && this.props.activeDatasourceInstance) { - this.onSelectDatasourceFilters([ - { label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance }, - ]); - } - } - - componentDidMount() { - this.initFilters(); - } - - /** - * Updating filters on didMount and didUpdate because we don't know when activeDatasourceInstance is ready - */ - componentDidUpdate(prevProps: RichHistoryProps) { - if (this.props.activeDatasourceInstance !== prevProps.activeDatasourceInstance) { - this.initFilters(); - } - } - render() { - const { activeDatasourceOnly, retentionPeriod } = this.props.richHistorySettings; - const { datasourceFilters, sortOrder } = this.props.richHistorySearchFilters; - const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab } = this.props; + const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab, activeDatasourceInstance } = + this.props; const QueriesTab: TabConfig = { label: 'Query history', @@ -102,12 +83,11 @@ class UnThemedRichHistory extends PureComponent { content: ( this.props.clearRichHistoryResults(this.props.exploreId)} + activeDatasourceInstance={activeDatasourceInstance} + richHistorySettings={this.props.richHistorySettings} + richHistorySearchFilters={this.props.richHistorySearchFilters} exploreId={exploreId} height={height} /> @@ -121,11 +101,11 @@ class UnThemedRichHistory extends PureComponent { content: ( this.props.clearRichHistoryResults(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 bdd07b4d6a5..83cc100d9db 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx @@ -1,7 +1,8 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { SortOrder } from '../../../core/utils/richHistoryTypes'; +import { SortOrder } from 'app/core/utils/richHistory'; + import { ExploreId } from '../../../types/explore'; import { Tabs } from './RichHistory'; @@ -9,6 +10,15 @@ import { RichHistoryContainer, Props } from './RichHistoryContainer'; jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() })); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => { + return { + getList: () => [], + }; + }, +})); + const setup = (propOverrides?: Partial) => { const props: Props = { width: 500, @@ -18,6 +28,8 @@ const setup = (propOverrides?: Partial) => { firstTab: Tabs.RichHistory, deleteRichHistory: jest.fn(), initRichHistory: jest.fn(), + loadRichHistory: jest.fn(), + clearRichHistoryResults: jest.fn(), updateHistorySearchFilters: jest.fn(), updateHistorySettings: jest.fn(), onClose: jest.fn(), @@ -27,6 +39,7 @@ const setup = (propOverrides?: Partial) => { datasourceFilters: [], from: 0, to: 7, + starred: false, }, richHistorySettings: { retentionPeriod: 0, @@ -42,8 +55,8 @@ const setup = (propOverrides?: Partial) => { }; describe('RichHistoryContainer', () => { - it('should show loading message when settings and filters are not ready', () => { - const { container } = setup({ richHistorySearchFilters: undefined, richHistorySettings: undefined }); + it('should show loading message when settings are not ready', () => { + const { container } = setup({ richHistorySettings: undefined }); expect(container).toHaveTextContent('Loading...'); }); it('should render component with correct width', () => { diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index bacf5c7254e..b24bfba15b2 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -12,6 +12,8 @@ import { ExploreDrawer } from '../ExploreDrawer'; import { deleteRichHistory, initRichHistory, + loadRichHistory, + clearRichHistoryResults, updateHistorySettings, updateHistorySearchFilters, } from '../state/history'; @@ -40,6 +42,8 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI const mapDispatchToProps = { initRichHistory, + loadRichHistory, + clearRichHistoryResults, updateHistorySettings, updateHistorySearchFilters, deleteRichHistory, @@ -66,6 +70,8 @@ export function RichHistoryContainer(props: Props) { exploreId, deleteRichHistory, initRichHistory, + loadRichHistory, + clearRichHistoryResults, richHistorySettings, updateHistorySettings, richHistorySearchFilters, @@ -74,10 +80,10 @@ export function RichHistoryContainer(props: Props) { } = props; useEffect(() => { - initRichHistory(exploreId); - }, [initRichHistory, exploreId]); + initRichHistory(); + }, [initRichHistory]); - if (!richHistorySettings || !richHistorySearchFilters) { + if (!richHistorySettings) { return Loading...; } @@ -100,6 +106,8 @@ export function RichHistoryContainer(props: Props) { richHistorySearchFilters={richHistorySearchFilters} updateHistorySettings={updateHistorySettings} updateHistorySearchFilters={updateHistorySearchFilters} + loadRichHistory={loadRichHistory} + clearRichHistoryResults={clearRichHistoryResults} /> ); diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index bf139dceb12..2d1181dba94 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -1,32 +1,30 @@ import { css } from '@emotion/css'; -import { uniqBy } from 'lodash'; -import React, { useState, useEffect } from 'react'; -import { useDebounce } from 'react-use'; +import React, { useEffect } from 'react'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; -import { stylesFactory, useTheme, RangeSlider, MultiSelect, Select, FilterInput } from '@grafana/ui'; +import { FilterInput, MultiSelect, RangeSlider, Select, stylesFactory, useTheme } from '@grafana/ui'; import { - SortOrder, + createDatasourcesList, mapNumbertoTimeInSlider, mapQueriesToHeadings, - createDatasourcesList, - filterAndSortQueries, + SortOrder, + RichHistorySearchFilters, + RichHistorySettings, } from 'app/core/utils/richHistory'; -import { RichHistoryQuery, ExploreId } from 'app/types/explore'; +import { ExploreId, RichHistoryQuery } from 'app/types/explore'; import { sortOrderOptions } from './RichHistory'; import RichHistoryCard from './RichHistoryCard'; export interface Props { queries: RichHistoryQuery[]; - sortOrder: SortOrder; - activeDatasourceOnly: boolean; - datasourceFilters: SelectableValue[]; - retentionPeriod: number; + activeDatasourceInstance?: string; + updateFilters: (filtersToUpdate?: Partial) => void; + clearRichHistoryResults: () => void; + richHistorySettings: RichHistorySettings; + richHistorySearchFilters?: RichHistorySearchFilters; exploreId: ExploreId; height: number; - onChangeSortOrder: (sortOrder: SortOrder) => void; - onSelectDatasourceFilters: (value: SelectableValue[]) => void; } const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { @@ -120,107 +118,106 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { export function RichHistoryQueriesTab(props: Props) { const { - datasourceFilters, - onSelectDatasourceFilters, queries, - onChangeSortOrder, - sortOrder, - activeDatasourceOnly, - retentionPeriod, + richHistorySearchFilters, + updateFilters, + clearRichHistoryResults, + richHistorySettings, exploreId, height, + activeDatasourceInstance, } = props; - const [timeFilter, setTimeFilter] = useState<[number, number]>([0, retentionPeriod]); - const [data, setData] = useState<[RichHistoryQuery[], ReturnType]>([[], []]); - const [searchInput, setSearchInput] = useState(''); - const [debouncedSearchInput, setDebouncedSearchInput] = useState(''); - const theme = useTheme(); const styles = getStyles(theme, height); - useDebounce( - () => { - setDebouncedSearchInput(searchInput); - }, - 300, - [searchInput] - ); + const listOfDatasources = createDatasourcesList(); useEffect(() => { - const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map((d) => d.datasourceName); - const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory); + const datasourceFilters = + richHistorySettings.activeDatasourceOnly && activeDatasourceInstance + ? [activeDatasourceInstance] + : richHistorySettings.lastUsedDatasourceFilters; + const filters: RichHistorySearchFilters = { + search: '', + sortOrder: SortOrder.Descending, + datasourceFilters, + from: 0, + to: richHistorySettings.retentionPeriod, + starred: false, + }; + updateFilters(filters); - setData([ - filterAndSortQueries( - queries, - sortOrder, - datasourceFilters.map((d) => d.value), - debouncedSearchInput, - timeFilter - ), - listOfDatasources, - ]); - }, [timeFilter, queries, sortOrder, datasourceFilters, debouncedSearchInput]); + return () => { + clearRichHistoryResults(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const [filteredQueries, listOfDatasources] = data; + if (!richHistorySearchFilters) { + return Loading...; + } /* mappedQueriesToHeadings is an object where query headings (stringified dates/data sources) * are keys and arrays with queries that belong to that headings are values. */ - const mappedQueriesToHeadings = mapQueriesToHeadings(filteredQueries, sortOrder); + const mappedQueriesToHeadings = mapQueriesToHeadings(queries, richHistorySearchFilters.sortOrder); return (
Filter history
-
{mapNumbertoTimeInSlider(timeFilter[0])}
+
{mapNumbertoTimeInSlider(richHistorySearchFilters.from)}
number[]} + onAfterChange={(value) => { + updateFilters({ from: value![0], to: value![1] }); + }} />
-
{mapNumbertoTimeInSlider(timeFilter[1])}
+
{mapNumbertoTimeInSlider(richHistorySearchFilters.to)}
- {!activeDatasourceOnly && ( + {!richHistorySettings.activeDatasourceOnly && ( { + return { value: ds.name, label: ds.name }; + })} + value={richHistorySearchFilters.datasourceFilters} placeholder="Filter queries for data sources(s)" aria-label="Filter queries for data sources(s)" - onChange={onSelectDatasourceFilters} + onChange={(options: SelectableValue[]) => { + updateFilters({ datasourceFilters: options.map((option) => option.value) }); + }} /> )}
{ - setSearchInput(value); - }} + value={richHistorySearchFilters.search} + onChange={(search: string) => updateFilters({ search })} />
order.value === richHistorySearchFilters.sortOrder)} options={sortOrderOptions} - value={sortOrderOptions.filter((order) => order.value === sortOrder)} placeholder="Sort queries by" - onChange={(e) => onChangeSortOrder(e.value as SortOrder)} + onChange={(e: SelectableValue) => updateFilters({ sortOrder: e.value })} />
- {filteredQueries.map((q) => { - const idx = listOfDatasources.findIndex((d) => d.label === q.datasourceName); + {queries.map((q) => { + const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName); return ( ); })} diff --git a/public/app/features/explore/spec/helper/assert.ts b/public/app/features/explore/spec/helper/assert.ts index 3bbd4c70290..2650ba50575 100644 --- a/public/app/features/explore/spec/helper/assert.ts +++ b/public/app/features/explore/spec/helper/assert.ts @@ -4,10 +4,10 @@ import { ExploreId } from '../../../../types'; import { withinExplore } from './setup'; -export const assertQueryHistoryExists = (query: string, exploreId: ExploreId = ExploreId.left) => { +export const assertQueryHistoryExists = async (query: string, exploreId: ExploreId = ExploreId.left) => { const selector = withinExplore(exploreId); - expect(selector.getByText('1 queries')).toBeInTheDocument(); + expect(await selector.findByText('1 queries')).toBeInTheDocument(); const queryItem = selector.getByLabelText('Query text'); expect(queryItem).toHaveTextContent(query); }; diff --git a/public/app/features/explore/spec/interpolation.test.tsx b/public/app/features/explore/spec/interpolation.test.tsx index 8e13045701f..7137c6fa8b9 100644 --- a/public/app/features/explore/spec/interpolation.test.tsx +++ b/public/app/features/explore/spec/interpolation.test.tsx @@ -8,6 +8,12 @@ import { LokiQuery } from '../../../plugins/datasource/loki/types'; import { makeLogsQueryResponse } from './helper/query'; import { setupExplore, waitForExplore } from './helper/setup'; +const fetch = jest.fn(); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => ({ fetch }), +})); + jest.mock('react-virtualized-auto-sizer', () => { return { __esModule: true, diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index af35b403341..755f2a0b781 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -26,6 +26,12 @@ import { import { makeLogsQueryResponse } from './helper/query'; import { setupExplore, tearDown, waitForExplore } from './helper/setup'; +const fetch = jest.fn(); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => ({ fetch }), +})); + jest.mock('react-virtualized-auto-sizer', () => { return { __esModule: true, diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index cd4e6375310..6c65d4bfb7f 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -7,7 +7,6 @@ import { deleteQueryInRichHistory, getRichHistory, getRichHistorySettings, - SortOrder, updateCommentInRichHistory, updateRichHistorySettings, updateStarredInRichHistory, @@ -118,39 +117,33 @@ export const deleteRichHistory = (): ThunkResult => { }; 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 })); + } + }; +}; + +export const clearRichHistoryResults = (exploreId: ExploreId): ThunkResult => { return async (dispatch) => { - // TODO: #45379 pass currently applied search filters - const richHistory = await getRichHistory(); - dispatch(richHistoryUpdatedAction({ richHistory, exploreId })); + dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId })); + dispatch(richHistoryUpdatedAction({ richHistory: [], exploreId })); }; }; /** * Initialize query history pane. To load history it requires settings to be loaded first - * (but only once per session) and filters initialised with default values based on settings. + * (but only once per session). Filters are initialised by the tab (starred or home). */ -export const initRichHistory = (exploreId: ExploreId): ThunkResult => { +export const initRichHistory = (): ThunkResult => { return async (dispatch, getState) => { let settings = getState().explore.richHistorySettings; if (!settings) { settings = await getRichHistorySettings(); dispatch(richHistorySettingsUpdatedAction(settings)); } - - dispatch( - richHistorySearchFiltersUpdatedAction({ - exploreId, - filters: { - search: '', - sortOrder: SortOrder.Descending, - datasourceFilters: settings!.lastUsedDatasourceFilters || [], - from: 0, - to: settings!.retentionPeriod, - }, - }) - ); - - dispatch(loadRichHistory(exploreId)); }; }; @@ -169,10 +162,9 @@ export const updateHistorySearchFilters = ( filters: RichHistorySearchFilters ): ThunkResult => { return async (dispatch, getState) => { - // TODO: #45379 get new rich history list based on filters - dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters })); + await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } })); const currentSettings = getState().explore.richHistorySettings!; - dispatch( + await dispatch( updateHistorySettings({ ...currentSettings, lastUsedDatasourceFilters: filters.datasourceFilters, diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 32de466c90f..73416a685a4 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -31,7 +31,7 @@ export const richHistoryLimitExceededAction = createAction('explore/richHistoryL export const richHistorySettingsUpdatedAction = createAction('explore/richHistorySettingsUpdated'); export const richHistorySearchFiltersUpdatedAction = createAction<{ exploreId: ExploreId; - filters: RichHistorySearchFilters; + filters?: RichHistorySearchFilters; }>('explore/richHistorySearchFiltersUpdatedAction'); /** diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index db4101ca86d..0180f1499d5 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -323,7 +323,8 @@ 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) - dispatch(loadRichHistory(exploreId)); + await dispatch(loadRichHistory(ExploreId.left)); + await dispatch(loadRichHistory(ExploreId.right)); } /**