mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelChrome: Add option to show actions on the right side (actions = leftItems) (#65762)
* PanelChrome: Add option to show actions on the right side * remove button style change * Added docs and minor tweaks to align the type with titleItems * Hover header fixes, storybook improvements, and title description fix * Fixed condition for drag icon in hover header
This commit is contained in:
@@ -10,7 +10,7 @@ import { PanelMenu } from './PanelMenu';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
menu: ReactElement | (() => ReactElement);
|
||||
menu?: ReactElement | (() => ReactElement);
|
||||
title?: string;
|
||||
offset?: number;
|
||||
dragClass?: string;
|
||||
@@ -41,17 +41,19 @@ export function HoverWidget({ menu, title, dragClass, children, offset = -32 }:
|
||||
style={{ top: `${offset}px` }}
|
||||
data-testid="hover-header-container"
|
||||
>
|
||||
<div
|
||||
className={cx(styles.square, styles.draggable, dragClass)}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
ref={draggableRef}
|
||||
>
|
||||
<Icon name="expand-arrows" className={styles.draggableIcon} />
|
||||
</div>
|
||||
{dragClass && (
|
||||
<div
|
||||
className={cx(styles.square, styles.draggable, dragClass)}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
ref={draggableRef}
|
||||
>
|
||||
<Icon name="expand-arrows" className={styles.draggableIcon} />
|
||||
</div>
|
||||
)}
|
||||
{!title && <h6 className={cx(styles.untitled, styles.draggable, dragClass)}>Untitled</h6>}
|
||||
{children}
|
||||
<div className={styles.square}>
|
||||
{menu && (
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
@@ -59,7 +61,7 @@ export function HoverWidget({ menu, title, dragClass, children, offset = -32 }:
|
||||
menuButtonClass={styles.menuButton}
|
||||
onVisibleChange={setMenuOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -92,7 +94,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
alignItems: 'center',
|
||||
width: theme.spacing(4),
|
||||
height: '100%',
|
||||
paddingRight: theme.spacing(0.5),
|
||||
}),
|
||||
draggable: css({
|
||||
cursor: 'move',
|
||||
@@ -109,12 +110,10 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
background: theme.colors.secondary.main,
|
||||
},
|
||||
}),
|
||||
title: css({
|
||||
padding: theme.spacing(0.75),
|
||||
}),
|
||||
untitled: css({
|
||||
color: theme.colors.text.disabled,
|
||||
fontStyle: 'italic',
|
||||
padding: theme.spacing(0, 1),
|
||||
marginBottom: 0,
|
||||
}),
|
||||
draggableIcon: css({
|
||||
|
||||
@@ -56,7 +56,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -133,7 +133,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -170,7 +170,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -202,7 +202,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'white',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -232,7 +232,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -258,7 +258,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -285,7 +285,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -300,7 +300,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
</HorizontalGroup>
|
||||
</Canvas>
|
||||
|
||||
### Extra options? Title Items
|
||||
### Extra options? Title items and actions
|
||||
|
||||
```tsx
|
||||
<PanelChrome
|
||||
@@ -311,8 +311,12 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
<Button fill="text" icon="sliders-v-alt" variant="secondary" tooltip="extra content2 to render" />
|
||||
</div>
|
||||
}
|
||||
description="Here I will put a description that explains a bit more this panel"
|
||||
width={400}
|
||||
actions={
|
||||
<Button size="sm" variant="secondary" key="A">
|
||||
Breakdown
|
||||
</Button>
|
||||
}
|
||||
width={500}
|
||||
height={200}
|
||||
>
|
||||
{(innerwidth, innerheight) => {
|
||||
@@ -321,7 +325,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'white',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -337,14 +341,18 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
<Canvas>
|
||||
<PanelChrome
|
||||
title="My awesome panel title"
|
||||
description="Here I will put a description that explains a bit more this panel"
|
||||
titleItems={
|
||||
<div>
|
||||
<Button fill="text" icon="github" variant="secondary" tooltip="extra content to render" />
|
||||
<Button fill="text" icon="sliders-v-alt" variant="secondary" tooltip="extra content2 to render" />
|
||||
</div>
|
||||
}
|
||||
width={400}
|
||||
actions={
|
||||
<Button size="sm" variant="secondary" key="A">
|
||||
Breakdown
|
||||
</Button>
|
||||
}
|
||||
width={500}
|
||||
height={200}
|
||||
>
|
||||
{(innerwidth, innerheight) => {
|
||||
@@ -353,7 +361,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -415,7 +423,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -470,7 +478,7 @@ Component used for rendering content wrapped in the same style as grafana panels
|
||||
style={{
|
||||
width: innerwidth,
|
||||
height: innerheight,
|
||||
background: 'gray',
|
||||
background: 'rgba(230,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { CSSProperties, useState, ReactNode } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { PanelChrome, PanelChromeProps } from '@grafana/ui';
|
||||
import { Button, Icon, PanelChrome, PanelChromeProps, RadioButtonGroup } from '@grafana/ui';
|
||||
|
||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
@@ -40,7 +40,7 @@ function getContentStyle(): CSSProperties {
|
||||
function renderPanel(name: string, overrides?: Partial<PanelChromeProps>) {
|
||||
const props: PanelChromeProps = {
|
||||
width: 400,
|
||||
height: 130,
|
||||
height: 150,
|
||||
children: () => undefined,
|
||||
};
|
||||
|
||||
@@ -131,10 +131,6 @@ export const Examples = () => {
|
||||
{renderPanel('No title, streaming loadingState', {
|
||||
loadingState: LoadingState.Streaming,
|
||||
})}
|
||||
{renderPanel('No title, loading loadingState', {
|
||||
loadingState: LoadingState.Loading,
|
||||
})}
|
||||
|
||||
{renderPanel('Error status, menu', {
|
||||
title: 'Default title',
|
||||
menu,
|
||||
@@ -183,22 +179,120 @@ export const Examples = () => {
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
{renderPanel('Deprecated error indicator, menu', {
|
||||
title: 'Default title',
|
||||
menu,
|
||||
leftItems: [
|
||||
<PanelChrome.ErrorIndicator
|
||||
key="errorIndicator"
|
||||
error="Error text"
|
||||
onClick={action('ErrorIndicator: onClick fired')}
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
{renderPanel('Display mode = transparent', {
|
||||
title: 'Default title',
|
||||
displayMode: 'transparent',
|
||||
menu,
|
||||
leftItems: [],
|
||||
})}
|
||||
{renderPanel('Actions with button no menu', {
|
||||
title: 'Actions with button no menu',
|
||||
actions: (
|
||||
<Button size="sm" variant="secondary" key="A">
|
||||
Breakdown
|
||||
</Button>
|
||||
),
|
||||
})}
|
||||
{renderPanel('Panel with two actions', {
|
||||
title: 'I have two buttons',
|
||||
actions: [
|
||||
<Button size="sm" variant="secondary" key="A">
|
||||
Breakdown
|
||||
</Button>,
|
||||
<Button size="sm" variant="secondary" icon="times" key="B" />,
|
||||
],
|
||||
})}
|
||||
{renderPanel('With radio button', {
|
||||
title: 'I have a radio button',
|
||||
actions: [
|
||||
<RadioButtonGroup
|
||||
key="radio-button-group"
|
||||
size="sm"
|
||||
value="A"
|
||||
options={[
|
||||
{ label: 'Graph', value: 'A' },
|
||||
{ label: 'Table', value: 'B' },
|
||||
]}
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
{renderPanel('Panel with action link', {
|
||||
title: 'Panel with action link',
|
||||
actions: (
|
||||
<a className="external-link" href="/some/page">
|
||||
Error details
|
||||
<Icon name="arrow-right" />
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
{renderPanel('Action and menu (should be rare)', {
|
||||
title: 'Action and menu',
|
||||
menu,
|
||||
actions: (
|
||||
<Button size="sm" variant="secondary">
|
||||
Breakdown
|
||||
</Button>
|
||||
),
|
||||
})}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</DashboardStoryCanvas>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExamplesHoverHeader = () => {
|
||||
return (
|
||||
<DashboardStoryCanvas>
|
||||
<div>
|
||||
<HorizontalGroup spacing="md" align="flex-start" wrap>
|
||||
{renderPanel('Title items, menu, hover header', {
|
||||
title: 'Default title',
|
||||
description: 'This is a description',
|
||||
menu,
|
||||
hoverHeader: true,
|
||||
dragClass: 'draggable',
|
||||
titleItems: (
|
||||
<PanelChrome.TitleItem title="Online">
|
||||
<Icon name="heart" />
|
||||
</PanelChrome.TitleItem>
|
||||
),
|
||||
})}
|
||||
{renderPanel('Multiple title items', {
|
||||
title: 'Default title',
|
||||
menu,
|
||||
hoverHeader: true,
|
||||
dragClass: 'draggable',
|
||||
titleItems: [
|
||||
<PanelChrome.TitleItem title="Online" key="A">
|
||||
<Icon name="heart" />
|
||||
</PanelChrome.TitleItem>,
|
||||
<PanelChrome.TitleItem title="Link" key="B" onClick={() => {}}>
|
||||
<Icon name="external-link-alt" />
|
||||
</PanelChrome.TitleItem>,
|
||||
],
|
||||
})}
|
||||
{renderPanel('Hover header, loading loadingState', {
|
||||
loadingState: LoadingState.Loading,
|
||||
hoverHeader: true,
|
||||
title: 'I am a hover header',
|
||||
dragClass: 'draggable',
|
||||
})}
|
||||
{renderPanel('No title, Hover header', {
|
||||
hoverHeader: true,
|
||||
dragClass: 'draggable',
|
||||
})}
|
||||
{renderPanel('Should not have drag icon', {
|
||||
title: 'No drag icon',
|
||||
hoverHeader: true,
|
||||
})}
|
||||
{renderPanel('With action link', {
|
||||
title: 'With link in hover header',
|
||||
hoverHeader: true,
|
||||
actions: (
|
||||
<a className="external-link" href="/some/page">
|
||||
Error details
|
||||
<Icon name="arrow-right" />
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
||||
@@ -47,13 +47,10 @@ export interface PanelChromeProps {
|
||||
*/
|
||||
statusMessageOnClick?: (e: React.SyntheticEvent) => void;
|
||||
/**
|
||||
* @deprecated in favor of props
|
||||
* statusMessage for error messages
|
||||
* and loadingState for loading and streaming data
|
||||
* which will serve the same purpose
|
||||
* of showing/interacting with the panel's state
|
||||
*/
|
||||
* @deprecated use `actions' instead
|
||||
**/
|
||||
leftItems?: ReactNode[];
|
||||
actions?: ReactNode;
|
||||
displayMode?: 'default' | 'transparent';
|
||||
onCancelQuery?: () => void;
|
||||
}
|
||||
@@ -84,6 +81,7 @@ export function PanelChrome({
|
||||
statusMessage,
|
||||
statusMessageOnClick,
|
||||
leftItems,
|
||||
actions,
|
||||
onCancelQuery,
|
||||
}: PanelChromeProps) {
|
||||
const theme = useTheme2();
|
||||
@@ -111,6 +109,11 @@ export function PanelChrome({
|
||||
containerStyles.border = 'none';
|
||||
}
|
||||
|
||||
/** Old property name now maps to actions */
|
||||
if (leftItems) {
|
||||
actions = leftItems;
|
||||
}
|
||||
|
||||
const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel';
|
||||
|
||||
const headerContent = (
|
||||
@@ -142,6 +145,9 @@ export function PanelChrome({
|
||||
</Tooltip>
|
||||
</DelayRender>
|
||||
)}
|
||||
<div className={styles.rightAligned}>
|
||||
{actions && <div className={styles.rightActions}>{itemsRenderer(actions, (item) => item)}</div>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -153,11 +159,10 @@ export function PanelChrome({
|
||||
|
||||
{hoverHeader && !isTouchDevice && (
|
||||
<>
|
||||
{menu && (
|
||||
<HoverWidget menu={menu} title={title} offset={hoverHeaderOffset} dragClass={dragClass}>
|
||||
{headerContent}
|
||||
</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" />
|
||||
@@ -176,23 +181,19 @@ export function PanelChrome({
|
||||
|
||||
{headerContent}
|
||||
|
||||
<div className={styles.rightAligned}>
|
||||
{menu && (
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
placement="bottom-end"
|
||||
menuButtonClass={cx(
|
||||
{ [styles.hiddenMenu]: !isTouchDevice },
|
||||
styles.menuItem,
|
||||
dragClassCancel,
|
||||
showOnHoverClass
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{leftItems && <div className={styles.leftItems}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
</div>
|
||||
{menu && (
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
placement="bottom-end"
|
||||
menuButtonClass={cx(
|
||||
{ [styles.hiddenMenu]: !isTouchDevice },
|
||||
styles.menuItem,
|
||||
dragClassCancel,
|
||||
showOnHoverClass
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -203,7 +204,7 @@ export function PanelChrome({
|
||||
);
|
||||
}
|
||||
|
||||
const itemsRenderer = (items: ReactNode[], renderer: (items: ReactNode[]) => ReactNode): ReactNode => {
|
||||
const itemsRenderer = (items: ReactNode[] | ReactNode, renderer: (items: ReactNode[]) => ReactNode): ReactNode => {
|
||||
const toRender = React.Children.toArray(items).filter(Boolean);
|
||||
return toRender.length > 0 ? renderer(toRender) : null;
|
||||
};
|
||||
@@ -339,9 +340,10 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
top: 0,
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
}),
|
||||
leftItems: css({
|
||||
rightActions: css({
|
||||
display: 'flex',
|
||||
paddingRight: theme.spacing(padding),
|
||||
padding: theme.spacing(0, padding),
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
rightAligned: css({
|
||||
label: 'right-aligned-container',
|
||||
|
||||
@@ -31,7 +31,7 @@ export function PanelDescription({ description, className }: Props) {
|
||||
return description !== '' ? (
|
||||
<Tooltip interactive content={getDescriptionContent}>
|
||||
<TitleItem className={cx(className, styles.description)}>
|
||||
<Icon name="info-circle" size="md" title="description" />
|
||||
<Icon name="info-circle" size="md" />
|
||||
</TitleItem>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
@@ -14,6 +14,7 @@ export const DashboardStoryCanvas = ({ children }: Props) => {
|
||||
height: 100%;
|
||||
padding: 32px;
|
||||
background: ${theme.colors.background.canvas};
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
return <div className={style}>{children}</div>;
|
||||
|
||||
Reference in New Issue
Block a user