mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
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:
co-authored by
polinaboneva
parent
4ee389676e
commit
fb55dd5df6
@@ -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) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user