diff --git a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx new file mode 100644 index 00000000000..a9a61a6a6e8 --- /dev/null +++ b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import { css } from 'emotion'; + +import { SelectableValue, GrafanaTheme } from '@grafana/data'; +import { stylesFactory, useTheme } from '../../themes'; +import { IconName, TabsBar, Tab, IconButton, CustomScrollbar, TabContent } from '../..'; + +export interface TabConfig { + label: string; + value: string; + content: React.ReactNode; + icon: IconName; +} + +export interface TabbedContainerProps { + tabs: TabConfig[]; + defaultTab?: string; + closeIconTooltip?: string; + onClose: () => void; +} + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + container: css` + height: 100%; + `, + tabContent: css` + padding: ${theme.spacing.md}; + background-color: ${theme.colors.bodyBg}; + `, + close: css` + position: absolute; + right: 16px; + top: 5px; + cursor: pointer; + font-size: ${theme.typography.size.lg}; + `, + tabs: css` + padding-top: ${theme.spacing.sm}; + border-color: ${theme.colors.formInputBorder}; + ul { + margin-left: ${theme.spacing.md}; + } + `, + scrollbar: css` + min-height: 100% !important; + background-color: ${theme.colors.panelBg}; + `, + }; +}); + +export function TabbedContainer(props: TabbedContainerProps) { + const [activeTab, setActiveTab] = useState( + props.tabs.some(tab => tab.value === props.defaultTab) ? props.defaultTab : props.tabs?.[0].value + ); + + const onSelectTab = (item: SelectableValue) => { + setActiveTab(item.value!); + }; + + const { tabs, onClose, closeIconTooltip } = props; + const theme = useTheme(); + const styles = getStyles(theme); + + return ( +
+ + {tabs.map(t => ( + onSelectTab(t)} + icon={t.icon} + /> + ))} + + + + {tabs.find(t => t.value === activeTab)?.content} + +
+ ); +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5dfdff65e0b..93eebc97817 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -7,6 +7,7 @@ export { PopoverController } from './Tooltip/PopoverController'; export { Popover } from './Tooltip/Popover'; export { Portal } from './Portal/Portal'; export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; +export { TabbedContainer, TabConfig } from './TabbedContainer/TabbedContainer'; export { ClipboardButton } from './ClipboardButton/ClipboardButton'; export { Cascader, CascaderOption } from './Cascader/Cascader'; diff --git a/public/app/features/dashboard/components/Inspector/InspectContent.tsx b/public/app/features/dashboard/components/Inspector/InspectContent.tsx index 7467d1114b8..78b15487a87 100644 --- a/public/app/features/dashboard/components/Inspector/InspectContent.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectContent.tsx @@ -91,7 +91,7 @@ export const InspectContent: React.FC = ({ )} {activeTab === InspectTab.Error && } - {data && activeTab === InspectTab.Stats && } + {data && activeTab === InspectTab.Stats && } {data && activeTab === InspectTab.Query && } diff --git a/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx b/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx index 3e64aedf469..d6508f0e736 100644 --- a/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectStatsTab.tsx @@ -1,14 +1,14 @@ -import { PanelData, QueryResultMetaStat } from '@grafana/data'; +import { PanelData, QueryResultMetaStat, TimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { InspectStatsTable } from './InspectStatsTable'; import React from 'react'; -import { DashboardModel } from 'app/features/dashboard/state'; interface InspectStatsTabProps { data: PanelData; - dashboard: DashboardModel; + timeZone: TimeZone; } -export const InspectStatsTab: React.FC = ({ data, dashboard }) => { + +export const InspectStatsTab: React.FC = ({ data, timeZone }) => { if (!data.request) { return null; } @@ -42,8 +42,8 @@ export const InspectStatsTab: React.FC = ({ data, dashboar return (
- - + +
); }; diff --git a/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx b/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx index 9778f30ca4e..7fff4cf297c 100644 --- a/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectStatsTable.tsx @@ -7,17 +7,17 @@ import { QueryResultMetaStat, TimeZone, } from '@grafana/data'; -import { DashboardModel } from 'app/features/dashboard/state'; import { config } from 'app/core/config'; import { stylesFactory, useTheme } from '@grafana/ui'; import { css } from 'emotion'; interface InspectStatsTableProps { - dashboard: DashboardModel; + timeZone: TimeZone; name: string; stats: QueryResultMetaStat[]; } -export const InspectStatsTable: React.FC = ({ dashboard, name, stats }) => { + +export const InspectStatsTable: React.FC = ({ timeZone, name, stats }) => { const theme = useTheme(); const styles = getStyles(theme); @@ -34,7 +34,7 @@ export const InspectStatsTable: React.FC = ({ dashboard, return ( {stat.displayName} - {formatStat(stat, dashboard.getTimezone())} + {formatStat(stat, timeZone)} ); })} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 82ef4be9efd..a10126955fd 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -28,6 +28,7 @@ import LogsContainer from './LogsContainer'; import QueryRows from './QueryRows'; import TableContainer from './TableContainer'; import RichHistoryContainer from './RichHistory/RichHistoryContainer'; +import ExploreQueryInspector from './ExploreQueryInspector'; import { addQueryRow, changeSize, @@ -128,8 +129,13 @@ export interface ExploreProps { showTrace: boolean; } +enum ExploreDrawer { + RichHistory, + QueryInspector, +} + interface ExploreState { - showRichHistory: boolean; + openDrawer?: ExploreDrawer; } /** @@ -164,7 +170,7 @@ export class Explore extends React.PureComponent { super(props); this.exploreEvents = new Emitter(); this.state = { - showRichHistory: false, + openDrawer: undefined, }; } @@ -276,7 +282,15 @@ export class Explore extends React.PureComponent { toggleShowRichHistory = () => { this.setState(state => { return { - showRichHistory: !state.showRichHistory, + openDrawer: state.openDrawer === ExploreDrawer.RichHistory ? undefined : ExploreDrawer.RichHistory, + }; + }); + }; + + toggleShowQueryInspector = () => { + this.setState(state => { + return { + openDrawer: state.openDrawer === ExploreDrawer.QueryInspector ? undefined : ExploreDrawer.QueryInspector, }; }); }; @@ -319,7 +333,7 @@ export class Explore extends React.PureComponent { showLogs, showTrace, } = this.props; - const { showRichHistory } = this.state; + const { openDrawer } = this.state; const exploreClass = split ? 'explore explore-split' : 'explore'; const styles = getStyles(theme); const StartPage = datasourceInstance?.components?.ExploreStartPage; @@ -329,6 +343,9 @@ export class Explore extends React.PureComponent { const queryErrors = queryResponse.error ? [queryResponse.error] : undefined; const queryError = getFirstNonQueryRowSpecificError(queryErrors); + const showRichHistory = openDrawer === ExploreDrawer.RichHistory; + const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector; + return (
@@ -343,8 +360,10 @@ export class Explore extends React.PureComponent { //TODO:unification addQueryRowButtonHidden={false} richHistoryButtonActive={showRichHistory} + queryInspectorButtonActive={showQueryInspector} onClickAddQueryRowButton={this.onClickAddQueryRowButton} onClickRichHistoryButton={this.toggleShowRichHistory} + onClickQueryInspectorButton={this.toggleShowQueryInspector} />
@@ -421,6 +440,13 @@ export class Explore extends React.PureComponent { onClose={this.toggleShowRichHistory} /> )} + {showQueryInspector && ( + + )} ); diff --git a/public/app/features/explore/ExploreDrawer.test.tsx b/public/app/features/explore/ExploreDrawer.test.tsx new file mode 100644 index 00000000000..2456073c217 --- /dev/null +++ b/public/app/features/explore/ExploreDrawer.test.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { ExploreDrawer } from './ExploreDrawer'; + +describe('', () => { + it('renders child element', () => { + const childElement =
Child element
; + const wrapper = mount({childElement}); + expect(wrapper.text()).toBe('Child element'); + }); +}); diff --git a/public/app/features/explore/ExploreDrawer.tsx b/public/app/features/explore/ExploreDrawer.tsx new file mode 100644 index 00000000000..ff7ace22d6d --- /dev/null +++ b/public/app/features/explore/ExploreDrawer.tsx @@ -0,0 +1,93 @@ +// Libraries +import React from 'react'; +import { Resizable, ResizeCallback } from 're-resizable'; +import { css, cx, keyframes } from 'emotion'; + +// Services & Utils +import { stylesFactory, useTheme } from '@grafana/ui'; + +// Types +import { GrafanaTheme } from '@grafana/data'; + +const drawerSlide = keyframes` + 0% { + transform: translateY(400px); + } + + 100% { + transform: translateY(0px); + } +`; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const shadowColor = theme.isLight ? theme.palette.gray4 : theme.palette.black; + + return { + container: css` + position: fixed !important; + bottom: 0; + background: ${theme.colors.pageHeaderBg}; + border-top: 1px solid ${theme.colors.formInputBorder}; + margin: 0px; + margin-right: -${theme.spacing.md}; + margin-left: -${theme.spacing.md}; + box-shadow: 0 0 4px ${shadowColor}; + z-index: ${theme.zIndex.sidemenu}; + `, + drawerActive: css` + opacity: 1; + animation: 0.5s ease-out ${drawerSlide}; + `, + rzHandle: css` + background: ${theme.colors.formInputBorder}; + transition: 0.3s background ease-in-out; + position: relative; + width: 200px !important; + height: 7px !important; + left: calc(50% - 100px) !important; + top: -4px !important; + cursor: grab; + border-radius: 4px; + &:hover { + background: ${theme.colors.formInputBorderHover}; + } + `, + }; +}); + +export interface Props { + width: number; + children: React.ReactNode; + onResize?: ResizeCallback; +} + +export function ExploreDrawer(props: Props) { + const { width, children, onResize } = props; + const theme = useTheme(); + const styles = getStyles(theme); + const drawerWidth = `${width + 31.5}px`; + + return ( + + {children} + + ); +} diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx new file mode 100644 index 00000000000..912a0c954b3 --- /dev/null +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -0,0 +1,183 @@ +import React, { useState } from 'react'; +import { Button, JSONFormatter, LoadingPlaceholder, TabbedContainer, TabConfig } from '@grafana/ui'; +import { AppEvents, PanelData, TimeZone } from '@grafana/data'; + +import appEvents from 'app/core/app_events'; +import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; +import { StoreState, ExploreItemState, ExploreId } from 'app/types'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { ExploreDrawer } from 'app/features/explore/ExploreDrawer'; +import { useEffectOnce } from 'react-use'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { InspectStatsTab } from '../dashboard/components/Inspector/InspectStatsTab'; +import { getPanelInspectorStyles } from '../dashboard/components/Inspector/styles'; + +function stripPropsFromResponse(response: any) { + // ignore silent requests + if (response.config?.silent) { + return {}; + } + + const clonedResponse = { ...response }; // clone - dont modify the response + + if (clonedResponse.headers) { + delete clonedResponse.headers; + } + + if (clonedResponse.config) { + clonedResponse.request = clonedResponse.config; + + delete clonedResponse.config; + delete clonedResponse.request.transformRequest; + delete clonedResponse.request.transformResponse; + delete clonedResponse.request.paramSerializer; + delete clonedResponse.request.jsonpCallbackParam; + delete clonedResponse.request.headers; + delete clonedResponse.request.requestId; + delete clonedResponse.request.inspect; + delete clonedResponse.request.retry; + delete clonedResponse.request.timeout; + } + + if (clonedResponse.data) { + clonedResponse.response = clonedResponse.data; + + delete clonedResponse.config; + delete clonedResponse.data; + delete clonedResponse.status; + delete clonedResponse.statusText; + delete clonedResponse.ok; + delete clonedResponse.url; + delete clonedResponse.redirected; + delete clonedResponse.type; + delete clonedResponse.$$config; + } + + return clonedResponse; +} + +interface Props { + loading: boolean; + width: number; + exploreId: ExploreId; + queryResponse?: PanelData; + onClose: () => void; +} + +function ExploreQueryInspector(props: Props) { + const [formattedJSON, setFormattedJSON] = useState({}); + + const getTextForClipboard = () => { + return JSON.stringify(formattedJSON, null, 2); + }; + + const onClipboardSuccess = () => { + appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']); + }; + + const [allNodesExpanded, setAllNodesExpanded] = useState(false); + const getOpenNodeCount = () => { + if (allNodesExpanded === null) { + return 3; // 3 is default, ie when state is null + } else if (allNodesExpanded) { + return 20; + } + return 1; + }; + + const onToggleExpand = () => { + setAllNodesExpanded(!allNodesExpanded); + }; + + const { loading, width, onClose, queryResponse } = props; + + const [response, setResponse] = useState({} as PanelData); + useEffectOnce(() => { + const inspectorStreamSub = getBackendSrv() + .getInspectorStream() + .subscribe(resp => { + const strippedResponse = stripPropsFromResponse(resp); + setResponse(strippedResponse); + }); + + return () => { + inspectorStreamSub?.unsubscribe(); + }; + }); + + const haveData = response && Object.keys(response).length > 0; + const styles = getPanelInspectorStyles(); + + const statsTab: TabConfig = { + label: 'Stats', + value: 'stats', + icon: 'chart-line', + content: , + }; + + const inspectorTab: TabConfig = { + label: 'Query Inspector', + value: 'query_inspector', + icon: 'info-circle', + content: ( + <> +
+ {haveData && ( + <> + + + + + + + )} +
+
+
+ {loading && } + {!loading && haveData && ( + + )} + {!loading && !haveData && ( +

No request & response collected yet. Run query to collect request & response.

+ )} +
+ + ), + }; + + const tabs = [statsTab, inspectorTab]; + return ( + {}}> + + + ); +} + +function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { loading, queryResponse } = item; + + return { + loading, + queryResponse, + }; +} + +export default hot(module)(connect(mapStateToProps)(ExploreQueryInspector)); diff --git a/public/app/features/explore/RichHistory/RichHistory.test.tsx b/public/app/features/explore/RichHistory/RichHistory.test.tsx index 01c1a5d989c..16beb363278 100644 --- a/public/app/features/explore/RichHistory/RichHistory.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.test.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme } from '@grafana/data'; import { ExploreId } from '../../../types/explore'; import { RichHistory, RichHistoryProps } from './RichHistory'; import { Tabs } from './RichHistory'; -import { Tab, Slider } from '@grafana/ui'; +import { Tab } from '@grafana/ui'; jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() })); @@ -31,6 +31,7 @@ describe('RichHistory', () => { const wrapper = setup(); expect(wrapper.find(Tab)).toHaveLength(3); }); + it('should render correct lebels of tabs in tab bar', () => { const wrapper = setup(); expect( @@ -52,12 +53,14 @@ describe('RichHistory', () => { .text() ).toEqual('Settings'); }); + it('should correctly render query history tab as active tab', () => { const wrapper = setup(); - expect(wrapper.find(Slider)).toHaveLength(1); + expect(wrapper.find('RichHistoryQueriesTab')).toHaveLength(1); }); + it('should correctly render starred tab as active tab', () => { const wrapper = setup({ firstTab: Tabs.Starred }); - expect(wrapper.find(Slider)).toHaveLength(0); + expect(wrapper.find('RichHistoryStarredTab')).toHaveLength(1); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 3025c115f09..d7eb02f3494 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -1,16 +1,15 @@ import React, { PureComponent } from 'react'; -import { css } from 'emotion'; //Services & Utils import { SortOrder } from 'app/core/utils/explore'; import { RICH_HISTORY_SETTING_KEYS } from 'app/core/utils/richHistory'; import store from 'app/core/store'; -import { stylesFactory, withTheme } from '@grafana/ui'; +import { withTheme, TabbedContainer, TabConfig } from '@grafana/ui'; //Types import { RichHistoryQuery, ExploreId } from 'app/types/explore'; -import { SelectableValue, GrafanaTheme } from '@grafana/data'; -import { TabsBar, Tab, TabContent, Themeable, CustomScrollbar, IconName, IconButton } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; +import { Themeable } from '@grafana/ui'; //Components import { RichHistorySettings } from './RichHistorySettings'; @@ -41,7 +40,6 @@ export interface RichHistoryProps extends Themeable { } interface RichHistoryState { - activeTab: Tabs; sortOrder: SortOrder; retentionPeriod: number; starredTabAsFirstTab: boolean; @@ -49,41 +47,10 @@ interface RichHistoryState { datasourceFilters: SelectableValue[] | null; } -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - container: css` - height: 100%; - `, - tabContent: css` - padding: ${theme.spacing.md}; - background-color: ${theme.colors.bodyBg}; - `, - close: css` - position: absolute; - right: 16px; - top: 5px; - cursor: pointer; - font-size: ${theme.typography.size.lg}; - `, - tabs: css` - padding-top: ${theme.spacing.sm}; - border-color: ${theme.colors.formInputBorder}; - ul { - margin-left: ${theme.spacing.md}; - } - `, - scrollbar: css` - min-height: 100% !important; - background-color: ${theme.colors.panelBg}; - `, - }; -}); - class UnThemedRichHistory extends PureComponent { constructor(props: RichHistoryProps) { super(props); this.state = { - activeTab: this.props.firstTab, sortOrder: SortOrder.Descending, datasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, null), retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7), @@ -107,7 +74,7 @@ class UnThemedRichHistory extends PureComponent { + toggleActiveDatasourceOnly = () => { const activeDatasourceOnly = !this.state.activeDatasourceOnly; this.setState({ activeDatasourceOnly, @@ -127,10 +94,6 @@ class UnThemedRichHistory extends PureComponent) => { - this.setState({ activeTab: item.value! }); - }; - onChangeSortOrder = (sortOrder: SortOrder) => this.setState({ sortOrder }); /* If user selects activeDatasourceOnly === true, set datasource filter to currently active datasource. @@ -148,6 +111,7 @@ class UnThemedRichHistory extends PureComponent ), @@ -217,23 +180,7 @@ class UnThemedRichHistory extends PureComponent - - {tabs.map(t => ( - this.onSelectTab(t)} - icon={t.icon as IconName} - /> - ))} - - - - {tabs.find(t => t.value === activeTab)?.content} - -
+ ); } } diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index fc8d114c0df..c4b2f241012 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -1,18 +1,14 @@ // Libraries import React, { useState } from 'react'; -import { Resizable } from 're-resizable'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { css, cx, keyframes } from 'emotion'; // Services & Utils import store from 'app/core/store'; -import { stylesFactory, useTheme } from '@grafana/ui'; import { RICH_HISTORY_SETTING_KEYS } from 'app/core/utils/richHistory'; // Types import { StoreState } from 'app/types'; -import { GrafanaTheme } from '@grafana/data'; import { ExploreId, RichHistoryQuery } from 'app/types/explore'; // Components, enums @@ -20,52 +16,7 @@ import { RichHistory, Tabs } from './RichHistory'; //Actions import { deleteRichHistory } from '../state/actions'; - -const drawerSlide = keyframes` - 0% { - transform: translateY(400px); - } - - 100% { - transform: translateY(0px); - } -`; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - const shadowColor = theme.isLight ? theme.palette.gray4 : theme.palette.black; - - return { - container: css` - position: fixed !important; - bottom: 0; - background: ${theme.colors.pageHeaderBg}; - border-top: 1px solid ${theme.colors.formInputBorder}; - margin: 0px; - margin-right: -${theme.spacing.md}; - margin-left: -${theme.spacing.md}; - box-shadow: 0 0 4px ${shadowColor}; - z-index: ${theme.zIndex.sidemenu}; - `, - drawerActive: css` - opacity: 1; - animation: 0.5s ease-out ${drawerSlide}; - `, - rzHandle: css` - background: ${theme.colors.formInputBorder}; - transition: 0.3s background ease-in-out; - position: relative; - width: 200px !important; - height: 7px !important; - left: calc(50% - 100px) !important; - top: -4px !important; - cursor: grab; - border-radius: 4px; - &:hover { - background: ${theme.colors.formInputBorderHover}; - } - `, - }; -}); +import { ExploreDrawer } from '../ExploreDrawer'; export interface Props { width: number; @@ -81,29 +32,11 @@ export function RichHistoryContainer(props: Props) { const [height, setHeight] = useState(400); const { richHistory, width, firstTab, activeDatasourceInstance, exploreId, deleteRichHistory, onClose } = props; - const theme = useTheme(); - const styles = getStyles(theme); - const drawerWidth = `${width + 31.5}px`; return ( - { + { setHeight(Number(ref.style.height.slice(0, -2))); }} > @@ -116,7 +49,7 @@ export function RichHistoryContainer(props: Props) { onClose={onClose} height={height} /> - +
); } diff --git a/public/app/features/explore/SecondaryActions.test.tsx b/public/app/features/explore/SecondaryActions.test.tsx index 1c7b45fcaa9..6b55113f6d8 100644 --- a/public/app/features/explore/SecondaryActions.test.tsx +++ b/public/app/features/explore/SecondaryActions.test.tsx @@ -5,10 +5,17 @@ import { SecondaryActions } from './SecondaryActions'; const addQueryRowButtonSelector = '[aria-label="Add row button"]'; const richHistoryButtonSelector = '[aria-label="Rich history button"]'; +const queryInspectorButtonSelector = '[aria-label="Query inspector button"]'; describe('SecondaryActions', () => { it('should render component two buttons', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1); expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1); }); @@ -19,6 +26,7 @@ describe('SecondaryActions', () => { addQueryRowButtonHidden={true} onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} + onClickQueryInspectorButton={noop} /> ); expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0); @@ -31,6 +39,7 @@ describe('SecondaryActions', () => { addQueryRowButtonDisabled={true} onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} + onClickQueryInspectorButton={noop} /> ); expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true); @@ -39,13 +48,22 @@ describe('SecondaryActions', () => { it('should map click handlers correctly', () => { const onClickAddRow = jest.fn(); const onClickHistory = jest.fn(); + const onClickQueryInspector = jest.fn(); const wrapper = shallow( - + ); + wrapper.find(addQueryRowButtonSelector).simulate('click'); expect(onClickAddRow).toBeCalled(); wrapper.find(richHistoryButtonSelector).simulate('click'); expect(onClickHistory).toBeCalled(); + + wrapper.find(queryInspectorButtonSelector).simulate('click'); + expect(onClickQueryInspector).toBeCalled(); }); }); diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index 74e72ec11c4..273c516ea79 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -4,10 +4,13 @@ import { stylesFactory, Icon } from '@grafana/ui'; type Props = { addQueryRowButtonDisabled?: boolean; - richHistoryButtonActive?: boolean; addQueryRowButtonHidden?: boolean; + richHistoryButtonActive?: boolean; + queryInspectorButtonActive?: boolean; + onClickAddQueryRowButton: () => void; onClickRichHistoryButton: () => void; + onClickQueryInspectorButton: () => void; }; const getStyles = stylesFactory(() => { @@ -42,6 +45,16 @@ export function SecondaryActions(props: Props) { {'\xA0' + 'Query history'} + ); } diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap index 63c285063ef..90e075ab8a2 100644 --- a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -31,7 +31,9 @@ exports[`Explore should render component 1`] = ` addQueryRowButtonDisabled={false} addQueryRowButtonHidden={false} onClickAddQueryRowButton={[Function]} + onClickQueryInspectorButton={[Function]} onClickRichHistoryButton={[Function]} + queryInspectorButtonActive={false} richHistoryButtonActive={false} />