mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
a320e942a6
commit
361cc18b45
@ -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": [
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
@ -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 };
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
69
public/app/core/history/richHistoryLocalStorageUtils.test.ts
Normal file
69
public/app/core/history/richHistoryLocalStorageUtils.test.ts
Normal 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' })]);
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
/>
|
||||
),
|
||||
|
@ -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', () => {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user