mirror of
https://github.com/grafana/grafana.git
synced 2025-09-24 15:04:02 +08:00
Explore: Adds query inspector drawer to explore (#26698)
* Explore: Adds query inspector drawer to explore
This commit is contained in:
@ -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<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>
|
||||
|
@ -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<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>
|
||||
);
|
||||
|
11
public/app/features/explore/ExploreDrawer.test.tsx
Normal file
11
public/app/features/explore/ExploreDrawer.test.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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');
|
||||
});
|
||||
});
|
93
public/app/features/explore/ExploreDrawer.tsx
Normal file
93
public/app/features/explore/ExploreDrawer.tsx
Normal file
@ -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 (
|
||||
<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>
|
||||
);
|
||||
}
|
183
public/app/features/explore/ExploreQueryInspector.tsx
Normal file
183
public/app/features/explore/ExploreQueryInspector.tsx
Normal file
@ -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<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);
|
||||
});
|
||||
});
|
||||
|
@ -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<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" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<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>
|
||||
|
Reference in New Issue
Block a user