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:
parent
174ee95153
commit
d5f8d976f0
@ -33,6 +33,7 @@ export const global = () => {
|
||||
{state.isOpen && (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
subtitle="This is a subtitle."
|
||||
onClose={() => {
|
||||
updateValue({ isOpen: !state.isOpen });
|
||||
}}
|
||||
@ -70,6 +71,7 @@ export const longContent = () => {
|
||||
{state.isOpen && (
|
||||
<Drawer
|
||||
scrollableContent
|
||||
expandable
|
||||
title="Drawer with long content"
|
||||
onClose={() => {
|
||||
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 RcDrawer from 'rc-drawer';
|
||||
import { css } from 'emotion';
|
||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
|
||||
export interface Props {
|
||||
children: ReactNode;
|
||||
/** 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 */
|
||||
closeOnMaskClick?: boolean;
|
||||
/** Render the drawer inside a container on the page */
|
||||
inline?: boolean;
|
||||
/** Either a number in px or a string with unit postfix */
|
||||
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 */
|
||||
scrollableContent?: boolean;
|
||||
@ -24,9 +28,6 @@ export interface Props {
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean) => {
|
||||
const closeButtonWidth = '50px';
|
||||
const borderColor = theme.colors.border2;
|
||||
|
||||
return {
|
||||
drawer: css`
|
||||
.drawer-content {
|
||||
@ -36,25 +37,23 @@ const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean)
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
titleWrapper: css`
|
||||
font-size: ${theme.typography.size.lg};
|
||||
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;
|
||||
header: css`
|
||||
background-color: ${theme.colors.bg2};
|
||||
z-index: 1;
|
||||
flex-grow: 0;
|
||||
padding-top: ${theme.spacing.xs};
|
||||
`,
|
||||
close: css`
|
||||
cursor: pointer;
|
||||
width: ${closeButtonWidth};
|
||||
height: 100%;
|
||||
actions: css`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
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`
|
||||
padding: ${theme.spacing.md};
|
||||
@ -72,10 +71,14 @@ export const Drawer: FC<Props> = ({
|
||||
closeOnMaskClick = false,
|
||||
scrollableContent = false,
|
||||
title,
|
||||
subtitle,
|
||||
width = '40%',
|
||||
expandable = false,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const drawerStyles = getStyles(theme, scrollableContent);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const currentWidth = isExpanded ? '100%' : width;
|
||||
|
||||
return (
|
||||
<RcDrawer
|
||||
@ -85,16 +88,26 @@ export const Drawer: FC<Props> = ({
|
||||
onClose={onClose}
|
||||
maskClosable={closeOnMaskClick}
|
||||
placement="right"
|
||||
width={width}
|
||||
width={currentWidth}
|
||||
getContainer={inline ? false : 'body'}
|
||||
style={{ position: `${inline && 'absolute'}` } as CSSProperties}
|
||||
className={drawerStyles.drawer}
|
||||
>
|
||||
{typeof title === 'string' && (
|
||||
<div className={drawerStyles.titleWrapper}>
|
||||
<div>{title}</div>
|
||||
<div className={drawerStyles.close} onClick={onClose}>
|
||||
<Icon name="times" />
|
||||
<div className={drawerStyles.header}>
|
||||
<div className={drawerStyles.actions}>
|
||||
{expandable && !isExpanded && (
|
||||
<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>
|
||||
)}
|
||||
|
@ -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 { Unsubscribable } from 'rxjs';
|
||||
import { connect, MapStateToProps } from 'react-redux';
|
||||
import { InspectHeader } from './InspectHeader';
|
||||
import { InspectSubtitle } from './InspectSubtitle';
|
||||
import { InspectJSONTab } from './InspectJSONTab';
|
||||
import { QueryInspector } from './QueryInspector';
|
||||
|
||||
@ -253,22 +253,10 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
drawerHeader(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) {
|
||||
const { panel } = this.props;
|
||||
drawerSubtitle(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) {
|
||||
const { last } = this.state;
|
||||
|
||||
return (
|
||||
<InspectHeader
|
||||
tabs={tabs}
|
||||
tab={activeTab}
|
||||
panelData={last}
|
||||
onSelectTab={this.onSelectTab}
|
||||
onClose={this.onClose}
|
||||
panel={panel}
|
||||
onToggleExpand={this.onToggleExpand}
|
||||
isExpanded={this.state.drawerWidth === '100%'}
|
||||
/>
|
||||
);
|
||||
return <InspectSubtitle tabs={tabs} tab={activeTab} panelData={last} onSelectTab={this.onSelectTab} />;
|
||||
}
|
||||
|
||||
getTabs() {
|
||||
@ -318,7 +306,13 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
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()}
|
||||
<CustomScrollbar autoHeightMin="100%">
|
||||
<TabContent className={styles.tabContent}>
|
||||
|
Loading…
Reference in New Issue
Block a user