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
pull/65797/head
Torkel Ödegaard 2 years ago committed by GitHub
parent e9aef20eb4
commit c63cb5a0bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx
  2. 40
      packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx
  3. 124
      packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx
  4. 32
      packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx
  5. 2
      packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx
  6. 1
      packages/grafana-ui/src/utils/storybook/DashboardStoryCanvas.tsx

@ -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,6 +41,7 @@ export function HoverWidget({ menu, title, dragClass, children, offset = -32 }:
style={{ top: `${offset}px` }}
data-testid="hover-header-container"
>
{dragClass && (
<div
className={cx(styles.square, styles.draggable, dragClass)}
onPointerDown={onPointerDown}
@ -49,9 +50,10 @@ export function HoverWidget({ menu, title, dragClass, children, offset = -32 }:
>
<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', {
{renderPanel('Display mode = transparent', {
title: 'Default title',
displayMode: 'transparent',
menu,
leftItems: [
<PanelChrome.ErrorIndicator
key="errorIndicator"
error="Error text"
onClick={action('ErrorIndicator: onClick fired')}
})}
{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('Display mode = transparent', {
{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',
displayMode: 'transparent',
description: 'This is a description',
menu,
leftItems: [],
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>
)}
{statusMessage && (
<div className={styles.errorContainerFloating}>
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} ariaLabel="Panel status" />
@ -176,7 +181,6 @@ export function PanelChrome({
{headerContent}
<div className={styles.rightAligned}>
{menu && (
<PanelMenu
menu={menu}
@ -190,9 +194,6 @@ export function PanelChrome({
)}
/>
)}
{leftItems && <div className={styles.leftItems}>{itemsRenderer(leftItems, (item) => item)}</div>}
</div>
</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>;

Loading…
Cancel
Save