diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index abda6b077c4..3ae6d8dc99c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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}`, diff --git a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx index 7a527bad325..59e635a3c28 100644 --- a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx +++ b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx @@ -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 ( -
+
{tabs.map((t) => ( { const settings: RichHistorySettings = { retentionPeriod: 2, starredTabAsFirstTab: true, - activeDatasourceOnly: true, + activeDatasourcesOnly: true, lastUsedDatasourceFilters: ['foobar'], }; await storage.updateSettings(settings); diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index ab84d420d49..4e5a1039d85 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -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( diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts index 3fe54d9c519..b3c2ebc0850 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.test.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -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); await storage.updateSettings({ - activeDatasourceOnly: false, + activeDatasourcesOnly: false, lastUsedDatasourceFilters: undefined, retentionPeriod: 14, starredTabAsFirstTab: true, diff --git a/public/app/core/history/RichHistoryRemoteStorage.ts b/public/app/core/history/RichHistoryRemoteStorage.ts index 0327444b5a7..2a7d4883916 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.ts @@ -85,7 +85,7 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage { async getSettings(): Promise { const preferences = await this.preferenceService.load(); return { - activeDatasourceOnly: false, + activeDatasourcesOnly: false, lastUsedDatasourceFilters: undefined, retentionPeriod: 14, starredTabAsFirstTab: preferences.queryHistory?.homeTab === 'starred', diff --git a/public/app/core/history/richHistoryLocalStorageUtils.ts b/public/app/core/history/richHistoryLocalStorageUtils.ts index 7f58de9a860..409bc4c64d6 100644 --- a/public/app/core/history/richHistoryLocalStorageUtils.ts +++ b/public/app/core/history/richHistoryLocalStorageUtils.ts @@ -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', }; diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index c49029c035b..4a02afbe594 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -14,7 +14,7 @@ export enum SortOrder { export interface RichHistorySettings { retentionPeriod: number; starredTabAsFirstTab: boolean; - activeDatasourceOnly: boolean; + activeDatasourcesOnly: boolean; lastUsedDatasourceFilters?: string[]; } diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 420fd5fd710..1b9af90f81a 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -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', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index da1c8540c82..6726e92a4e3 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { 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 { 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 { 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 { //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)} /> @@ -664,22 +645,6 @@ export class Explore extends React.PureComponent { {showNoData && {this.renderNoData()}} )} - {showRichHistory && ( - - )} - {showQueryInspector && ( - - )} ); @@ -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); diff --git a/public/app/features/explore/ExploreDrawer.tsx b/public/app/features/explore/ExploreDrawer.tsx index 3e6fac27359..03c595e88a4 100644 --- a/public/app/features/explore/ExploreDrawer.tsx +++ b/public/app/features/explore/ExploreDrawer.tsx @@ -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 ( {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, }), diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 9f797a06514..11999d6554d 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -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 ); })} + {showQueryHistory && ( + + { + dispatch(changeShowQueryHistory(false)); + }} + /> + + )}
); } @@ -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}`, diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index 7eec0b39ed4..a8b8feace21 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -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 (
- + + {showQueryInspector && ( + setShowQueryInspector(false)} + timeZone={getTimeZone()} + /> + )}
); diff --git a/public/app/features/explore/ExploreQueryInspector.test.tsx b/public/app/features/explore/ExploreQueryInspector.test.tsx index c52d0533a88..64fb4622982 100644 --- a/public/app/features/explore/ExploreQueryInspector.test.tsx +++ b/public/app/features/explore/ExploreQueryInspector.test.tsx @@ -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, diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 36b371bdc82..f04cc153c89 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -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; export function ExploreQueryInspector(props: Props) { - const { width, onClose, queryResponse, timeZone, isMixed, exploreId } = props; + const { onClose, queryResponse, timeZone, isMixed, exploreId } = props; const [dataOptions, setDataOptions] = useState({ withTransforms: false, withFieldConfig: true, @@ -105,7 +103,7 @@ export function ExploreQueryInspector(props: Props) { tabs.push(errorTab); } return ( - + ); @@ -118,6 +116,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } return { queryResponse, + isMixed: item.datasourceInstance?.meta.mixed || false, }; } diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 1bcb5111fd6..07e6d5881d2 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -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: [], diff --git a/public/app/features/explore/RichHistory/RichHistory.test.tsx b/public/app/features/explore/RichHistory/RichHistory.test.tsx index d4199815da1..35987fa1773 100644 --- a/public/app/features/explore/RichHistory/RichHistory.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.test.tsx @@ -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) => { const props: RichHistoryProps = { - exploreId: 'left', height: 100, - activeDatasourceInstance: 'Test datasource', richHistory: [], richHistoryTotal: 0, firstTab: Tabs.RichHistory, @@ -42,7 +46,7 @@ const setup = (propOverrides?: Partial) => { richHistorySettings: { retentionPeriod: 0, starredTabAsFirstTab: false, - activeDatasourceOnly: true, + activeDatasourcesOnly: true, lastUsedDatasourceFilters: [], }, updateHistorySearchFilters: jest.fn(), @@ -51,7 +55,11 @@ const setup = (propOverrides?: Partial) => { Object.assign(props, propOverrides); - render(); + render( + + + + ); }; describe('RichHistory', () => { diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index e6dfd82ec7b..fc0158b6220 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -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) { ), @@ -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} /> ); } diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index 38650bb3f2c..a8cb484823e 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -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 implements DataSourceApi { 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 = { alertmanager: new MockDatasourceApi('Alertmanager', 3, 'alertmanager', 'alertmanager'), loki: new MockDatasourceApi('Loki', 2, 'loki', 'loki'), prometheus: new MockDatasourceApi('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>) => { 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(); + render( + + + + ); }; const starredQueryWithComment: RichHistoryQuery = { @@ -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 }); diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index 5823340f9d0..9d1dd68aad1 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -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 { + datasourceInstances?: DataSourceApi[]; queryHistoryItem: RichHistoryQuery; } @@ -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(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 && ( ); + // 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 ( + + ); + } else { + const 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 ( + { + onRunQuery(pane[0]); + }} + label={`${paneLabel}: ${buttonText.translation}`} + disabled={disabled} + /> + ); + })} + + ); + + return ( + setOpenRunQueryButton(state)} placement="bottom-start" overlay={menu}> + + {t('explore.rich-history-card.run-query-button', 'Run query')} + + + ); + } + }; + return (
- + {queryActionButtons}
- {historyCardData?.queries.map((q, i) => { - return ; + {queryHistoryItem?.queries.map((q, i) => { + const queryDs = datasourceInstances?.find((ds) => ds.uid === q.datasource?.uid); + return ( + + ); })} {!activeUpdateComment && queryHistoryItem.comment && (
- {!activeUpdateComment && ( -
- -
- )} + {!activeUpdateComment &&
{runButton()}
}
- {loading && ( - - )}
); } diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx index 64fed881262..f2a492b0fc7 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx @@ -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) => { 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) => { richHistorySettings: { retentionPeriod: 0, starredTabAsFirstTab: false, - activeDatasourceOnly: true, + activeDatasourcesOnly: true, lastUsedDatasourceFilters: [], }, richHistoryTotal: 0, @@ -52,7 +55,7 @@ const setup = (propOverrides?: Partial) => { Object.assign(props, propOverrides); - return render(); + return render(, { 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); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index 60a435586aa..acaf618b636 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -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 & 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 ( - { - setHeight(Number(ref.style.height.slice(0, -2))); - }} - > - - + ); } diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.test.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.test.tsx index 9bd74449c9f..06b51138a08 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.test.tsx @@ -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) => { 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) => { }, richHistorySettings: { retentionPeriod: 30, - activeDatasourceOnly: false, - lastUsedDatasourceFilters: [], + activeDatasourcesOnly: false, + lastUsedDatasourceFilters: ['test-ds'], starredTabAsFirstTab: false, }, - exploreId: 'left', height: 100, }; Object.assign(props, propOverrides); - return render(); + return render(, { 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); + }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 7a7b5cc9d0e..469371caa4a 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -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) => 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 ( - Loading...; + Loading... ); } @@ -199,7 +226,7 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
- {!richHistorySettings.activeDatasourceOnly && ( + {!richHistorySettings.activeDatasourcesOnly && ( { @@ -237,13 +264,13 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
- {loading && ( + {(loading || loadingDs) && ( Loading results... )} - {!loading && + {!(loading || loadingDs) && Object.keys(mappedQueriesToHeadings).map((heading) => { return (
@@ -266,7 +293,7 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
{mappedQueriesToHeadings[heading].map((q) => { - return ; + return ; })}
); diff --git a/public/app/features/explore/RichHistory/RichHistorySettingsTab.test.tsx b/public/app/features/explore/RichHistory/RichHistorySettingsTab.test.tsx index 3d69a6beb8d..3b31f66799e 100644 --- a/public/app/features/explore/RichHistory/RichHistorySettingsTab.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistorySettingsTab.test.tsx @@ -7,10 +7,10 @@ const setup = (propOverrides?: Partial) => { 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); diff --git a/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx index 5da70895364..d8e4a2eac7a 100644 --- a/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx @@ -16,10 +16,10 @@ import { ShowConfirmModalEvent } from '../../../types/events'; export interface RichHistorySettingsProps { retentionPeriod: number; starredTabAsFirstTab: boolean; - activeDatasourceOnly: boolean; + activeDatasourcesOnly: boolean; onChangeRetentionPeriod: (option: SelectableValue) => 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) { > )} diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx index 56b6ab5d148..e7e64dd0f1f 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx @@ -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) => { 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) => { Object.assign(props, propOverrides); - const container = render(); + const container = render(, { 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: '|=' })); diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index c8b824da52d..f534d084709 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -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) => 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 ( @@ -119,7 +145,7 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) {
- {!richHistorySettings.activeDatasourceOnly && ( + {!richHistorySettings.activeDatasourcesOnly && ( { @@ -159,14 +185,14 @@ export function RichHistoryStarredTab(props: RichHistoryStarredTabProps) { />
- {loading && ( + {loading && loadingDs && ( Loading results... )} - {!loading && + {!(loading && loadingDs) && queries.map((q) => { - return ; + return ; })} {queries.length && queries.length !== totalQueries ? (
diff --git a/public/app/features/explore/spec/helper/assert.ts b/public/app/features/explore/spec/helper/assert.ts index 123db21a233..6e8f97d15e3 100644 --- a/public/app/features/explore/spec/helper/assert.ts +++ b/public/app/features/explore/spec/helper/assert.ts @@ -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(); }; diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index 3afd11fef18..5304115ead0 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -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]); }; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index db5dfa966f5..6630602fa53 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -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 }); diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 7aed7e67ba3..66eefeea309 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -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 () => { diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index e6feeda2995..b83eb938b3b 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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 }; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index fa8348fd8dc..d582ef42878 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -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('explore/historyUpdated'); // // Action creators @@ -37,27 +48,23 @@ type SyncHistoryUpdatesOptions = { */ const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesOptions): ThunkResult => { 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 => { }; export const deleteRichHistory = (): ThunkResult => { - 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 => { +export const loadRichHistory = (): ThunkResult => { 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 => { +export const loadMoreRichHistory = (): ThunkResult => { 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 => { +export const clearRichHistoryResults = (): ThunkResult => { 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 => { +export const updateHistorySearchFilters = (filters: RichHistorySearchFilters): ThunkResult => { 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( diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 7fb7f02622f..07c234fa634 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -26,7 +26,7 @@ export interface SyncTimesPayload { } export const syncTimesAction = createAction('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('explore/richHistorySettingsUpdated'); export const richHistorySearchFiltersUpdatedAction = createAction<{ - exploreId: string; filters?: RichHistorySearchFilters; }>('explore/richHistorySearchFiltersUpdatedAction'); @@ -125,6 +124,8 @@ export const changeCorrelationEditorDetails = createAction('explore/changeShowQueryHistory'); + export interface NavigateToExploreDependencies { timeRange: TimeRange; getExploreUrl: (args: GetExploreUrlArguments) => Promise; @@ -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 { diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 320f1b47108..d6e2d017512 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -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 { diff --git a/public/app/features/explore/state/selectors.test.ts b/public/app/features/explore/state/selectors.test.ts new file mode 100644 index 00000000000..8fda241937b --- /dev/null +++ b/public/app/features/explore/state/selectors.test.ts @@ -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, + { + name: 'testDs2', + type: 'mysql', + uid: 'ds2', + getRef: () => { + return { type: 'mysql', uid: 'ds2' }; + }, + } as DataSourceApi, +]; + +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); + + 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); + + 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'); + }); +}); diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index a836e174270..a475e8b5b04 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -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) => 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 }; +}); diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 0918049c03e..9ee49059123 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -75,7 +75,6 @@ export const makeExplorePaneState = (overrides?: Partial): Exp rawPrometheusResult: null, eventBridge: null as unknown as EventBusExtended, cache: [], - richHistory: [], supplementaryQueries: loadSupplementaryQueries(), panelsState: {}, correlations: undefined, diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index e0b5afaa0df..3f91e26c6d4 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -63,6 +63,18 @@ export interface ExploreState { panes: Record; + /** + * 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. diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index b037d9e8b35..dc683b42f36 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index fe9435e5c32..c6df2a9ed4b 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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",