mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Inspect: Inspect header design update (#22120)
* first things * rename * add stats, remove table related changes * header size and expand icon * add case for nan request time * fixing null checks * reverting changes to table * fix background on header * Some small fixes after review * do not use formInputBg
This commit is contained in:
parent
e846a26c8c
commit
0cde7decd7
@ -8,7 +8,7 @@ import { stylesFactory, useTheme, selectThemeVariant } from '../../themes';
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
/** Title shown at the top of the drawer */
|
/** Title shown at the top of the drawer */
|
||||||
title?: string;
|
title?: (() => JSX.Element) | string;
|
||||||
/** Should the Drawer be closable by clicking on the mask */
|
/** Should the Drawer be closable by clicking on the mask */
|
||||||
closeOnMaskClick?: boolean;
|
closeOnMaskClick?: boolean;
|
||||||
/** Render the drawer inside a container on the page */
|
/** Render the drawer inside a container on the page */
|
||||||
@ -43,7 +43,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean)
|
|||||||
titleWrapper: css`
|
titleWrapper: css`
|
||||||
font-size: ${theme.typography.size.lg};
|
font-size: ${theme.typography.size.lg};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid ${borderColor};
|
border-bottom: 1px solid ${borderColor};
|
||||||
padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md};
|
padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md};
|
||||||
@ -94,12 +94,15 @@ export const Drawer: FC<Props> = ({
|
|||||||
style={{ position: `${inline && 'absolute'}` } as CSSProperties}
|
style={{ position: `${inline && 'absolute'}` } as CSSProperties}
|
||||||
className={drawerStyles.drawer}
|
className={drawerStyles.drawer}
|
||||||
>
|
>
|
||||||
<div className={drawerStyles.titleWrapper}>
|
{typeof title === 'string' && (
|
||||||
<div>{title}</div>
|
<div className={drawerStyles.titleWrapper}>
|
||||||
<div className={drawerStyles.close} onClick={onClose}>
|
<div>{title}</div>
|
||||||
<i className="fa fa-close" />
|
<div className={drawerStyles.close} onClick={onClose}>
|
||||||
|
<i className="fa fa-close" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{typeof title === 'function' && title()}
|
||||||
<div className={drawerStyles.content}>
|
<div className={drawerStyles.content}>
|
||||||
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
|
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +35,7 @@ export const getTableStyles = stylesFactory(
|
|||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
`,
|
`,
|
||||||
thead: css`
|
thead: css`
|
||||||
|
label: thead;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: ${headerBg};
|
background: ${headerBg};
|
||||||
@ -46,6 +47,7 @@ export const getTableStyles = stylesFactory(
|
|||||||
color: ${colors.blue};
|
color: ${colors.blue};
|
||||||
`,
|
`,
|
||||||
row: css`
|
row: css`
|
||||||
|
label: row;
|
||||||
border-bottom: 2px solid ${colors.bodyBg};
|
border-bottom: 2px solid ${colors.bodyBg};
|
||||||
`,
|
`,
|
||||||
tableCell: css`
|
tableCell: css`
|
||||||
|
@ -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<InspectTab>) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onToggleExpand: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InspectHeader: FC<Props> = ({
|
||||||
|
tab,
|
||||||
|
tabs,
|
||||||
|
onSelectTab,
|
||||||
|
onClose,
|
||||||
|
onToggleExpand,
|
||||||
|
panel,
|
||||||
|
stats,
|
||||||
|
isExpanded,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<div className={styles.iconWrapper} onClick={onToggleExpand}>
|
||||||
|
<Icon name={isExpanded ? 'chevron-right' : 'chevron-left'} className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.iconWrapper} onClick={onClose}>
|
||||||
|
<Icon name="times" className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.titleWrapper}>
|
||||||
|
<h3>{panel.title}</h3>
|
||||||
|
<div>{formatStats(stats)}</div>
|
||||||
|
</div>
|
||||||
|
<TabsBar>
|
||||||
|
{tabs.map((t, index) => {
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
key={`${t.value}-${index}`}
|
||||||
|
label={t.label}
|
||||||
|
active={t.value === tab}
|
||||||
|
onChangeTab={() => onSelectTab(t)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabsBar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
@ -3,19 +3,10 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
|
import { InspectHeader } from './InspectHeader';
|
||||||
|
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import {
|
import { JSONFormatter, Drawer, Select, Table, TabContent, Forms, stylesFactory, CustomScrollbar } from '@grafana/ui';
|
||||||
JSONFormatter,
|
|
||||||
Drawer,
|
|
||||||
Select,
|
|
||||||
Table,
|
|
||||||
TabsBar,
|
|
||||||
Tab,
|
|
||||||
TabContent,
|
|
||||||
Forms,
|
|
||||||
stylesFactory,
|
|
||||||
CustomScrollbar,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
|
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
@ -44,7 +35,7 @@ export enum InspectTab {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
// The last raw response
|
// The last raw response
|
||||||
last?: PanelData;
|
last: PanelData;
|
||||||
|
|
||||||
// Data frem the last response
|
// Data frem the last response
|
||||||
data: DataFrame[];
|
data: DataFrame[];
|
||||||
@ -57,6 +48,10 @@ interface State {
|
|||||||
|
|
||||||
// If the datasource supports custom metadata
|
// If the datasource supports custom metadata
|
||||||
metaDS?: DataSourceApi;
|
metaDS?: DataSourceApi;
|
||||||
|
|
||||||
|
stats: { requestTime: number; queries: number; dataSources: number };
|
||||||
|
|
||||||
|
drawerWidth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => {
|
const getStyles = stylesFactory(() => {
|
||||||
@ -89,9 +84,12 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
last: {} as PanelData,
|
||||||
data: [],
|
data: [],
|
||||||
selected: 0,
|
selected: 0,
|
||||||
tab: props.selectedTab || InspectTab.Data,
|
tab: props.selectedTab || InspectTab.Data,
|
||||||
|
drawerWidth: '40%',
|
||||||
|
stats: { requestTime: 0, queries: 0, dataSources: 0 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +108,14 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
// Find the first DataSource wanting to show custom metadata
|
// Find the first DataSource wanting to show custom metadata
|
||||||
let metaDS: DataSourceApi;
|
let metaDS: DataSourceApi;
|
||||||
const data = lastResult?.series;
|
const data = lastResult.series;
|
||||||
const error = lastResult?.error;
|
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) {
|
if (data) {
|
||||||
for (const frame of data) {
|
for (const frame of data) {
|
||||||
@ -132,6 +136,11 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
data,
|
data,
|
||||||
metaDS,
|
metaDS,
|
||||||
tab: error ? InspectTab.Error : prevState.tab,
|
tab: error ? InspectTab.Error : prevState.tab,
|
||||||
|
stats: {
|
||||||
|
requestTime,
|
||||||
|
queries,
|
||||||
|
dataSources,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +151,12 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onToggleExpand = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
drawerWidth: prevState.drawerWidth === '100%' ? '40%' : '100%',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
onSelectTab = (item: SelectableValue<InspectTab>) => {
|
onSelectTab = (item: SelectableValue<InspectTab>) => {
|
||||||
this.setState({ tab: item.value || InspectTab.Data });
|
this.setState({ tab: item.value || InspectTab.Data });
|
||||||
};
|
};
|
||||||
@ -261,17 +276,9 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
drawerHeader = () => {
|
||||||
const { panel } = this.props;
|
const { tab, last, stats } = this.state;
|
||||||
const { last, tab } = this.state;
|
|
||||||
const styles = getStyles();
|
|
||||||
|
|
||||||
const error = last?.error;
|
const error = last?.error;
|
||||||
if (!panel) {
|
|
||||||
this.onDismiss(); // Try to close the component
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
if (last && last?.series?.length > 0) {
|
if (last && last?.series?.length > 0) {
|
||||||
tabs.push({ label: 'Data', value: InspectTab.Data });
|
tabs.push({ label: 'Data', value: InspectTab.Data });
|
||||||
@ -285,19 +292,26 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
tabs.push({ label: 'Raw JSON', value: InspectTab.Raw });
|
tabs.push({ label: 'Raw JSON', value: InspectTab.Raw });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer title={panel.title} onClose={this.onDismiss}>
|
<InspectHeader
|
||||||
<TabsBar>
|
tabs={tabs}
|
||||||
{tabs.map((t, index) => {
|
tab={tab}
|
||||||
return (
|
stats={stats}
|
||||||
<Tab
|
onSelectTab={this.onSelectTab}
|
||||||
key={`${t.value}-${index}`}
|
onClose={this.onDismiss}
|
||||||
label={t.label}
|
panel={this.props.panel}
|
||||||
active={t.value === tab}
|
onToggleExpand={this.onToggleExpand}
|
||||||
onChangeTab={() => this.onSelectTab(t)}
|
isExpanded={this.state.drawerWidth === '100%'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
</TabsBar>
|
|
||||||
|
render() {
|
||||||
|
const { last, tab, drawerWidth } = this.state;
|
||||||
|
const styles = getStyles();
|
||||||
|
const error = last?.error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer title={this.drawerHeader} width={drawerWidth} onClose={this.onDismiss}>
|
||||||
<TabContent className={styles.tabContent}>
|
<TabContent className={styles.tabContent}>
|
||||||
{tab === InspectTab.Data ? (
|
{tab === InspectTab.Data ? (
|
||||||
this.renderDataTab()
|
this.renderDataTab()
|
||||||
|
Loading…
Reference in New Issue
Block a user