diff --git a/.betterer.results b/.betterer.results index f22623f5951..66357fac2e3 100644 --- a/.betterer.results +++ b/.betterer.results @@ -200,7 +200,7 @@ exports[`no enzyme tests`] = { "public/app/features/dimensions/editors/ThresholdsEditor/ThresholdsEditor.test.tsx:4164297658": [ [0, 17, 13, "RegExp match", "2409514259"] ], - "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3933225580": [ + "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3420464349": [ [0, 17, 13, "RegExp match", "2409514259"] ], "public/app/features/folders/FolderSettingsPage.test.tsx:1109052730": [ diff --git a/e2e/various-suite/explore.spec.ts b/e2e/various-suite/explore.spec.ts index 57dab64ceec..d77248ead51 100644 --- a/e2e/various-suite/explore.spec.ts +++ b/e2e/various-suite/explore.spec.ts @@ -43,5 +43,9 @@ e2e.scenario({ const canvases = e2e().get('canvas'); canvases.should('have.length', 1); + + // Both queries above should have been run and be shown in the query history + e2e.components.QueryTab.queryHistoryButton().should('be.visible').click(); + e2e.components.QueryHistory.queryText().should('have.length', 2).should('contain', 'csv_metric_values'); }, }); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index f226ae538d6..66bf730396a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -163,8 +163,12 @@ export const Components = { QueryTab: { content: 'Query editor tab content', queryInspectorButton: 'Query inspector button', + queryHistoryButton: 'Rich history button', addQuery: 'Query editor add query button', }, + QueryHistory: { + queryText: 'Query text', + }, QueryEditorRows: { rows: 'Query editor row', }, diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index 4a39f9d7871..5a52a6cd746 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -2,6 +2,7 @@ import { DataQuery } from '@grafana/data'; import store from 'app/core/store'; import { afterEach, beforeEach } from '../../../test/lib/common'; +import { DatasourceSrv } from '../../features/plugins/datasource_srv'; import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; @@ -11,19 +12,21 @@ import { RichHistoryStorageWarning } from './RichHistoryStorage'; const key = 'grafana.explore.richHistory'; +const dsMock = new DatasourceSrv(); +dsMock.init( + { + // @ts-ignore + 'name-of-dev-test': { uid: 'dev-test', name: 'name-of-dev-test' }, + // @ts-ignore + 'name-of-dev-test-2': { uid: 'dev-test-2', name: 'name-of-dev-test-2' }, + }, + '' +); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, - getDataSourceSrv: () => { - return { - getList: () => { - return [ - { uid: 'dev-test-uid', name: 'dev-test' }, - { uid: 'dev-test-2-uid', name: 'dev-test-2' }, - ]; - }, - }; - }, + getDataSourceSrv: () => dsMock, })); interface MockQuery extends DataQuery { @@ -43,8 +46,8 @@ const mockItem: RichHistoryQuery = { id: '2', createdAt: 2, starred: true, - datasourceUid: 'dev-test-uid', - datasourceName: 'dev-test', + datasourceUid: 'dev-test', + datasourceName: 'name-of-dev-test', comment: 'test', queries: [{ refId: 'ref', query: 'query-test' }], }; @@ -53,8 +56,8 @@ const mockItem2: RichHistoryQuery = { id: '3', createdAt: 3, starred: true, - datasourceUid: 'dev-test-2-uid', - datasourceName: 'dev-test-2', + datasourceUid: 'dev-test-2', + datasourceName: 'name-of-dev-test-2', comment: 'test-2', queries: [{ refId: 'ref-2', query: 'query-2' }], }; @@ -130,17 +133,41 @@ describe('RichHistoryLocalStorage', () => { describe('retention policy and max limits', () => { it('should clear old not-starred items', async () => { - 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 historyStarredOld = { + starred: true, + ts: old.getTime(), + queries: [], + comment: 'old starred', + datasourceName: 'name-of-dev-test', + }; + const historyNotStarredOld = { + starred: false, + ts: old.getTime(), + queries: [], + comment: 'new not starred', + datasourceName: 'name-of-dev-test', + }; + const historyStarredNew = { + starred: true, + ts: now.getTime(), + queries: [], + comment: 'new starred', + datasourceName: 'name-of-dev-test', + }; + const historyNotStarredNew = { + starred: false, + ts: now.getTime(), + queries: [], + comment: 'new not starred', + datasourceName: 'name-of-dev-test', + }; const history = [historyNotStarredNew, historyStarredNew, historyStarredOld, historyNotStarredOld]; store.setObject(key, history); const historyNew = { starred: true, - datasourceUid: 'dev-test-uid', - datasourceName: 'dev-test', + datasourceUid: 'dev-test', + datasourceName: 'name-of-dev-test', comment: 'recently added', queries: [{ refId: 'ref' }], }; @@ -209,7 +236,7 @@ describe('RichHistoryLocalStorage', () => { { ts: 2, starred: true, - datasourceName: 'dev-test', + datasourceName: 'name-of-dev-test', comment: 'test', queries: ['test query 1', 'test query 2', 'test query 3'], }, @@ -218,8 +245,8 @@ describe('RichHistoryLocalStorage', () => { id: '2', createdAt: 2, starred: true, - datasourceUid: 'dev-test-uid', - datasourceName: 'dev-test', + datasourceUid: 'dev-test', + datasourceName: 'name-of-dev-test', comment: 'test', queries: [ { @@ -246,7 +273,7 @@ describe('RichHistoryLocalStorage', () => { { ts: 2, starred: true, - datasourceName: 'dev-test', + datasourceName: 'name-of-dev-test', comment: 'test', queries: ['{"refId":"A","key":"key1","metrics":[]}', '{"refId":"B","key":"key2","metrics":[]}'], }, @@ -255,8 +282,8 @@ describe('RichHistoryLocalStorage', () => { id: '2', createdAt: 2, starred: true, - datasourceUid: 'dev-test-uid', - datasourceName: 'dev-test', + datasourceUid: 'dev-test', + datasourceName: 'name-of-dev-test', comment: 'test', queries: [ { diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts new file mode 100644 index 00000000000..ad60e3cfa7f --- /dev/null +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -0,0 +1,84 @@ +import { of } from 'rxjs'; + +import { DatasourceSrv } from '../../features/plugins/datasource_srv'; +import { RichHistoryQuery } from '../../types'; +import { SortOrder } from '../utils/richHistoryTypes'; + +import RichHistoryRemoteStorage, { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; + +const dsMock = new DatasourceSrv(); +dsMock.init( + { + // @ts-ignore + 'name-of-ds1': { uid: 'ds1', name: 'name-of-ds1' }, + // @ts-ignore + 'name-of-ds2': { uid: 'ds2', name: 'name-of-ds2' }, + }, + '' +); + +const fetchMock = jest.fn(); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => ({ + fetch: fetchMock, + }), + getDataSourceSrv: () => dsMock, +})); + +describe('RichHistoryRemoteStorage', () => { + let storage: RichHistoryRemoteStorage; + + beforeEach(() => { + fetchMock.mockReset(); + storage = new RichHistoryRemoteStorage(); + }); + + it('returns list of query history items', async () => { + const expectedViewModel: RichHistoryQuery = { + id: '123', + createdAt: 200 * 1000, + datasourceUid: 'ds1', + datasourceName: 'name-of-ds1', + starred: true, + comment: 'comment', + queries: [{ foo: 'bar ' }], + }; + const returnedDTOs: RichHistoryRemoteStorageDTO[] = [ + { + uid: expectedViewModel.id, + createdAt: expectedViewModel.createdAt / 1000, + datasourceUid: expectedViewModel.datasourceUid, + starred: expectedViewModel.starred, + comment: expectedViewModel.comment, + queries: expectedViewModel.queries, + }, + ]; + fetchMock.mockReturnValue( + of({ + data: { + result: { + queryHistory: returnedDTOs, + }, + }, + }) + ); + const search = 'foo'; + const datasourceFilters = ['name-of-ds1', 'name-of-ds2']; + const sortOrder = SortOrder.Descending; + const starred = true; + const from = 100; + const to = 200; + const expectedLimit = 100; + const expectedPage = 1; + + const items = await storage.getRichHistory({ search, datasourceFilters, sortOrder, starred, to, from }); + + expect(fetchMock).toBeCalledWith({ + method: 'GET', + url: `/api/query-history?datasourceUid=ds1&datasourceUid=ds2&searchString=${search}&sort=time-desc&to=now-${from}d&from=now-${to}d&limit=${expectedLimit}&page=${expectedPage}&onlyStarred=${starred}`, + requestId: 'query-history-get-all', + }); + expect(items).toMatchObject([expectedViewModel]); + }); +}); diff --git a/public/app/core/history/RichHistoryRemoteStorage.ts b/public/app/core/history/RichHistoryRemoteStorage.ts new file mode 100644 index 00000000000..53aeab48a1a --- /dev/null +++ b/public/app/core/history/RichHistoryRemoteStorage.ts @@ -0,0 +1,107 @@ +import { lastValueFrom } from 'rxjs'; + +import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; +import { RichHistoryQuery } from 'app/types/explore'; + +import { DataQuery } from '../../../../packages/grafana-data'; +import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; + +import RichHistoryStorage, { RichHistoryStorageWarningDetails } from './RichHistoryStorage'; +import { fromDTO } from './remoteStorageConverter'; + +export type RichHistoryRemoteStorageDTO = { + uid: string; + createdAt: number; + datasourceUid: string; + starred: boolean; + comment: string; + queries: DataQuery[]; +}; + +type RichHistoryRemoteStorageResultsPayloadDTO = { + result: { + queryHistory: RichHistoryRemoteStorageDTO[]; + }; +}; + +export default class RichHistoryRemoteStorage implements RichHistoryStorage { + async addToRichHistory( + newRichHistoryQuery: Omit + ): Promise<{ warning?: RichHistoryStorageWarningDetails; richHistoryQuery: RichHistoryQuery }> { + const { result } = await getBackendSrv().post(`/api/query-history`, { + dataSourceUid: newRichHistoryQuery.datasourceUid, + queries: newRichHistoryQuery.queries, + }); + return { + richHistoryQuery: fromDTO(result), + }; + } + + async deleteAll(): Promise { + throw new Error('not supported'); + } + + async deleteRichHistory(id: string): Promise { + throw new Error('not supported yet'); + } + + async getRichHistory(filters: RichHistorySearchFilters): Promise { + const params = buildQueryParams(filters); + const queryHistory = await lastValueFrom( + getBackendSrv().fetch({ + method: 'GET', + url: `/api/query-history?${params}`, + // to ensure any previous requests are cancelled + requestId: 'query-history-get-all', + }) + ); + return ((queryHistory.data as RichHistoryRemoteStorageResultsPayloadDTO).result.queryHistory || []).map(fromDTO); + } + + async getSettings(): Promise { + return { + activeDatasourceOnly: false, + lastUsedDatasourceFilters: undefined, + retentionPeriod: 14, + starredTabAsFirstTab: false, + }; + } + + async updateComment(id: string, comment: string | undefined): Promise { + throw new Error('not supported yet'); + } + + async updateSettings(settings: RichHistorySettings): Promise { + throw new Error('not supported yet'); + } + + async updateStarred(id: string, starred: boolean): Promise { + throw new Error('not supported yet'); + } +} + +function buildQueryParams(filters: RichHistorySearchFilters): string { + let params = `${filters.datasourceFilters + .map((datasourceName) => { + const uid = getDataSourceSrv().getInstanceSettings(datasourceName)!.uid; + return `datasourceUid=${encodeURIComponent(uid)}`; + }) + .join('&')}`; + if (filters.search) { + params = params + `&searchString=${filters.search}`; + } + if (filters.sortOrder) { + params = params + `&sort=${filters.sortOrder === SortOrder.Ascending ? 'time-asc' : 'time-desc'}`; + } + const relativeFrom = filters.from === 0 ? 'now' : `now-${filters.from}d`; + const relativeTo = filters.to === 0 ? 'now' : `now-${filters.to}d`; + // TODO: Unify: remote storage from/to params are swapped comparing to frontend and local storage filters + params = params + `&to=${relativeFrom}`; + params = params + `&from=${relativeTo}`; + params = params + `&limit=100`; + params = params + `&page=1`; + if (filters.starred) { + params = params + `&onlyStarred=${filters.starred}`; + } + return params; +} diff --git a/public/app/core/history/localStorageConverter.test.ts b/public/app/core/history/localStorageConverter.test.ts index 960ebbe9e5a..20b8701a258 100644 --- a/public/app/core/history/localStorageConverter.test.ts +++ b/public/app/core/history/localStorageConverter.test.ts @@ -1,26 +1,31 @@ +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; + import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; import { fromDTO, toDTO } from './localStorageConverter'; +const dsMock = new DatasourceSrv(); +dsMock.init( + { + // @ts-ignore + 'name-of-dev-test': { uid: 'dev-test', name: 'name-of-dev-test' }, + }, + '' +); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, - getDataSourceSrv: () => { - return { - getList: () => { - return [{ uid: 'uid', name: 'dev-test' }]; - }, - }; - }, + getDataSourceSrv: () => dsMock, })); const validRichHistory: RichHistoryQuery = { comment: 'comment', createdAt: 1, - datasourceName: 'dev-test', - datasourceUid: 'uid', + datasourceName: 'name-of-dev-test', + datasourceUid: 'dev-test', id: '1', queries: [{ refId: 'A' }], starred: true, @@ -28,7 +33,7 @@ const validRichHistory: RichHistoryQuery = { const validDTO: RichHistoryLocalStorageDTO = { comment: 'comment', - datasourceName: 'dev-test', + datasourceName: 'name-of-dev-test', queries: [{ refId: 'A' }], starred: true, ts: 1, diff --git a/public/app/core/history/localStorageConverter.ts b/public/app/core/history/localStorageConverter.ts index 8e480ffc8d6..62be110bcdd 100644 --- a/public/app/core/history/localStorageConverter.ts +++ b/public/app/core/history/localStorageConverter.ts @@ -1,6 +1,3 @@ -import { find } from 'lodash'; - -import { DataSourceInstanceSettings } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { RichHistoryQuery } from '../../types'; @@ -8,11 +5,7 @@ import { RichHistoryQuery } from '../../types'; import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; export const fromDTO = (dto: RichHistoryLocalStorageDTO): RichHistoryQuery => { - const datasource = find( - getDataSourceSrv().getList(), - (settings: DataSourceInstanceSettings) => settings.name === dto.datasourceName - ); - + const datasource = getDataSourceSrv().getInstanceSettings(dto.datasourceName); return { id: dto.ts.toString(), createdAt: dto.ts, @@ -25,10 +18,7 @@ export const fromDTO = (dto: RichHistoryLocalStorageDTO): RichHistoryQuery => { }; export const toDTO = (richHistoryQuery: RichHistoryQuery): RichHistoryLocalStorageDTO => { - const datasource = find( - getDataSourceSrv().getList(), - (settings: DataSourceInstanceSettings) => settings.uid === richHistoryQuery.datasourceUid - ); + const datasource = getDataSourceSrv().getInstanceSettings({ uid: richHistoryQuery.datasourceUid }); if (!datasource) { throw new Error('Datasource not found.'); diff --git a/public/app/core/history/remoteStorageConverter.test.ts b/public/app/core/history/remoteStorageConverter.test.ts new file mode 100644 index 00000000000..dc021c05397 --- /dev/null +++ b/public/app/core/history/remoteStorageConverter.test.ts @@ -0,0 +1,46 @@ +import { DatasourceSrv } from '../../features/plugins/datasource_srv'; +import { RichHistoryQuery } from '../../types'; +import { backendSrv } from '../services/backend_srv'; + +import { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; +import { fromDTO } from './remoteStorageConverter'; + +const dsMock = new DatasourceSrv(); +dsMock.init( + { + // @ts-ignore + 'name-of-dev-test': { uid: 'dev-test', name: 'name-of-dev-test' }, + }, + '' +); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, + getDataSourceSrv: () => dsMock, +})); + +const validRichHistory: RichHistoryQuery = { + comment: 'comment', + createdAt: 1000, + datasourceName: 'name-of-dev-test', + datasourceUid: 'dev-test', + id: 'ID', + queries: [{ refId: 'A' }], + starred: true, +}; + +const validDTO: RichHistoryRemoteStorageDTO = { + comment: 'comment', + datasourceUid: 'dev-test', + queries: [{ refId: 'A' }], + starred: true, + uid: 'ID', + createdAt: 1, +}; + +describe('RemoteStorage converter', () => { + it('converts DTO to RichHistoryQuery', () => { + expect(fromDTO(validDTO)).toMatchObject(validRichHistory); + }); +}); diff --git a/public/app/core/history/remoteStorageConverter.ts b/public/app/core/history/remoteStorageConverter.ts new file mode 100644 index 00000000000..85fe40442d7 --- /dev/null +++ b/public/app/core/history/remoteStorageConverter.ts @@ -0,0 +1,19 @@ +import { getDataSourceSrv } from '@grafana/runtime'; + +import { RichHistoryQuery } from '../../types'; + +import { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; + +export const fromDTO = (dto: RichHistoryRemoteStorageDTO): RichHistoryQuery => { + const datasource = getDataSourceSrv().getInstanceSettings({ uid: dto.datasourceUid }); + + return { + id: dto.uid, + createdAt: dto.createdAt * 1000, + datasourceName: datasource?.name || '', // will be show on the list as coming from a removed data source + datasourceUid: dto.datasourceUid, + starred: dto.starred, + comment: dto.comment, + queries: dto.queries, + }; +}; diff --git a/public/app/core/history/richHistoryStorageProvider.ts b/public/app/core/history/richHistoryStorageProvider.ts index fed05c0261d..f3d3fe680e0 100644 --- a/public/app/core/history/richHistoryStorageProvider.ts +++ b/public/app/core/history/richHistoryStorageProvider.ts @@ -1,8 +1,31 @@ +import { config } from '@grafana/runtime'; + +import { SortOrder } from '../utils/richHistoryTypes'; + import RichHistoryLocalStorage from './RichHistoryLocalStorage'; +import RichHistoryRemoteStorage from './RichHistoryRemoteStorage'; import RichHistoryStorage from './RichHistoryStorage'; const richHistoryLocalStorage = new RichHistoryLocalStorage(); +const richHistoryRemoteStorage = new RichHistoryRemoteStorage(); export const getRichHistoryStorage = (): RichHistoryStorage => { - return richHistoryLocalStorage; + return config.queryHistoryEnabled ? richHistoryRemoteStorage : richHistoryLocalStorage; +}; + +interface RichHistorySupportedFeatures { + availableFilters: SortOrder[]; + lastUsedDataSourcesAvailable: boolean; +} + +export const supportedFeatures = (): RichHistorySupportedFeatures => { + return config.queryHistoryEnabled + ? { + availableFilters: [SortOrder.Descending, SortOrder.Ascending], + lastUsedDataSourcesAvailable: false, + } + : { + availableFilters: [SortOrder.Descending, SortOrder.Ascending, SortOrder.DatasourceAZ, SortOrder.DatasourceZA], + lastUsedDataSourcesAvailable: true, + }; }; diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index 4903fd40a93..1501c59c924 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -1,7 +1,13 @@ export enum SortOrder { Descending = 'Descending', Ascending = 'Ascending', + /** + * @deprecated supported only by local storage. It will be removed in the future + */ DatasourceAZ = 'Datasource A-Z', + /** + * @deprecated supported only by local storage. It will be removed in the future + */ DatasourceZA = 'Datasource Z-A', } @@ -9,12 +15,13 @@ export interface RichHistorySettings { retentionPeriod: number; starredTabAsFirstTab: boolean; activeDatasourceOnly: boolean; - lastUsedDatasourceFilters: string[]; + lastUsedDatasourceFilters?: string[]; } export type RichHistorySearchFilters = { search: string; sortOrder: SortOrder; + /** Names of data sources (not uids) - used by local and remote storage **/ datasourceFilters: string[]; from: number; to: number; diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 2dad677500e..cd441bb97d0 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -6,6 +6,8 @@ import { Themeable, withTheme, TabbedContainer, TabConfig } from '@grafana/ui'; import { SortOrder, RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; import { RichHistoryQuery, ExploreId } from 'app/types/explore'; +import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; + import { RichHistoryQueriesTab } from './RichHistoryQueriesTab'; import { RichHistorySettingsTab } from './RichHistorySettingsTab'; import { RichHistoryStarredTab } from './RichHistoryStarredTab'; @@ -16,12 +18,13 @@ export enum Tabs { Settings = 'Settings', } -export const sortOrderOptions = [ - { label: 'Newest first', value: SortOrder.Descending }, - { label: 'Oldest first', value: SortOrder.Ascending }, - { label: 'Data source A-Z', value: SortOrder.DatasourceAZ }, - { label: 'Data source Z-A', value: SortOrder.DatasourceZA }, -]; +export const getSortOrderOptions = () => + [ + { label: 'Newest first', value: SortOrder.Descending }, + { label: 'Oldest first', value: SortOrder.Ascending }, + { label: 'Data source A-Z', value: SortOrder.DatasourceAZ }, + { label: 'Data source Z-A', value: SortOrder.DatasourceZA }, + ].filter((option) => supportedFeatures().availableFilters.includes(option.value)); export interface RichHistoryProps extends Themeable { richHistory: RichHistoryQuery[]; @@ -32,14 +35,22 @@ export interface RichHistoryProps extends Themeable { loadRichHistory: (exploreId: ExploreId) => void; clearRichHistoryResults: (exploreId: ExploreId) => void; deleteRichHistory: () => void; - activeDatasourceInstance?: string; + activeDatasourceInstance: string; firstTab: Tabs; exploreId: ExploreId; height: number; onClose: () => void; } +type RichHistoryState = { + loading: boolean; +}; + class UnThemedRichHistory extends PureComponent { + state: RichHistoryState = { + loading: false, + }; + updateSettings = (settingsToUpdate: Partial) => { this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate }); }; @@ -59,6 +70,9 @@ class UnThemedRichHistory extends PureComponent { loadRichHistory = debounce(() => { this.props.loadRichHistory(this.props.exploreId); + this.setState({ + loading: true, + }); }, 300); onChangeRetentionPeriod = (retentionPeriod: SelectableValue) => { @@ -73,9 +87,18 @@ class UnThemedRichHistory extends PureComponent { toggleActiveDatasourceOnly = () => this.updateSettings({ activeDatasourceOnly: !this.props.richHistorySettings.activeDatasourceOnly }); + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>, snapshot?: any) { + if (prevProps.richHistory !== this.props.richHistory) { + this.setState({ + loading: false, + }); + } + } + render() { const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab, activeDatasourceInstance } = this.props; + const { loading } = this.state; const QueriesTab: TabConfig = { label: 'Query history', @@ -83,6 +106,7 @@ class UnThemedRichHistory extends PureComponent { content: ( this.props.clearRichHistoryResults(this.props.exploreId)} activeDatasourceInstance={activeDatasourceInstance} @@ -101,6 +125,7 @@ class UnThemedRichHistory extends PureComponent { content: ( this.props.clearRichHistoryResults(this.props.exploreId)} diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index b24bfba15b2..9715b9b236d 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -34,7 +34,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI return { richHistory, firstTab, - activeDatasourceInstance: datasourceInstance?.name, + activeDatasourceInstance: datasourceInstance!.name, richHistorySettings, richHistorySearchFilters, }; diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 5b62f2faac2..25b887a1d50 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { useEffect } from 'react'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { FilterInput, MultiSelect, RangeSlider, Select, stylesFactory, useTheme } from '@grafana/ui'; import { createDatasourcesList, @@ -13,12 +14,13 @@ import { } from 'app/core/utils/richHistory'; import { ExploreId, RichHistoryQuery } from 'app/types/explore'; -import { sortOrderOptions } from './RichHistory'; +import { getSortOrderOptions } from './RichHistory'; import RichHistoryCard from './RichHistoryCard'; export interface Props { queries: RichHistoryQuery[]; - activeDatasourceInstance?: string; + loading: boolean; + activeDatasourceInstance: string; updateFilters: (filtersToUpdate?: Partial) => void; clearRichHistoryResults: () => void; richHistorySettings: RichHistorySettings; @@ -119,6 +121,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { export function RichHistoryQueriesTab(props: Props) { const { queries, + loading, richHistorySearchFilters, updateFilters, clearRichHistoryResults, @@ -135,9 +138,9 @@ export function RichHistoryQueriesTab(props: Props) { useEffect(() => { const datasourceFilters = - richHistorySettings.activeDatasourceOnly && activeDatasourceInstance - ? [activeDatasourceInstance] - : richHistorySettings.lastUsedDatasourceFilters; + richHistorySettings.activeDatasourceOnly && richHistorySettings.lastUsedDatasourceFilters + ? richHistorySettings.lastUsedDatasourceFilters + : [activeDatasourceInstance]; const filters: RichHistorySearchFilters = { search: '', sortOrder: SortOrder.Descending, @@ -162,6 +165,7 @@ export function RichHistoryQueriesTab(props: Props) { * are keys and arrays with queries that belong to that headings are values. */ const mappedQueriesToHeadings = mapQueriesToHeadings(queries, richHistorySearchFilters.sortOrder); + const sortOrderOptions = getSortOrderOptions(); return (
@@ -219,28 +223,34 @@ export function RichHistoryQueriesTab(props: Props) { />
- {Object.keys(mappedQueriesToHeadings).map((heading) => { - return ( -
-
- {heading} {mappedQueriesToHeadings[heading].length} queries + + {loading && Loading results...} + + {!loading && + Object.keys(mappedQueriesToHeadings).map((heading) => { + return ( +
+
+ {heading} {mappedQueriesToHeadings[heading].length} queries +
+ {mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => { + const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName); + return ( + + ); + })}
- {mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => { - const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName); - return ( - - ); - })} -
- ); - })} -
The history is local to your browser and is not shared with others.
+ ); + })} +
+ {!config.queryHistoryEnabled ? 'The history is local to your browser and is not shared with others.' : ''} +
); diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx index 6f6431ab913..9d4e0e5f193 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx @@ -21,6 +21,8 @@ jest.mock('@grafana/runtime', () => ({ const setup = (activeDatasourceOnly = false) => { const props: Props = { queries: [], + loading: false, + activeDatasourceInstance: {} as any, updateFilters: jest.fn(), clearRichHistoryResults: jest.fn(), exploreId: ExploreId.left, diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 01aec0f4a67..9b98172420c 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { useEffect } from 'react'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { stylesFactory, useTheme, Select, MultiSelect, FilterInput } from '@grafana/ui'; import { createDatasourcesList, @@ -11,12 +12,13 @@ import { } from 'app/core/utils/richHistory'; import { RichHistoryQuery, ExploreId } from 'app/types/explore'; -import { sortOrderOptions } from './RichHistory'; +import { getSortOrderOptions } from './RichHistory'; import RichHistoryCard from './RichHistoryCard'; export interface Props { queries: RichHistoryQuery[]; - activeDatasourceInstance?: string; + loading: boolean; + activeDatasourceInstance: string; updateFilters: (filtersToUpdate: Partial) => void; clearRichHistoryResults: () => void; richHistorySearchFilters?: RichHistorySearchFilters; @@ -75,6 +77,7 @@ export function RichHistoryStarredTab(props: Props) { activeDatasourceInstance, richHistorySettings, queries, + loading, richHistorySearchFilters, exploreId, } = props; @@ -86,9 +89,9 @@ export function RichHistoryStarredTab(props: Props) { useEffect(() => { const datasourceFilters = - richHistorySettings.activeDatasourceOnly && activeDatasourceInstance - ? [activeDatasourceInstance] - : richHistorySettings.lastUsedDatasourceFilters; + richHistorySettings.activeDatasourceOnly && richHistorySettings.lastUsedDatasourceFilters + ? richHistorySettings.lastUsedDatasourceFilters + : [activeDatasourceInstance]; const filters: RichHistorySearchFilters = { search: '', sortOrder: SortOrder.Descending, @@ -108,6 +111,8 @@ export function RichHistoryStarredTab(props: Props) { return Loading...; } + const sortOrderOptions = getSortOrderOptions(); + return (
@@ -142,19 +147,23 @@ export function RichHistoryStarredTab(props: Props) { />
- {queries.map((q) => { - const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName); - return ( - - ); - })} -
The history is local to your browser and is not shared with others.
+ {loading && Loading results...} + {!loading && + queries.map((q) => { + const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName); + return ( + + ); + })} +
+ {!config.queryHistoryEnabled ? 'The history is local to your browser and is not shared with others.' : ''} +
); diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index ad56d246106..010d47489f4 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { Route, Router } from 'react-router-dom'; -import { DataSourceApi, DataSourceInstanceSettings, QueryEditorProps, ScopedVars } from '@grafana/data'; +import { DataSourceApi, DataSourceInstanceSettings, DataSourceRef, QueryEditorProps, ScopedVars } from '@grafana/data'; import { locationService, setDataSourceSrv, setEchoSrv } from '@grafana/runtime'; import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; import { Echo } from 'app/core/services/echo/Echo'; @@ -51,8 +51,8 @@ export function setupExplore(options?: SetupOptions): { getList(): DataSourceInstanceSettings[] { return dsSettings.map((d) => d.settings); }, - getInstanceSettings(name: string) { - return dsSettings.map((d) => d.settings).find((x) => x.name === name || x.uid === name); + getInstanceSettings(ref: DataSourceRef) { + return dsSettings.map((d) => d.settings).find((x) => x.name === ref || x.uid === ref || x.uid === ref.uid); }, get(name?: string | null, scopedVars?: ScopedVars): Promise { return Promise.resolve( diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index 6c65d4bfb7f..ff6379ce7cb 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -13,6 +13,7 @@ import { } from 'app/core/utils/richHistory'; import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types'; +import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; import { @@ -164,12 +165,14 @@ export const updateHistorySearchFilters = ( return async (dispatch, getState) => { await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } })); const currentSettings = getState().explore.richHistorySettings!; - await dispatch( - updateHistorySettings({ - ...currentSettings, - lastUsedDatasourceFilters: filters.datasourceFilters, - }) - ); + if (supportedFeatures().lastUsedDataSourcesAvailable) { + await dispatch( + updateHistorySettings({ + ...currentSettings, + lastUsedDatasourceFilters: filters.datasourceFilters, + }) + ); + } }; };