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:
Agnès Toulet
2020-04-20 15:27:33 +02:00
committed by GitHub
parent 174ee95153
commit d5f8d976f0
5 changed files with 108 additions and 139 deletions

View File

@@ -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 });

View File

@@ -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>
)} )}

View File

@@ -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}`;
}

View File

@@ -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}`;
}

View File

@@ -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}>