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
This commit is contained in:
Piotr Jamróz 2022-05-23 13:42:12 +02:00 committed by GitHub
parent 34d77fd584
commit 16a948fc88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 168 additions and 46 deletions

View File

@ -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<Props, State> {
timezone: '',
weekStart: '',
dashboards: [],
queryHistory: { homeTab: '' },
};
}
@ -90,12 +89,13 @@ export class SharedPreferences extends PureComponent<Props, State> {
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();
};

View File

@ -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<UserPreferencesDTO>);
await storage.updateSettings({
activeDatasourceOnly: false,
lastUsedDatasourceFilters: undefined,
retentionPeriod: 14,
starredTabAsFirstTab: true,
});
expect(preferencesServiceMock.patch).toBeCalledWith({
queryHistory: { homeTab: 'starred' },
} as Partial<UserPreferencesDTO>);
});
it('migrates provided rich history items', async () => {
const { richHistoryQuery, dto } = setup();
fetchMock.mockReturnValue(of({}));

View File

@ -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<RichHistoryQuery, 'id' | 'createdAt'>
): Promise<{ warning?: RichHistoryStorageWarningDetails; richHistoryQuery: RichHistoryQuery }> {
@ -71,11 +78,12 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage {
}
async getSettings(): Promise<RichHistorySettings> {
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<void> {
throw new Error('not supported yet');
updateSettings(settings: RichHistorySettings): Promise<void> {
return this.preferenceService.patch({
queryHistory: {
homeTab: settings.starredTabAsFirstTab ? 'starred' : 'query',
},
});
}
async updateStarred(id: string, starred: boolean): Promise<RichHistoryQuery> {

View File

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

View File

@ -5,10 +5,20 @@ import { backendSrv } from './backend_srv';
export class PreferencesService {
constructor(private resourceUri: string) {}
/**
* Overrides all preferences
*/
update(preferences: UserPreferencesDTO): Promise<any> {
return backendSrv.put(`/api/${this.resourceUri}/preferences`, preferences);
}
/**
* Updates only provided preferences
*/
patch(preferences: Partial<UserPreferencesDTO>): Promise<any> {
return backendSrv.patch(`/api/${this.resourceUri}/preferences`, preferences);
}
load(): Promise<UserPreferencesDTO> {
return backendSrv.get<UserPreferencesDTO>(`/api/${this.resourceUri}/preferences`);
}

View File

@ -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 (
<div className={styles.container}>
<Field
label="History time span"
description={`Select the period of time for which Grafana will save your query history. Up to ${MAX_HISTORY_ITEMS} entries will be stored.`}
className="space-between"
>
<div className={styles.input}>
<Select value={selectedOption} options={retentionPeriodOptions} onChange={onChangeRetentionPeriod}></Select>
</div>
</Field>
{supportedFeatures().changeRetention ? (
<Field
label="History time span"
description={`Select the period of time for which Grafana will save your query history. Up to ${MAX_HISTORY_ITEMS} entries will be stored.`}
className="space-between"
>
<div className={styles.input}>
<Select value={selectedOption} options={retentionPeriodOptions} onChange={onChangeRetentionPeriod}></Select>
</div>
</Field>
) : (
<Alert severity="info" title="History time span">
Grafana will keep entries up to {selectedOption?.label}.
</Alert>
)}
<InlineField label="Change the default active tab from “Query history” to “Starred”" className="space-between">
<InlineSwitch
id="explore-query-history-settings-default-active-tab"
@ -89,30 +96,36 @@ export function RichHistorySettingsTab(props: RichHistorySettingsProps) {
onChange={toggleStarredTabAsFirstTab}
/>
</InlineField>
<InlineField label="Only show queries for data source currently active in Explore" className="space-between">
<InlineSwitch
id="explore-query-history-settings-data-source-behavior"
value={activeDatasourceOnly}
onChange={toggleactiveDatasourceOnly}
/>
</InlineField>
<div
className={css`
font-weight: ${theme.typography.weight.bold};
`}
>
Clear query history
</div>
<div
className={css`
margin-bottom: ${theme.spacing.sm};
`}
>
Delete all of your query history, permanently.
</div>
<Button variant="destructive" onClick={onDelete}>
Clear query history
</Button>
{supportedFeatures().onlyActiveDataSource && (
<InlineField label="Only show queries for data source currently active in Explore" className="space-between">
<InlineSwitch
id="explore-query-history-settings-data-source-behavior"
value={activeDatasourceOnly}
onChange={toggleactiveDatasourceOnly}
/>
</InlineField>
)}
{supportedFeatures().clearHistory && (
<div>
<div
className={css`
font-weight: ${theme.typography.weight.bold};
`}
>
Clear query history
</div>
<div
className={css`
margin-bottom: ${theme.spacing.sm};
`}
>
Delete all of your query history, permanently.
</div>
<Button variant="destructive" onClick={onDelete}>
Clear query history
</Button>
</div>
)}
</div>
);
}

View File

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

View File

@ -5,4 +5,7 @@ export interface UserPreferencesDTO {
weekStart: string;
homeDashboardId: number;
theme: string;
queryHistory: {
homeTab: '' | 'query' | 'starred';
};
}