mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Drawer: align component style with PanelInspector (#23694)
* Drawer: add subtitle, expandable button and refactor style * Drawer: update header style and z-index * Drawer: refactor Drawer component and PanelInspector * PanelInspector: add expandable * Drawer: update stories with new props * Inspector: rename InspectHeader -> InspectSubtitle * Inspector: fix tabs spacing * Drawer: remove z-index * Update public/app/features/dashboard/components/Inspector/InspectSubtitle.tsx Co-Authored-By: Dominik Prokop <dominik.prokop@grafana.com> * Drawer: apply PR feedbacks Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
@@ -33,6 +33,7 @@ export const global = () => {
|
|||||||
{state.isOpen && (
|
{state.isOpen && (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={drawerTitle}
|
title={drawerTitle}
|
||||||
|
subtitle="This is a subtitle."
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
updateValue({ isOpen: !state.isOpen });
|
updateValue({ isOpen: !state.isOpen });
|
||||||
}}
|
}}
|
||||||
@@ -70,6 +71,7 @@ export const longContent = () => {
|
|||||||
{state.isOpen && (
|
{state.isOpen && (
|
||||||
<Drawer
|
<Drawer
|
||||||
scrollableContent
|
scrollableContent
|
||||||
|
expandable
|
||||||
title="Drawer with long content"
|
title="Drawer with long content"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
updateValue({ isOpen: !state.isOpen });
|
updateValue({ isOpen: !state.isOpen });
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import React, { CSSProperties, FC, ReactNode } from 'react';
|
import React, { CSSProperties, FC, ReactNode, useState } from 'react';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import RcDrawer from 'rc-drawer';
|
import RcDrawer from 'rc-drawer';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { IconButton } from '../IconButton/IconButton';
|
||||||
import { stylesFactory, useTheme } from '../../themes';
|
import { stylesFactory, useTheme } 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?: JSX.Element | string;
|
title?: ReactNode;
|
||||||
|
/** Subtitle shown below the title */
|
||||||
|
subtitle?: ReactNode;
|
||||||
/** 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 */
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
/** Either a number in px or a string with unit postfix */
|
/** Either a number in px or a string with unit postfix */
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
|
/** Should the Drawer be expandable to full width */
|
||||||
|
expandable?: boolean;
|
||||||
|
|
||||||
/** Set to true if the component rendered within in drawer content has its own scroll */
|
/** Set to true if the component rendered within in drawer content has its own scroll */
|
||||||
scrollableContent?: boolean;
|
scrollableContent?: boolean;
|
||||||
@@ -24,9 +28,6 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean) => {
|
||||||
const closeButtonWidth = '50px';
|
|
||||||
const borderColor = theme.colors.border2;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
drawer: css`
|
drawer: css`
|
||||||
.drawer-content {
|
.drawer-content {
|
||||||
@@ -36,25 +37,23 @@ const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean)
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
titleWrapper: css`
|
header: css`
|
||||||
font-size: ${theme.typography.size.lg};
|
background-color: ${theme.colors.bg2};
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid ${borderColor};
|
|
||||||
padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md};
|
|
||||||
background-color: ${theme.colors.bodyBg};
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
padding-top: ${theme.spacing.xs};
|
||||||
`,
|
`,
|
||||||
close: css`
|
actions: css`
|
||||||
cursor: pointer;
|
|
||||||
width: ${closeButtonWidth};
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
align-items: baseline;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
|
`,
|
||||||
|
titleWrapper: css`
|
||||||
|
margin-bottom: ${theme.spacing.lg};
|
||||||
|
padding: 0 ${theme.spacing.sm} 0 ${theme.spacing.lg};
|
||||||
|
`,
|
||||||
|
titleSpacing: css`
|
||||||
|
margin-bottom: ${theme.spacing.md};
|
||||||
`,
|
`,
|
||||||
content: css`
|
content: css`
|
||||||
padding: ${theme.spacing.md};
|
padding: ${theme.spacing.md};
|
||||||
@@ -72,10 +71,14 @@ export const Drawer: FC<Props> = ({
|
|||||||
closeOnMaskClick = false,
|
closeOnMaskClick = false,
|
||||||
scrollableContent = false,
|
scrollableContent = false,
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
width = '40%',
|
width = '40%',
|
||||||
|
expandable = false,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const drawerStyles = getStyles(theme, scrollableContent);
|
const drawerStyles = getStyles(theme, scrollableContent);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const currentWidth = isExpanded ? '100%' : width;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RcDrawer
|
<RcDrawer
|
||||||
@@ -85,16 +88,26 @@ export const Drawer: FC<Props> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
maskClosable={closeOnMaskClick}
|
maskClosable={closeOnMaskClick}
|
||||||
placement="right"
|
placement="right"
|
||||||
width={width}
|
width={currentWidth}
|
||||||
getContainer={inline ? false : 'body'}
|
getContainer={inline ? false : 'body'}
|
||||||
style={{ position: `${inline && 'absolute'}` } as CSSProperties}
|
style={{ position: `${inline && 'absolute'}` } as CSSProperties}
|
||||||
className={drawerStyles.drawer}
|
className={drawerStyles.drawer}
|
||||||
>
|
>
|
||||||
{typeof title === 'string' && (
|
{typeof title === 'string' && (
|
||||||
<div className={drawerStyles.titleWrapper}>
|
<div className={drawerStyles.header}>
|
||||||
<div>{title}</div>
|
<div className={drawerStyles.actions}>
|
||||||
<div className={drawerStyles.close} onClick={onClose}>
|
{expandable && !isExpanded && (
|
||||||
<Icon name="times" />
|
<IconButton name="angle-left" size="xl" onClick={() => setIsExpanded(true)} surface="header" />
|
||||||
|
)}
|
||||||
|
{expandable && isExpanded && (
|
||||||
|
<IconButton name="angle-right" size="xl" onClick={() => setIsExpanded(false)} surface="header" />
|
||||||
|
)}
|
||||||
|
<IconButton name="times" size="xl" onClick={onClose} surface="header" />
|
||||||
|
</div>
|
||||||
|
<div className={drawerStyles.titleWrapper}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{typeof subtitle === 'string' && <div className="muted">{subtitle}</div>}
|
||||||
|
{typeof subtitle !== 'string' && subtitle}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import React, { FC } from 'react';
|
|
||||||
import { css } from 'emotion';
|
|
||||||
import { stylesFactory, Tab, TabsBar, useTheme, IconButton } from '@grafana/ui';
|
|
||||||
import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data';
|
|
||||||
import { InspectTab } from './PanelInspector';
|
|
||||||
import { PanelModel } from '../../state';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
tab: InspectTab;
|
|
||||||
tabs: Array<{ label: string; value: InspectTab }>;
|
|
||||||
panelData: PanelData;
|
|
||||||
panel: PanelModel;
|
|
||||||
isExpanded: boolean;
|
|
||||||
onSelectTab: (tab: SelectableValue<InspectTab>) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
onToggleExpand: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InspectHeader: FC<Props> = ({
|
|
||||||
tab,
|
|
||||||
tabs,
|
|
||||||
onSelectTab,
|
|
||||||
onClose,
|
|
||||||
onToggleExpand,
|
|
||||||
panel,
|
|
||||||
panelData,
|
|
||||||
isExpanded,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const styles = getStyles(theme);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div className={styles.actions}>
|
|
||||||
{!isExpanded && <IconButton name="angle-left" size="xl" onClick={onToggleExpand} surface="header" />}
|
|
||||||
{isExpanded && <IconButton name="angle-right" size="xl" onClick={onToggleExpand} surface="header" />}
|
|
||||||
<IconButton name="times" size="xl" onClick={onClose} surface="header" />
|
|
||||||
</div>
|
|
||||||
<div className={styles.titleWrapper}>
|
|
||||||
<h3>{panel.title || 'Panel inspect'}</h3>
|
|
||||||
<div className="muted">{formatStats(panelData)}</div>
|
|
||||||
</div>
|
|
||||||
<TabsBar className={styles.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 = theme.colors.bg2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
header: css`
|
|
||||||
background-color: ${headerBackground};
|
|
||||||
z-index: 1;
|
|
||||||
flex-grow: 0;
|
|
||||||
padding-top: ${theme.spacing.sm};
|
|
||||||
`,
|
|
||||||
actions: css`
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
right: ${theme.spacing.sm};
|
|
||||||
`,
|
|
||||||
tabsBar: css`
|
|
||||||
padding-left: ${theme.spacing.md};
|
|
||||||
`,
|
|
||||||
titleWrapper: css`
|
|
||||||
margin-bottom: ${theme.spacing.lg};
|
|
||||||
padding: ${theme.spacing.sm} ${theme.spacing.sm} 0 ${theme.spacing.lg};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatStats(panelData: PanelData) {
|
|
||||||
const { request } = panelData;
|
|
||||||
if (!request) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryCount = request.targets.length;
|
|
||||||
const requestTime = request.endTime ? request.endTime - request.startTime : 0;
|
|
||||||
const formatted = formattedValueToString(getValueFormat('ms')(requestTime));
|
|
||||||
|
|
||||||
return `${queryCount} queries with total query time of ${formatted}`;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { stylesFactory, Tab, TabsBar, useTheme } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme, SelectableValue, PanelData, getValueFormat, formattedValueToString } from '@grafana/data';
|
||||||
|
import { InspectTab } from './PanelInspector';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tab: InspectTab;
|
||||||
|
tabs: Array<{ label: string; value: InspectTab }>;
|
||||||
|
panelData: PanelData;
|
||||||
|
onSelectTab: (tab: SelectableValue<InspectTab>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InspectSubtitle: FC<Props> = ({ tab, tabs, onSelectTab, panelData }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="muted">{formatStats(panelData)}</div>
|
||||||
|
<TabsBar className={styles.tabsBar}>
|
||||||
|
{tabs.map((t, index) => {
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
key={`${t.value}-${index}`}
|
||||||
|
label={t.label}
|
||||||
|
active={t.value === tab}
|
||||||
|
onChangeTab={() => onSelectTab(t)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabsBar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
tabsBar: css`
|
||||||
|
padding-left: ${theme.spacing.md};
|
||||||
|
margin: ${theme.spacing.lg} -${theme.spacing.sm} -${theme.spacing.lg} -${theme.spacing.lg};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatStats(panelData: PanelData) {
|
||||||
|
const { request } = panelData;
|
||||||
|
if (!request) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryCount = request.targets.length;
|
||||||
|
const requestTime = request.endTime ? request.endTime - request.startTime : 0;
|
||||||
|
const formatted = formattedValueToString(getValueFormat('ms')(requestTime));
|
||||||
|
|
||||||
|
return `${queryCount} queries with total query time of ${formatted}`;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { connect, MapStateToProps } from 'react-redux';
|
import { connect, MapStateToProps } from 'react-redux';
|
||||||
import { InspectHeader } from './InspectHeader';
|
import { InspectSubtitle } from './InspectSubtitle';
|
||||||
import { InspectJSONTab } from './InspectJSONTab';
|
import { InspectJSONTab } from './InspectJSONTab';
|
||||||
import { QueryInspector } from './QueryInspector';
|
import { QueryInspector } from './QueryInspector';
|
||||||
|
|
||||||
@@ -253,22 +253,10 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerHeader(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) {
|
drawerSubtitle(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) {
|
||||||
const { panel } = this.props;
|
|
||||||
const { last } = this.state;
|
const { last } = this.state;
|
||||||
|
|
||||||
return (
|
return <InspectSubtitle tabs={tabs} tab={activeTab} panelData={last} onSelectTab={this.onSelectTab} />;
|
||||||
<InspectHeader
|
|
||||||
tabs={tabs}
|
|
||||||
tab={activeTab}
|
|
||||||
panelData={last}
|
|
||||||
onSelectTab={this.onSelectTab}
|
|
||||||
onClose={this.onClose}
|
|
||||||
panel={panel}
|
|
||||||
onToggleExpand={this.onToggleExpand}
|
|
||||||
isExpanded={this.state.drawerWidth === '100%'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTabs() {
|
getTabs() {
|
||||||
@@ -318,7 +306,13 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer title={this.drawerHeader(tabs, activeTab)} width={drawerWidth} onClose={this.onClose}>
|
<Drawer
|
||||||
|
title={panel.title || 'Panel inspect'}
|
||||||
|
subtitle={this.drawerSubtitle(tabs, activeTab)}
|
||||||
|
width={drawerWidth}
|
||||||
|
onClose={this.onClose}
|
||||||
|
expandable
|
||||||
|
>
|
||||||
{activeTab === InspectTab.Data && this.renderDataTab()}
|
{activeTab === InspectTab.Data && this.renderDataTab()}
|
||||||
<CustomScrollbar autoHeightMin="100%">
|
<CustomScrollbar autoHeightMin="100%">
|
||||||
<TabContent className={styles.tabContent}>
|
<TabContent className={styles.tabContent}>
|
||||||
|
|||||||
Reference in New Issue
Block a user