Explore: Move Query History to be screen wide (#84321)

* WIP

* Use splitpanewrapper for drawer

* Get rich history pulling from multiple datasources

* highlight pane

* Fix datasource data handling

* create ds/explore map, move around ds lookup

* Handle no filters

* Fix tests and some errors

* Fix context menu issue

* (Poorly) enable scrolling, fix onClose to function

* Remove highlighting, use legacy key, fix casing

* fix filtering to handle non-simple data

* Fix linter, add translations

* Fixing tests~~

* Move to explore drawer and fix some more tests

* Kinda fix drawer stuff?

* Fix remaining card tests

* Fix test

* Fix tests

* Partially fix starred tab tests

* Fix integration tests

* Fix remaining tests 🤞

* Add a test and a clarifying comment behind a couple hooks

* Remove unused code

* Fix button styling and fix animation (but break width)

* Make Drawer using parent width (100%)

* Fix tests and some small catches

* Add tests for selectExploreDSMaps selector

---------

Co-authored-by: Piotr Jamroz <pm.jamroz@gmail.com>
This commit is contained in:
Kristina 2024-04-09 07:36:46 -05:00 committed by GitHub
parent 3420e942ac
commit 5305316f5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 728 additions and 522 deletions

View File

@ -289,6 +289,9 @@ export const Pages = {
table: 'Explore Table',
scrollView: 'data-testid explorer scroll view',
},
QueryHistory: {
container: 'data-testid QueryHistory',
},
},
SoloPanel: {
url: (page: string) => `/d-solo/${page}`,

View File

@ -21,9 +21,10 @@ export interface TabbedContainerProps {
defaultTab?: string;
closeIconTooltip?: string;
onClose: () => void;
testId?: string;
}
export function TabbedContainer({ tabs, defaultTab, closeIconTooltip, onClose }: TabbedContainerProps) {
export function TabbedContainer({ tabs, defaultTab, closeIconTooltip, onClose, testId }: TabbedContainerProps) {
const [activeTab, setActiveTab] = useState(tabs.some((tab) => tab.value === defaultTab) ? defaultTab : tabs[0].value);
const styles = useStyles2(getStyles);
const theme = useTheme2();
@ -35,7 +36,7 @@ export function TabbedContainer({ tabs, defaultTab, closeIconTooltip, onClose }:
const autoHeight = `calc(100% - (${theme.components.menuTabs.height}px + ${theme.spacing(1)}))`;
return (
<div className={styles.container}>
<div className={styles.container} data-testid={testId}>
<TabsBar className={styles.tabs}>
{tabs.map((t) => (
<Tab

View File

@ -120,7 +120,7 @@ describe('RichHistoryLocalStorage', () => {
const settings: RichHistorySettings = {
retentionPeriod: 2,
starredTabAsFirstTab: true,
activeDatasourceOnly: true,
activeDatasourcesOnly: true,
lastUsedDatasourceFilters: ['foobar'],
};
await storage.updateSettings(settings);

View File

@ -124,8 +124,11 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
}
async getSettings() {
// get the new key without a default. If undefined, use the legacy key, or false as the default
const activeDatasource: boolean | undefined = store.getObject(RICH_HISTORY_SETTING_KEYS.activeDatasourcesOnly);
return {
activeDatasourceOnly: store.getObject(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, false),
activeDatasourcesOnly:
activeDatasource ?? store.getObject(RICH_HISTORY_SETTING_KEYS.legacyActiveDatasourceOnly, false),
retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7),
starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false),
lastUsedDatasourceFilters: store
@ -135,7 +138,7 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
}
async updateSettings(settings: RichHistorySettings) {
store.set(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, settings.activeDatasourceOnly);
store.set(RICH_HISTORY_SETTING_KEYS.activeDatasourcesOnly, settings.activeDatasourcesOnly);
store.set(RICH_HISTORY_SETTING_KEYS.retentionPeriod, settings.retentionPeriod);
store.set(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, settings.starredTabAsFirstTab);
store.setObject(

View File

@ -188,7 +188,7 @@ describe('RichHistoryRemoteStorage', () => {
} as UserPreferencesDTO);
const settings = await storage.getSettings();
expect(settings).toMatchObject({
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: undefined,
retentionPeriod: 14,
starredTabAsFirstTab: true,
@ -203,7 +203,7 @@ describe('RichHistoryRemoteStorage', () => {
} as UserPreferencesDTO);
const settings = await storage.getSettings();
expect(settings).toMatchObject({
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: undefined,
retentionPeriod: 14,
starredTabAsFirstTab: false,
@ -212,7 +212,7 @@ describe('RichHistoryRemoteStorage', () => {
it('updates user settings', async () => {
await storage.updateSettings({
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: undefined,
retentionPeriod: 14,
starredTabAsFirstTab: false,
@ -222,7 +222,7 @@ describe('RichHistoryRemoteStorage', () => {
} as Partial<UserPreferencesDTO>);
await storage.updateSettings({
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: undefined,
retentionPeriod: 14,
starredTabAsFirstTab: true,

View File

@ -85,7 +85,7 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage {
async getSettings(): Promise<RichHistorySettings> {
const preferences = await this.preferenceService.load();
return {
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: undefined,
retentionPeriod: 14,
starredTabAsFirstTab: preferences.queryHistory?.homeTab === 'starred',

View File

@ -94,6 +94,7 @@ export const sortQueries = (array: RichHistoryQuery[], sortOrder: SortOrder) =>
export const RICH_HISTORY_SETTING_KEYS = {
retentionPeriod: 'grafana.explore.richHistory.retentionPeriod',
starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab',
activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
legacyActiveDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
activeDatasourcesOnly: 'grafana.explore.richHistory.activeDatasourcesOnly',
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
};

View File

@ -14,7 +14,7 @@ export enum SortOrder {
export interface RichHistorySettings {
retentionPeriod: number;
starredTabAsFirstTab: boolean;
activeDatasourceOnly: boolean;
activeDatasourcesOnly: boolean;
lastUsedDatasourceFilters?: string[];
}

View File

@ -10,7 +10,7 @@ import { configureStore } from 'app/store/configureStore';
import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext';
import { Explore, Props } from './Explore';
import { initialExploreState } from './state/main';
import { changeShowQueryHistory, initialExploreState } from './state/main';
import { scanStopAction } from './state/query';
import { createEmptyQueryResponse, makeExplorePaneState } from './state/utils';
@ -51,6 +51,8 @@ const makeEmptyQueryResponse = (loadingState: LoadingState) => {
};
const dummyProps: Props = {
setShowQueryInspector: (value: boolean) => {},
showQueryInspector: false,
logsResult: undefined,
changeSize: jest.fn(),
datasourceInstance: {
@ -98,6 +100,8 @@ const dummyProps: Props = {
setSupplementaryQueryEnabled: jest.fn(),
correlationEditorDetails: undefined,
correlationEditorHelperData: undefined,
showQueryHistory: false,
changeShowQueryHistory: changeShowQueryHistory,
};
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {

View File

@ -41,7 +41,6 @@ import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineCo
import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem';
import { CorrelationHelper } from './CorrelationHelper';
import { CustomContainer } from './CustomContainer';
import ExploreQueryInspector from './ExploreQueryInspector';
import { ExploreToolbar } from './ExploreToolbar';
import { FlameGraphExploreContainer } from './FlameGraph/FlameGraphExploreContainer';
import { GraphContainer } from './Graph/GraphContainer';
@ -53,12 +52,11 @@ import { NodeGraphContainer } from './NodeGraph/NodeGraphContainer';
import { QueryRows } from './QueryRows';
import RawPrometheusContainer from './RawPrometheus/RawPrometheusContainer';
import { ResponseErrorContainer } from './ResponseErrorContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import { SecondaryActions } from './SecondaryActions';
import TableContainer from './Table/TableContainer';
import { TraceViewContainer } from './TraceView/TraceViewContainer';
import { changeSize } from './state/explorePane';
import { splitOpen } from './state/main';
import { changeShowQueryHistory, splitOpen } from './state/main';
import {
addQueryRow,
modifyQueries,
@ -108,15 +106,11 @@ export interface ExploreProps extends Themeable2 {
exploreId: string;
theme: GrafanaTheme2;
eventBus: EventBus;
}
enum ExploreDrawer {
RichHistory,
QueryInspector,
setShowQueryInspector: (value: boolean) => void;
showQueryInspector: boolean;
}
interface ExploreState {
openDrawer?: ExploreDrawer;
contentOutlineVisible: boolean;
}
@ -156,7 +150,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
constructor(props: Props) {
super(props);
this.state = {
openDrawer: undefined,
contentOutlineVisible: false,
};
this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false });
@ -311,20 +304,8 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
updateTimeRange({ exploreId, absoluteRange });
};
toggleShowRichHistory = () => {
this.setState((state) => {
return {
openDrawer: state.openDrawer === ExploreDrawer.RichHistory ? undefined : ExploreDrawer.RichHistory,
};
});
};
toggleShowQueryInspector = () => {
this.setState((state) => {
return {
openDrawer: state.openDrawer === ExploreDrawer.QueryInspector ? undefined : ExploreDrawer.QueryInspector,
};
});
toggleShowQueryHistory = () => {
this.props.changeShowQueryHistory(!this.props.showQueryHistory);
};
onSplitOpen = (panelType: string) => {
@ -551,17 +532,17 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
showCustom,
showNodeGraph,
showFlameGraph,
timeZone,
showLogsSample,
correlationEditorDetails,
correlationEditorHelperData,
showQueryHistory,
showQueryInspector,
setShowQueryInspector,
} = this.props;
const { openDrawer, contentOutlineVisible } = this.state;
const { contentOutlineVisible } = this.state;
const styles = getStyles(theme);
const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
const showRichHistory = openDrawer === ExploreDrawer.RichHistory;
const richHistoryRowButtonHidden = !supportedFeatures().queryHistoryAvailable;
const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
const showNoData =
queryResponse.state === LoadingState.Done &&
[
@ -622,11 +603,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
//TODO:unification
addQueryRowButtonHidden={false}
richHistoryRowButtonHidden={richHistoryRowButtonHidden}
richHistoryButtonActive={showRichHistory}
richHistoryButtonActive={showQueryHistory}
queryInspectorButtonActive={showQueryInspector}
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
onClickRichHistoryButton={this.toggleShowRichHistory}
onClickQueryInspectorButton={this.toggleShowQueryInspector}
onClickRichHistoryButton={this.toggleShowQueryHistory}
onClickQueryInspectorButton={() => setShowQueryInspector(!showQueryInspector)}
/>
<ResponseErrorContainer exploreId={exploreId} />
</PanelContainer>
@ -664,22 +645,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
{showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
</>
)}
{showRichHistory && (
<RichHistoryContainer
width={width}
exploreId={exploreId}
onClose={this.toggleShowRichHistory}
/>
)}
{showQueryInspector && (
<ExploreQueryInspector
exploreId={exploreId}
width={width}
onClose={this.toggleShowQueryInspector}
timeZone={timeZone}
isMixed={datasourceInstance.meta.mixed || false}
/>
)}
</ErrorBoundaryAlert>
</main>
);
@ -756,6 +721,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
showLogsSample,
correlationEditorHelperData,
correlationEditorDetails: explore.correlationEditorDetails,
showQueryHistory: explore.showQueryHistory,
};
}
@ -769,6 +735,7 @@ const mapDispatchToProps = {
addQueryRow,
splitOpen,
setSupplementaryQueryEnabled,
changeShowQueryHistory,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@ -8,22 +8,20 @@ import { GrafanaTheme2 } from '@grafana/data';
import { getDragStyles, useStyles2, useTheme2 } from '@grafana/ui';
export interface Props {
width: number;
children: React.ReactNode;
onResize?: ResizeCallback;
}
export function ExploreDrawer(props: Props) {
const { width, children, onResize } = props;
const { children, onResize } = props;
const theme = useTheme2();
const styles = useStyles2(getStyles);
const dragStyles = getDragStyles(theme);
const drawerWidth = `${width + 31.5}px`;
return (
<Resizable
className={cx(styles.fixed, styles.container, styles.drawerActive)}
defaultSize={{ width: drawerWidth, height: `${theme.components.horizontalDrawer.defaultHeight}px` }}
defaultSize={{ width: '100%', height: `${theme.components.horizontalDrawer.defaultHeight}px` }}
handleClasses={{ top: dragStyles.dragHandleHorizontal }}
enable={{
top: true,
@ -36,8 +34,6 @@ export function ExploreDrawer(props: Props) {
topLeft: false,
}}
maxHeight="100vh"
maxWidth={drawerWidth}
minWidth={drawerWidth}
onResize={onResize}
>
{children}
@ -58,13 +54,12 @@ const drawerSlide = (theme: GrafanaTheme2) => keyframes`
const getStyles = (theme: GrafanaTheme2) => ({
// @ts-expect-error csstype doesn't allow !important. see https://github.com/frenic/csstype/issues/114
fixed: css({
position: 'fixed !important',
position: 'absolute !important',
}),
container: css({
bottom: 0,
background: theme.colors.background.primary,
borderTop: `1px solid ${theme.colors.border.weak}`,
margin: theme.spacing(0, -2, 0, -2),
boxShadow: theme.shadows.z3,
zIndex: theme.zIndex.navbarFixed,
}),

View File

@ -8,18 +8,21 @@ import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPane
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { useSelector } from 'app/types';
import { useDispatch, useSelector } from 'app/types';
import { ExploreQueryParams } from 'app/types/explore';
import { CorrelationEditorModeBar } from './CorrelationEditorModeBar';
import { ExploreActions } from './ExploreActions';
import { ExploreDrawer } from './ExploreDrawer';
import { ExplorePaneContainer } from './ExplorePaneContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater';
import { useStateSync } from './hooks/useStateSync';
import { useTimeSrvFix } from './hooks/useTimeSrvFix';
import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors';
import { changeShowQueryHistory } from './state/main';
import { isSplit, selectCorrelationDetails, selectPanesEntries, selectShowQueryHistory } from './state/selectors';
const MIN_PANE_WIDTH = 200;
@ -36,11 +39,13 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
useExplorePageTitle(props.queryParams);
const { chrome } = useGrafana();
const navModel = useNavModel('explore');
const dispatch = useDispatch();
const { updateSplitSize, widthCalc } = useSplitSizeUpdater(MIN_PANE_WIDTH);
const panes = useSelector(selectPanesEntries);
const hasSplit = useSelector(isSplit);
const correlationDetails = useSelector(selectCorrelationDetails);
const showQueryHistory = useSelector(selectShowQueryHistory);
const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false);
useEffect(() => {
@ -80,6 +85,15 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
);
})}
</SplitPaneWrapper>
{showQueryHistory && (
<ExploreDrawer>
<RichHistoryContainer
onClose={() => {
dispatch(changeShowQueryHistory(false));
}}
/>
</ExploreDrawer>
)}
</div>
);
}
@ -92,6 +106,7 @@ const getStyles = (theme: GrafanaTheme2) => {
minHeight: 0,
height: '100%',
position: 'relative',
overflow: 'hidden',
}),
correlationsEditorIndicator: css({
borderLeft: `4px solid ${theme.colors.primary.main}`,

View File

@ -1,14 +1,15 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useRef } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { connect } from 'react-redux';
import { EventBusSrv } from '@grafana/data';
import { EventBusSrv, getTimeZone } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { CustomScrollbar } from '@grafana/ui';
import { stopQueryState } from 'app/core/utils/explore';
import { StoreState, useSelector } from 'app/types';
import Explore from './Explore';
import ExploreQueryInspector from './ExploreQueryInspector';
import { getExploreItemSelector } from './state/selectors';
const containerStyles = css({
@ -27,7 +28,7 @@ interface Props {
Connected components subscribe to the store before function components (using hooks) and can react to store changes. Thus, this connector function is called before the parent component (ExplorePage) is rerendered.
This means that child components' mapStateToProps will be executed with a zombie `exploreId` that is not present anymore in the store if the pane gets closed.
By connecting this component and returning the pane we workaround the zombie children issue here instead of modifying every children.
This is definitely not the ideal solution and we should in the future invest more time in exploring other approaches to better handle this scenario, potentially by refactoring panels to be function components
This is definitely not the ideal solution and we should in the future invest more time in exploring other approaches to better handle this scenario, potentially by refactoring panels to be function components
(therefore immune to this behaviour), or by forbidding them to access the store directly and instead pass them all the data they need via props or context.
You can read more about this issue here: https://react-redux.js.org/api/hooks#stale-props-and-zombie-children
@ -36,6 +37,7 @@ function ExplorePaneContainerUnconnected({ exploreId }: Props) {
useStopQueries(exploreId);
const eventBus = useRef(new EventBusSrv());
const ref = useRef(null);
const [showQueryInspector, setShowQueryInspector] = useState(false);
useEffect(() => {
const bus = eventBus.current;
@ -45,7 +47,19 @@ function ExplorePaneContainerUnconnected({ exploreId }: Props) {
return (
<CustomScrollbar hideVerticalTrack>
<div className={containerStyles} ref={ref} data-testid={selectors.pages.Explore.General.container}>
<Explore exploreId={exploreId} eventBus={eventBus.current} />
<Explore
exploreId={exploreId}
eventBus={eventBus.current}
showQueryInspector={showQueryInspector}
setShowQueryInspector={setShowQueryInspector}
/>
{showQueryInspector && (
<ExploreQueryInspector
exploreId={exploreId}
onClose={() => setShowQueryInspector(false)}
timeZone={getTimeZone()}
/>
)}
</div>
</CustomScrollbar>
);

View File

@ -47,7 +47,6 @@ jest.mock('react-virtualized-auto-sizer', () => {
const setup = (propOverrides = {}) => {
const props: ExploreQueryInspectorProps = {
width: 100,
exploreId: 'left',
onClose: jest.fn(),
timeZone: InternalTimeZones.utc,

View File

@ -21,17 +21,15 @@ import { GetDataOptions } from '../query/state/PanelQueryRunner';
import { runQueries } from './state/query';
interface DispatchProps {
width: number;
exploreId: string;
timeZone: TimeZone;
onClose: () => void;
isMixed: boolean;
}
type Props = DispatchProps & ConnectedProps<typeof connector>;
export function ExploreQueryInspector(props: Props) {
const { width, onClose, queryResponse, timeZone, isMixed, exploreId } = props;
const { onClose, queryResponse, timeZone, isMixed, exploreId } = props;
const [dataOptions, setDataOptions] = useState<GetDataOptions>({
withTransforms: false,
withFieldConfig: true,
@ -105,7 +103,7 @@ export function ExploreQueryInspector(props: Props) {
tabs.push(errorTab);
}
return (
<ExploreDrawer width={width}>
<ExploreDrawer>
<TabbedContainer tabs={tabs} onClose={onClose} closeIconTooltip="Close query inspector" />
</ExploreDrawer>
);
@ -118,6 +116,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
return {
queryResponse,
isMixed: item.datasourceInstance?.meta.mixed || false,
};
}

View File

@ -51,10 +51,11 @@ function setup(queries: DataQuery[]) {
const leftState = makeExplorePaneState();
const initialState: ExploreState = {
richHistory: [],
showQueryHistory: false,
panes: {
left: {
...leftState,
richHistory: [],
datasourceInstance: datasources['someDs-uid'],
queries,
correlations: [],

View File

@ -1,11 +1,12 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { SortOrder } from 'app/core/utils/richHistory';
import { RichHistory, RichHistoryProps, Tabs } from './RichHistory';
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
jest.mock('../state/selectors', () => ({ selectExploreDSMaps: jest.fn().mockReturnValue({ dsToExplore: [] }) }));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@ -18,11 +19,14 @@ jest.mock('@grafana/runtime', () => ({
},
}));
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useAsync: () => ({ loading: false, value: [] }),
}));
const setup = (propOverrides?: Partial<RichHistoryProps>) => {
const props: RichHistoryProps = {
exploreId: 'left',
height: 100,
activeDatasourceInstance: 'Test datasource',
richHistory: [],
richHistoryTotal: 0,
firstTab: Tabs.RichHistory,
@ -42,7 +46,7 @@ const setup = (propOverrides?: Partial<RichHistoryProps>) => {
richHistorySettings: {
retentionPeriod: 0,
starredTabAsFirstTab: false,
activeDatasourceOnly: true,
activeDatasourcesOnly: true,
lastUsedDatasourceFilters: [],
},
updateHistorySearchFilters: jest.fn(),
@ -51,7 +55,11 @@ const setup = (propOverrides?: Partial<RichHistoryProps>) => {
Object.assign(props, propOverrides);
render(<RichHistory {...props} />);
render(
<TestProvider>
<RichHistory {...props} />
</TestProvider>
);
};
describe('RichHistory', () => {

View File

@ -2,6 +2,7 @@ import { debounce } from 'lodash';
import React, { useState, useEffect } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { TabbedContainer, TabConfig } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { SortOrder, RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory';
@ -33,29 +34,18 @@ export interface RichHistoryProps {
richHistorySettings: RichHistorySettings;
richHistorySearchFilters?: RichHistorySearchFilters;
updateHistorySettings: (settings: RichHistorySettings) => void;
updateHistorySearchFilters: (exploreId: string, filters: RichHistorySearchFilters) => void;
loadRichHistory: (exploreId: string) => void;
loadMoreRichHistory: (exploreId: string) => void;
clearRichHistoryResults: (exploreId: string) => void;
updateHistorySearchFilters: (filters: RichHistorySearchFilters) => void;
loadRichHistory: () => void;
loadMoreRichHistory: () => void;
clearRichHistoryResults: () => void;
deleteRichHistory: () => void;
activeDatasourceInstance: string;
firstTab: Tabs;
exploreId: string;
height: number;
onClose: () => void;
}
export function RichHistory(props: RichHistoryProps) {
const {
richHistory,
richHistoryTotal,
height,
exploreId,
deleteRichHistory,
onClose,
firstTab,
activeDatasourceInstance,
} = props;
const { richHistory, richHistoryTotal, height, deleteRichHistory, onClose, firstTab } = props;
const [loading, setLoading] = useState(false);
@ -69,12 +59,12 @@ export function RichHistory(props: RichHistoryProps) {
...filtersToUpdate,
page: 1, // always load fresh results when updating filters
};
props.updateHistorySearchFilters(props.exploreId, filters);
props.updateHistorySearchFilters(filters);
loadRichHistory();
};
const loadRichHistory = debounce(() => {
props.loadRichHistory(props.exploreId);
props.loadRichHistory();
setLoading(true);
}, 300);
@ -87,8 +77,8 @@ export function RichHistory(props: RichHistoryProps) {
const toggleStarredTabAsFirstTab = () =>
updateSettings({ starredTabAsFirstTab: !props.richHistorySettings.starredTabAsFirstTab });
const toggleActiveDatasourceOnly = () =>
updateSettings({ activeDatasourceOnly: !props.richHistorySettings.activeDatasourceOnly });
const toggleActiveDatasourcesOnly = () =>
updateSettings({ activeDatasourcesOnly: !props.richHistorySettings.activeDatasourcesOnly });
useEffect(() => {
setLoading(false);
@ -103,12 +93,10 @@ export function RichHistory(props: RichHistoryProps) {
totalQueries={richHistoryTotal || 0}
loading={loading}
updateFilters={updateFilters}
clearRichHistoryResults={() => props.clearRichHistoryResults(props.exploreId)}
loadMoreRichHistory={() => props.loadMoreRichHistory(props.exploreId)}
activeDatasourceInstance={activeDatasourceInstance}
clearRichHistoryResults={() => props.clearRichHistoryResults()}
loadMoreRichHistory={() => props.loadMoreRichHistory()}
richHistorySettings={props.richHistorySettings}
richHistorySearchFilters={props.richHistorySearchFilters}
exploreId={exploreId}
height={height}
/>
),
@ -123,13 +111,11 @@ export function RichHistory(props: RichHistoryProps) {
queries={richHistory}
totalQueries={richHistoryTotal || 0}
loading={loading}
activeDatasourceInstance={activeDatasourceInstance}
updateFilters={updateFilters}
clearRichHistoryResults={() => props.clearRichHistoryResults(props.exploreId)}
loadMoreRichHistory={() => props.loadMoreRichHistory(props.exploreId)}
clearRichHistoryResults={() => props.clearRichHistoryResults()}
loadMoreRichHistory={() => props.loadMoreRichHistory()}
richHistorySettings={props.richHistorySettings}
richHistorySearchFilters={props.richHistorySearchFilters}
exploreId={exploreId}
/>
),
icon: 'star',
@ -142,10 +128,10 @@ export function RichHistory(props: RichHistoryProps) {
<RichHistorySettingsTab
retentionPeriod={props.richHistorySettings.retentionPeriod}
starredTabAsFirstTab={props.richHistorySettings.starredTabAsFirstTab}
activeDatasourceOnly={props.richHistorySettings.activeDatasourceOnly}
activeDatasourcesOnly={props.richHistorySettings.activeDatasourcesOnly}
onChangeRetentionPeriod={onChangeRetentionPeriod}
toggleStarredTabAsFirstTab={toggleStarredTabAsFirstTab}
toggleactiveDatasourceOnly={toggleActiveDatasourceOnly}
toggleActiveDatasourcesOnly={toggleActiveDatasourcesOnly}
deleteRichHistory={deleteRichHistory}
/>
),
@ -159,6 +145,7 @@ export function RichHistory(props: RichHistoryProps) {
onClose={onClose}
defaultTab={firstTab}
closeIconTooltip={t('explore.rich-history.close-tooltip', 'Close query history')}
testId={selectors.pages.Explore.QueryHistory.container}
/>
);
}

View File

@ -1,11 +1,13 @@
import { fireEvent, render, screen, getByText, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
import { DataQuery, DataSourceRef } from '@grafana/schema';
import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource';
import { RichHistoryQuery } from 'app/types';
import { configureStore } from 'app/store/configureStore';
import { ExploreState, RichHistoryQuery } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { RichHistoryCard, Props } from './RichHistoryCard';
@ -47,7 +49,7 @@ class MockDatasourceApi<T extends DataQuery> implements DataSourceApi<T> {
throw new Error('Method not implemented.');
}
getRef(): DataSourceRef {
throw new Error('Method not implemented.');
return { uid: this.uid, type: this.type };
}
}
@ -55,7 +57,7 @@ const dsStore: Record<string, DataSourceApi> = {
alertmanager: new MockDatasourceApi('Alertmanager', 3, 'alertmanager', 'alertmanager'),
loki: new MockDatasourceApi('Loki', 2, 'loki', 'loki'),
prometheus: new MockDatasourceApi<MockQuery>('Prometheus', 1, 'prometheus', 'prometheus', {
getQueryDisplayText: (query: MockQuery) => query.queryText || 'Unknwon query',
getQueryDisplayText: (query: MockQuery) => query.queryText || 'Unknown query',
}),
mixed: new MixedDatasource({
id: 4,
@ -97,6 +99,7 @@ jest.mock('app/core/utils/explore', () => ({
jest.mock('app/core/app_events', () => ({
publish: jest.fn(),
subscribe: jest.fn(),
}));
interface MockQuery extends DataQuery {
@ -124,13 +127,31 @@ const setup = (propOverrides?: Partial<Props<MockQuery>>) => {
deleteHistoryItem: deleteRichHistoryMock,
commentHistoryItem: jest.fn(),
setQueries: jest.fn(),
exploreId: 'left',
datasourceInstance: dsStore.loki,
datasourceInstances: [dsStore.loki],
};
const store = configureStore({
explore: {
panes: {
left: {
queries: [{ query: 'query1', refId: 'A' }],
datasourceInstance: dsStore.loki,
queryResponse: {},
range: {
raw: { from: 'now-1h', to: 'now' },
},
},
},
} as unknown as ExploreState,
});
Object.assign(props, propOverrides);
render(<RichHistoryCard {...props} />);
render(
<TestProvider store={store}>
<RichHistoryCard {...props} />
</TestProvider>
);
};
const starredQueryWithComment: RichHistoryQuery<MockQuery> = {
@ -236,8 +257,11 @@ describe('RichHistoryCard', () => {
datasourceName: 'Test datasource',
starred: false,
comment: '',
queries: [{ query: 'query1', refId: 'A', queryText: 'query1' }],
queries: [
{ query: 'query1', refId: 'A', queryText: 'query1', datasource: { uid: 'prometheus', type: 'prometheus' } },
],
},
datasourceInstances: [dsStore.prometheus],
});
const copyQueriesButton = await screen.findByRole('button', { name: 'Copy query to clipboard' });
expect(copyQueriesButton).toBeInTheDocument();
@ -260,6 +284,7 @@ describe('RichHistoryCard', () => {
{ query: 'query2', refId: 'B', datasource: { uid: 'loki' } },
],
},
datasourceInstances: [dsStore.loki, dsStore.prometheus, dsStore.mixed],
});
const copyQueriesButton = await screen.findByRole('button', { name: 'Copy query to clipboard' });
expect(copyQueriesButton).toBeInTheDocument();
@ -367,6 +392,7 @@ describe('RichHistoryCard', () => {
comment: '',
queries,
},
datasourceInstances: [dsStore.loki, dsStore.prometheus, dsStore.mixed],
});
const runQueryButton = await screen.findByRole('button', { name: /run query/i });

View File

@ -1,12 +1,11 @@
import { css, cx } from '@emotion/css';
import React, { useCallback, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { useAsync } from 'react-use';
import { GrafanaTheme2, DataSourceApi } from '@grafana/data';
import { config, getDataSourceSrv, reportInteraction, getAppEvents } from '@grafana/runtime';
import { config, reportInteraction, getAppEvents } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { TextArea, Button, IconButton, useStyles2, LoadingPlaceholder } from '@grafana/ui';
import { TextArea, Button, IconButton, useStyles2, ToolbarButton, Dropdown, Menu } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createSuccessNotification } from 'app/core/copy/appNotification';
import { Trans, t } from 'app/core/internationalization';
@ -17,18 +16,11 @@ import { changeDatasource } from 'app/features/explore/state/datasource';
import { starHistoryItem, commentHistoryItem, deleteHistoryItem } from 'app/features/explore/state/history';
import { setQueries } from 'app/features/explore/state/query';
import { dispatch } from 'app/store/store';
import { StoreState } from 'app/types';
import { useSelector } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { RichHistoryQuery } from 'app/types/explore';
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
const explore = state.explore;
const { datasourceInstance } = explore.panes[exploreId]!;
return {
exploreId,
datasourceInstance,
};
}
import { isSplit, selectExploreDSMaps, selectPanesEntries } from '../state/selectors';
const mapDispatchToProps = {
changeDatasource,
@ -38,9 +30,10 @@ const mapDispatchToProps = {
setQueries,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
const connector = connect(undefined, mapDispatchToProps);
interface OwnProps<T extends DataQuery = DataQuery> {
datasourceInstances?: DataSourceApi[];
queryHistoryItem: RichHistoryQuery<T>;
}
@ -147,46 +140,29 @@ export function RichHistoryCard(props: Props) {
starHistoryItem,
deleteHistoryItem,
changeDatasource,
exploreId,
datasourceInstance,
setQueries,
datasourceInstances,
} = props;
const [activeUpdateComment, setActiveUpdateComment] = useState(false);
const [openRunQueryButton, setOpenRunQueryButton] = useState(false);
const [comment, setComment] = useState<string | undefined>(queryHistoryItem.comment);
const { value: historyCardData, loading } = useAsync(async () => {
let datasourceInstance: DataSourceApi | undefined;
try {
datasourceInstance = await getDataSourceSrv().get(queryHistoryItem.datasourceUid);
} catch (e) {}
return {
datasourceInstance,
queries: await Promise.all(
queryHistoryItem.queries.map(async (query) => {
let datasource;
if (datasourceInstance?.meta.mixed) {
try {
datasource = await getDataSourceSrv().get(query.datasource);
} catch (e) {}
} else {
datasource = datasourceInstance;
}
return {
query,
datasource,
};
})
),
};
}, [queryHistoryItem.datasourceUid, queryHistoryItem.queries]);
const panesEntries = useSelector(selectPanesEntries);
const exploreActiveDS = useSelector(selectExploreDSMaps);
const isPaneSplit = useSelector(isSplit);
const styles = useStyles2(getStyles);
const onRunQuery = async () => {
const cardRootDatasource = datasourceInstances
? datasourceInstances.find((di) => di.uid === queryHistoryItem.datasourceUid)
: undefined;
const isDifferentDatasource = (uid: string, exploreId: string) =>
!exploreActiveDS.dsToExplore.find((di) => di.datasource.uid === uid)?.exploreIds.includes(exploreId);
const onRunQuery = async (exploreId: string) => {
const queriesToRun = queryHistoryItem.queries;
const differentDataSource = queryHistoryItem.datasourceUid !== datasourceInstance?.uid;
const differentDataSource = isDifferentDatasource(queryHistoryItem.datasourceUid, exploreId);
if (differentDataSource) {
await changeDatasource({ exploreId, datasource: queryHistoryItem.datasourceUid });
}
@ -202,16 +178,16 @@ export function RichHistoryCard(props: Props) {
const datasources = [...queryHistoryItem.queries.map((query) => query.datasource?.type || 'unknown')];
reportInteraction('grafana_explore_query_history_copy_query', {
datasources,
mixed: Boolean(historyCardData?.datasourceInstance?.meta.mixed),
mixed: Boolean(cardRootDatasource?.meta.mixed),
});
if (loading || !historyCardData) {
return;
}
const queriesText = historyCardData.queries
const queriesText = queryHistoryItem.queries
.map((query) => {
return createQueryText(query.query, query.datasource);
let queryDS = datasourceInstances?.find((di) => di.uid === queryHistoryItem.datasourceUid);
if (queryDS?.meta.mixed) {
queryDS = datasourceInstances?.find((di) => di.uid === query.datasource?.uid);
}
return createQueryText(query, queryDS);
})
.join('\n');
@ -258,7 +234,7 @@ export function RichHistoryCard(props: Props) {
}
};
const onStarrQuery = () => {
const onStarQuery = () => {
starHistoryItem(queryHistoryItem.id, !queryHistoryItem.starred);
reportInteraction('grafana_explore_query_history_starred', {
queryHistoryEnabled: config.queryHistoryEnabled,
@ -338,7 +314,7 @@ export function RichHistoryCard(props: Props) {
onClick={onCopyQuery}
tooltip={t('explore.rich-history-card.copy-query-tooltip', 'Copy query to clipboard')}
/>
{historyCardData?.datasourceInstance && (
{cardRootDatasource && (
<IconButton
name="share-alt"
onClick={onCreateShortLink}
@ -358,7 +334,7 @@ export function RichHistoryCard(props: Props) {
<IconButton
name={queryHistoryItem.starred ? 'favorite' : 'star'}
iconType={queryHistoryItem.starred ? 'mono' : 'default'}
onClick={onStarrQuery}
onClick={onStarQuery}
tooltip={
queryHistoryItem.starred
? t('explore.rich-history-card.unstar-query-tooltip', 'Unstar query')
@ -368,17 +344,86 @@ export function RichHistoryCard(props: Props) {
</div>
);
// exploreId on where the query will be ran, and the datasource ID for the item's DS
const runQueryText = (exploreId: string, dsUid: string) => {
return dsUid !== undefined && isDifferentDatasource(dsUid, exploreId)
? {
fallbackText: 'Switch data source and run query',
translation: t('explore.rich-history-card.switch-datasource-button', 'Switch data source and run query'),
}
: {
fallbackText: 'Run query',
translation: t('explore.rich-history-card.run-query-button', 'Run query'),
};
};
const runButton = () => {
const disabled = cardRootDatasource?.uid === undefined;
if (!isPaneSplit) {
const exploreId = exploreActiveDS.exploreToDS[0].exploreId;
const buttonText = runQueryText(exploreId, props.queryHistoryItem.datasourceUid);
return (
<Button
variant="secondary"
aria-label={buttonText.translation}
onClick={() => onRunQuery(exploreId)}
disabled={disabled}
>
{buttonText.translation}
</Button>
);
} else {
const menu = (
<Menu>
{panesEntries.map((pane, i) => {
const buttonText = runQueryText(pane[0], props.queryHistoryItem.datasourceUid);
const paneLabel =
i === 0
? t('explore.rich-history-card.left-pane', 'Left pane')
: t('explore.rich-history-card.right-pane', 'Right pane');
return (
<Menu.Item
key={i}
ariaLabel={buttonText.fallbackText}
onClick={() => {
onRunQuery(pane[0]);
}}
label={`${paneLabel}: ${buttonText.translation}`}
disabled={disabled}
/>
);
})}
</Menu>
);
return (
<Dropdown onVisibleChange={(state) => setOpenRunQueryButton(state)} placement="bottom-start" overlay={menu}>
<ToolbarButton aria-label="run query options" variant="canvas" isOpen={openRunQueryButton}>
{t('explore.rich-history-card.run-query-button', 'Run query')}
</ToolbarButton>
</Dropdown>
);
}
};
return (
<div className={styles.queryCard}>
<div className={styles.cardRow}>
<DatasourceInfo dsApi={historyCardData?.datasourceInstance} size="sm" />
<DatasourceInfo dsApi={cardRootDatasource} size="sm" />
{queryActionButtons}
</div>
<div className={cx(styles.cardRow)}>
<div className={styles.queryContainer}>
{historyCardData?.queries.map((q, i) => {
return <Query query={q} key={`${q}-${i}`} showDsInfo={historyCardData?.datasourceInstance?.meta.mixed} />;
{queryHistoryItem?.queries.map((q, i) => {
const queryDs = datasourceInstances?.find((ds) => ds.uid === q.datasource?.uid);
return (
<Query
query={{ query: q, datasource: queryDs }}
key={`${q}-${i}`}
showDsInfo={cardRootDatasource?.meta.mixed}
/>
);
})}
{!activeUpdateComment && queryHistoryItem.comment && (
<div
@ -390,32 +435,8 @@ export function RichHistoryCard(props: Props) {
)}
{activeUpdateComment && updateComment}
</div>
{!activeUpdateComment && (
<div className={styles.runButton}>
<Button
variant="secondary"
onClick={onRunQuery}
disabled={
!historyCardData?.datasourceInstance || historyCardData.queries.some((query) => !query.datasource)
}
>
{datasourceInstance?.uid === queryHistoryItem.datasourceUid ? (
<Trans i18nKey="explore.rich-history-card.run-query-button">Run query</Trans>
) : (
<Trans i18nKey="explore.rich-history-card.switch-datasource-button">
Switch data source and run query
</Trans>
)}
</Button>
</div>
)}
{!activeUpdateComment && <div className={styles.runButton}>{runButton()}</div>}
</div>
{loading && (
<LoadingPlaceholder
text={t('explore.rich-history-card.loading-text', 'loading...')}
className={styles.loader}
/>
)}
</div>
);
}

View File

@ -1,13 +1,12 @@
import { render } from '@testing-library/react';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { SortOrder } from 'app/core/utils/richHistory';
import { Tabs } from './RichHistory';
import { RichHistoryContainer, Props } from './RichHistoryContainer';
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
@ -18,11 +17,15 @@ jest.mock('@grafana/runtime', () => ({
reportInteraction: jest.fn(),
}));
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useAsync: () => ({ loading: false, value: [] }),
}));
jest.mock('../state/selectors', () => ({ selectExploreDSMaps: jest.fn().mockReturnValue({ dsToExplore: [] }) }));
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
width: 500,
exploreId: 'left',
activeDatasourceInstance: 'Test datasource',
richHistory: [],
firstTab: Tabs.RichHistory,
deleteRichHistory: jest.fn(),
@ -44,7 +47,7 @@ const setup = (propOverrides?: Partial<Props>) => {
richHistorySettings: {
retentionPeriod: 0,
starredTabAsFirstTab: false,
activeDatasourceOnly: true,
activeDatasourcesOnly: true,
lastUsedDatasourceFilters: [],
},
richHistoryTotal: 0,
@ -52,7 +55,7 @@ const setup = (propOverrides?: Partial<Props>) => {
Object.assign(props, propOverrides);
return render(<RichHistoryContainer {...props} />);
return render(<RichHistoryContainer {...props} />, { wrapper: TestProvider });
};
describe('RichHistoryContainer', () => {
@ -60,23 +63,15 @@ describe('RichHistoryContainer', () => {
const { container } = setup({ 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');
});
it('should render component with correct height', () => {
const { container } = setup();
expect(container.firstElementChild!.getAttribute('style')).toContain('height: 400px');
});
it('should re-request rich history every time the component is mounted', () => {
const initRichHistory = jest.fn();
const { unmount } = setup({ initRichHistory });
expect(initRichHistory).toBeCalledTimes(1);
expect(initRichHistory).toHaveBeenCalledTimes(1);
unmount();
expect(initRichHistory).toBeCalledTimes(1);
expect(initRichHistory).toHaveBeenCalledTimes(1);
setup({ initRichHistory });
expect(initRichHistory).toBeCalledTimes(2);
expect(initRichHistory).toHaveBeenCalledTimes(2);
});
});

View File

@ -1,15 +1,14 @@
// Libraries
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { config, reportInteraction } from '@grafana/runtime';
import { useTheme2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
// Types
import { ExploreItemState, StoreState } from 'app/types';
import { StoreState } from 'app/types';
// Components, enums
import { ExploreDrawer } from '../ExploreDrawer';
import {
deleteRichHistory,
initRichHistory,
@ -24,19 +23,16 @@ import { RichHistory, Tabs } from './RichHistory';
//Actions
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
function mapStateToProps(state: StoreState) {
const explore = state.explore;
const item: ExploreItemState = explore.panes[exploreId]!;
const richHistorySearchFilters = item.richHistorySearchFilters;
const richHistorySettings = explore.richHistorySettings;
const { datasourceInstance } = item;
const richHistorySearchFilters = explore.richHistorySearchFilters;
const { richHistorySettings, richHistory, richHistoryTotal } = explore;
const firstTab = richHistorySettings?.starredTabAsFirstTab ? Tabs.Starred : Tabs.RichHistory;
const { richHistory, richHistoryTotal } = item;
return {
richHistory,
richHistoryTotal,
firstTab,
activeDatasourceInstance: datasourceInstance!.name,
richHistorySettings,
richHistorySearchFilters,
};
@ -55,23 +51,17 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps);
interface OwnProps {
width: number;
exploreId: string;
onClose: () => void;
}
export type Props = ConnectedProps<typeof connector> & OwnProps;
export function RichHistoryContainer(props: Props) {
const theme = useTheme2();
const [height, setHeight] = useState(theme.components.horizontalDrawer.defaultHeight);
const {
richHistory,
richHistoryTotal,
width,
firstTab,
activeDatasourceInstance,
exploreId,
deleteRichHistory,
initRichHistory,
loadRichHistory,
@ -100,30 +90,21 @@ export function RichHistoryContainer(props: Props) {
}
return (
<ExploreDrawer
width={width}
onResize={(_e, _dir, ref) => {
setHeight(Number(ref.style.height.slice(0, -2)));
}}
>
<RichHistory
richHistory={richHistory}
richHistoryTotal={richHistoryTotal}
firstTab={firstTab}
activeDatasourceInstance={activeDatasourceInstance}
exploreId={exploreId}
onClose={onClose}
height={height}
deleteRichHistory={deleteRichHistory}
richHistorySettings={richHistorySettings}
richHistorySearchFilters={richHistorySearchFilters}
updateHistorySettings={updateHistorySettings}
updateHistorySearchFilters={updateHistorySearchFilters}
loadRichHistory={loadRichHistory}
loadMoreRichHistory={loadMoreRichHistory}
clearRichHistoryResults={clearRichHistoryResults}
/>
</ExploreDrawer>
<RichHistory
richHistory={richHistory}
richHistoryTotal={richHistoryTotal}
firstTab={firstTab}
onClose={onClose}
height={theme.components.horizontalDrawer.defaultHeight}
deleteRichHistory={deleteRichHistory}
richHistorySettings={richHistorySettings}
richHistorySearchFilters={richHistorySearchFilters}
updateHistorySettings={updateHistorySettings}
updateHistorySearchFilters={updateHistorySearchFilters}
loadRichHistory={loadRichHistory}
loadMoreRichHistory={loadMoreRichHistory}
clearRichHistoryResults={clearRichHistoryResults}
/>
);
}

View File

@ -1,17 +1,23 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import * as reactUse from 'react-use';
import { TestProvider } from 'test/helpers/TestProvider';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { SortOrder } from 'app/core/utils/richHistoryTypes';
import { RichHistoryQueriesTab, RichHistoryQueriesTabProps } from './RichHistoryQueriesTab';
const asyncSpy = jest
.spyOn(reactUse, 'useAsync')
.mockReturnValue({ loading: false, value: [new MockDataSourceApi('test-ds')] });
const setup = (propOverrides?: Partial<RichHistoryQueriesTabProps>) => {
const props: RichHistoryQueriesTabProps = {
queries: [],
totalQueries: 0,
loading: false,
activeDatasourceInstance: 'test-ds',
updateFilters: jest.fn(),
clearRichHistoryResults: jest.fn(),
loadMoreRichHistory: jest.fn(),
@ -25,39 +31,52 @@ const setup = (propOverrides?: Partial<RichHistoryQueriesTabProps>) => {
},
richHistorySettings: {
retentionPeriod: 30,
activeDatasourceOnly: false,
lastUsedDatasourceFilters: [],
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: ['test-ds'],
starredTabAsFirstTab: false,
},
exploreId: 'left',
height: 100,
};
Object.assign(props, propOverrides);
return render(<RichHistoryQueriesTab {...props} />);
return render(<RichHistoryQueriesTab {...props} />, { wrapper: TestProvider });
};
describe('RichHistoryQueriesTab', () => {
beforeAll(() => {
const testDS = new MockDataSourceApi('test-ds');
setDataSourceSrv({
getList() {
return [];
return [testDS];
},
} as unknown as DataSourceSrv);
});
it('should render', () => {
afterEach(() => {
asyncSpy.mockClear();
});
it('should render', async () => {
setup();
expect(screen.queryByText('Filter history')).toBeInTheDocument();
const filterHistory = await screen.findByText('Filter history');
expect(filterHistory).toBeInTheDocument();
});
it('should not regex escape filter input', () => {
it('should not regex escape filter input', async () => {
const updateFiltersSpy = jest.fn();
setup({ updateFilters: updateFiltersSpy });
const input = screen.getByPlaceholderText(/search queries/i);
const input = await screen.findByPlaceholderText(/search queries/i);
fireEvent.change(input, { target: { value: '|=' } });
expect(updateFiltersSpy).toHaveBeenCalledWith(expect.objectContaining({ search: '|=' }));
});
it('should update the filter and get data once on mount, and update the filter when the it changes', async () => {
const updateFiltersSpy = jest.fn();
setup({ updateFilters: updateFiltersSpy });
expect(updateFiltersSpy).toHaveBeenCalledTimes(1);
expect(asyncSpy).toHaveBeenCalledTimes(1);
const input = await screen.findByLabelText(/remove/i);
fireEvent.click(input);
expect(updateFiltersSpy).toHaveBeenCalledTimes(2);
});
});

View File

@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import React, { useEffect } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { DataSourceApi, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Button, FilterInput, MultiSelect, RangeSlider, Select, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import {
@ -13,8 +14,11 @@ import {
RichHistorySearchFilters,
RichHistorySettings,
} from 'app/core/utils/richHistory';
import { useSelector } from 'app/types';
import { RichHistoryQuery } from 'app/types/explore';
import { selectExploreDSMaps } from '../state/selectors';
import { getSortOrderOptions } from './RichHistory';
import RichHistoryCard from './RichHistoryCard';
@ -22,13 +26,11 @@ export interface RichHistoryQueriesTabProps {
queries: RichHistoryQuery[];
totalQueries: number;
loading: boolean;
activeDatasourceInstance: string;
updateFilters: (filtersToUpdate?: Partial<RichHistorySearchFilters>) => void;
clearRichHistoryResults: () => void;
loadMoreRichHistory: () => void;
richHistorySettings: RichHistorySettings;
richHistorySearchFilters?: RichHistorySearchFilters;
exploreId: string;
height: number;
}
@ -122,20 +124,22 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
clearRichHistoryResults,
loadMoreRichHistory,
richHistorySettings,
exploreId,
height,
activeDatasourceInstance,
} = props;
const exploreActiveDS = useSelector(selectExploreDSMaps);
const styles = useStyles2(getStyles, height);
const listOfDatasources = createDatasourcesList();
// on mount, set filter to either active datasource or all datasources
useEffect(() => {
const datasourceFilters =
!richHistorySettings.activeDatasourceOnly && richHistorySettings.lastUsedDatasourceFilters
!richHistorySettings.activeDatasourcesOnly && richHistorySettings.lastUsedDatasourceFilters
? richHistorySettings.lastUsedDatasourceFilters
: [activeDatasourceInstance];
: exploreActiveDS.dsToExplore
.map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name)
.filter((name): name is string => !!name);
const filters: RichHistorySearchFilters = {
search: '',
sortOrder: SortOrder.Descending,
@ -152,10 +156,33 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// whenever the filter changes, get all datasource information for the filtered datasources
const { value: datasourceFilterApis, loading: loadingDs } = useAsync(async () => {
const datasourcesToGet =
richHistorySearchFilters?.datasourceFilters && richHistorySearchFilters?.datasourceFilters.length > 0
? richHistorySearchFilters?.datasourceFilters
: listOfDatasources.map((ds) => ds.uid);
const dsGetProm = await datasourcesToGet.map(async (dsf) => {
try {
// this get works off datasource names
return getDataSourceSrv().get(dsf);
} catch (e) {
return Promise.resolve();
}
});
if (dsGetProm !== undefined) {
const enhancedDatasourceData = (await Promise.all(dsGetProm)).filter((dsi): dsi is DataSourceApi => !!dsi);
return enhancedDatasourceData;
} else {
return [];
}
}, [richHistorySearchFilters?.datasourceFilters]);
if (!richHistorySearchFilters) {
return (
<span>
<Trans i18nKey="explore.rich-history-queries-tab.loading">Loading...</Trans>;
<Trans i18nKey="explore.rich-history-queries-tab.loading">Loading...</Trans>
</span>
);
}
@ -199,7 +226,7 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
<div className={styles.containerContent} data-testid="query-history-queries-tab">
<div className={styles.selectors}>
{!richHistorySettings.activeDatasourceOnly && (
{!richHistorySettings.activeDatasourcesOnly && (
<MultiSelect
className={styles.multiselect}
options={listOfDatasources.map((ds) => {
@ -237,13 +264,13 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
</div>
</div>
{loading && (
{(loading || loadingDs) && (
<span>
<Trans i18nKey="explore.rich-history-queries-tab.loading-results">Loading results...</Trans>
</span>
)}
{!loading &&
{!(loading || loadingDs) &&
Object.keys(mappedQueriesToHeadings).map((heading) => {
return (
<div key={heading}>
@ -266,7 +293,7 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
</span>
</div>
{mappedQueriesToHeadings[heading].map((q) => {
return <RichHistoryCard queryHistoryItem={q} key={q.id} exploreId={exploreId} />;
return <RichHistoryCard datasourceInstances={datasourceFilterApis} queryHistoryItem={q} key={q.id} />;
})}
</div>
);

View File

@ -7,10 +7,10 @@ const setup = (propOverrides?: Partial<RichHistorySettingsProps>) => {
const props: RichHistorySettingsProps = {
retentionPeriod: 14,
starredTabAsFirstTab: true,
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
onChangeRetentionPeriod: jest.fn(),
toggleStarredTabAsFirstTab: jest.fn(),
toggleactiveDatasourceOnly: jest.fn(),
toggleActiveDatasourcesOnly: jest.fn(),
deleteRichHistory: jest.fn(),
};
@ -24,7 +24,7 @@ describe('RichHistorySettings', () => {
setup();
expect(screen.queryByText('2 weeks')).toBeInTheDocument();
});
it('should render component with correctly checked starredTabAsFirstTab and uncheched toggleActiveDatasourceOnly settings', () => {
it('should render component with correctly checked starredTabAsFirstTab and uncheched toggleactiveDatasourcesOnly settings', () => {
setup();
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes.length).toBe(2);

View File

@ -16,10 +16,10 @@ import { ShowConfirmModalEvent } from '../../../types/events';
export interface RichHistorySettingsProps {
retentionPeriod: number;
starredTabAsFirstTab: boolean;
activeDatasourceOnly: boolean;
activeDatasourcesOnly: boolean;
onChangeRetentionPeriod: (option: SelectableValue<number>) => void;
toggleStarredTabAsFirstTab: () => void;
toggleactiveDatasourceOnly: () => void;
toggleActiveDatasourcesOnly: () => void;
deleteRichHistory: () => void;
}
@ -54,10 +54,10 @@ export function RichHistorySettingsTab(props: RichHistorySettingsProps) {
const {
retentionPeriod,
starredTabAsFirstTab,
activeDatasourceOnly,
activeDatasourcesOnly,
onChangeRetentionPeriod,
toggleStarredTabAsFirstTab,
toggleactiveDatasourceOnly,
toggleActiveDatasourcesOnly,
deleteRichHistory,
} = props;
const styles = useStyles2(getStyles);
@ -136,8 +136,8 @@ export function RichHistorySettingsTab(props: RichHistorySettingsProps) {
>
<InlineSwitch
id="explore-query-history-settings-data-source-behavior"
value={activeDatasourceOnly}
onChange={toggleactiveDatasourceOnly}
value={activeDatasourcesOnly}
onChange={toggleActiveDatasourcesOnly}
/>
</InlineField>
)}

View File

@ -1,11 +1,12 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { SortOrder } from 'app/core/utils/richHistory';
import { RichHistoryStarredTab, RichHistoryStarredTabProps } from './RichHistoryStarredTab';
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
jest.mock('../state/selectors', () => ({ selectExploreDSMaps: jest.fn().mockReturnValue({ dsToExplore: [] }) }));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@ -21,15 +22,13 @@ const setup = (propOverrides?: Partial<RichHistoryStarredTabProps>) => {
queries: [],
loading: false,
totalQueries: 0,
activeDatasourceInstance: '',
updateFilters: jest.fn(),
loadMoreRichHistory: jest.fn(),
clearRichHistoryResults: jest.fn(),
exploreId: 'left',
richHistorySettings: {
retentionPeriod: 7,
starredTabAsFirstTab: false,
activeDatasourceOnly: false,
activeDatasourcesOnly: false,
lastUsedDatasourceFilters: [],
},
richHistorySearchFilters: {
@ -44,41 +43,46 @@ const setup = (propOverrides?: Partial<RichHistoryStarredTabProps>) => {
Object.assign(props, propOverrides);
const container = render(<RichHistoryStarredTab {...props} />);
const container = render(<RichHistoryStarredTab {...props} />, { wrapper: TestProvider });
return container;
};
describe('RichHistoryStarredTab', () => {
describe('sorter', () => {
it('should render sorter', () => {
it('should render sorter', async () => {
const container = setup();
expect(container.queryByLabelText('Sort queries')).toBeInTheDocument();
const sortText = await container.findByLabelText('Sort queries');
expect(sortText).toBeInTheDocument();
});
});
describe('select datasource', () => {
it('should render select datasource if activeDatasourceOnly is false', () => {
it('should render select datasource if activeDatasourcesOnly is false', async () => {
const container = setup();
expect(container.queryByLabelText('Filter queries for data sources(s)')).toBeInTheDocument();
const filterText = await container.findByLabelText('Filter queries for data sources(s)');
expect(filterText).toBeInTheDocument();
});
it('should not render select datasource if activeDatasourceOnly is true', () => {
it('should not render select datasource if activeDatasourcesOnly is true', async () => {
const container = setup({
richHistorySettings: {
retentionPeriod: 7,
starredTabAsFirstTab: false,
activeDatasourceOnly: true,
activeDatasourcesOnly: true,
lastUsedDatasourceFilters: [],
},
});
expect(container.queryByLabelText('Filter queries for data sources(s)')).not.toBeInTheDocument();
// trying to wait for placeholder text to render before proceeding does not work
await container.findByPlaceholderText(/search queries/i);
const filterText = container.queryByLabelText('Filter queries for data sources(s)');
expect(filterText).not.toBeInTheDocument();
});
});
it('should not regex escape filter input', () => {
it('should not regex escape filter input', async () => {
const updateFiltersSpy = jest.fn();
setup({ updateFilters: updateFiltersSpy });
const input = screen.getByPlaceholderText(/search queries/i);
const input = await screen.findByPlaceholderText(/search queries/i);
fireEvent.change(input, { target: { value: '|=' } });
expect(updateFiltersSpy).toHaveBeenCalledWith(expect.objectContaining({ search: '|=' }));

View File

@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import React, { useEffect } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { DataSourceApi, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { useStyles2, Select, MultiSelect, FilterInput, Button } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import {
@ -11,8 +12,11 @@ import {
RichHistorySearchFilters,
RichHistorySettings,
} from 'app/core/utils/richHistory';
import { useSelector } from 'app/types';
import { RichHistoryQuery } from 'app/types/explore';
import { selectExploreDSMaps } from '../state/selectors';
import { getSortOrderOptions } from './RichHistory';
import RichHistoryCard from './RichHistoryCard';
@ -20,13 +24,11 @@ export interface RichHistoryStarredTabProps {
queries: RichHistoryQuery[];
totalQueries: number;
loading: boolean;
activeDatasourceInstance: string;
updateFilters: (filtersToUpdate: Partial<RichHistorySearchFilters>) => void;
clearRichHistoryResults: () => void;
loadMoreRichHistory: () => void;
richHistorySearchFilters?: RichHistorySearchFilters;
richHistorySettings: RichHistorySettings;
exploreId: string;
}
const getStyles = (theme: GrafanaTheme2) => {
@ -72,24 +74,25 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) {
updateFilters,
clearRichHistoryResults,
loadMoreRichHistory,
activeDatasourceInstance,
richHistorySettings,
queries,
totalQueries,
loading,
richHistorySearchFilters,
exploreId,
} = props;
const styles = useStyles2(getStyles);
const exploreActiveDS = useSelector(selectExploreDSMaps);
const listOfDatasources = createDatasourcesList();
useEffect(() => {
const datasourceFilters =
richHistorySettings.activeDatasourceOnly && richHistorySettings.lastUsedDatasourceFilters
richHistorySettings.activeDatasourcesOnly && richHistorySettings.lastUsedDatasourceFilters
? richHistorySettings.lastUsedDatasourceFilters
: [activeDatasourceInstance];
: exploreActiveDS.dsToExplore
.map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name)
.filter((name): name is string => !!name);
const filters: RichHistorySearchFilters = {
search: '',
sortOrder: SortOrder.Descending,
@ -105,6 +108,29 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { value: datasourceFilterApis, loading: loadingDs } = useAsync(async () => {
const datasourcesToGet =
richHistorySearchFilters?.datasourceFilters && richHistorySearchFilters?.datasourceFilters.length > 0
? richHistorySearchFilters?.datasourceFilters
: listOfDatasources.map((ds) => ds.uid);
const dsGetProm = await datasourcesToGet.map(async (dsf) => {
try {
// this get works off datasource names
return getDataSourceSrv().get(dsf);
} catch (e) {
return Promise.resolve();
}
});
if (dsGetProm !== undefined) {
const enhancedDatasourceData = (await Promise.all(dsGetProm)).filter((dsi): dsi is DataSourceApi => !!dsi);
//setDatasourceFilterApiList(enhancedDatasourceData)
return enhancedDatasourceData;
} else {
return [];
}
}, [richHistorySearchFilters?.datasourceFilters]);
if (!richHistorySearchFilters) {
return (
<span>
@ -119,7 +145,7 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) {
<div className={styles.container}>
<div className={styles.containerContent}>
<div className={styles.selectors}>
{!richHistorySettings.activeDatasourceOnly && (
{!richHistorySettings.activeDatasourcesOnly && (
<MultiSelect
className={styles.multiselect}
options={listOfDatasources.map((ds) => {
@ -159,14 +185,14 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) {
/>
</div>
</div>
{loading && (
{loading && loadingDs && (
<span>
<Trans i18nKey="explore.rich-history-starred-tab.loading-results">Loading results...</Trans>
</span>
)}
{!loading &&
{!(loading && loadingDs) &&
queries.map((q) => {
return <RichHistoryCard queryHistoryItem={q} key={q.id} exploreId={exploreId} />;
return <RichHistoryCard queryHistoryItem={q} key={q.id} datasourceInstances={datasourceFilterApis} />;
})}
{queries.length && queries.length !== totalQueries ? (
<div>

View File

@ -1,26 +1,18 @@
import { waitFor } from '@testing-library/react';
import { getAllByRoleInQueryHistoryTab, withinExplore } from './setup';
import { withinQueryHistory } from './setup';
export const assertQueryHistoryExists = async (query: string, exploreId = 'left') => {
const selector = withinExplore(exploreId);
export const assertQueryHistoryExists = async (query: string) => {
const selector = withinQueryHistory();
expect(await selector.findByText('1 queries')).toBeInTheDocument();
const queryItem = selector.getByLabelText('Query text');
expect(queryItem).toHaveTextContent(query);
};
export const assertQueryHistoryContains = async (query: string, exploreId = 'left') => {
const selector = withinExplore(exploreId);
export const assertQueryHistory = async (expectedQueryTexts: string[]) => {
const selector = withinQueryHistory();
await waitFor(() => {
const containsQuery = selector.getAllByLabelText('Query text').map((e) => (e.textContent || '').includes(query));
expect(containsQuery).toContain(true);
});
};
export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId = 'left') => {
const selector = withinExplore(exploreId);
await waitFor(() => {
expect(selector.getByText(new RegExp(`${expectedQueryTexts.length} queries`))).toBeInTheDocument();
const queryTexts = selector.getAllByLabelText('Query text');
@ -30,15 +22,15 @@ export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId
});
};
export const assertQueryHistoryIsEmpty = async (exploreId = 'left') => {
const selector = withinExplore(exploreId);
export const assertQueryHistoryIsEmpty = async () => {
const selector = withinQueryHistory();
const queryTexts = selector.queryAllByLabelText('Query text');
expect(await queryTexts).toHaveLength(0);
};
export const assertQueryHistoryComment = async (expectedQueryComments: string[], exploreId = 'left') => {
const selector = withinExplore(exploreId);
export const assertQueryHistoryComment = async (expectedQueryComments: string[]) => {
const selector = withinQueryHistory();
await waitFor(() => {
expect(selector.getByText(new RegExp(`${expectedQueryComments.length} queries`))).toBeInTheDocument();
const queryComments = selector.getAllByLabelText('Query comment');
@ -48,25 +40,12 @@ export const assertQueryHistoryComment = async (expectedQueryComments: string[],
});
};
export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], exploreId = 'left') => {
const starButtons = getAllByRoleInQueryHistoryTab(exploreId, 'button', /Star query|Unstar query/);
await waitFor(() =>
expectedStars.forEach((starred, queryIndex) => {
expect(starButtons[queryIndex]).toHaveAccessibleName(starred ? 'Unstar query' : 'Star query');
})
);
export const assertQueryHistoryTabIsSelected = (tabName: 'Query history' | 'Starred' | 'Settings') => {
expect(withinQueryHistory().getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
};
export const assertQueryHistoryTabIsSelected = (
tabName: 'Query history' | 'Starred' | 'Settings',
exploreId = 'left'
) => {
expect(withinExplore(exploreId).getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
};
export const assertDataSourceFilterVisibility = (visible: boolean, exploreId = 'left') => {
const filterInput = withinExplore(exploreId).queryByLabelText('Filter queries for data sources(s)');
export const assertDataSourceFilterVisibility = (visible: boolean) => {
const filterInput = withinQueryHistory().queryByLabelText('Filter queries for data sources(s)');
if (visible) {
expect(filterInput).toBeInTheDocument();
} else {
@ -74,10 +53,10 @@ export const assertDataSourceFilterVisibility = (visible: boolean, exploreId = '
}
};
export const assertQueryHistoryElementsShown = (shown: number, total: number, exploreId = 'left') => {
expect(withinExplore(exploreId).queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument();
export const assertQueryHistoryElementsShown = (shown: number, total: number) => {
expect(withinQueryHistory().queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument();
};
export const assertLoadMoreQueryHistoryNotVisible = (exploreId = 'left') => {
expect(withinExplore(exploreId).queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
export const assertLoadMoreQueryHistoryNotVisible = () => {
expect(withinQueryHistory().queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
};

View File

@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event';
import { selectors } from '@grafana/e2e-selectors';
import { getAllByRoleInQueryHistoryTab, withinExplore } from './setup';
import { getAllByRoleInQueryHistoryTab, withinExplore, withinQueryHistory } from './setup';
export const changeDatasource = async (name: string) => {
const datasourcePicker = (await screen.findByTestId(selectors.components.DataSourcePicker.container)).children[0];
@ -25,56 +25,57 @@ export const runQuery = async (exploreId = 'left') => {
await userEvent.click(button);
};
export const openQueryHistory = async (exploreId = 'left') => {
const selector = withinExplore(exploreId);
const button = selector.getByRole('button', { name: 'Query history' });
export const openQueryHistory = async () => {
const explore = withinExplore('left');
const button = explore.getByRole('button', { name: 'Query history' });
await userEvent.click(button);
expect(await selector.findByPlaceholderText('Search queries')).toBeInTheDocument();
expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument();
};
export const closeQueryHistory = async (exploreId = 'left') => {
const closeButton = withinExplore(exploreId).getByRole('button', { name: 'Close query history' });
export const closeQueryHistory = async () => {
const selector = withinQueryHistory();
const closeButton = selector.getByRole('button', { name: 'Close query history' });
await userEvent.click(closeButton);
};
export const switchToQueryHistoryTab = async (name: 'Settings' | 'Query History', exploreId = 'left') => {
await userEvent.click(withinExplore(exploreId).getByRole('tab', { name: `Tab ${name}` }));
export const switchToQueryHistoryTab = async (name: 'Settings' | 'Query History') => {
await userEvent.click(withinQueryHistory().getByRole('tab', { name: `Tab ${name}` }));
};
export const selectStarredTabFirst = async (exploreId = 'left') => {
const checkbox = withinExplore(exploreId).getByRole('checkbox', {
export const selectStarredTabFirst = async () => {
const checkbox = withinQueryHistory().getByRole('checkbox', {
name: /Change the default active tab from “Query history” to “Starred”/,
});
await userEvent.click(checkbox);
};
export const selectOnlyActiveDataSource = async (exploreId = 'left') => {
const checkbox = withinExplore(exploreId).getByLabelText(/Only show queries for data source currently active.*/);
export const selectOnlyActiveDataSource = async () => {
const checkbox = withinQueryHistory().getByLabelText(/Only show queries for data source currently active.*/);
await userEvent.click(checkbox);
};
export const starQueryHistory = async (queryIndex: number, exploreId = 'left') => {
await invokeAction(queryIndex, 'Star query', exploreId);
export const starQueryHistory = async (queryIndex: number) => {
await invokeAction(queryIndex, 'Star query');
};
export const commentQueryHistory = async (queryIndex: number, comment: string, exploreId = 'left') => {
await invokeAction(queryIndex, 'Add comment', exploreId);
const input = withinExplore(exploreId).getByPlaceholderText('An optional description of what the query does.');
export const commentQueryHistory = async (queryIndex: number, comment: string) => {
await invokeAction(queryIndex, 'Add comment');
const input = withinQueryHistory().getByPlaceholderText('An optional description of what the query does.');
await userEvent.clear(input);
await userEvent.type(input, comment);
await invokeAction(queryIndex, 'Save comment', exploreId);
await invokeAction(queryIndex, 'Save comment');
};
export const deleteQueryHistory = async (queryIndex: number, exploreId = 'left') => {
await invokeAction(queryIndex, 'Delete query', exploreId);
export const deleteQueryHistory = async (queryIndex: number) => {
await invokeAction(queryIndex, 'Delete query');
};
export const loadMoreQueryHistory = async (exploreId = 'left') => {
const button = withinExplore(exploreId).getByRole('button', { name: 'Load more' });
export const loadMoreQueryHistory = async () => {
const button = withinQueryHistory().getByRole('button', { name: 'Load more' });
await userEvent.click(button);
};
const invokeAction = async (queryIndex: number, actionAccessibleName: string | RegExp, exploreId: string) => {
const buttons = getAllByRoleInQueryHistoryTab(exploreId, 'button', actionAccessibleName);
const invokeAction = async (queryIndex: number, actionAccessibleName: string | RegExp) => {
const buttons = getAllByRoleInQueryHistoryTab('button', actionAccessibleName);
await userEvent.click(buttons[queryIndex]);
};

View File

@ -282,6 +282,11 @@ export const withinExplore = (exploreId: string) => {
return within(container[exploreId === 'left' ? 0 : 1]);
};
export const withinQueryHistory = () => {
const container = screen.getByTestId('data-testid QueryHistory');
return within(container);
};
const exploreTestsHelper: { setupExplore: typeof setupExplore; tearDownExplore?: (options?: TearDownOptions) => void } =
{
setupExplore,
@ -291,8 +296,8 @@ const exploreTestsHelper: { setupExplore: typeof setupExplore; tearDownExplore?:
/**
* Optimized version of getAllByRole to avoid timeouts in tests. Please check #70158, #59116 and #47635, #78236.
*/
export const getAllByRoleInQueryHistoryTab = (exploreId: string, role: ByRoleMatcher, name: string | RegExp) => {
const selector = withinExplore(exploreId);
export const getAllByRoleInQueryHistoryTab = (role: ByRoleMatcher, name: string | RegExp) => {
const selector = withinQueryHistory();
// Test ID is used to avoid test timeouts reported in
const queriesContainer = selector.getByTestId('query-history-queries-tab');
return within(queriesContainer).getAllByRole(role, { name });

View File

@ -12,11 +12,9 @@ import {
assertLoadMoreQueryHistoryNotVisible,
assertQueryHistory,
assertQueryHistoryComment,
assertQueryHistoryContains,
assertQueryHistoryElementsShown,
assertQueryHistoryExists,
assertQueryHistoryIsEmpty,
assertQueryHistoryIsStarred,
assertQueryHistoryTabIsSelected,
} from './helper/assert';
import {
@ -29,7 +27,6 @@ import {
runQuery,
selectOnlyActiveDataSource,
selectStarredTabFirst,
starQueryHistory,
switchToQueryHistoryTab,
} from './helper/interactions';
import { makeLogsQueryResponse } from './helper/query';
@ -141,58 +138,6 @@ describe('Explore: Query History', () => {
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}']);
});
describe('updates the state in both Explore panes', () => {
beforeEach(async () => {
const urlParams = {
left: serializeStateToUrlParam({
datasource: 'loki',
queries: [{ refId: 'A', expr: 'query #1' }],
range: { from: 'now-1h', to: 'now' },
}),
right: serializeStateToUrlParam({
datasource: 'loki',
queries: [{ refId: 'A', expr: 'query #2' }],
range: { from: 'now-1h', to: 'now' },
}),
};
const { datasources } = setupExplore({ urlParams });
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
await waitForExplore();
await waitForExplore('right');
await openQueryHistory('left');
await openQueryHistory('right');
});
it('initial state is in sync', async () => {
await assertQueryHistoryContains('{"expr":"query #1"}', 'left');
await assertQueryHistoryContains('{"expr":"query #2"}', 'left');
await assertQueryHistoryContains('{"expr":"query #1"}', 'right');
await assertQueryHistoryContains('{"expr":"query #2"}', 'right');
});
it('starred queries are synced', async () => {
// star one one query
await starQueryHistory(1, 'left');
await assertQueryHistoryIsStarred([false, true], 'left');
await assertQueryHistoryIsStarred([false, true], 'right');
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_starred', {
queryHistoryEnabled: false,
newValue: true,
});
});
it('deleted queries are synced', async () => {
await deleteQueryHistory(0, 'left');
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
await assertQueryHistory(['{"expr":"query #1"}'], 'right');
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_deleted', {
queryHistoryEnabled: false,
});
});
});
it('add comments to query history', async () => {
const urlParams = {
left: serializeStateToUrlParam({
@ -206,9 +151,9 @@ describe('Explore: Query History', () => {
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
await waitForExplore();
await openQueryHistory();
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
await assertQueryHistory(['{"expr":"query #1"}']);
await commentQueryHistory(0, 'test comment');
await assertQueryHistoryComment(['test comment'], 'left');
await assertQueryHistoryComment(['test comment']);
});
it('removes the query item from the history panel when user deletes a regular query', async () => {
@ -227,13 +172,13 @@ describe('Explore: Query History', () => {
await openQueryHistory();
// queries in history
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
await assertQueryHistory(['{"expr":"query #1"}']);
// delete query
await deleteQueryHistory(0, 'left');
await deleteQueryHistory(0);
// there was only one query in history so assert that query history is empty
await assertQueryHistoryIsEmpty('left');
await assertQueryHistoryIsEmpty();
});
it('updates query history settings', async () => {

View File

@ -20,7 +20,6 @@ import { createAsyncThunk, ThunkResult } from 'app/types';
import { ExploreItemState } from 'app/types/explore';
import { datasourceReducer } from './datasource';
import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction } from './main';
import { queryReducer, runQueries } from './query';
import { timeReducer, updateTime } from './time';
import {
@ -214,23 +213,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
state = datasourceReducer(state, action);
state = timeReducer(state, action);
if (richHistoryUpdatedAction.match(action)) {
const { richHistory, total } = action.payload.richHistoryResults;
return {
...state,
richHistory,
richHistoryTotal: total,
};
}
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

@ -1,3 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import { HistoryItem } from '@grafana/data';
import { DataQuery } from '@grafana/schema';
import {
addToRichHistory,
@ -9,7 +12,7 @@ import {
updateRichHistorySettings,
updateStarredInRichHistory,
} from 'app/core/utils/richHistory';
import { ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types';
import { RichHistoryQuery, ThunkResult } from 'app/types';
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
@ -21,7 +24,15 @@ import {
richHistoryStorageFullAction,
richHistoryUpdatedAction,
} from './main';
import { selectPanesEntries } from './selectors';
//
// Actions and Payloads
//
export interface HistoryUpdatedPayload {
history: HistoryItem[];
}
export const historyUpdatedAction = createAction<HistoryUpdatedPayload>('explore/historyUpdated');
//
// Action creators
@ -37,27 +48,23 @@ type SyncHistoryUpdatesOptions = {
*/
const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesOptions): ThunkResult<void> => {
return async (dispatch, getState) => {
forEachExplorePane(getState().explore, (item, exploreId) => {
const newRichHistory = item.richHistory
// update
.map((query) => (query.id === updatedQuery?.id ? updatedQuery : query))
// or remove
.filter((query) => query.id !== deletedId);
const deletedItems = item.richHistory.length - newRichHistory.length;
dispatch(
richHistoryUpdatedAction({
richHistoryResults: { richHistory: newRichHistory, total: item.richHistoryTotal! - deletedItems },
exploreId,
})
);
});
};
};
const richHistory = getState().explore.richHistory;
const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: string) => void) => {
Object.entries(state.panes).forEach(([exploreId, item]) => {
item && callback(item, exploreId);
});
// update or remove entries
const newRichHistory = richHistory
.map((query) => (query.id === updatedQuery?.id ? updatedQuery : query))
.filter((query) => query.id !== deletedId);
const deletedItems = richHistory.length - newRichHistory.length;
dispatch(
richHistoryUpdatedAction({
richHistoryResults: {
richHistory: newRichHistory,
total: getState().explore.richHistoryTotal! - deletedItems,
},
})
);
};
};
export const addHistoryItem = (
@ -114,45 +121,41 @@ export const deleteHistoryItem = (id: string): ThunkResult<void> => {
};
export const deleteRichHistory = (): ThunkResult<void> => {
return async (dispatch, getState) => {
return async (dispatch) => {
await deleteAllFromRichHistory();
selectPanesEntries(getState()).forEach(([exploreId]) => {
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
});
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 } }));
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 } }));
};
};
export const loadRichHistory = (exploreId: string): ThunkResult<void> => {
export const loadRichHistory = (): ThunkResult<void> => {
return async (dispatch, getState) => {
const filters = getState().explore.panes[exploreId]!.richHistorySearchFilters;
const filters = getState().explore.richHistorySearchFilters;
if (filters) {
const richHistoryResults = await getRichHistory(filters);
dispatch(richHistoryUpdatedAction({ richHistoryResults, exploreId }));
dispatch(richHistoryUpdatedAction({ richHistoryResults }));
}
};
};
export const loadMoreRichHistory = (exploreId: string): ThunkResult<void> => {
export const loadMoreRichHistory = (): ThunkResult<void> => {
return async (dispatch, getState) => {
const currentFilters = getState().explore.panes[exploreId]?.richHistorySearchFilters;
const currentRichHistory = getState().explore.panes[exploreId]?.richHistory;
const currentFilters = getState().explore.richHistorySearchFilters;
const currentRichHistory = getState().explore.richHistory;
if (currentFilters && currentRichHistory) {
const nextFilters = { ...currentFilters, page: (currentFilters?.page || 1) + 1 };
const moreRichHistory = await getRichHistory(nextFilters);
const richHistory = [...currentRichHistory, ...moreRichHistory.richHistory];
dispatch(richHistorySearchFiltersUpdatedAction({ filters: nextFilters, exploreId }));
dispatch(
richHistoryUpdatedAction({ richHistoryResults: { richHistory, total: moreRichHistory.total }, exploreId })
);
dispatch(richHistorySearchFiltersUpdatedAction({ filters: nextFilters }));
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory, total: moreRichHistory.total } }));
}
};
};
export const clearRichHistoryResults = (exploreId: string): ThunkResult<void> => {
export const clearRichHistoryResults = (): ThunkResult<void> => {
return async (dispatch) => {
dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId }));
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined }));
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 } }));
};
};
@ -180,9 +183,9 @@ export const updateHistorySettings = (settings: RichHistorySettings): ThunkResul
/**
* Assumed this can be called only when settings and filters are initialised
*/
export const updateHistorySearchFilters = (exploreId: string, filters: RichHistorySearchFilters): ThunkResult<void> => {
export const updateHistorySearchFilters = (filters: RichHistorySearchFilters): ThunkResult<void> => {
return async (dispatch, getState) => {
await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } }));
await dispatch(richHistorySearchFiltersUpdatedAction({ filters: { ...filters } }));
const currentSettings = getState().explore.richHistorySettings!;
if (supportedFeatures().lastUsedDataSourcesAvailable) {
await dispatch(

View File

@ -26,7 +26,7 @@ export interface SyncTimesPayload {
}
export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults; exploreId: string }>(
export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults }>(
'explore/richHistoryUpdated'
);
export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
@ -34,7 +34,6 @@ export const richHistoryLimitExceededAction = createAction('explore/richHistoryL
export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
export const richHistorySearchFiltersUpdatedAction = createAction<{
exploreId: string;
filters?: RichHistorySearchFilters;
}>('explore/richHistorySearchFiltersUpdatedAction');
@ -125,6 +124,8 @@ export const changeCorrelationEditorDetails = createAction<CorrelationEditorDeta
'explore/changeCorrelationEditorDetails'
);
export const changeShowQueryHistory = createAction<boolean>('explore/changeShowQueryHistory');
export interface NavigateToExploreDependencies {
timeRange: TimeRange;
getExploreUrl: (args: GetExploreUrlArguments) => Promise<string | undefined>;
@ -168,6 +169,8 @@ export const initialExploreState: ExploreState = {
largerExploreId: undefined,
maxedExploreId: undefined,
evenSplitPanes: true,
showQueryHistory: false,
richHistory: [],
};
/**
@ -243,6 +246,23 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (richHistoryUpdatedAction.match(action)) {
const { richHistory, total } = action.payload.richHistoryResults;
return {
...state,
richHistory,
richHistoryTotal: total,
};
}
if (richHistorySearchFiltersUpdatedAction.match(action)) {
const richHistorySearchFilters = action.payload.filters;
return {
...state,
richHistorySearchFilters,
};
}
if (createNewSplitOpenPane.pending.match(action)) {
return {
...state,
@ -303,6 +323,13 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (changeShowQueryHistory.match(action)) {
return {
...state,
showQueryHistory: action.payload,
};
}
const exploreId: string | undefined = action.payload?.exploreId;
if (typeof exploreId === 'string') {
return {

View File

@ -494,10 +494,7 @@ async function handleHistory(
// Because filtering happens in the backend we cannot add a new entry without checking if it matches currently
// used filters. Instead, we refresh the query history list.
// TODO: run only if Query History list is opened (#47252)
for (const exploreId in state.panes) {
await dispatch(loadRichHistory(exploreId));
}
await dispatch(loadRichHistory());
}
interface RunQueriesOptions {

View File

@ -0,0 +1,123 @@
import { DataSourceApi, DataSourceJsonData } from '@grafana/data';
import { DataQuery } from '@grafana/schema/dist/esm/index';
import { configureStore } from 'app/store/configureStore';
import { StoreState, ThunkDispatch } from 'app/types';
import { createDefaultInitialState } from './helpers';
import { selectExploreDSMaps } from './selectors';
const { defaultInitialState } = createDefaultInitialState();
const datasources: DataSourceApi[] = [
{
name: 'testDs',
type: 'postgres',
uid: 'ds1',
getRef: () => {
return { type: 'postgres', uid: 'ds1' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
{
name: 'testDs2',
type: 'mysql',
uid: 'ds2',
getRef: () => {
return { type: 'mysql', uid: 'ds2' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
];
describe('selectExploreDSMaps', () => {
it('returns datasource information as empty with empty state', () => {
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore();
const dsMaps = selectExploreDSMaps(store.getState());
expect(dsMaps.dsToExplore).toEqual([]);
expect(dsMaps.exploreToDS).toEqual([]);
});
it('returns root datasources from 2 panes with empty queries', () => {
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: [],
},
right: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[1],
queries: [],
},
},
},
} as unknown as Partial<StoreState>);
const dsMaps = selectExploreDSMaps(store.getState());
expect(dsMaps.dsToExplore.length).toEqual(2);
// ds 1
expect(dsMaps.dsToExplore[0].datasource.uid).toEqual('ds1');
expect(dsMaps.dsToExplore[0].exploreIds.length).toEqual(1);
expect(dsMaps.dsToExplore[0].exploreIds[0]).toEqual('left');
// ds 2
expect(dsMaps.dsToExplore[1].datasource.uid).toEqual('ds2');
expect(dsMaps.dsToExplore[1].exploreIds.length).toEqual(1);
expect(dsMaps.dsToExplore[1].exploreIds[0]).toEqual('right');
expect(dsMaps.exploreToDS.length).toEqual(2);
// pane 1
expect(dsMaps.exploreToDS[0].exploreId).toEqual('left');
expect(dsMaps.exploreToDS[0].datasources.length).toEqual(1);
expect(dsMaps.exploreToDS[0].datasources[0].uid).toEqual('ds1');
//pane 2
expect(dsMaps.exploreToDS[1].exploreId).toEqual('right');
expect(dsMaps.exploreToDS[1].datasources.length).toEqual(1);
expect(dsMaps.exploreToDS[1].datasources[0].uid).toEqual('ds2');
});
it('returns all datasources from 2 panes with queries', () => {
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
panes: {
different: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: [{ datasource: datasources[1] }],
},
match: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[1],
queries: [{ datasource: datasources[1] }],
},
},
},
} as unknown as Partial<StoreState>);
const dsMaps = selectExploreDSMaps(store.getState());
expect(dsMaps.dsToExplore.length).toEqual(2);
// ds 1
expect(dsMaps.dsToExplore[0].datasource.uid).toEqual('ds1');
expect(dsMaps.dsToExplore[0].exploreIds.length).toEqual(1);
expect(dsMaps.dsToExplore[0].exploreIds[0]).toEqual('different');
// ds2
expect(dsMaps.dsToExplore[1].datasource.uid).toEqual('ds2');
expect(dsMaps.dsToExplore[1].exploreIds.length).toEqual(2);
expect(dsMaps.dsToExplore[1].exploreIds[0]).toEqual('different');
expect(dsMaps.dsToExplore[1].exploreIds[1]).toEqual('match');
expect(dsMaps.exploreToDS.length).toEqual(2);
// pane 1
expect(dsMaps.exploreToDS[0].exploreId).toEqual('different');
expect(dsMaps.exploreToDS[0].datasources.length).toEqual(2);
expect(dsMaps.exploreToDS[0].datasources[0].uid).toEqual('ds1');
expect(dsMaps.exploreToDS[0].datasources[1].uid).toEqual('ds2');
// pane 2
expect(dsMaps.exploreToDS[1].exploreId).toEqual('match');
expect(dsMaps.exploreToDS[1].datasources.length).toEqual(1);
expect(dsMaps.exploreToDS[1].datasources[0].uid).toEqual('ds2');
});
});

View File

@ -1,5 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { flatten, uniqBy } from 'lodash';
import { DataSourceRef } from '@grafana/schema';
import { ExploreItemState, StoreState } from 'app/types';
export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes;
@ -23,3 +25,43 @@ export const isLeftPaneSelector = (exploreId: string) =>
export const getExploreItemSelector = (exploreId: string) => createSelector(selectPanes, (panes) => panes[exploreId]);
export const selectCorrelationDetails = createSelector(selectExploreRoot, (state) => state.correlationEditorDetails);
export const selectShowQueryHistory = createSelector(selectExploreRoot, (state) => state.showQueryHistory);
export const selectExploreDSMaps = createSelector(selectPanesEntries, (panes) => {
const exploreDSMap = panes
.map(([exploreId, pane]) => {
const rootDatasource = [pane?.datasourceInstance?.getRef()];
const queryDatasources = pane?.queries.map((q) => q.datasource) || [];
const datasources = [...rootDatasource, ...queryDatasources].filter(
(datasource): datasource is DataSourceRef => !!datasource
);
if (datasources === undefined || datasources.length === 0) {
return undefined;
} else {
return {
exploreId,
datasources: uniqBy(datasources, (ds) => ds.uid),
};
}
})
.filter((pane): pane is { exploreId: string; datasources: DataSourceRef[] } => !!pane);
const uniqueDataSources = uniqBy(flatten(exploreDSMap.map((pane) => pane.datasources)), (ds) => ds.uid);
const dsToExploreMap = uniqueDataSources.map((ds) => {
let exploreIds: string[] = [];
exploreDSMap.forEach((eds) => {
if (eds.datasources.some((edsDs) => edsDs.uid === ds.uid)) {
exploreIds.push(eds.exploreId);
}
});
return {
datasource: ds,
exploreIds: exploreIds,
};
});
return { exploreToDS: exploreDSMap, dsToExplore: dsToExploreMap };
});

View File

@ -75,7 +75,6 @@ export const makeExplorePaneState = (overrides?: Partial<ExploreItemState>): Exp
rawPrometheusResult: null,
eventBridge: null as unknown as EventBusExtended,
cache: [],
richHistory: [],
supplementaryQueries: loadSupplementaryQueries(),
panelsState: {},
correlations: undefined,

View File

@ -63,6 +63,18 @@ export interface ExploreState {
panes: Record<string, ExploreItemState | undefined>;
/**
* Is the drawer for query history showing
*/
showQueryHistory: boolean;
/**
* History of all queries
*/
richHistory: RichHistoryQuery[];
richHistorySearchFilters?: RichHistorySearchFilters;
richHistoryTotal?: number;
/**
* Settings for rich history (note: filters are stored per each pane separately)
*/
@ -206,13 +218,6 @@ export interface ExploreItemState {
showFlameGraph?: boolean;
showCustom?: boolean;
/**
* History of all queries
*/
richHistory: RichHistoryQuery[];
richHistorySearchFilters?: RichHistorySearchFilters;
richHistoryTotal?: number;
/**
* We are using caching to store query responses of queries run from logs navigation.
* In logs navigation, we do pagination and we don't want our users to unnecessarily run the same queries that they've run just moments before.

View File

@ -446,10 +446,11 @@
"delete-query-tooltip": "Delete query",
"delete-starred-query-confirmation-text": "Are you sure you want to permanently delete your starred query?",
"edit-comment-tooltip": "Edit comment",
"loading-text": "loading...",
"left-pane": "Left pane",
"optional-description": "An optional description of what the query does.",
"query-comment-label": "Query comment",
"query-text-label": "Query text",
"right-pane": "Right pane",
"run-query-button": "Run query",
"save-comment": "Save comment",
"star-query-tooltip": "Star query",

View File

@ -446,10 +446,11 @@
"delete-query-tooltip": "Đęľęŧę qūęřy",
"delete-starred-query-confirmation-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő pęřmäʼnęʼnŧľy đęľęŧę yőūř şŧäřřęđ qūęřy?",
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
"loading-text": "ľőäđįʼnģ...",
"left-pane": "Ŀęƒŧ päʼnę",
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
"query-comment-label": "Qūęřy čőmmęʼnŧ",
"query-text-label": "Qūęřy ŧęχŧ",
"right-pane": "Ŗįģĥŧ päʼnę",
"run-query-button": "Ŗūʼn qūęřy",
"save-comment": "Ŝävę čőmmęʼnŧ",
"star-query-tooltip": "Ŝŧäř qūęřy",