mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
d0abe1bb3d
commit
5cb5141c72
@ -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": [
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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', () => {
|
||||
|
@ -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(
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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, 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),
|
||||
class UnThemedRichHistory extends PureComponent<RichHistoryProps> {
|
||||
updateSettings = (settingsToUpdate: Partial<RichHistorySettings>) => {
|
||||
this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate });
|
||||
};
|
||||
|
||||
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([
|
||||
initFilters() {
|
||||
if (this.props.richHistorySettings.activeDatasourceOnly && this.props.activeDatasourceInstance) {
|
||||
this.onSelectDatasourceFilters([
|
||||
{ label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance },
|
||||
])
|
||||
: this.onSelectDatasourceFilters(this.state.datasourceFilters);
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
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>
|
||||
)}
|
||||
<div className={styles.filterInput}>
|
||||
<FilterInput
|
||||
|
@ -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', () => {
|
@ -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};
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
className={styles.multiselect}
|
||||
menuShouldPortal
|
||||
options={listOfDatasources}
|
||||
value={datasourceFilters}
|
||||
placeholder="Filter queries for specific data sources(s)"
|
||||
placeholder="Filter queries for data sources(s)"
|
||||
aria-label="Filter queries for data sources(s)"
|
||||
onChange={onSelectDatasourceFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.filterInput}>
|
||||
<FilterInput
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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 };
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user