diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index 7f867c948e0..cc836672d5d 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -8,7 +8,7 @@ import { stylesFactory, useTheme, selectThemeVariant } from '../../themes'; export interface Props { children: ReactNode; /** Title shown at the top of the drawer */ - title?: string; + title?: (() => JSX.Element) | string; /** Should the Drawer be closable by clicking on the mask */ closeOnMaskClick?: boolean; /** Render the drawer inside a container on the page */ @@ -43,7 +43,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean) titleWrapper: css` font-size: ${theme.typography.size.lg}; display: flex; - align-items: center; + align-items: baseline; justify-content: space-between; border-bottom: 1px solid ${borderColor}; padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md}; @@ -94,12 +94,15 @@ export const Drawer: FC = ({ style={{ position: `${inline && 'absolute'}` } as CSSProperties} className={drawerStyles.drawer} > -
-
{title}
-
- + {typeof title === 'string' && ( +
+
{title}
+
+ +
-
+ )} + {typeof title === 'function' && title()}
{!scrollableContent ? children : {children}}
diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index 846d00f2ed8..9b4056a53f8 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -35,6 +35,7 @@ export const getTableStyles = stylesFactory( border-spacing: 0; `, thead: css` + label: thead; overflow-y: auto; overflow-x: hidden; background: ${headerBg}; @@ -46,6 +47,7 @@ export const getTableStyles = stylesFactory( color: ${colors.blue}; `, row: css` + label: row; border-bottom: 2px solid ${colors.bodyBg}; `, tableCell: css` diff --git a/public/app/features/dashboard/components/Inspector/InspectHeader.tsx b/public/app/features/dashboard/components/Inspector/InspectHeader.tsx new file mode 100644 index 00000000000..0568e83efbb --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/InspectHeader.tsx @@ -0,0 +1,101 @@ +import React, { FC } from 'react'; +import { css } from 'emotion'; +import { Icon, selectThemeVariant, stylesFactory, Tab, TabsBar, useTheme } from '@grafana/ui'; +import { GrafanaTheme, SelectableValue } from '@grafana/data'; +import { InspectTab } from './PanelInspector'; +import { PanelModel } from '../../state'; + +interface Props { + tab: InspectTab; + tabs: Array<{ label: string; value: InspectTab }>; + stats: { requestTime: number; queries: number; dataSources: number }; + panel: PanelModel; + isExpanded: boolean; + + onSelectTab: (tab: SelectableValue) => void; + onClose: () => void; + onToggleExpand: () => void; +} + +export const InspectHeader: FC = ({ + tab, + tabs, + onSelectTab, + onClose, + onToggleExpand, + panel, + stats, + isExpanded, +}) => { + const theme = useTheme(); + const styles = getStyles(theme); + + return ( +
+
+
+ +
+
+ +
+
+
+

{panel.title}

+
{formatStats(stats)}
+
+ + {tabs.map((t, index) => { + return ( + onSelectTab(t)} + /> + ); + })} + +
+ ); +}; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const headerBackground = selectThemeVariant({ dark: theme.colors.gray15, light: theme.colors.white }, theme.type); + return { + header: css` + background-color: ${headerBackground}; + z-index: 1; + flex-grow: 0; + padding: ${theme.spacing.sm} ${theme.spacing.sm} 0 ${theme.spacing.lg}; + `, + actions: css` + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: ${theme.spacing.md}; + `, + iconWrapper: css` + cursor: pointer; + width: 25px; + height: 100%; + display: flex; + flex-shrink: 0; + justify-content: center; + `, + icon: css` + font-size: ${theme.typography.size.lg}; + `, + titleWrapper: css` + margin-bottom: ${theme.spacing.lg}; + `, + }; +}); + +function formatStats(stats: { requestTime: number; queries: number; dataSources: number }) { + const queries = `${stats.queries} ${stats.queries === 1 ? 'query' : 'queries'}`; + const dataSources = `${stats.dataSources} ${stats.dataSources === 1 ? 'data source' : 'data sources'}`; + const requestTime = `${stats.requestTime === -1 ? 'N/A' : stats.requestTime}ms`; + + return `${queries} - ${dataSources} - ${requestTime}`; +} diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 9534e4f7321..d45ea9b4511 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -3,19 +3,10 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { saveAs } from 'file-saver'; import { css } from 'emotion'; +import { InspectHeader } from './InspectHeader'; + import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { - JSONFormatter, - Drawer, - Select, - Table, - TabsBar, - Tab, - TabContent, - Forms, - stylesFactory, - CustomScrollbar, -} from '@grafana/ui'; +import { JSONFormatter, Drawer, Select, Table, TabContent, Forms, stylesFactory, CustomScrollbar } from '@grafana/ui'; import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime'; import { DataFrame, @@ -44,7 +35,7 @@ export enum InspectTab { interface State { // The last raw response - last?: PanelData; + last: PanelData; // Data frem the last response data: DataFrame[]; @@ -57,6 +48,10 @@ interface State { // If the datasource supports custom metadata metaDS?: DataSourceApi; + + stats: { requestTime: number; queries: number; dataSources: number }; + + drawerWidth: string; } const getStyles = stylesFactory(() => { @@ -89,9 +84,12 @@ export class PanelInspector extends PureComponent { constructor(props: Props) { super(props); this.state = { + last: {} as PanelData, data: [], selected: 0, tab: props.selectedTab || InspectTab.Data, + drawerWidth: '40%', + stats: { requestTime: 0, queries: 0, dataSources: 0 }, }; } @@ -110,8 +108,14 @@ export class PanelInspector extends PureComponent { // Find the first DataSource wanting to show custom metadata let metaDS: DataSourceApi; - const data = lastResult?.series; - const error = lastResult?.error; + const data = lastResult.series; + const error = lastResult.error; + + const targets = lastResult.request?.targets; + const requestTime = lastResult.request?.endTime ? lastResult.request?.endTime - lastResult.request.startTime : -1; + const queries = targets ? targets.length : 0; + + const dataSources = new Set(targets.map(t => t.datasource)).size; if (data) { for (const frame of data) { @@ -132,6 +136,11 @@ export class PanelInspector extends PureComponent { data, metaDS, tab: error ? InspectTab.Error : prevState.tab, + stats: { + requestTime, + queries, + dataSources, + }, })); } @@ -142,6 +151,12 @@ export class PanelInspector extends PureComponent { }); }; + onToggleExpand = () => { + this.setState(prevState => ({ + drawerWidth: prevState.drawerWidth === '100%' ? '40%' : '100%', + })); + }; + onSelectTab = (item: SelectableValue) => { this.setState({ tab: item.value || InspectTab.Data }); }; @@ -261,17 +276,9 @@ export class PanelInspector extends PureComponent { ); } - render() { - const { panel } = this.props; - const { last, tab } = this.state; - const styles = getStyles(); - + drawerHeader = () => { + const { tab, last, stats } = this.state; const error = last?.error; - if (!panel) { - this.onDismiss(); // Try to close the component - return null; - } - const tabs = []; if (last && last?.series?.length > 0) { tabs.push({ label: 'Data', value: InspectTab.Data }); @@ -285,19 +292,26 @@ export class PanelInspector extends PureComponent { tabs.push({ label: 'Raw JSON', value: InspectTab.Raw }); return ( - - - {tabs.map((t, index) => { - return ( - this.onSelectTab(t)} - /> - ); - })} - + + ); + }; + + render() { + const { last, tab, drawerWidth } = this.state; + const styles = getStyles(); + const error = last?.error; + + return ( + {tab === InspectTab.Data ? ( this.renderDataTab()