mirror of https://github.com/grafana/grafana.git
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:
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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { css } from '@emotion/css';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { QueryResultMetaNotice } from '@grafana/data';
|
||||
import { Icon, Tooltip } from '@grafana/ui';
|
||||
import { GrafanaTheme2, QueryResultMetaNotice } from '@grafana/data';
|
||||
import { Icon, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins';
|
||||
|
||||
interface Props {
|
||||
notice: QueryResultMetaNotice;
|
||||
|
|
@ -9,20 +11,67 @@ interface Props {
|
|||
}
|
||||
|
||||
export const PanelHeaderNotice: FC<Props> = ({ notice, onClick }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const iconName =
|
||||
notice.severity === 'error' || notice.severity === 'warning' ? 'exclamation-triangle' : 'info-circle';
|
||||
|
||||
if (notice.inspect && onClick) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
className={styles.notice}
|
||||
icon={iconName}
|
||||
key={notice.severity}
|
||||
tooltip={notice.text}
|
||||
onClick={(e) => onClick(e, notice.inspect!)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (notice.link) {
|
||||
return (
|
||||
<a className={styles.notice} aria-label={notice.text} href={notice.link} target="_blank" rel="noreferrer">
|
||||
<Icon name={iconName} style={{ marginRight: '8px' }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={notice.text} key={notice.severity}>
|
||||
{notice.inspect ? (
|
||||
<div className="panel-info-notice pointer" onClick={(e) => onClick(e, notice.inspect!)}>
|
||||
<Icon name={iconName} style={{ marginRight: '8px' }} />
|
||||
</div>
|
||||
) : (
|
||||
<a className="panel-info-notice" href={notice.link} target="_blank" rel="noreferrer">
|
||||
<Icon name={iconName} style={{ marginRight: '8px' }} />
|
||||
</a>
|
||||
)}
|
||||
<Tooltip key={notice.severity} content={notice.text}>
|
||||
<span className={styles.iconTooltip}>
|
||||
<Icon name={iconName} size="lg" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
notice: css({
|
||||
border: 'none',
|
||||
borderRadius: theme.shape.borderRadius(),
|
||||
}),
|
||||
iconTooltip: 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}`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { PanelData, GrafanaTheme2, PanelModel, LinkModel, AlertState, DataLink } from '@grafana/data';
|
||||
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins';
|
||||
|
||||
import { PanelLinks } from '../PanelLinks';
|
||||
|
||||
import { PanelHeaderNotices } from './PanelHeaderNotices';
|
||||
|
||||
export interface Props {
|
||||
alertState?: string;
|
||||
data: PanelData;
|
||||
panelId: number;
|
||||
onShowPanelLinks?: () => Array<LinkModel<PanelModel>>;
|
||||
panelLinks?: DataLink[];
|
||||
}
|
||||
|
||||
export function PanelHeaderTitleItems(props: Props) {
|
||||
const { alertState, data, panelId, onShowPanelLinks, panelLinks } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// panel health
|
||||
const alertStateItem = (
|
||||
<Tooltip content={`alerting is ${alertState}`}>
|
||||
<span
|
||||
className={cx(styles.item, {
|
||||
[styles.ok]: alertState === AlertState.OK,
|
||||
[styles.pending]: alertState === AlertState.Pending,
|
||||
[styles.alerting]: alertState === AlertState.Alerting,
|
||||
})}
|
||||
>
|
||||
<Icon name={alertState === 'alerting' ? 'heart-break' : 'heart'} className="panel-alert-icon" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const timeshift = (
|
||||
<>
|
||||
<Tooltip
|
||||
content={data.request?.range ? `Time Range: ${data.request.range.from} to ${data.request.range.to}` : ''}
|
||||
>
|
||||
<span className={cx(styles.item, styles.timeshift)}>
|
||||
<Icon name="clock-nine" />
|
||||
{data.request?.timeInfo}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{panelLinks && panelLinks.length > 0 && onShowPanelLinks && (
|
||||
<PanelLinks onShowPanelLinks={onShowPanelLinks} panelLinks={panelLinks} />
|
||||
)}
|
||||
|
||||
{<PanelHeaderNotices panelId={panelId} frames={data.series} />}
|
||||
{data.request && data.request.timeInfo && timeshift}
|
||||
{alertState && alertStateItem}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
item: css({
|
||||
label: 'panel-header-item',
|
||||
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}`,
|
||||
background: `${theme.colors.background.secondary}`,
|
||||
},
|
||||
}),
|
||||
ok: css({
|
||||
color: theme.colors.success.text,
|
||||
}),
|
||||
pending: css({
|
||||
color: theme.colors.warning.text,
|
||||
}),
|
||||
alerting: css({
|
||||
color: theme.colors.error.text,
|
||||
}),
|
||||
timeshift: css({
|
||||
color: theme.colors.text.link,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.emphasize(theme.colors.text.link, 0.03),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataLink, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { Dropdown, Icon, Menu, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins';
|
||||
|
||||
interface Props {
|
||||
panelLinks: DataLink[];
|
||||
onShowPanelLinks: () => LinkModel[];
|
||||
}
|
||||
|
||||
export function PanelLinks({ panelLinks, onShowPanelLinks }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const getLinksContent = (): JSX.Element => {
|
||||
const interpolatedLinks = onShowPanelLinks();
|
||||
return (
|
||||
<Menu>
|
||||
{interpolatedLinks?.map((link, idx) => {
|
||||
return <Menu.Item key={idx} label={link.title} url={link.href} target={link.target} />;
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
if (panelLinks.length === 1) {
|
||||
const linkModel = onShowPanelLinks()[0];
|
||||
return (
|
||||
<a
|
||||
href={linkModel.href}
|
||||
onClick={linkModel.onClick}
|
||||
target={linkModel.target}
|
||||
title={linkModel.title}
|
||||
className={styles.singleLink}
|
||||
>
|
||||
<Icon name="external-link-alt" size="lg" />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Dropdown overlay={getLinksContent}>
|
||||
<ToolbarButton icon="external-link-alt" aria-label="panel links" className={styles.menuTrigger} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
menuTrigger: css({
|
||||
border: 'none',
|
||||
borderRadius: `${theme.shape.borderRadius()}`,
|
||||
cursor: 'context-menu',
|
||||
}),
|
||||
singleLink: css({
|
||||
color: theme.colors.text.secondary,
|
||||
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}`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
@ -11,17 +11,19 @@ import {
|
|||
EventFilterOptions,
|
||||
FieldConfigSource,
|
||||
getDefaultTimeRange,
|
||||
LinkModel,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelPlugin,
|
||||
PanelPluginMeta,
|
||||
PluginContextProvider,
|
||||
renderMarkdown,
|
||||
TimeRange,
|
||||
toDataFrameDTO,
|
||||
toUtc,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService, RefreshEvent } from '@grafana/runtime';
|
||||
import { getTemplateSrv, config, locationService, RefreshEvent } from '@grafana/runtime';
|
||||
import { VizLegendOptions } from '@grafana/schema';
|
||||
import {
|
||||
ErrorBoundary,
|
||||
|
|
@ -35,6 +37,7 @@ import { PANEL_BORDER } from 'app/core/constants';
|
|||
import { profiler } from 'app/core/profiler';
|
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory';
|
||||
import { RenderEvent } from 'app/types/events';
|
||||
|
||||
|
|
@ -47,6 +50,7 @@ import { loadSnapshotData } from '../utils/loadSnapshotData';
|
|||
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper';
|
||||
import { PanelHeaderTitleItems } from './PanelHeader/PanelHeaderTitleItems';
|
||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { liveTimer } from './liveTimer';
|
||||
|
||||
|
|
@ -567,6 +571,27 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
|||
return !panel.hasTitle();
|
||||
}
|
||||
|
||||
onShowPanelDescription = () => {
|
||||
const { panel } = this.props;
|
||||
const descriptionMarkdown = getTemplateSrv().replace(panel.description, panel.scopedVars);
|
||||
const interpolatedDescription = renderMarkdown(descriptionMarkdown);
|
||||
return interpolatedDescription;
|
||||
};
|
||||
|
||||
onShowPanelLinks = (): LinkModel[] => {
|
||||
const { panel } = this.props;
|
||||
const linkSupplier = getPanelLinksSupplier(panel);
|
||||
if (linkSupplier) {
|
||||
const panelLinks = linkSupplier && linkSupplier.getLinks(panel.replaceVariables);
|
||||
return panelLinks;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
onOpenInspector = (e: React.SyntheticEvent, tab: string) => {
|
||||
e.stopPropagation();
|
||||
locationService.partial({ inspect: this.props.panel.id, inspectTab: tab });
|
||||
};
|
||||
onOpenErrorInspect(e: React.SyntheticEvent, tab: string) {
|
||||
e.stopPropagation();
|
||||
locationService.partial({ inspect: this.props.panel.id, inspectTab: tab });
|
||||
|
|
@ -590,6 +615,17 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
|||
const title = panel.getDisplayTitle();
|
||||
const padding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
||||
|
||||
const titleItems = [
|
||||
<PanelHeaderTitleItems
|
||||
key="title-items"
|
||||
alertState={alertState}
|
||||
data={data}
|
||||
panelId={panel.id}
|
||||
panelLinks={panel.links}
|
||||
onShowPanelLinks={this.onShowPanelLinks}
|
||||
/>,
|
||||
];
|
||||
|
||||
let menu;
|
||||
if (!dashboard.meta.publicDashboardAccessToken) {
|
||||
menu = (
|
||||
|
|
@ -610,14 +646,16 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
|||
<PanelChrome
|
||||
width={width}
|
||||
height={height}
|
||||
padding={padding}
|
||||
title={title}
|
||||
menu={menu}
|
||||
loadingState={data.state}
|
||||
status={{
|
||||
message: errorMessage,
|
||||
onClick: (e: React.SyntheticEvent) => this.onOpenErrorInspect(e, InspectTab.Error),
|
||||
}}
|
||||
description={!!panel.description ? this.onShowPanelDescription : undefined}
|
||||
titleItems={titleItems}
|
||||
menu={menu}
|
||||
padding={padding}
|
||||
>
|
||||
{(innerWidth, innerHeight) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>
|
||||
|
After Width: | Height: | Size: 99 B |
|
|
@ -574,4 +574,4 @@
|
|||
"option-tooltip": "Cľęäř şęľęčŧįőʼnş"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue