Query History: Move local storage specific settings to persistence layer (#47500)

* 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
This commit is contained in:
Piotr Jamróz 2022-04-12 18:55:39 +02:00 committed by GitHub
parent d0abe1bb3d
commit 5cb5141c72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 402 additions and 142 deletions

View File

@ -230,7 +230,7 @@ exports[`no enzyme tests`] = {
"public/app/features/explore/LiveLogs.test.tsx:1667605379": [
[2, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3948011811": [
"public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:492930613": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/explore/RunButton.test.tsx:138299098": [

View File

@ -38,18 +38,20 @@ export const Tab = React.forwardRef<HTMLAnchorElement, TabProps>(
const linkClass = cx(tabsStyles.link, active ? tabsStyles.activeStyle : tabsStyles.notActive);
return (
<li className={tabsStyles.item}>
<div className={tabsStyles.item}>
<a
href={href}
className={linkClass}
{...otherProps}
onClick={onChangeTab}
aria-label={otherProps['aria-label'] || selectors.components.Tab.title(label)}
role="tab"
aria-selected={active}
ref={ref}
>
{content()}
</a>
</li>
</div>
);
}
);

View File

@ -32,7 +32,9 @@ export const TabsBar = React.forwardRef<HTMLDivElement, Props>(({ children, clas
return (
<div className={cx(tabsStyles.tabsWrapper, className)} ref={ref}>
<ul className={tabsStyles.tabs}>{children}</ul>
<div className={tabsStyles.tabs} role="tablist">
{children}
</div>
</div>
);
});

View File

@ -5,6 +5,7 @@ import { DataQuery } from '@grafana/data';
import { afterEach, beforeEach } from '../../../test/lib/common';
import { RichHistoryStorageWarning } from './RichHistoryStorage';
import { backendSrv } from '../services/backend_srv';
import { RichHistorySettings } from '../utils/richHistoryTypes';
const key = 'grafana.explore.richHistory';
@ -99,6 +100,19 @@ describe('RichHistoryLocalStorage', () => {
expect(await storage.getRichHistory()).toEqual([]);
expect(store.getObject(key)).toEqual([]);
});
it('should save and read settings', async () => {
const settings: RichHistorySettings = {
retentionPeriod: 2,
starredTabAsFirstTab: true,
activeDatasourceOnly: true,
lastUsedDatasourceFilters: [{ value: 'foobar' }],
};
await storage.updateSettings(settings);
const storageSettings = storage.getSettings();
expect(settings).toMatchObject(storageSettings);
});
});
describe('retention policy and max limits', () => {

View File

@ -5,6 +5,7 @@ import { DataQuery } from '@grafana/data';
import { find, isEqual, omit } from 'lodash';
import { createRetentionPeriodBoundary, RICH_HISTORY_SETTING_KEYS } from './richHistoryLocalStorageUtils';
import { fromDTO, toDTO } from './localStorageConverter';
import { RichHistorySettings } from '../utils/richHistoryTypes';
export const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
export const MAX_HISTORY_ITEMS = 10000;
@ -101,6 +102,22 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
async updateComment(id: string, comment: string) {
return updateRichHistory(id, (richHistoryDTO) => (richHistoryDTO.comment = comment));
}
async getSettings() {
return {
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, []),
};
}
async updateSettings(settings: RichHistorySettings) {
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);
}
}
function updateRichHistory(

View File

@ -1,4 +1,5 @@
import { RichHistoryQuery } from '../../types';
import { RichHistorySettings } from '../utils/richHistoryTypes';
/**
* Errors are used when the operation on Rich History was not successful.
@ -44,4 +45,7 @@ export default interface RichHistoryStorage {
deleteRichHistory(id: string): Promise<void>;
updateStarred(id: string, starred: boolean): Promise<RichHistoryQuery>;
updateComment(id: string, comment: string | undefined): Promise<RichHistoryQuery>;
getSettings(): Promise<RichHistorySettings>;
updateSettings(settings: RichHistorySettings): Promise<void>;
}

View File

@ -23,7 +23,7 @@ import {
filterQueriesByTime,
sortQueries,
} from 'app/core/history/richHistoryLocalStorageUtils';
import { SortOrder } from './richHistoryTypes';
import { RichHistorySettings, SortOrder } from './richHistoryTypes';
export { SortOrder };
@ -86,6 +86,14 @@ export async function getRichHistory(): Promise<RichHistoryQuery[]> {
return await getRichHistoryStorage().getRichHistory();
}
export async function updateRichHistorySettings(settings: RichHistorySettings): Promise<void> {
await getRichHistoryStorage().updateSettings(settings);
}
export async function getRichHistorySettings(): Promise<RichHistorySettings> {
return await getRichHistoryStorage().getSettings();
}
export async function deleteAllFromRichHistory(): Promise<void> {
return getRichHistoryStorage().deleteAll();
}

View File

@ -1,6 +1,23 @@
import { SelectableValue } from '@grafana/data';
export enum SortOrder {
Descending = 'Descending',
Ascending = 'Ascending',
DatasourceAZ = 'Datasource A-Z',
DatasourceZA = 'Datasource Z-A',
}
export interface RichHistorySettings {
retentionPeriod: number;
starredTabAsFirstTab: boolean;
activeDatasourceOnly: boolean;
lastUsedDatasourceFilters: SelectableValue[];
}
export type RichHistorySearchFilters = {
search: string;
sortOrder: SortOrder;
datasourceFilters: SelectableValue[];
from: number;
to: number;
};

View File

@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
import { GrafanaTheme } from '@grafana/data';
import { ExploreId } from '../../../types/explore';
import { RichHistory, RichHistoryProps, Tabs } from './RichHistory';
import { SortOrder } from '../../../core/utils/richHistoryTypes';
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
@ -16,6 +17,21 @@ const setup = (propOverrides?: Partial<RichHistoryProps>) => {
firstTab: Tabs.RichHistory,
deleteRichHistory: jest.fn(),
onClose: jest.fn(),
richHistorySearchFilters: {
search: '',
sortOrder: SortOrder.Descending,
datasourceFilters: [],
from: 0,
to: 7,
},
richHistorySettings: {
retentionPeriod: 0,
starredTabAsFirstTab: false,
activeDatasourceOnly: true,
lastUsedDatasourceFilters: [],
},
updateHistorySearchFilters: jest.fn(),
updateHistorySettings: jest.fn(),
};
Object.assign(props, propOverrides);

View File

@ -2,8 +2,6 @@ import React, { PureComponent } from 'react';
//Services & Utils
import { SortOrder } from 'app/core/utils/richHistory';
import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils';
import store from 'app/core/store';
import { Themeable, withTheme, TabbedContainer, TabConfig } from '@grafana/ui';
//Types
@ -11,9 +9,10 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { SelectableValue } from '@grafana/data';
//Components
import { RichHistorySettings } from './RichHistorySettings';
import { RichHistorySettingsTab } from './RichHistorySettingsTab';
import { RichHistoryQueriesTab } from './RichHistoryQueriesTab';
import { RichHistoryStarredTab } from './RichHistoryStarredTab';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
export enum Tabs {
RichHistory = 'Query history',
@ -30,100 +29,74 @@ export const sortOrderOptions = [
export interface RichHistoryProps extends Themeable {
richHistory: RichHistoryQuery[];
richHistorySettings: RichHistorySettings;
richHistorySearchFilters: RichHistorySearchFilters;
updateHistorySettings: (settings: RichHistorySettings) => void;
updateHistorySearchFilters: (exploreId: ExploreId, filters: RichHistorySearchFilters) => void;
deleteRichHistory: () => void;
activeDatasourceInstance?: string;
firstTab: Tabs;
exploreId: ExploreId;
height: number;
deleteRichHistory: () => void;
onClose: () => void;
}
interface RichHistoryState {
sortOrder: SortOrder;
retentionPeriod: number;
starredTabAsFirstTab: boolean;
activeDatasourceOnly: boolean;
datasourceFilters: SelectableValue[];
}
class UnThemedRichHistory extends PureComponent<RichHistoryProps> {
updateSettings = (settingsToUpdate: Partial<RichHistorySettings>) => {
this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate });
};
class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistoryState> {
constructor(props: RichHistoryProps) {
super(props);
this.state = {
sortOrder: SortOrder.Descending,
datasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, []),
retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7),
starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false),
activeDatasourceOnly: store.getBool(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, true),
};
}
updateFilters = (filtersToUpdate: Partial<RichHistorySearchFilters>) => {
this.props.updateHistorySearchFilters(this.props.exploreId, {
...this.props.richHistorySearchFilters,
...filtersToUpdate,
});
};
onChangeRetentionPeriod = (retentionPeriod: SelectableValue<number>) => {
if (retentionPeriod.value !== undefined) {
this.setState({
retentionPeriod: retentionPeriod.value,
});
store.set(RICH_HISTORY_SETTING_KEYS.retentionPeriod, retentionPeriod.value);
this.updateSettings({ retentionPeriod: retentionPeriod.value });
}
};
toggleStarredTabAsFirstTab = () => {
const starredTabAsFirstTab = !this.state.starredTabAsFirstTab;
this.setState({
starredTabAsFirstTab,
});
store.set(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, starredTabAsFirstTab);
};
toggleStarredTabAsFirstTab = () =>
this.updateSettings({ starredTabAsFirstTab: !this.props.richHistorySettings.starredTabAsFirstTab });
toggleActiveDatasourceOnly = () => {
const activeDatasourceOnly = !this.state.activeDatasourceOnly;
this.setState({
activeDatasourceOnly,
});
store.set(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, activeDatasourceOnly);
};
toggleActiveDatasourceOnly = () =>
this.updateSettings({ activeDatasourceOnly: !this.props.richHistorySettings.activeDatasourceOnly });
onSelectDatasourceFilters = (value: SelectableValue[]) => {
try {
store.setObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, value);
} catch (error) {
console.error(error);
}
/* Set data source filters to state even though they were not successfully saved in
* localStorage to allow interaction and filtering.
**/
this.setState({ datasourceFilters: value });
};
onSelectDatasourceFilters = (datasourceFilters: SelectableValue[]) => this.updateFilters({ datasourceFilters });
onChangeSortOrder = (sortOrder: SortOrder) => this.setState({ sortOrder });
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.
*/
updateFilters() {
this.state.activeDatasourceOnly && this.props.activeDatasourceInstance
? this.onSelectDatasourceFilters([
{ label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance },
])
: this.onSelectDatasourceFilters(this.state.datasourceFilters);
initFilters() {
if (this.props.richHistorySettings.activeDatasourceOnly && this.props.activeDatasourceInstance) {
this.onSelectDatasourceFilters([
{ label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance },
]);
}
}
componentDidMount() {
this.updateFilters();
this.initFilters();
}
componentDidUpdate(prevProps: RichHistoryProps, prevState: RichHistoryState) {
if (
this.props.activeDatasourceInstance !== prevProps.activeDatasourceInstance ||
this.state.activeDatasourceOnly !== prevState.activeDatasourceOnly
) {
this.updateFilters();
/**
* 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 { datasourceFilters, sortOrder, activeDatasourceOnly, retentionPeriod } = this.state;
const { activeDatasourceOnly, retentionPeriod } = this.props.richHistorySettings;
const { datasourceFilters, sortOrder } = this.props.richHistorySearchFilters;
const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab } = this.props;
const QueriesTab: TabConfig = {
@ -166,10 +139,10 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
label: 'Settings',
value: Tabs.Settings,
content: (
<RichHistorySettings
retentionPeriod={this.state.retentionPeriod}
starredTabAsFirstTab={this.state.starredTabAsFirstTab}
activeDatasourceOnly={this.state.activeDatasourceOnly}
<RichHistorySettingsTab
retentionPeriod={this.props.richHistorySettings.retentionPeriod}
starredTabAsFirstTab={this.props.richHistorySettings.starredTabAsFirstTab}
activeDatasourceOnly={this.props.richHistorySettings.activeDatasourceOnly}
onChangeRetentionPeriod={this.onChangeRetentionPeriod}
toggleStarredTabAsFirstTab={this.toggleStarredTabAsFirstTab}
toggleactiveDatasourceOnly={this.toggleActiveDatasourceOnly}

View File

@ -4,6 +4,7 @@ import { render } from '@testing-library/react';
import { ExploreId } from '../../../types/explore';
import { RichHistoryContainer, Props } from './RichHistoryContainer';
import { Tabs } from './RichHistory';
import { SortOrder } from '../../../core/utils/richHistoryTypes';
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
@ -15,8 +16,23 @@ const setup = (propOverrides?: Partial<Props>) => {
richHistory: [],
firstTab: Tabs.RichHistory,
deleteRichHistory: jest.fn(),
loadRichHistory: jest.fn(),
initRichHistory: jest.fn(),
updateHistorySearchFilters: jest.fn(),
updateHistorySettings: jest.fn(),
onClose: jest.fn(),
richHistorySearchFilters: {
search: '',
sortOrder: SortOrder.Descending,
datasourceFilters: [],
from: 0,
to: 7,
},
richHistorySettings: {
retentionPeriod: 0,
starredTabAsFirstTab: false,
activeDatasourceOnly: true,
lastUsedDatasourceFilters: [],
},
};
Object.assign(props, propOverrides);
@ -25,6 +41,10 @@ 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 });
expect(container).toHaveTextContent('Loading...');
});
it('should render component with correct width', () => {
const { container } = setup();
expect(container.firstElementChild!.getAttribute('style')).toContain('width: 531.5px');
@ -34,14 +54,14 @@ describe('RichHistoryContainer', () => {
expect(container.firstElementChild!.getAttribute('style')).toContain('height: 400px');
});
it('should re-request rich history every time the component is mounted', () => {
const loadRichHistory = jest.fn();
const { unmount } = setup({ loadRichHistory });
expect(loadRichHistory).toBeCalledTimes(1);
const initRichHistory = jest.fn();
const { unmount } = setup({ initRichHistory });
expect(initRichHistory).toBeCalledTimes(1);
unmount();
expect(loadRichHistory).toBeCalledTimes(1);
expect(initRichHistory).toBeCalledTimes(1);
setup({ loadRichHistory });
expect(loadRichHistory).toBeCalledTimes(2);
setup({ initRichHistory });
expect(initRichHistory).toBeCalledTimes(2);
});
});

View File

@ -2,10 +2,6 @@
import React, { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
// Services & Utils
import store from 'app/core/store';
import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils';
// Types
import { ExploreItemState, StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore';
@ -14,27 +10,36 @@ import { ExploreId } from 'app/types/explore';
import { RichHistory, Tabs } from './RichHistory';
//Actions
import { deleteRichHistory, loadRichHistory } from '../state/history';
import {
deleteRichHistory,
initRichHistory,
updateHistorySettings,
updateHistorySearchFilters,
} from '../state/history';
import { ExploreDrawer } from '../ExploreDrawer';
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const richHistorySearchFilters = item.richHistorySearchFilters;
const richHistorySettings = explore.richHistorySettings;
const { datasourceInstance } = item;
const firstTab = store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false)
? Tabs.Starred
: Tabs.RichHistory;
const firstTab = richHistorySettings?.starredTabAsFirstTab ? Tabs.Starred : Tabs.RichHistory;
const { richHistory } = item;
return {
richHistory,
firstTab,
activeDatasourceInstance: datasourceInstance?.name,
richHistorySettings,
richHistorySearchFilters,
};
}
const mapDispatchToProps = {
loadRichHistory,
initRichHistory,
updateHistorySettings,
updateHistorySearchFilters,
deleteRichHistory,
};
@ -57,13 +62,21 @@ export function RichHistoryContainer(props: Props) {
activeDatasourceInstance,
exploreId,
deleteRichHistory,
loadRichHistory,
initRichHistory,
richHistorySettings,
updateHistorySettings,
richHistorySearchFilters,
updateHistorySearchFilters,
onClose,
} = props;
useEffect(() => {
loadRichHistory(exploreId);
}, [loadRichHistory, exploreId]);
initRichHistory(exploreId);
}, [initRichHistory, exploreId]);
if (!richHistorySettings || !richHistorySearchFilters) {
return <span>Loading...</span>;
}
return (
<ExploreDrawer
@ -77,9 +90,13 @@ export function RichHistoryContainer(props: Props) {
firstTab={firstTab}
activeDatasourceInstance={activeDatasourceInstance}
exploreId={exploreId}
deleteRichHistory={deleteRichHistory}
onClose={onClose}
height={height}
deleteRichHistory={deleteRichHistory}
richHistorySettings={richHistorySettings}
richHistorySearchFilters={richHistorySearchFilters}
updateHistorySettings={updateHistorySettings}
updateHistorySearchFilters={updateHistorySearchFilters}
/>
</ExploreDrawer>
);

View File

@ -200,15 +200,15 @@ export function RichHistoryQueriesTab(props: Props) {
<div className={styles.containerContent}>
<div className={styles.selectors}>
{!activeDatasourceOnly && (
<div aria-label="Filter datasources" className={styles.multiselect}>
<MultiSelect
menuShouldPortal
options={listOfDatasources}
value={datasourceFilters}
placeholder="Filter queries for data sources(s)"
onChange={onSelectDatasourceFilters}
/>
</div>
<MultiSelect
className={styles.multiselect}
menuShouldPortal
options={listOfDatasources}
value={datasourceFilters}
placeholder="Filter queries for data sources(s)"
aria-label="Filter queries for data sources(s)"
onChange={onSelectDatasourceFilters}
/>
)}
<div className={styles.filterInput}>
<FilterInput

View File

@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { RichHistorySettings, RichHistorySettingsProps } from './RichHistorySettings';
import { RichHistorySettingsTab, RichHistorySettingsProps } from './RichHistorySettingsTab';
const setup = (propOverrides?: Partial<RichHistorySettingsProps>) => {
const props: RichHistorySettingsProps = {
@ -15,7 +15,7 @@ const setup = (propOverrides?: Partial<RichHistorySettingsProps>) => {
Object.assign(props, propOverrides);
return render(<RichHistorySettings {...props} />);
return render(<RichHistorySettingsTab {...props} />);
};
describe('RichHistorySettings', () => {

View File

@ -1,6 +1,6 @@
import React from 'react';
import { css } from '@emotion/css';
import { stylesFactory, useTheme, Select, Button, Switch, Field } from '@grafana/ui';
import { stylesFactory, useTheme, Select, Button, Field, InlineField, InlineSwitch } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { ShowConfirmModalEvent } from '../../../types/events';
@ -30,13 +30,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
input: css`
max-width: 200px;
`,
switch: css`
display: flex;
align-items: center;
`,
label: css`
margin-left: ${theme.spacing.md};
`,
};
});
@ -47,7 +40,7 @@ const retentionPeriodOptions = [
{ value: 14, label: '2 weeks' },
];
export function RichHistorySettings(props: RichHistorySettingsProps) {
export function RichHistorySettingsTab(props: RichHistorySettingsProps) {
const {
retentionPeriod,
starredTabAsFirstTab,
@ -92,18 +85,20 @@ export function RichHistorySettings(props: RichHistorySettingsProps) {
></Select>
</div>
</Field>
<Field label="Default active tab" description=" " className="space-between">
<div className={styles.switch}>
<Switch value={starredTabAsFirstTab} onChange={toggleStarredTabAsFirstTab}></Switch>
<div className={styles.label}>Change the default active tab from Query history to Starred</div>
</div>
</Field>
<Field label="Data source behaviour" description=" " className="space-between">
<div className={styles.switch}>
<Switch value={activeDatasourceOnly} onChange={toggleactiveDatasourceOnly}></Switch>
<div className={styles.label}>Only show queries for data source currently active in Explore</div>
</div>
</Field>
<InlineField label="Change the default active tab from “Query history” to “Starred”" className="space-between">
<InlineSwitch
id="explore-query-history-settings-default-active-tab"
value={starredTabAsFirstTab}
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};

View File

@ -34,12 +34,12 @@ describe('RichHistoryStarredTab', () => {
describe('select datasource', () => {
it('should render select datasource if activeDatasourceOnly is false', () => {
const wrapper = setup();
expect(wrapper.find({ 'aria-label': 'Filter datasources' })).toHaveLength(1);
expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeTruthy();
});
it('should not render select datasource if activeDatasourceOnly is true', () => {
const wrapper = setup({ activeDatasourceOnly: true });
expect(wrapper.find({ 'aria-label': 'Filter datasources' })).toHaveLength(0);
expect(wrapper.find({ 'aria-label': 'Filter queries for data sources(s)' }).exists()).toBeFalsy();
});
});
});

View File

@ -117,15 +117,15 @@ export function RichHistoryStarredTab(props: Props) {
<div className={styles.containerContent}>
<div className={styles.selectors}>
{!activeDatasourceOnly && (
<div aria-label="Filter datasources" className={styles.multiselect}>
<MultiSelect
menuShouldPortal
options={listOfDatasources}
value={datasourceFilters}
placeholder="Filter queries for specific data sources(s)"
onChange={onSelectDatasourceFilters}
/>
</div>
<MultiSelect
className={styles.multiselect}
menuShouldPortal
options={listOfDatasources}
value={datasourceFilters}
placeholder="Filter queries for data sources(s)"
aria-label="Filter queries for data sources(s)"
onChange={onSelectDatasourceFilters}
/>
)}
<div className={styles.filterInput}>
<FilterInput

View File

@ -30,3 +30,19 @@ export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], expl
})
);
};
export const assertQueryHistoryTabIsSelected = (
tabName: 'Query history' | 'Starred' | 'Settings',
exploreId: ExploreId = ExploreId.left
) => {
expect(withinExplore(exploreId).getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
};
export const assertDataSourceFilterVisibility = (visible: boolean, exploreId: ExploreId = ExploreId.left) => {
const filterInput = withinExplore(exploreId).queryByLabelText('Filter queries for data sources(s)');
if (visible) {
expect(filterInput).toBeInTheDocument();
} else {
expect(filterInput).not.toBeInTheDocument();
}
};

View File

@ -33,6 +33,30 @@ export const openQueryHistory = async (exploreId: ExploreId = ExploreId.left) =>
).toBeInTheDocument();
};
export const closeQueryHistory = async (exploreId: ExploreId = ExploreId.left) => {
const closeButton = withinExplore(exploreId).getByRole('button', { name: 'Close query history' });
userEvent.click(closeButton);
};
export const switchToQueryHistoryTab = async (
name: 'Settings' | 'Query History',
exploreId: ExploreId = ExploreId.left
) => {
userEvent.click(withinExplore(exploreId).getByRole('tab', { name: `Tab ${name}` }));
};
export const selectStarredTabFirst = (exploreId: ExploreId = ExploreId.left) => {
const checkbox = withinExplore(exploreId).getByRole('checkbox', {
name: 'Change the default active tab from “Query history” to “Starred”',
});
userEvent.click(checkbox);
};
export const selectOnlyActiveDataSource = (exploreId: ExploreId = ExploreId.left) => {
const checkbox = withinExplore(exploreId).getByLabelText(/Only show queries for data source currently active.*/);
userEvent.click(checkbox);
};
export const starQueryHistory = (queryIndex: number, exploreId: ExploreId = ExploreId.left) => {
invokeAction(queryIndex, 'Star query', exploreId);
};

View File

@ -1,8 +1,24 @@
import React from 'react';
import { serializeStateToUrlParam } from '@grafana/data';
import { setupExplore, tearDown, waitForExplore } from './helper/setup';
import { deleteQueryHistory, inputQuery, openQueryHistory, runQuery, starQueryHistory } from './helper/interactions';
import { assertQueryHistory, assertQueryHistoryExists, assertQueryHistoryIsStarred } from './helper/assert';
import {
closeQueryHistory,
deleteQueryHistory,
inputQuery,
openQueryHistory,
runQuery,
selectOnlyActiveDataSource,
selectStarredTabFirst,
starQueryHistory,
switchToQueryHistoryTab,
} from './helper/interactions';
import {
assertDataSourceFilterVisibility,
assertQueryHistory,
assertQueryHistoryExists,
assertQueryHistoryIsStarred,
assertQueryHistoryTabIsSelected,
} from './helper/assert';
import { makeLogsQueryResponse } from './helper/query';
import { ExploreId } from '../../../types';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
@ -106,4 +122,26 @@ describe('Explore: Query History', () => {
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left);
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.right);
});
it('updates query history settings', async () => {
// open settings page
setupExplore();
await waitForExplore();
await openQueryHistory();
// assert default values
assertQueryHistoryTabIsSelected('Query history');
assertDataSourceFilterVisibility(true);
await switchToQueryHistoryTab('Settings');
// change settings
selectStarredTabFirst();
selectOnlyActiveDataSource();
await closeQueryHistory();
await openQueryHistory();
// assert new settings
assertQueryHistoryTabIsSelected('Starred');
assertDataSourceFilterVisibility(false);
});
});

View File

@ -36,7 +36,7 @@ import {
import { ThunkResult } from 'app/types';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { getDataSourceSrv } from '@grafana/runtime';
import { richHistoryUpdatedAction, stateSave } from './main';
import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction, stateSave } from './main';
import { keybindingSrv } from 'app/core/services/keybindingSrv';
//
@ -262,6 +262,14 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
};
}
if (richHistorySearchFiltersUpdatedAction.match(action)) {
const richHistorySearchFilters = action.payload.filters;
return {
...state,
richHistorySearchFilters,
};
}
if (changeSizeAction.match(action)) {
const containerWidth = action.payload.width;
return { ...state, containerWidth };

View File

@ -3,13 +3,23 @@ import {
deleteAllFromRichHistory,
deleteQueryInRichHistory,
getRichHistory,
getRichHistorySettings,
SortOrder,
updateCommentInRichHistory,
updateRichHistorySettings,
updateStarredInRichHistory,
} from 'app/core/utils/richHistory';
import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types';
import { richHistoryLimitExceededAction, richHistoryStorageFullAction, richHistoryUpdatedAction } from './main';
import {
richHistoryLimitExceededAction,
richHistorySearchFiltersUpdatedAction,
richHistorySettingsUpdatedAction,
richHistoryStorageFullAction,
richHistoryUpdatedAction,
} from './main';
import { DataQuery, HistoryItem } from '@grafana/data';
import { AnyAction, createAction } from '@reduxjs/toolkit';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
//
// Actions and Payloads
@ -106,11 +116,68 @@ export const deleteRichHistory = (): ThunkResult<void> => {
export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
return async (dispatch) => {
// TODO: #45379 pass currently applied search filters
const richHistory = await getRichHistory();
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.
*/
export const initRichHistory = (exploreId: ExploreId): 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));
};
};
export const updateHistorySettings = (settings: RichHistorySettings): ThunkResult<void> => {
return async (dispatch) => {
dispatch(richHistorySettingsUpdatedAction(settings));
await updateRichHistorySettings(settings);
};
};
/**
* Assumed this can be called only when settings and filters are initialised
*/
export const updateHistorySearchFilters = (
exploreId: ExploreId,
filters: RichHistorySearchFilters
): ThunkResult<void> => {
return async (dispatch, getState) => {
// TODO: #45379 get new rich history list based on filters
dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters }));
const currentSettings = getState().explore.richHistorySettings!;
dispatch(
updateHistorySettings({
...currentSettings,
lastUsedDatasourceFilters: filters.datasourceFilters,
})
);
};
};
export const historyReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => {
if (historyUpdatedAction.match(action)) {
return {

View File

@ -9,6 +9,7 @@ import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
import { ThunkResult } from '../../../types';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
//
// Actions and Payloads
@ -24,6 +25,12 @@ export const richHistoryUpdatedAction =
export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction');
export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
export const richHistorySearchFiltersUpdatedAction = createAction<{
exploreId: ExploreId;
filters: RichHistorySearchFilters;
}>('explore/richHistorySearchFiltersUpdatedAction');
/**
* Resets state for explore.
*/
@ -243,6 +250,14 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (richHistorySettingsUpdatedAction.match(action)) {
const richHistorySettings = action.payload;
return {
...state,
richHistorySettings,
};
}
if (action.payload) {
const { exploreId } = action.payload;
if (exploreId !== undefined) {

View File

@ -15,6 +15,7 @@ import {
DataQueryResponse,
ExplorePanelsState,
} from '@grafana/data';
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
export enum ExploreId {
left = 'left',
@ -43,6 +44,11 @@ export interface ExploreState {
*/
right?: ExploreItemState;
/**
* Settings for rich history (note: filters are stored per each pane separately)
*/
richHistorySettings?: RichHistorySettings;
/**
* True if local storage quota was exceeded when a rich history item was added. This is to prevent showing
* multiple errors when local storage is full.
@ -153,6 +159,7 @@ export interface ExploreItemState {
* History of all queries
*/
richHistory: RichHistoryQuery[];
richHistorySearchFilters?: RichHistorySearchFilters;
/**
* We are using caching to store query responses of queries run from logs navigation.