Commit 02b12d3a by kay delaney Committed by GitHub

Explore: Adds query inspector drawer to explore (#26698)

* Explore: Adds query inspector drawer to explore
parent 145d2219
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<string>) => {
setActiveTab(item.value!);
};
const { tabs, onClose, closeIconTooltip } = props;
const theme = useTheme();
const styles = getStyles(theme);
return (
<div className={styles.container}>
<TabsBar className={styles.tabs}>
{tabs.map(t => (
<Tab
key={t.value}
label={t.label}
active={t.value === activeTab}
onChangeTab={() => onSelectTab(t)}
icon={t.icon}
/>
))}
<IconButton className={styles.close} onClick={onClose} name="times" title={closeIconTooltip ?? 'Close'} />
</TabsBar>
<CustomScrollbar className={styles.scrollbar}>
<TabContent className={styles.tabContent}>{tabs.find(t => t.value === activeTab)?.content}</TabContent>
</CustomScrollbar>
</div>
);
}
......@@ -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';
......
......@@ -91,7 +91,7 @@ export const InspectContent: React.FC<Props> = ({
<InspectJSONTab panel={panel} dashboard={dashboard} data={data} onClose={onClose} />
)}
{activeTab === InspectTab.Error && <InspectErrorTab error={error} />}
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} dashboard={dashboard} />}
{data && activeTab === InspectTab.Stats && <InspectStatsTab data={data} timeZone={dashboard.getTimezone()} />}
{data && activeTab === InspectTab.Query && <QueryInspector panel={panel} data={data.series} />}
</TabContent>
</CustomScrollbar>
......
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<InspectStatsTabProps> = ({ data, dashboard }) => {
export const InspectStatsTab: React.FC<InspectStatsTabProps> = ({ data, timeZone }) => {
if (!data.request) {
return null;
}
......@@ -42,8 +42,8 @@ export const InspectStatsTab: React.FC<InspectStatsTabProps> = ({ data, dashboar
return (
<div aria-label={selectors.components.PanelInspector.Stats.content}>
<InspectStatsTable dashboard={dashboard} name={'Stats'} stats={stats} />
<InspectStatsTable dashboard={dashboard} name={'Data source stats'} stats={dataStats} />
<InspectStatsTable timeZone={timeZone} name={'Stats'} stats={stats} />
<InspectStatsTable timeZone={timeZone} name={'Data source stats'} stats={dataStats} />
</div>
);
};
......@@ -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<InspectStatsTableProps> = ({ dashboard, name, stats }) => {
export const InspectStatsTable: React.FC<InspectStatsTableProps> = ({ timeZone, name, stats }) => {
const theme = useTheme();
const styles = getStyles(theme);
......@@ -34,7 +34,7 @@ export const InspectStatsTable: React.FC<InspectStatsTableProps> = ({ dashboard,
return (
<tr key={`${stat.displayName}-${index}`}>
<td>{stat.displayName}</td>
<td className={styles.cell}>{formatStat(stat, dashboard.getTimezone())}</td>
<td className={styles.cell}>{formatStat(stat, timeZone)}</td>
</tr>
);
})}
......
......@@ -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<ExploreProps, ExploreState> {
super(props);
this.exploreEvents = new Emitter();
this.state = {
showRichHistory: false,
openDrawer: undefined,
};
}
......@@ -276,7 +282,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
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<ExploreProps, ExploreState> {
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<ExploreProps, ExploreState> {
const queryErrors = queryResponse.error ? [queryResponse.error] : undefined;
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
const showRichHistory = openDrawer === ExploreDrawer.RichHistory;
const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
return (
<div className={exploreClass} ref={this.getRef} aria-label={selectors.pages.Explore.General.container}>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
......@@ -343,8 +360,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
//TODO:unification
addQueryRowButtonHidden={false}
richHistoryButtonActive={showRichHistory}
queryInspectorButtonActive={showQueryInspector}
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
onClickRichHistoryButton={this.toggleShowRichHistory}
onClickQueryInspectorButton={this.toggleShowQueryInspector}
/>
</div>
<ErrorContainer queryError={queryError} />
......@@ -421,6 +440,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onClose={this.toggleShowRichHistory}
/>
)}
{showQueryInspector && (
<ExploreQueryInspector
exploreId={exploreId}
width={width}
onClose={this.toggleShowQueryInspector}
/>
)}
</ErrorBoundaryAlert>
</main>
);
......
import React from 'react';
import { mount } from 'enzyme';
import { ExploreDrawer } from './ExploreDrawer';
describe('<ExploreDrawer />', () => {
it('renders child element', () => {
const childElement = <div>Child element</div>;
const wrapper = mount(<ExploreDrawer width={400}>{childElement}</ExploreDrawer>);
expect(wrapper.text()).toBe('Child element');
});
});
// 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 (
<Resizable
className={cx(styles.container, styles.drawerActive)}
defaultSize={{ width: drawerWidth, height: '400px' }}
handleClasses={{ top: styles.rzHandle }}
enable={{
top: true,
right: false,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
maxHeight="100vh"
maxWidth={drawerWidth}
minWidth={drawerWidth}
onResize={onResize}
>
{children}
</Resizable>
);
}
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<PanelData>({} 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: <InspectStatsTab data={queryResponse!} timeZone={queryResponse?.request?.timezone as TimeZone} />,
};
const inspectorTab: TabConfig = {
label: 'Query Inspector',
value: 'query_inspector',
icon: 'info-circle',
content: (
<>
<div className={styles.toolbar}>
{haveData && (
<>
<Button
icon={allNodesExpanded ? 'minus' : 'plus'}
variant="secondary"
className={styles.toolbarItem}
onClick={onToggleExpand}
>
{allNodesExpanded ? 'Collapse' : 'Expand'} all
</Button>
<CopyToClipboard
text={getTextForClipboard}
onSuccess={onClipboardSuccess}
elType="div"
className={styles.toolbarItem}
>
<Button icon="copy" variant="secondary">
Copy to clipboard
</Button>
</CopyToClipboard>
</>
)}
<div className="flex-grow-1" />
</div>
<div className={styles.contentQueryInspector}>
{loading && <LoadingPlaceholder text="Loading query inspector..." />}
{!loading && haveData && (
<JSONFormatter json={response!} open={getOpenNodeCount()} onDidRender={setFormattedJSON} />
)}
{!loading && !haveData && (
<p className="muted">No request & response collected yet. Run query to collect request & response.</p>
)}
</div>
</>
),
};
const tabs = [statsTab, inspectorTab];
return (
<ExploreDrawer width={width} onResize={() => {}}>
<TabbedContainer tabs={tabs} onClose={onClose} closeIconTooltip="Close query inspector" />
</ExploreDrawer>
);
}
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));
......@@ -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);
});
});
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<RichHistoryProps, RichHistoryState> {
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<RichHistoryProps, RichHistorySta
store.set(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, starredTabAsFirstTab);
};
toggleactiveDatasourceOnly = () => {
toggleActiveDatasourceOnly = () => {
const activeDatasourceOnly = !this.state.activeDatasourceOnly;
this.setState({
activeDatasourceOnly,
......@@ -127,10 +94,6 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
this.setState({ datasourceFilters: value });
};
onSelectTab = (item: SelectableValue<Tabs>) => {
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<RichHistoryProps, RichHistorySta
componentDidMount() {
this.updateFilters();
}
componentDidUpdate(prevProps: RichHistoryProps, prevState: RichHistoryState) {
if (
this.props.activeDatasourceInstance !== prevProps.activeDatasourceInstance ||
......@@ -158,11 +122,10 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
}
render() {
const { datasourceFilters, sortOrder, activeTab, activeDatasourceOnly, retentionPeriod } = this.state;
const { theme, richHistory, height, exploreId, deleteRichHistory, onClose } = this.props;
const styles = getStyles(theme);
const { datasourceFilters, sortOrder, activeDatasourceOnly, retentionPeriod } = this.state;
const { richHistory, height, exploreId, deleteRichHistory, onClose, firstTab } = this.props;
const QueriesTab = {
const QueriesTab: TabConfig = {
label: 'Query history',
value: Tabs.RichHistory,
content: (
......@@ -181,7 +144,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
icon: 'history',
};
const StarredTab = {
const StarredTab: TabConfig = {
label: 'Starred',
value: Tabs.Starred,
content: (
......@@ -198,7 +161,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
icon: 'star',
};
const SettingsTab = {
const SettingsTab: TabConfig = {
label: 'Settings',
value: Tabs.Settings,
content: (
......@@ -208,7 +171,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
activeDatasourceOnly={this.state.activeDatasourceOnly}
onChangeRetentionPeriod={this.onChangeRetentionPeriod}
toggleStarredTabAsFirstTab={this.toggleStarredTabAsFirstTab}
toggleactiveDatasourceOnly={this.toggleactiveDatasourceOnly}
toggleactiveDatasourceOnly={this.toggleActiveDatasourceOnly}
deleteRichHistory={deleteRichHistory}
/>
),
......@@ -217,23 +180,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
let tabs = [QueriesTab, StarredTab, SettingsTab];
return (
<div className={styles.container}>
<TabsBar className={styles.tabs}>
{tabs.map(t => (
<Tab
key={t.value}
label={t.label}
active={t.value === activeTab}
onChangeTab={() => this.onSelectTab(t)}
icon={t.icon as IconName}
/>
))}
<IconButton className={styles.close} onClick={onClose} name="times" title="Close query history" />
</TabsBar>
<CustomScrollbar className={styles.scrollbar}>
<TabContent className={styles.tabContent}>{tabs.find(t => t.value === activeTab)?.content}</TabContent>
</CustomScrollbar>
</div>
<TabbedContainer tabs={tabs} onClose={onClose} defaultTab={firstTab} closeIconTooltip="Close query history" />
);
}
}
......
// 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 (
<Resizable
className={cx(styles.container, styles.drawerActive)}
defaultSize={{ width: drawerWidth, height: '400px' }}
handleClasses={{ top: styles.rzHandle }}
enable={{
top: true,
right: false,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
maxHeight="100vh"
maxWidth={drawerWidth}
minWidth={drawerWidth}
onResize={(e, dir, ref) => {
<ExploreDrawer
width={width}
onResize={(_e, _dir, ref) => {
setHeight(Number(ref.style.height.slice(0, -2)));
}}
>
......@@ -116,7 +49,7 @@ export function RichHistoryContainer(props: Props) {
onClose={onClose}
height={height}
/>
</Resizable>
</ExploreDrawer>
);
}
......
......@@ -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(<SecondaryActions onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} />);
const wrapper = shallow(
<SecondaryActions
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
/>
);
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(
<SecondaryActions onClickAddQueryRowButton={onClickAddRow} onClickRichHistoryButton={onClickHistory} />
<SecondaryActions
onClickAddQueryRowButton={onClickAddRow}
onClickRichHistoryButton={onClickHistory}
onClickQueryInspectorButton={onClickQueryInspector}
/>
);
wrapper.find(addQueryRowButtonSelector).simulate('click');
expect(onClickAddRow).toBeCalled();
wrapper.find(richHistoryButtonSelector).simulate('click');
expect(onClickHistory).toBeCalled();
wrapper.find(queryInspectorButtonSelector).simulate('click');
expect(onClickQueryInspector).toBeCalled();
});
});
......@@ -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) {
<Icon className="icon-margin-right" name="history" size="sm" />
<span className="btn-title">{'\xA0' + 'Query history'}</span>
</button>
<button
aria-label="Query inspector button"
className={cx(`gf-form-label gf-form-label--btn ${styles.button}`, {
['explore-active-button']: props.queryInspectorButtonActive,
})}
onClick={props.onClickQueryInspectorButton}
>
<Icon className="icon-margin-right" name="info-circle" size="sm" />
<span className="btn-title">{'\xA0' + 'Query inspector'}</span>
</button>
</div>
);
}
......@@ -31,7 +31,9 @@ exports[`Explore should render component 1`] = `
addQueryRowButtonDisabled={false}
addQueryRowButtonHidden={false}
onClickAddQueryRowButton={[Function]}
onClickQueryInspectorButton={[Function]}
onClickRichHistoryButton={[Function]}
queryInspectorButtonActive={false}
richHistoryButtonActive={false}
/>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment