From 16a948fc88425b6f86a3ed553e1edb52ed496671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Mon, 23 May 2022 13:42:12 +0200 Subject: [PATCH] Query History: Implement RemoteStorage methods: settings (#49320) * Implement methods to get an update user preferences * Update integration test * Update label * Remove unused type * Simplify async code --- .../SharedPreferences/SharedPreferences.tsx | 16 ++-- .../history/RichHistoryRemoteStorage.test.ts | 64 ++++++++++++++- .../core/history/RichHistoryRemoteStorage.ts | 18 ++++- .../history/richHistoryStorageProvider.ts | 9 +++ .../app/core/services/PreferencesService.ts | 10 +++ .../RichHistory/RichHistorySettingsTab.tsx | 81 +++++++++++-------- .../explore/spec/queryHistory.test.tsx | 13 +++ public/app/types/preferences.ts | 3 + 8 files changed, 168 insertions(+), 46 deletions(-) diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index de072f5189c..5e3d780c9a9 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -22,18 +22,16 @@ import { PreferencesService } from 'app/core/services/PreferencesService'; import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; +import { UserPreferencesDTO } from '../../../types'; + export interface Props { resourceUri: string; disabled?: boolean; } -export interface State { - homeDashboardId: number; - theme: string; - timezone: string; - weekStart: string; +export type State = UserPreferencesDTO & { dashboards: DashboardSearchHit[]; -} +}; const themes: SelectableValue[] = [ { value: '', label: t({ id: 'shared-preferences.theme.default-label', message: 'Default' }) }, @@ -54,6 +52,7 @@ export class SharedPreferences extends PureComponent { timezone: '', weekStart: '', dashboards: [], + queryHistory: { homeTab: '' }, }; } @@ -90,12 +89,13 @@ export class SharedPreferences extends PureComponent { timezone: prefs.timezone, weekStart: prefs.weekStart, dashboards: [defaultDashboardHit, ...dashboards], + queryHistory: prefs.queryHistory, }); } onSubmitForm = async () => { - const { homeDashboardId, theme, timezone, weekStart } = this.state; - await this.service.update({ homeDashboardId, theme, timezone, weekStart }); + const { homeDashboardId, theme, timezone, weekStart, queryHistory } = this.state; + await this.service.update({ homeDashboardId, theme, timezone, weekStart, queryHistory }); window.location.reload(); }; diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts index 673ac3390be..0471a4cfd27 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.test.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -1,7 +1,7 @@ import { of } from 'rxjs'; import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery, UserPreferencesDTO } from '../../types'; import { SortOrder } from '../utils/richHistoryTypes'; import RichHistoryRemoteStorage, { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; @@ -26,6 +26,16 @@ jest.mock('@grafana/runtime', () => ({ getDataSourceSrv: () => dsMock, })); +const preferencesServiceMock = { + patch: jest.fn(), + load: jest.fn(), +}; +jest.mock('../services/PreferencesService', () => ({ + PreferencesService: function () { + return preferencesServiceMock; + }, +})); + describe('RichHistoryRemoteStorage', () => { let storage: RichHistoryRemoteStorage; @@ -91,6 +101,58 @@ describe('RichHistoryRemoteStorage', () => { expect(items).toMatchObject([richHistoryQuery]); }); + it('read starred home tab preferences', async () => { + preferencesServiceMock.load.mockResolvedValue({ + queryHistory: { + homeTab: 'starred', + }, + } as UserPreferencesDTO); + const settings = await storage.getSettings(); + expect(settings).toMatchObject({ + activeDatasourceOnly: false, + lastUsedDatasourceFilters: undefined, + retentionPeriod: 14, + starredTabAsFirstTab: true, + }); + }); + + it('uses default home tab preferences', async () => { + preferencesServiceMock.load.mockResolvedValue({ + queryHistory: { + homeTab: '', + }, + } as UserPreferencesDTO); + const settings = await storage.getSettings(); + expect(settings).toMatchObject({ + activeDatasourceOnly: false, + lastUsedDatasourceFilters: undefined, + retentionPeriod: 14, + starredTabAsFirstTab: false, + }); + }); + + it('updates user settings', async () => { + await storage.updateSettings({ + activeDatasourceOnly: false, + lastUsedDatasourceFilters: undefined, + retentionPeriod: 14, + starredTabAsFirstTab: false, + }); + expect(preferencesServiceMock.patch).toBeCalledWith({ + queryHistory: { homeTab: 'query' }, + } as Partial); + + await storage.updateSettings({ + activeDatasourceOnly: false, + lastUsedDatasourceFilters: undefined, + retentionPeriod: 14, + starredTabAsFirstTab: true, + }); + expect(preferencesServiceMock.patch).toBeCalledWith({ + queryHistory: { homeTab: 'starred' }, + } as Partial); + }); + it('migrates provided rich history items', async () => { const { richHistoryQuery, dto } = setup(); fetchMock.mockReturnValue(of({})); diff --git a/public/app/core/history/RichHistoryRemoteStorage.ts b/public/app/core/history/RichHistoryRemoteStorage.ts index b09e32d33a6..eafcc9eea85 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.ts @@ -4,6 +4,7 @@ import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; import { RichHistoryQuery } from 'app/types/explore'; import { DataQuery } from '../../../../packages/grafana-data'; +import { PreferencesService } from '../services/PreferencesService'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; import RichHistoryStorage, { RichHistoryStorageWarningDetails } from './RichHistoryStorage'; @@ -37,6 +38,12 @@ type RichHistoryRemoteStorageResultsPayloadDTO = { }; export default class RichHistoryRemoteStorage implements RichHistoryStorage { + private readonly preferenceService: PreferencesService; + + constructor() { + this.preferenceService = new PreferencesService('user'); + } + async addToRichHistory( newRichHistoryQuery: Omit ): Promise<{ warning?: RichHistoryStorageWarningDetails; richHistoryQuery: RichHistoryQuery }> { @@ -71,11 +78,12 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage { } async getSettings(): Promise { + const preferences = await this.preferenceService.load(); return { activeDatasourceOnly: false, lastUsedDatasourceFilters: undefined, retentionPeriod: 14, - starredTabAsFirstTab: false, + starredTabAsFirstTab: preferences.queryHistory?.homeTab === 'starred', }; } @@ -83,8 +91,12 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage { throw new Error('not supported yet'); } - async updateSettings(settings: RichHistorySettings): Promise { - throw new Error('not supported yet'); + updateSettings(settings: RichHistorySettings): Promise { + return this.preferenceService.patch({ + queryHistory: { + homeTab: settings.starredTabAsFirstTab ? 'starred' : 'query', + }, + }); } async updateStarred(id: string, starred: boolean): Promise { diff --git a/public/app/core/history/richHistoryStorageProvider.ts b/public/app/core/history/richHistoryStorageProvider.ts index f3d3fe680e0..a4086776ddd 100644 --- a/public/app/core/history/richHistoryStorageProvider.ts +++ b/public/app/core/history/richHistoryStorageProvider.ts @@ -16,6 +16,9 @@ export const getRichHistoryStorage = (): RichHistoryStorage => { interface RichHistorySupportedFeatures { availableFilters: SortOrder[]; lastUsedDataSourcesAvailable: boolean; + clearHistory: boolean; + onlyActiveDataSource: boolean; + changeRetention: boolean; } export const supportedFeatures = (): RichHistorySupportedFeatures => { @@ -23,9 +26,15 @@ export const supportedFeatures = (): RichHistorySupportedFeatures => { ? { availableFilters: [SortOrder.Descending, SortOrder.Ascending], lastUsedDataSourcesAvailable: false, + clearHistory: false, + onlyActiveDataSource: false, + changeRetention: false, } : { availableFilters: [SortOrder.Descending, SortOrder.Ascending, SortOrder.DatasourceAZ, SortOrder.DatasourceZA], lastUsedDataSourcesAvailable: true, + clearHistory: true, + onlyActiveDataSource: true, + changeRetention: true, }; }; diff --git a/public/app/core/services/PreferencesService.ts b/public/app/core/services/PreferencesService.ts index d2ab11e23cd..fe431964725 100644 --- a/public/app/core/services/PreferencesService.ts +++ b/public/app/core/services/PreferencesService.ts @@ -5,10 +5,20 @@ import { backendSrv } from './backend_srv'; export class PreferencesService { constructor(private resourceUri: string) {} + /** + * Overrides all preferences + */ update(preferences: UserPreferencesDTO): Promise { return backendSrv.put(`/api/${this.resourceUri}/preferences`, preferences); } + /** + * Updates only provided preferences + */ + patch(preferences: Partial): Promise { + return backendSrv.patch(`/api/${this.resourceUri}/preferences`, preferences); + } + load(): Promise { return backendSrv.get(`/api/${this.resourceUri}/preferences`); } diff --git a/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx index dfbf135a009..122f25a1b63 100644 --- a/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx @@ -2,13 +2,14 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; -import { stylesFactory, useTheme, Select, Button, Field, InlineField, InlineSwitch } from '@grafana/ui'; +import { stylesFactory, useTheme, Select, Button, Field, InlineField, InlineSwitch, Alert } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import appEvents from 'app/core/app_events'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { MAX_HISTORY_ITEMS } from 'app/core/history/RichHistoryLocalStorage'; import { dispatch } from 'app/store/store'; +import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { ShowConfirmModalEvent } from '../../../types/events'; export interface RichHistorySettingsProps { @@ -73,15 +74,21 @@ export function RichHistorySettingsTab(props: RichHistorySettingsProps) { return (
- -
- -
-
+ {supportedFeatures().changeRetention ? ( + +
+ +
+
+ ) : ( + + Grafana will keep entries up to {selectedOption?.label}. + + )} - - - -
- Clear query history -
-
- Delete all of your query history, permanently. -
- + {supportedFeatures().onlyActiveDataSource && ( + + + + )} + {supportedFeatures().clearHistory && ( +
+
+ Clear query history +
+
+ Delete all of your query history, permanently. +
+ +
+ )}
); } diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 99b2a3afb8d..f0ec2816a60 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -36,6 +36,19 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }), })); +jest.mock('app/core/services/PreferencesService', () => ({ + PreferencesService: function () { + return { + patch: jest.fn(), + load: jest.fn().mockResolvedValue({ + queryHistory: { + homeTab: 'query', + }, + }), + }; + }, +})); + jest.mock('react-virtualized-auto-sizer', () => { return { __esModule: true, diff --git a/public/app/types/preferences.ts b/public/app/types/preferences.ts index ea255d469f7..ad9a07c1e5b 100644 --- a/public/app/types/preferences.ts +++ b/public/app/types/preferences.ts @@ -5,4 +5,7 @@ export interface UserPreferencesDTO { weekStart: string; homeDashboardId: number; theme: string; + queryHistory: { + homeTab: '' | 'query' | 'starred'; + }; }