PanelChrome: Improve error state design (#63776)

* PanelChrome: Improve error state design

* Simplify logic for hoverHeader

* padding for the title only for the title, not the whole header

* Review fixes

* missed saving file

---------

Co-authored-by: polinaboneva <polina.boneva@grafana.com>
This commit is contained in:
Torkel Ödegaard
2023-03-09 10:06:25 +01:00
committed by GitHub
co-authored by polinaboneva
parent 4ee389676e
commit fb55dd5df6
6 changed files with 40 additions and 65 deletions
@@ -76,6 +76,7 @@ function getStyles(theme: GrafanaTheme2) {
display: 'flex', display: 'flex',
position: 'absolute', position: 'absolute',
zIndex: 1, zIndex: 1,
right: 0,
boxSizing: 'border-box', boxSizing: 'border-box',
alignItems: 'center', alignItems: 'center',
background: theme.colors.background.secondary, background: theme.colors.background.secondary,
@@ -60,11 +60,6 @@ it('renders panel with a header if prop leftItems', () => {
expect(screen.getByTestId('header-container')).toBeInTheDocument(); expect(screen.getByTestId('header-container')).toBeInTheDocument();
}); });
it('renders panel with hover header if no title, no leftItems, hoverHeader is undefined but menu is present', () => {
setup({ title: '', leftItems: undefined, hoverHeader: undefined, menu: <div>Menu</div> });
expect(screen.getByTestId('hover-header-container')).toBeInTheDocument();
});
it('renders panel with a hovering header if prop hoverHeader is true', () => { it('renders panel with a hovering header if prop hoverHeader is true', () => {
setup({ title: 'Test Panel Header', hoverHeader: true }); setup({ title: 'Test Panel Header', hoverHeader: true });
@@ -87,12 +82,6 @@ it('renders panel with a header with icons in place if prop titleItems', () => {
expect(screen.getByTestId('title-items-container')).toBeInTheDocument(); expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
}); });
it('renders panel with a hover header if prop menu is present and hoverHeader is false', () => {
setup({ menu: <div> Menu </div>, hoverHeader: false });
expect(screen.getByTestId('hover-header-container')).toBeInTheDocument();
});
it('renders panel with a show-on-hover menu icon if prop menu', () => { it('renders panel with a show-on-hover menu icon if prop menu', () => {
setup({ menu: <div> Menu </div> }); setup({ menu: <div> Menu </div> });
@@ -84,19 +84,7 @@ export function PanelChrome({
}: PanelChromeProps) { }: PanelChromeProps) {
const theme = useTheme2(); const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const hasHeader = !hoverHeader;
// To Do rely on hoverHeader prop for header, not separate props
// once hoverHeader is implemented
//
// Backwards compatibility for having a designated space for the header
const hasHeader =
hoverHeader === false &&
(title.length > 0 ||
titleItems !== undefined ||
description !== '' ||
loadingState === LoadingState.Streaming ||
(leftItems?.length ?? 0) > 0);
const headerHeight = getHeaderHeight(theme, hasHeader); const headerHeight = getHeaderHeight(theme, hasHeader);
const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height); const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height);
@@ -122,13 +110,10 @@ export function PanelChrome({
</h6> </h6>
)} )}
<PanelDescription description={description} className={dragClassCancel} /> <div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
<PanelDescription description={description} className={dragClassCancel} />
{titleItems !== undefined && ( {titleItems}
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container"> </div>
{titleItems}
</div>
)}
{loadingState === LoadingState.Streaming && ( {loadingState === LoadingState.Streaming && (
<Tooltip content="Streaming"> <Tooltip content="Streaming">
@@ -141,23 +126,34 @@ export function PanelChrome({
); );
return ( return (
<div <div className={styles.container} style={containerStyles} aria-label={ariaLabel}>
className={cx(styles.container, { [styles.regularHeader]: hasHeader })}
style={containerStyles}
aria-label={ariaLabel}
>
<div className={styles.loadingBarContainer}> <div className={styles.loadingBarContainer}>
{loadingState === LoadingState.Loading ? <LoadingBar width={width} ariaLabel="Panel loading bar" /> : null} {loadingState === LoadingState.Loading ? <LoadingBar width={width} ariaLabel="Panel loading bar" /> : null}
</div> </div>
{(hoverHeader || !hasHeader) && menu && ( {hoverHeader && (
<HoverWidget menu={menu} title={title} offset={hoverHeaderOffset} dragClass={dragClass}> <>
{headerContent} {menu && (
</HoverWidget> <HoverWidget menu={menu} title={title} offset={hoverHeaderOffset} dragClass={dragClass}>
{headerContent}
</HoverWidget>
)}
{statusMessage && (
<div className={styles.errorContainerFloating}>
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} ariaLabel="Panel status" />
</div>
)}
</>
)} )}
{hasHeader && ( {hasHeader && (
<div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container"> <div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container">
{statusMessage && (
<div className={dragClassCancel}>
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} ariaLabel="Panel status" />
</div>
)}
{headerContent} {headerContent}
<div className={styles.rightAligned}> <div className={styles.rightAligned}>
@@ -175,15 +171,6 @@ export function PanelChrome({
</div> </div>
)} )}
{statusMessage && (
<PanelStatus
className={cx(styles.errorContainer, dragClassCancel)}
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel="Panel status"
/>
)}
<div className={styles.content} style={contentStyle}> <div className={styles.content} style={contentStyle}>
{children(innerWidth, innerHeight)} {children(innerWidth, innerHeight)}
</div> </div>
@@ -245,6 +232,7 @@ const getStyles = (theme: GrafanaTheme2) => {
visibility: 'hidden', visibility: 'hidden',
opacity: '0', opacity: '0',
}, },
'&:focus-visible, &:hover': { '&:focus-visible, &:hover': {
// only show menu icon on hover or focused panel // only show menu icon on hover or focused panel
'.show-on-hover': { '.show-on-hover': {
@@ -256,8 +244,7 @@ const getStyles = (theme: GrafanaTheme2) => {
'&:focus-visible': { '&:focus-visible': {
outline: `1px solid ${theme.colors.action.focus}`, outline: `1px solid ${theme.colors.action.focus}`,
}, },
}),
regularHeader: css({
'&:focus-within': { '&:focus-within': {
'.show-on-hover': { '.show-on-hover': {
visibility: 'visible', visibility: 'visible',
@@ -281,7 +268,6 @@ const getStyles = (theme: GrafanaTheme2) => {
label: 'panel-header', label: 'panel-header',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: theme.spacing(0, 0, 0, padding),
}), }),
streaming: css({ streaming: css({
label: 'panel-streaming', label: 'panel-streaming',
@@ -295,7 +281,7 @@ const getStyles = (theme: GrafanaTheme2) => {
title: css({ title: css({
label: 'panel-title', label: 'panel-title',
marginBottom: 0, // override default h6 margin-bottom marginBottom: 0, // override default h6 margin-bottom
paddingRight: theme.spacing(1), padding: theme.spacing(0, padding),
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
overflow: 'hidden', overflow: 'hidden',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@@ -316,14 +302,11 @@ const getStyles = (theme: GrafanaTheme2) => {
visibility: 'hidden', visibility: 'hidden',
border: 'none', border: 'none',
}), }),
errorContainer: css({ errorContainerFloating: css({
label: 'error-container', label: 'error-container',
position: 'absolute', position: 'absolute',
left: '50%', left: 0,
transform: 'translateX(-50%)', top: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: theme.zIndex.tooltip, zIndex: theme.zIndex.tooltip,
}), }),
leftItems: css({ leftItems: css({
@@ -1,4 +1,4 @@
import { cx, css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
@@ -7,18 +7,17 @@ import { useStyles2 } from '../../themes';
import { ToolbarButton } from '../ToolbarButton/ToolbarButton'; import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
export interface Props { export interface Props {
className?: string;
message?: string; message?: string;
onClick?: (e: React.SyntheticEvent) => void; onClick?: (e: React.SyntheticEvent) => void;
ariaLabel?: string; ariaLabel?: string;
} }
export function PanelStatus({ className, message, onClick, ariaLabel = 'status' }: Props) { export function PanelStatus({ message, onClick, ariaLabel = 'status' }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return ( return (
<ToolbarButton <ToolbarButton
className={cx(className, styles.buttonStyles)} className={styles.buttonStyles}
onClick={onClick} onClick={onClick}
variant={'destructive'} variant={'destructive'}
icon="exclamation-triangle" icon="exclamation-triangle"
@@ -73,6 +73,7 @@ const getStyles = (theme: GrafanaTheme2) => {
}), }),
timeshift: css({ timeshift: css({
color: theme.colors.text.link, color: theme.colors.text.link,
gap: theme.spacing(0.5),
'&:hover': { '&:hover': {
color: theme.colors.emphasize(theme.colors.text.link, 0.03), color: theme.colors.emphasize(theme.colors.text.link, 0.03),
@@ -631,12 +631,13 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
const { transparent } = panel; const { transparent } = panel;
const alertState = data.alertState?.state; const alertState = data.alertState?.state;
const hasHoverHeader = this.hasOverlayHeader();
const containerClassNames = classNames({ const containerClassNames = classNames({
'panel-container': true, 'panel-container': true,
'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname), 'panel-container--absolute': isSoloRoute(locationService.getLocation().pathname),
'panel-container--transparent': transparent, 'panel-container--transparent': transparent,
'panel-container--no-title': this.hasOverlayHeader(), 'panel-container--no-title': hasHoverHeader,
[`panel-alert-state--${alertState}`]: alertState !== undefined, [`panel-alert-state--${alertState}`]: alertState !== undefined,
}); });
@@ -648,6 +649,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
(data.series.length > 0 && data.series.some((v) => (v.meta?.notices?.length ?? 0) > 0)) || (data.series.length > 0 && data.series.some((v) => (v.meta?.notices?.length ?? 0) > 0)) ||
(data.request && data.request.timeInfo) || (data.request && data.request.timeInfo) ||
alertState; alertState;
const titleItems = showTitleItems && ( const titleItems = showTitleItems && (
<PanelHeaderTitleItems <PanelHeaderTitleItems
key="title-items" key="title-items"
@@ -685,7 +687,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
dragClassCancel="grid-drag-cancel" dragClassCancel="grid-drag-cancel"
padding={padding} padding={padding}
hoverHeaderOffset={hoverHeaderOffset} hoverHeaderOffset={hoverHeaderOffset}
hoverHeader={title ? false : true} hoverHeader={this.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'} displayMode={transparent ? 'transparent' : 'default'}
> >
{(innerWidth, innerHeight) => ( {(innerWidth, innerHeight) => (