PanelChrome: Refactor and refine items next to title (#60514)

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Polina Boneva <13227501+polibb@users.noreply.github.com>
Co-authored-by: polinaboneva <polina.boneva@grafana.com>
This commit is contained in:
Alexa V
2023-01-16 15:56:39 +00:00
committed by GitHub
co-authored by Torkel Ödegaard Polina Boneva polinaboneva
parent 8ae4b9060b
commit 0eeeeef08b
14 changed files with 419 additions and 96 deletions
+1
View File
@@ -44,6 +44,7 @@ export const availableIconsIndex = {
'check-circle': true,
'check-square': true,
circle: true,
'circle-mono': true,
'clipboard-alt': true,
'clock-nine': true,
cloud: true,
@@ -1,6 +1,14 @@
import { IconName, IconSize } from '../../types/icon';
const alwaysMonoIcons: IconName[] = ['grafana', 'favorite', 'heart-break', 'heart', 'panel-add', 'library-panel'];
const alwaysMonoIcons: IconName[] = [
'grafana',
'favorite',
'heart-break',
'heart',
'panel-add',
'library-panel',
'circle-mono',
];
export function getIconSubDir(name: IconName, type: string): string {
if (name?.startsWith('gf-')) {
@@ -12,8 +12,6 @@ import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
import { Menu } from '../Menu/Menu';
import { PanelChromeInfoState } from './PanelChrome';
const meta: ComponentMeta<typeof PanelChrome> = {
title: 'Visualizations/PanelChrome',
component: PanelChrome,
@@ -235,29 +233,11 @@ const ErrorIcon = [
const leftItems = { LoadingIcon, ErrorIcon, Default };
const titleItems: PanelChromeInfoState[] = [
{
icon: 'info',
tooltip:
'Description text with very long descriptive words that describe what is going on in the panel and not beyond. Or maybe beyond, not up to us.',
},
{
icon: 'external-link-alt',
tooltip: 'wearegoingonanadventure.openanewtab.maybe',
onClick: () => {},
},
{
icon: 'clock-nine',
tooltip: 'Time range: 2021-09-01 00:00:00 to 2021-09-01 00:00:00',
onClick: () => {},
},
{
icon: 'heart',
tooltip: 'Health of the panel',
},
];
const description =
'Description text with very long descriptive words that describe what is going on in the panel and not beyond. Or maybe beyond, not up to us.';
Basic.argTypes = {
description: { control: { type: 'text' } },
leftItems: {
options: Object.keys(leftItems),
mapping: leftItems,
@@ -276,9 +256,8 @@ Basic.args = {
width: 400,
height: 200,
title: 'Very long title that should get ellipsis when there is no more space',
titleItems,
description,
menu,
loadingState: LoadingState.Loading,
};
export default meta;
@@ -49,13 +49,7 @@ it('renders panel with a header with title in place if prop title', () => {
it('renders panel with a header if prop titleItems', () => {
setup({
titleItems: [
{
icon: 'info-circle',
tooltip: 'This is the panel description',
onClick: () => {},
},
],
titleItems: [<div key="title-item-test"> This should be a self-contained node </div>],
});
expect(screen.getByTestId('header-container')).toBeInTheDocument();
@@ -63,13 +57,7 @@ it('renders panel with a header if prop titleItems', () => {
it('renders panel with a header with icons in place if prop titleItems', () => {
setup({
titleItems: [
{
icon: 'info-circle',
tooltip: 'This is the panel description',
onClick: () => {},
},
],
titleItems: [<div key="title-item-test"> This should be a self-contained node </div>],
});
expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
@@ -1,18 +1,18 @@
import { css, cx } from '@emotion/css';
import { isEmpty } from 'lodash';
import React, { CSSProperties, ReactElement, ReactNode } from 'react';
import React, { CSSProperties, ReactNode, ReactElement } from 'react';
import { GrafanaTheme2, isIconName, LoadingState } from '@grafana/data';
import { GrafanaTheme2, LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2, useTheme2 } from '../../themes';
import { IconName } from '../../types/icon';
import { Dropdown } from '../Dropdown/Dropdown';
import { Icon } from '../Icon/Icon';
import { IconButton, IconButtonVariant } from '../IconButton/IconButton';
import { LoadingBar } from '../LoadingBar/LoadingBar';
import { ToolbarButton } from '../ToolbarButton';
import { PopoverContent, Tooltip } from '../Tooltip';
import { Tooltip } from '../Tooltip';
import { PanelDescription } from './PanelDescription';
import { PanelStatus } from './PanelStatus';
interface Status {
@@ -20,17 +20,6 @@ interface Status {
onClick?: (e: React.SyntheticEvent) => void;
}
/**
* @internal
*/
export interface PanelChromeInfoState {
icon: IconName;
label?: string | ReactNode;
tooltip?: PopoverContent;
variant?: IconButtonVariant;
onClick?: () => void;
}
/**
* @internal
*/
@@ -40,7 +29,8 @@ export interface PanelChromeProps {
children: (innerWidth: number, innerHeight: number) => ReactNode;
padding?: PanelPadding;
title?: string;
titleItems?: PanelChromeInfoState[];
description?: string | (() => string);
titleItems?: ReactNode[];
menu?: ReactElement | (() => ReactElement);
/** dragClass, hoverHeader not yet implemented */
// dragClass?: string;
@@ -69,6 +59,7 @@ export function PanelChrome({
children,
padding = 'md',
title = '',
description = '',
titleItems = [],
menu,
// dragClass,
@@ -82,7 +73,16 @@ export function PanelChrome({
// To Do rely on hoverHeader prop for header, not separate props
// once hoverHeader is implemented
const hasHeader = title.length > 0 || leftItems.length > 0;
//
// Backwards compatibility for having a designated space for the header
const hasHeader =
hoverHeader === false &&
(title.length > 0 ||
titleItems.length > 0 ||
description !== '' ||
loadingState === LoadingState.Streaming ||
leftItems.length > 0);
const headerHeight = getHeaderHeight(theme, hasHeader);
const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height);
@@ -114,8 +114,10 @@ export function PanelChrome({
return null;
}
};
const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel';
return (
<div className={styles.container} style={containerStyles}>
<div className={styles.container} style={containerStyles} aria-label={ariaLabel}>
<div className={styles.loadingBarContainer}>
{showLoading ? <LoadingBar width={'28%'} height={'2px'} /> : null}
</div>
@@ -127,29 +129,19 @@ export function PanelChrome({
</h6>
)}
{showStreaming && (
<div className={styles.item} style={itemStyles}>
<Tooltip content="Streaming">
<Icon name="circle" type="mono" size="sm" className={styles.streaming} />
</Tooltip>
<PanelDescription description={description} />
{titleItems && (
<div className={styles.titleItems} data-testid="title-items-container">
{titleItems.map((item) => item)}
</div>
)}
{titleItems.length > 0 && (
<div className={styles.items} data-testid="title-items-container">
{titleItems
.filter((item) => isIconName(item.icon))
.map((item, i) => (
<div key={`${item.icon}-${i}`} className={styles.item} style={itemStyles}>
{item.onClick ? (
<IconButton tooltip={item.tooltip} name={item.icon} size="sm" onClick={item.onClick} />
) : (
<Tooltip content={item.tooltip ?? ''}>
<Icon name={item.icon} size="sm" />
</Tooltip>
)}
</div>
))}
{showStreaming && (
<div className={styles.item} style={itemStyles}>
<Tooltip content="Streaming">
<Icon name="circle-mono" size="sm" className={styles.streaming} />
</Tooltip>
</div>
)}
@@ -172,7 +164,6 @@ export function PanelChrome({
{renderStatus()}
</div>
<div className={styles.content} style={contentStyle}>
{children(innerWidth, innerHeight)}
</div>
@@ -299,5 +290,11 @@ const getStyles = (theme: GrafanaTheme2) => {
display: 'flex',
alignItems: 'center',
}),
titleItems: css({
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
padding: theme.spacing(1),
}),
};
};
@@ -0,0 +1,75 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2 } from '../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
import { Icon } from '../Icon/Icon';
import { Tooltip } from '../Tooltip';
interface Props {
description: string | (() => string);
}
export function PanelDescription({ description }: Props) {
const theme = useTheme2();
const styles = getStyles(theme);
const getDescriptionContent = (): JSX.Element => {
// description
const panelDescription = typeof description === 'function' ? description() : description;
return (
<div className="panel-info-content markdown-html">
<div dangerouslySetInnerHTML={{ __html: panelDescription }} />
</div>
);
};
return description !== '' ? (
<Tooltip interactive content={getDescriptionContent}>
<span className={styles.description}>
<Icon name="info-circle" size="lg" aria-label="description" />
</span>
</Tooltip>
) : null;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
description: css({
color: `${theme.colors.text.secondary}`,
backgroundColor: `${theme.colors.background.primary}`,
cursor: 'auto',
border: 'none',
borderRadius: `${theme.shape.borderRadius()}`,
padding: `${theme.spacing(0, 1)}`,
height: ` ${theme.spacing(theme.components.height.md)}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:focus, &:focus-visible': {
...getFocusStyles(theme),
zIndex: 1,
},
'&: focus:not(:focus-visible)': getMouseFocusStyles(theme),
'&:hover ': {
boxShadow: `${theme.shadows.z1}`,
color: `${theme.colors.text.primary}`,
background: `${theme.colors.background.secondary}`,
},
code: {
whiteSpace: 'normal',
wordWrap: 'break-word',
},
'pre > code': {
display: 'block',
},
}),
};
};
@@ -38,6 +38,8 @@ export {
type ErrorIndicatorProps as PanelChromeErrorIndicatorProps,
} from './ErrorIndicator';
export { PanelDescription } from './PanelDescription';
export { usePanelContext, PanelContextProvider, type PanelContext, PanelContextRoot } from './PanelContext';
export * from './types';
@@ -58,6 +58,7 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme
<>
{React.cloneElement(children, {
ref: setTriggerRef,
tabIndex: 0, // tooltip should be keyboard focusable
})}
{visible && (
<Portal>