Query History: Move filtering to service layer (#48008)

* Load Rich History when the container is opened

* Store rich history for each pane separately

* Do not update currently opened query history when an item is added

It's impossible to figure out if the item should be added or not, because filters are applied in the backend. We don't want to replicate that filtering logic in frontend. One way to make it work could be by refreshing both panes.

* Test starring and deleting query history items when both panes are open

* Remove e2e dependency on ExploreId

* Fix unit test

* Assert exact queries

* Simplify test

* Fix e2e tests

* Fix toolbar a11y

* Reload the history after an item is added

* Fix unit test

* Remove references to Explore from generic PageToolbar component

* Update test name

* Fix test assertion

* Add issue item to TODO

* Improve test assertion

* Simplify test setup

* Move query history settings to persistence layer

* Fix test import

* Fix unit test

* Fix unit test

* Test local storage settings API

* Code formatting

* Fix linting errors

* Add an integration test

* Add missing aria role

* Fix a11y issues

* Fix a11y issues

* Use divs instead of ul/li

Otherwis,e pa11y-ci reports the error below claiming there are no children with role=tab:

Certain ARIA roles must contain particular children
   (https://dequeuniversity.com/rules/axe/4.3/aria-required-children?application=axeAPI)

   (#reactRoot > div > main > div:nth-child(3) > div > div:nth-child(1) > div >
   div:nth-child(1) > div > div > nav > div:nth-child(2) > ul)

   <ul class="css-af3vye" role="tablist"><li class="css-1ciwanz"><a href...</ul>

* Clean up settings tab

* Remove redundant aria label

* Remove redundant container

* Clean up test assertions

* Move filtering to persistence layer

* Move filtering to persistence layer

* Simplify applying filters

* Split applying filters and reloading the history

* Debounce updating filters

* Update tests

* Fix waiting for debounced results

* Clear results when switching tabs

* Improve test coverage

* Update docs

* Revert extra handling for uid (will be added when we introduce remote storage)

* Fix betterer conflicts

* Fix imports

* Fix imports

* Simplify test setup

* Simplify assertion

* Improve readability

* Remove unnecessary casting

* Mock backend in integration tests
This commit is contained in:
Piotr Jamróz 2022-04-27 15:07:44 +02:00 committed by GitHub
parent a320e942a6
commit 361cc18b45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 442 additions and 339 deletions

View File

@ -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": [

View File

@ -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<MockQuery> = {
id: '2',
createdAt: 2,
@ -53,26 +62,28 @@ const mockItem2: RichHistoryQuery<MockQuery> = {
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]);
});
});

View File

@ -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<RichHistoryQuery, 'id' | 'createdAt'>) {
@ -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 };
})
);
}
}

View File

@ -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<RichHistoryQuery[]>;
getRichHistory(filters: RichHistorySearchFilters): Promise<RichHistoryQuery[]>;
/**
* Creates new RichHistoryQuery, returns object with unique id and created date

View File

@ -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 {

View File

@ -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<RichHistoryQuery<MockQuery>> = [
{
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' })]);
});
});

View File

@ -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;

View File

@ -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

View File

@ -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<RichHistoryQuery[]> {
return await getRichHistoryStorage().getRichHistory();
export async function getRichHistory(filters: RichHistorySearchFilters): Promise<RichHistoryQuery[]> {
return await getRichHistoryStorage().getRichHistory(filters);
}
export async function updateRichHistorySettings(settings: RichHistorySettings): Promise<void> {
@ -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) {

View File

@ -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;
};

View File

@ -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<RichHistoryProps>) => {
const props: RichHistoryProps = {
theme: {} as GrafanaTheme,
@ -19,6 +30,8 @@ const setup = (propOverrides?: Partial<RichHistoryProps>) => {
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<RichHistoryProps>) => {
datasourceFilters: [],
from: 0,
to: 7,
starred: false,
},
richHistorySettings: {
retentionPeriod: 0,

View File

@ -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<RichHistoryProps> {
this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate });
};
updateFilters = (filtersToUpdate: Partial<RichHistorySearchFilters>) => {
this.props.updateHistorySearchFilters(this.props.exploreId, {
...this.props.richHistorySearchFilters,
updateFilters = (filtersToUpdate?: Partial<RichHistorySearchFilters>) => {
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<number>) => {
if (retentionPeriod.value !== undefined) {
this.updateSettings({ retentionPeriod: retentionPeriod.value });
@ -62,39 +73,9 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps> {
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<RichHistoryProps> {
content: (
<RichHistoryQueriesTab
queries={richHistory}
sortOrder={sortOrder}
datasourceFilters={datasourceFilters}
activeDatasourceOnly={activeDatasourceOnly}
retentionPeriod={retentionPeriod}
onChangeSortOrder={this.onChangeSortOrder}
onSelectDatasourceFilters={this.onSelectDatasourceFilters}
updateFilters={this.updateFilters}
clearRichHistoryResults={() => 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<RichHistoryProps> {
content: (
<RichHistoryStarredTab
queries={richHistory}
sortOrder={sortOrder}
datasourceFilters={datasourceFilters}
activeDatasourceOnly={activeDatasourceOnly}
onChangeSortOrder={this.onChangeSortOrder}
onSelectDatasourceFilters={this.onSelectDatasourceFilters}
activeDatasourceInstance={activeDatasourceInstance}
updateFilters={this.updateFilters}
clearRichHistoryResults={() => this.props.clearRichHistoryResults(this.props.exploreId)}
richHistorySettings={this.props.richHistorySettings}
richHistorySearchFilters={this.props.richHistorySearchFilters}
exploreId={exploreId}
/>
),

View File

@ -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<Props>) => {
const props: Props = {
width: 500,
@ -18,6 +28,8 @@ const setup = (propOverrides?: Partial<Props>) => {
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<Props>) => {
datasourceFilters: [],
from: 0,
to: 7,
starred: false,
},
richHistorySettings: {
retentionPeriod: 0,
@ -42,8 +55,8 @@ const setup = (propOverrides?: Partial<Props>) => {
};
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', () => {

View File

@ -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 <span>Loading...</span>;
}
@ -100,6 +106,8 @@ export function RichHistoryContainer(props: Props) {
richHistorySearchFilters={richHistorySearchFilters}
updateHistorySettings={updateHistorySettings}
updateHistorySearchFilters={updateHistorySearchFilters}
loadRichHistory={loadRichHistory}
clearRichHistoryResults={clearRichHistoryResults}
/>
</ExploreDrawer>
);

View File

@ -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<RichHistorySearchFilters>) => 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<typeof createDatasourcesList>]>([[], []]);
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 <span>Loading...</span>;
}
/* 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 (
<div className={styles.container}>
<div className={styles.containerSlider}>
<div className={styles.slider}>
<div className="label-slider">Filter history</div>
<div className="label-slider">{mapNumbertoTimeInSlider(timeFilter[0])}</div>
<div className="label-slider">{mapNumbertoTimeInSlider(richHistorySearchFilters.from)}</div>
<div className="slider">
<RangeSlider
tooltipAlwaysVisible={false}
min={0}
max={retentionPeriod}
value={timeFilter}
max={richHistorySettings.retentionPeriod}
value={[richHistorySearchFilters.from, richHistorySearchFilters.to]}
orientation="vertical"
formatTooltipResult={mapNumbertoTimeInSlider}
reverse={true}
onAfterChange={setTimeFilter as () => number[]}
onAfterChange={(value) => {
updateFilters({ from: value![0], to: value![1] });
}}
/>
</div>
<div className="label-slider">{mapNumbertoTimeInSlider(timeFilter[1])}</div>
<div className="label-slider">{mapNumbertoTimeInSlider(richHistorySearchFilters.to)}</div>
</div>
</div>
<div className={styles.containerContent}>
<div className={styles.selectors}>
{!activeDatasourceOnly && (
{!richHistorySettings.activeDatasourceOnly && (
<MultiSelect
className={styles.multiselect}
menuShouldPortal
options={listOfDatasources}
value={datasourceFilters}
options={listOfDatasources.map((ds) => {
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) });
}}
/>
)}
<div className={styles.filterInput}>
<FilterInput
placeholder="Search queries"
value={searchInput}
onChange={(value: string) => {
setSearchInput(value);
}}
value={richHistorySearchFilters.search}
onChange={(search: string) => updateFilters({ search })}
/>
</div>
<div aria-label="Sort queries" className={styles.sort}>
<Select
menuShouldPortal
value={sortOrderOptions.filter((order) => order.value === sortOrder)}
value={sortOrderOptions.filter((order) => order.value === richHistorySearchFilters.sortOrder)}
options={sortOrderOptions}
placeholder="Sort queries by"
onChange={(e) => onChangeSortOrder(e.value as SortOrder)}
onChange={(e: SelectableValue<SortOrder>) => updateFilters({ sortOrder: e.value })}
/>
</div>
</div>
@ -231,14 +228,14 @@ export function RichHistoryQueriesTab(props: Props) {
{heading} <span className={styles.queries}>{mappedQueriesToHeadings[heading].length} queries</span>
</div>
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => {
const idx = listOfDatasources.findIndex((d) => d.label === q.datasourceName);
const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName);
return (
<RichHistoryCard
query={q}
key={q.id}
exploreId={exploreId}
dsImg={listOfDatasources[idx].imgUrl}
isRemoved={listOfDatasources[idx].isRemoved}
dsImg={idx === -1 ? 'public/img/icn-datasource.svg' : listOfDatasources[idx].imgUrl}
isRemoved={idx === -1}
/>
);
})}

View File

@ -9,19 +9,37 @@ import { RichHistoryStarredTab, Props } from './RichHistoryStarredTab';
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
const setup = (propOverrides?: Partial<Props>) => {
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
return {
getList: () => [],
};
},
}));
const setup = (activeDatasourceOnly = false) => {
const props: Props = {
queries: [],
sortOrder: SortOrder.Ascending,
activeDatasourceOnly: false,
datasourceFilters: [],
updateFilters: jest.fn(),
clearRichHistoryResults: jest.fn(),
exploreId: ExploreId.left,
onChangeSortOrder: jest.fn(),
onSelectDatasourceFilters: jest.fn(),
richHistorySettings: {
retentionPeriod: 7,
starredTabAsFirstTab: false,
activeDatasourceOnly,
lastUsedDatasourceFilters: [],
},
richHistorySearchFilters: {
search: '',
sortOrder: SortOrder.Ascending,
datasourceFilters: [],
from: 0,
to: 7,
starred: false,
},
};
Object.assign(props, propOverrides);
const wrapper = mount(<RichHistoryStarredTab {...props} />);
return wrapper;
};
@ -41,7 +59,7 @@ describe('RichHistoryStarredTab', () => {
});
it('should not render select datasource if activeDatasourceOnly is true', () => {
const wrapper = setup({ activeDatasourceOnly: true });
const wrapper = setup(true);
expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeFalsy();
});
});

View File

@ -1,11 +1,14 @@
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, Select, MultiSelect, FilterInput } from '@grafana/ui';
import { filterAndSortQueries, createDatasourcesList, SortOrder } from 'app/core/utils/richHistory';
import {
createDatasourcesList,
SortOrder,
RichHistorySearchFilters,
RichHistorySettings,
} from 'app/core/utils/richHistory';
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { sortOrderOptions } from './RichHistory';
@ -13,12 +16,12 @@ import RichHistoryCard from './RichHistoryCard';
export interface Props {
queries: RichHistoryQuery[];
sortOrder: SortOrder;
activeDatasourceOnly: boolean;
datasourceFilters: SelectableValue[];
activeDatasourceInstance?: string;
updateFilters: (filtersToUpdate: Partial<RichHistorySearchFilters>) => void;
clearRichHistoryResults: () => void;
richHistorySearchFilters?: RichHistorySearchFilters;
richHistorySettings: RichHistorySettings;
exploreId: ExploreId;
onChangeSortOrder: (sortOrder: SortOrder) => void;
onSelectDatasourceFilters: (value: SelectableValue[]) => void;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
@ -67,90 +70,89 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
export function RichHistoryStarredTab(props: Props) {
const {
datasourceFilters,
onSelectDatasourceFilters,
updateFilters,
clearRichHistoryResults,
activeDatasourceInstance,
richHistorySettings,
queries,
onChangeSortOrder,
sortOrder,
activeDatasourceOnly,
richHistorySearchFilters,
exploreId,
} = props;
const [data, setData] = useState<[RichHistoryQuery[], ReturnType<typeof createDatasourcesList>]>([[], []]);
const [searchInput, setSearchInput] = useState('');
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
const theme = useTheme();
const styles = getStyles(theme);
useDebounce(
() => {
setDebouncedSearchInput(searchInput);
},
300,
[searchInput]
);
const listOfDatasources = createDatasourcesList();
useEffect(() => {
const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map((d) => d.datasourceName);
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
const starredQueries = queries.filter((q) => q.starred === true);
setData([
filterAndSortQueries(
starredQueries,
sortOrder,
datasourceFilters.map((d) => d.value),
debouncedSearchInput
),
listOfDatasources,
]);
}, [queries, sortOrder, datasourceFilters, debouncedSearchInput]);
const datasourceFilters =
richHistorySettings.activeDatasourceOnly && activeDatasourceInstance
? [activeDatasourceInstance]
: richHistorySettings.lastUsedDatasourceFilters;
const filters: RichHistorySearchFilters = {
search: '',
sortOrder: SortOrder.Descending,
datasourceFilters,
from: 0,
to: richHistorySettings.retentionPeriod,
starred: true,
};
updateFilters(filters);
return () => {
clearRichHistoryResults();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [filteredQueries, listOfDatasources] = data;
if (!richHistorySearchFilters) {
return <span>Loading...</span>;
}
return (
<div className={styles.container}>
<div className={styles.containerContent}>
<div className={styles.selectors}>
{!activeDatasourceOnly && (
{!richHistorySettings.activeDatasourceOnly && (
<MultiSelect
className={styles.multiselect}
menuShouldPortal
options={listOfDatasources}
value={datasourceFilters}
options={listOfDatasources.map((ds) => {
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) });
}}
/>
)}
<div className={styles.filterInput}>
<FilterInput
placeholder="Search queries"
value={searchInput}
onChange={(value: string) => {
setSearchInput(value);
}}
value={richHistorySearchFilters.search}
onChange={(search: string) => updateFilters({ search })}
/>
</div>
<div aria-label="Sort queries" className={styles.sort}>
<Select
menuShouldPortal
value={sortOrderOptions.filter((order) => 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<SortOrder>) => updateFilters({ sortOrder: e.value })}
/>
</div>
</div>
{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 (
<RichHistoryCard
query={q}
key={q.id}
exploreId={exploreId}
dsImg={listOfDatasources[idx].imgUrl}
isRemoved={listOfDatasources[idx].isRemoved}
dsImg={idx === -1 ? 'public/img/icn-datasource.svg' : listOfDatasources[idx].imgUrl}
isRemoved={idx === -1}
/>
);
})}

View File

@ -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);
};

View File

@ -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,

View File

@ -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,

View File

@ -7,7 +7,6 @@ import {
deleteQueryInRichHistory,
getRichHistory,
getRichHistorySettings,
SortOrder,
updateCommentInRichHistory,
updateRichHistorySettings,
updateStarredInRichHistory,
@ -118,39 +117,33 @@ export const deleteRichHistory = (): ThunkResult<void> => {
};
export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
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<void> => {
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<void> => {
export const initRichHistory = (): ThunkResult<void> => {
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<void> => {
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,

View File

@ -31,7 +31,7 @@ export const richHistoryLimitExceededAction = createAction('explore/richHistoryL
export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
export const richHistorySearchFiltersUpdatedAction = createAction<{
exploreId: ExploreId;
filters: RichHistorySearchFilters;
filters?: RichHistorySearchFilters;
}>('explore/richHistorySearchFiltersUpdatedAction');
/**

View File

@ -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));
}
/**