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:
co-authored by
Torkel Ödegaard
Polina Boneva
polinaboneva
parent
8ae4b9060b
commit
0eeeeef08b
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user