diff --git a/.betterer.results b/.betterer.results index b57253c101f..e0cea5bc339 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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": [ diff --git a/packages/grafana-ui/src/components/Tabs/Tab.tsx b/packages/grafana-ui/src/components/Tabs/Tab.tsx index 562eb2669ee..d45f33cc436 100644 --- a/packages/grafana-ui/src/components/Tabs/Tab.tsx +++ b/packages/grafana-ui/src/components/Tabs/Tab.tsx @@ -38,18 +38,20 @@ export const Tab = React.forwardRef( const linkClass = cx(tabsStyles.link, active ? tabsStyles.activeStyle : tabsStyles.notActive); return ( -
  • +
    {content()} -
  • + ); } ); diff --git a/packages/grafana-ui/src/components/Tabs/TabsBar.tsx b/packages/grafana-ui/src/components/Tabs/TabsBar.tsx index b5eb2a07206..13fccb9223f 100644 --- a/packages/grafana-ui/src/components/Tabs/TabsBar.tsx +++ b/packages/grafana-ui/src/components/Tabs/TabsBar.tsx @@ -32,7 +32,9 @@ export const TabsBar = React.forwardRef(({ children, clas return (
    -
      {children}
    +
    + {children} +
    ); }); diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index 1bbc7f7b30f..747b6720935 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -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', () => { diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index b4d05c0c931..ca98b0c6a45 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -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( diff --git a/public/app/core/history/RichHistoryStorage.ts b/public/app/core/history/RichHistoryStorage.ts index 4242e13658f..5e833c73f44 100644 --- a/public/app/core/history/RichHistoryStorage.ts +++ b/public/app/core/history/RichHistoryStorage.ts @@ -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; updateStarred(id: string, starred: boolean): Promise; updateComment(id: string, comment: string | undefined): Promise; + + getSettings(): Promise; + updateSettings(settings: RichHistorySettings): Promise; } diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index feebda044e3..5e08a4fa046 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -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 { return await getRichHistoryStorage().getRichHistory(); } +export async function updateRichHistorySettings(settings: RichHistorySettings): Promise { + await getRichHistoryStorage().updateSettings(settings); +} + +export async function getRichHistorySettings(): Promise { + return await getRichHistoryStorage().getSettings(); +} + export async function deleteAllFromRichHistory(): Promise { return getRichHistoryStorage().deleteAll(); } diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index 2322258de13..0f2030069ac 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -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; +}; diff --git a/public/app/features/explore/RichHistory/RichHistory.test.tsx b/public/app/features/explore/RichHistory/RichHistory.test.tsx index b49c35b06e2..b588e6f3362 100644 --- a/public/app/features/explore/RichHistory/RichHistory.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.test.tsx @@ -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) => { 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); diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 410ad7f7ea6..893a17ca796 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -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 { + updateSettings = (settingsToUpdate: Partial) => { + this.props.updateHistorySettings({ ...this.props.richHistorySettings, ...settingsToUpdate }); + }; -class UnThemedRichHistory extends PureComponent { - 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) => { + this.props.updateHistorySearchFilters(this.props.exploreId, { + ...this.props.richHistorySearchFilters, + ...filtersToUpdate, + }); + }; onChangeRetentionPeriod = (retentionPeriod: SelectableValue) => { 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 ({ getExploreDatasources: jest.fn() })); @@ -15,8 +16,23 @@ const setup = (propOverrides?: Partial) => { 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) => { }; 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); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index aa13771e438..93f03298c10 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -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 Loading...; + } return ( ); diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 44fd55e42c3..aa410be7004 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -200,15 +200,15 @@ export function RichHistoryQueriesTab(props: Props) {
    {!activeDatasourceOnly && ( -
    - -
    + )}
    ) => { const props: RichHistorySettingsProps = { @@ -15,7 +15,7 @@ const setup = (propOverrides?: Partial) => { Object.assign(props, propOverrides); - return render(); + return render(); }; describe('RichHistorySettings', () => { diff --git a/public/app/features/explore/RichHistory/RichHistorySettings.tsx b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx similarity index 74% rename from public/app/features/explore/RichHistory/RichHistorySettings.tsx rename to public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx index 2a83c6d04be..e482dfadc74 100644 --- a/public/app/features/explore/RichHistory/RichHistorySettings.tsx +++ b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx @@ -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) { >
    - -
    - -
    Change the default active tab from “Query history” to “Starred”
    -
    -
    - -
    - -
    Only show queries for data source currently active in Explore
    -
    -
    + + + + + +
    { 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(); }); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index b1db75f57f1..cbe7915f791 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -117,15 +117,15 @@ export function RichHistoryStarredTab(props: Props) {
    {!activeDatasourceOnly && ( -
    - -
    + )}
    { + 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(); + } +}; diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index df8f51fb0bc..f1ed4542cc4 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -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); }; diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 976fbb7bcd8..b62c8d2558d 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -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); + }); }); diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 38b07eb3f5b..a9265a441fa 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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 }; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index a44329ba995..4c5982514db 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -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 => { export const loadRichHistory = (exploreId: ExploreId): ThunkResult => { 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 => { + 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 => { + 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 => { + 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 { diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 249d7cda74c..cd439b72d85 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -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('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) { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 626b894070b..ca313a28f06 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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.