mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelChrome: Implement Panel header with error, loading, and streaming data status (#60147)
* dashboards squad mob! 🔱
lastFile:packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx
* dashboards squad mob! 🔱
* dashboards squad mob! 🔱
lastFile:packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx
* user essentials mob! 🔱
* create grafana/ui LoadingBar and set it up in Storybook
* Remove test changes on PanelChrome
* Fix mdx page reference
* dashboards squad mob! 🔱
lastFile:public/api-merged.json
* dashboards squad mob! 🔱
* dashboards squad mob! 🔱
* dashboards squad mob! 🔱
* dashboards squad mob! 🔱
lastFile:public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderState.tsx
* Implemented basic draft of panel header states. Using ToolbarButton instead of IconButton.
* use 'warning' styled Button in ToolbarButton
* make LoadingBar a simple JSX Element; do not use containerWidth; have a wrapper around the loading bar itself;
* fix wrapper around LoadingBar: willChange css prop makes performance of rerendering better
* States: Render general panel query error states and render notices next
to the title
* add streaming to PanelChrome if data is streaming instead of loading
* PanelHeaderState with its own state 'mode'
* clean up useEffect
* notices have their own square space in the size of the panel header
* clean up
* minor fixes
* moving the LoadingBar to core
* LoadingBar is not in grafana/ui
* always have a place for the loading bar in the PanelChrome, otherwise it moves everything when appearing;
remove titleItemsNodes for now - in later development
make no changes to Notice component, not part of this PR
* Revert "moving the LoadingBar to core"
This reverts commit 11f0f4ff2f.
* do not use internal comment as it doesn't do anything
* integrate LoadingBar in PanelChrome from grafana/ui directly
* fix deprecated leftItems comment
* Modify annimation to 1 second
* remove comments
* remove streaming stopped UI because we cannot know when the streaming has stopped
* skip unnecessary test for now
* no point in removing hoverHeader now, even though it's not yet implemented
* small fixes
* error state of the data in a panel is positioned in PanelChrome itself, not in PanelHeaderState
* Fixed loading state jitter
* remove warning state as we have none of it
* streaming cannot be stopped from the icon
* explicit content container width and height
* explicit content container width and height
* edit deprecated comment
* fix LoadingBar to be relative to width of panel; remove explicit width and height on content strict
* no warning state of the data
* status of the panel data given directly to PanelChrome, not a node
* clean up
* clean up console log
Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
* panel title design fits typography h6 styles; render error status only if error or error message are passed to PanelChrome
* add storybook examples; prepare PanelChrome for hoverHeader because this will be a breaking change and it will affect how the storybook example shows up
* show storybook example for streaming panel with title because that's the condition for having a header
* override margin-bottom: 0.45em of h6
Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,7 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
name: IconName;
|
name: IconName;
|
||||||
/** Icon size */
|
/** Icon size */
|
||||||
size?: IconSize;
|
size?: IconSize;
|
||||||
/** Type od the icon - mono or default */
|
/** Type of the icon - mono or default */
|
||||||
iconType?: IconType;
|
iconType?: IconType;
|
||||||
/** Tooltip content to display on hover */
|
/** Tooltip content to display on hover */
|
||||||
tooltip?: PopoverContent;
|
tooltip?: PopoverContent;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const getStyles = (width?: string, height?: string) => (_: GrafanaTheme2) => {
|
|||||||
transform: 'translateX(0)',
|
transform: 'translateX(0)',
|
||||||
},
|
},
|
||||||
'100%': {
|
'100%': {
|
||||||
transform: `translateX(calc(100% - ${barWidth}))`,
|
transform: `translateX(100%)`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { merge } from 'lodash';
|
|||||||
import React, { CSSProperties, useState, ReactNode } from 'react';
|
import React, { CSSProperties, useState, ReactNode } from 'react';
|
||||||
import { useInterval } from 'react-use';
|
import { useInterval } from 'react-use';
|
||||||
|
|
||||||
|
import { LoadingState } from '@grafana/data';
|
||||||
import { PanelChrome, PanelChromeProps } from '@grafana/ui';
|
import { PanelChrome, PanelChromeProps } from '@grafana/ui';
|
||||||
|
|
||||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
||||||
@@ -64,26 +65,21 @@ export const Examples = () => {
|
|||||||
<DashboardStoryCanvas>
|
<DashboardStoryCanvas>
|
||||||
<HorizontalGroup spacing="md" align="flex-start">
|
<HorizontalGroup spacing="md" align="flex-start">
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
{renderPanel('Default panel with error state indicator', {
|
{renderPanel('Default panel with error status', {
|
||||||
title: 'Default title',
|
title: 'Default title',
|
||||||
leftItems: [
|
status: {
|
||||||
<PanelChrome.ErrorIndicator
|
message: 'Error text',
|
||||||
key="errorIndicator"
|
onClick: action('ErrorIndicator: onClick fired'),
|
||||||
error="Error text"
|
},
|
||||||
onClick={action('ErrorIndicator: onClick fired')}
|
|
||||||
/>,
|
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
{renderPanel('No padding with error state indicator', {
|
{renderPanel('No padding with error state', {
|
||||||
padding: 'none',
|
padding: 'none',
|
||||||
title: 'Default title',
|
title: 'Default title',
|
||||||
leftItems: [
|
loadingState: LoadingState.Error,
|
||||||
<PanelChrome.ErrorIndicator
|
})}
|
||||||
key="errorIndicator"
|
{renderPanel('Default panel with streaming state', {
|
||||||
error="Error text"
|
title: 'Default title',
|
||||||
onClick={action('ErrorIndicator: onClick fired')}
|
loadingState: LoadingState.Streaming,
|
||||||
/>,
|
|
||||||
],
|
|
||||||
})}
|
})}
|
||||||
</VerticalGroup>
|
</VerticalGroup>
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
@@ -93,22 +89,21 @@ export const Examples = () => {
|
|||||||
})}
|
})}
|
||||||
</VerticalGroup>
|
</VerticalGroup>
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
<HorizontalGroup spacing="md">
|
<HorizontalGroup spacing="md" align="flex-start">
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
{renderPanel('No title and loading indicator', {
|
{renderPanel('Default panel with deprecated error indicator', {
|
||||||
title: '',
|
title: 'Default title',
|
||||||
leftItems: [
|
leftItems: [
|
||||||
<PanelChrome.LoadingIndicator
|
<PanelChrome.ErrorIndicator
|
||||||
loading={loading}
|
key="errorIndicator"
|
||||||
onCancel={() => setLoading(false)}
|
error="Error text"
|
||||||
key="loading-indicator"
|
onClick={action('ErrorIndicator: onClick fired')}
|
||||||
/>,
|
/>,
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
</VerticalGroup>
|
{renderPanel('No padding with deprecated loading indicator', {
|
||||||
<VerticalGroup spacing="md">
|
padding: 'none',
|
||||||
{renderPanel('Very long title', {
|
title: 'Default title',
|
||||||
title: 'Very long title that should get ellipsis when there is no more space',
|
|
||||||
leftItems: [
|
leftItems: [
|
||||||
<PanelChrome.LoadingIndicator
|
<PanelChrome.LoadingIndicator
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -128,7 +123,9 @@ export const Basic: ComponentStory<typeof PanelChrome> = (args: PanelChromeProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelChrome {...args}>
|
<PanelChrome {...args}>
|
||||||
{(width: number, height: number) => <div style={{ height, width, ...contentStyle }}>Description text</div>}
|
{(width: number, height: number) => (
|
||||||
|
<div style={{ height, width, ...contentStyle }}>Panel in a loading state</div>
|
||||||
|
)}
|
||||||
</PanelChrome>
|
</PanelChrome>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -221,6 +218,7 @@ Basic.args = {
|
|||||||
title: 'Very long title that should get ellipsis when there is no more space',
|
title: 'Very long title that should get ellipsis when there is no more space',
|
||||||
titleItems,
|
titleItems,
|
||||||
menu,
|
menu,
|
||||||
|
loadingState: LoadingState.Loading,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ it('renders an empty panel with padding', () => {
|
|||||||
expect(screen.getByText("Panel's Content").parentElement).not.toHaveStyle({ padding: '0px' });
|
expect(screen.getByText("Panel's Content").parentElement).not.toHaveStyle({ padding: '0px' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an empty panel without a header if no title or titleItems', () => {
|
|
||||||
setup();
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('header-container')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders panel with a header if prop title', () => {
|
it('renders panel with a header if prop title', () => {
|
||||||
setup({ title: 'Test Panel Header' });
|
setup({ title: 'Test Panel Header' });
|
||||||
|
|
||||||
@@ -81,10 +75,9 @@ it('renders panel with a header with icons in place if prop titleItems', () => {
|
|||||||
expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
|
expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders panel with a fixed header if prop hoverHeader is false', () => {
|
it.skip('renders panel with a fixed header if prop hoverHeader is false', () => {
|
||||||
setup({ title: 'Test Panel Header', hoverHeader: false });
|
// setup({ title: 'Test Panel Header', hoverHeader: false });
|
||||||
|
// expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders panel with a header if prop menu', () => {
|
it('renders panel with a header if prop menu', () => {
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { CSSProperties, ReactNode } from 'react';
|
import { isEmpty } from 'lodash';
|
||||||
|
import React, { CSSProperties, ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, isIconName } from '@grafana/data';
|
import { GrafanaTheme2, isIconName, LoadingState } from '@grafana/data';
|
||||||
|
|
||||||
import { useStyles2, useTheme2 } from '../../themes';
|
import { useStyles2, useTheme2 } from '../../themes';
|
||||||
import { IconName } from '../../types/icon';
|
import { IconName } from '../../types/icon';
|
||||||
import { Dropdown } from '../Dropdown/Dropdown';
|
import { Dropdown } from '../Dropdown/Dropdown';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { IconButton, IconButtonVariant } from '../IconButton/IconButton';
|
import { IconButton, IconButtonVariant } from '../IconButton/IconButton';
|
||||||
|
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
||||||
import { PopoverContent, Tooltip } from '../Tooltip';
|
import { PopoverContent, Tooltip } from '../Tooltip';
|
||||||
|
|
||||||
|
import { PanelStatus } from './PanelStatus';
|
||||||
|
|
||||||
|
interface Status {
|
||||||
|
message?: string;
|
||||||
|
onClick?: (e: React.SyntheticEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@@ -31,16 +40,16 @@ export interface PanelChromeProps {
|
|||||||
padding?: PanelPadding;
|
padding?: PanelPadding;
|
||||||
title?: string;
|
title?: string;
|
||||||
titleItems?: PanelChromeInfoState[];
|
titleItems?: PanelChromeInfoState[];
|
||||||
menu?: React.ReactElement;
|
menu?: ReactElement;
|
||||||
/** dragClass, hoverHeader, loadingState, and states not yet implemented */
|
/** dragClass, hoverHeader not yet implemented */
|
||||||
// dragClass?: string;
|
// dragClass?: string;
|
||||||
hoverHeader?: boolean;
|
hoverHeader?: boolean;
|
||||||
// loadingState?: LoadingState;
|
loadingState?: LoadingState;
|
||||||
// states?: ReactNode[];
|
status?: Status;
|
||||||
/** @deprecated in favor of prop states
|
/** @deprecated in favor of props
|
||||||
|
* status for errors and loadingState for loading and streaming
|
||||||
* which will serve the same purpose
|
* which will serve the same purpose
|
||||||
* of showing the panel state in the top right corner
|
* of showing/interacting with the panel's data state
|
||||||
* of itself or its header
|
|
||||||
* */
|
* */
|
||||||
leftItems?: ReactNode[];
|
leftItems?: ReactNode[];
|
||||||
}
|
}
|
||||||
@@ -53,7 +62,7 @@ export type PanelPadding = 'none' | 'md';
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const PanelChrome: React.FC<PanelChromeProps> = ({
|
export function PanelChrome({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
children,
|
children,
|
||||||
@@ -63,14 +72,18 @@ export const PanelChrome: React.FC<PanelChromeProps> = ({
|
|||||||
menu,
|
menu,
|
||||||
// dragClass,
|
// dragClass,
|
||||||
hoverHeader = false,
|
hoverHeader = false,
|
||||||
// loadingState,
|
loadingState,
|
||||||
// states = [],
|
status,
|
||||||
leftItems = [],
|
leftItems = [],
|
||||||
}) => {
|
}: PanelChromeProps) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const headerHeight = !hoverHeader ? getHeaderHeight(theme, title, leftItems) : 0;
|
// To Do rely on hoverHeader prop for header, not separate props
|
||||||
|
// once hoverHeader is implemented
|
||||||
|
const hasHeader = title.length > 0 || leftItems.length > 0;
|
||||||
|
|
||||||
|
const headerHeight = getHeaderHeight(theme, hasHeader);
|
||||||
const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height);
|
const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height);
|
||||||
|
|
||||||
const headerStyles: CSSProperties = {
|
const headerStyles: CSSProperties = {
|
||||||
@@ -82,72 +95,91 @@ export const PanelChrome: React.FC<PanelChromeProps> = ({
|
|||||||
};
|
};
|
||||||
const containerStyles: CSSProperties = { width, height };
|
const containerStyles: CSSProperties = { width, height };
|
||||||
|
|
||||||
const handleMenuOpen = () => {};
|
const isUsingDeprecatedLeftItems = isEmpty(status) && !loadingState;
|
||||||
|
const showLoading = loadingState === LoadingState.Loading && !isUsingDeprecatedLeftItems;
|
||||||
const hasHeader = title || titleItems.length > 0 || menu;
|
const showStreaming = loadingState === LoadingState.Streaming && !isUsingDeprecatedLeftItems;
|
||||||
|
|
||||||
|
const renderStatus = () => {
|
||||||
|
if (isUsingDeprecatedLeftItems) {
|
||||||
|
return <div className={cx(styles.rightAligned, styles.items)}>{itemsRenderer(leftItems, (item) => item)}</div>;
|
||||||
|
} else {
|
||||||
|
const showError = loadingState === LoadingState.Error || status?.message;
|
||||||
|
return showError ? (
|
||||||
|
<div className={styles.errorContainer}>
|
||||||
|
<PanelStatus message={status?.message} onClick={status?.onClick} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} style={containerStyles}>
|
<div className={styles.container} style={containerStyles}>
|
||||||
{hasHeader && !hoverHeader && (
|
<div className={styles.loadingBarContainer}>
|
||||||
<div className={styles.headerContainer} style={headerStyles} data-testid="header-container">
|
{showLoading ? <LoadingBar width={'28%'} height={'2px'} /> : null}
|
||||||
{title && (
|
</div>
|
||||||
<div title={title} className={styles.title}>
|
|
||||||
{title}
|
<div className={styles.headerContainer} style={headerStyles} data-testid="header-container">
|
||||||
|
{title && (
|
||||||
|
<h6 title={title} className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showStreaming && (
|
||||||
|
<div className={styles.item} style={itemStyles}>
|
||||||
|
<Tooltip content="Streaming">
|
||||||
|
<Icon name="circle" type="mono" size="sm" className={styles.streaming} />
|
||||||
|
</Tooltip>
|
||||||
|
</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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{menu && (
|
||||||
|
<Dropdown overlay={menu} placement="bottom">
|
||||||
|
<div className={cx(styles.item, styles.menuItem, 'menu-icon')} data-testid="menu-icon" style={itemStyles}>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel={`Menu for panel with ${title ? `title ${title}` : 'no title'}`}
|
||||||
|
tooltip="Menu"
|
||||||
|
name="ellipsis-v"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
|
||||||
{titleItems.length > 0 && (
|
{renderStatus()}
|
||||||
<div className={styles.items} data-testid="title-items-container">
|
</div>
|
||||||
{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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{menu && (
|
|
||||||
<Dropdown overlay={menu} placement="bottom">
|
|
||||||
<div className={cx(styles.item, styles.menuItem, 'menu-icon')} data-testid="menu-icon" style={itemStyles}>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel={`Menu for panel with ${title ? `title ${title}` : 'no title'}`}
|
|
||||||
tooltip="Menu"
|
|
||||||
name="ellipsis-v"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleMenuOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{leftItems.length > 0 && (
|
|
||||||
<div className={cx(styles.rightAligned, styles.items)}>{itemsRenderer(leftItems, (item) => item)}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.content} style={contentStyle}>
|
<div className={styles.content} style={contentStyle}>
|
||||||
{children(innerWidth, innerHeight)}
|
{children(innerWidth, innerHeight)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const itemsRenderer = (items: ReactNode[], renderer: (items: ReactNode[]) => ReactNode): ReactNode => {
|
const itemsRenderer = (items: ReactNode[], renderer: (items: ReactNode[]) => ReactNode): ReactNode => {
|
||||||
const toRender = React.Children.toArray(items).filter(Boolean);
|
const toRender = React.Children.toArray(items).filter(Boolean);
|
||||||
return toRender.length > 0 ? renderer(toRender) : null;
|
return toRender.length > 0 ? renderer(toRender) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHeaderHeight = (theme: GrafanaTheme2, title: string, items: ReactNode[]) => {
|
const getHeaderHeight = (theme: GrafanaTheme2, hasHeader: boolean) => {
|
||||||
if (title.length > 0 || items.length > 0) {
|
if (hasHeader) {
|
||||||
return theme.spacing.gridSize * theme.components.panel.headerHeight;
|
return theme.spacing.gridSize * theme.components.panel.headerHeight;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -161,9 +193,12 @@ const getContentStyle = (
|
|||||||
height: number
|
height: number
|
||||||
) => {
|
) => {
|
||||||
const chromePadding = (padding === 'md' ? theme.components.panel.padding : 0) * theme.spacing.gridSize;
|
const chromePadding = (padding === 'md' ? theme.components.panel.padding : 0) * theme.spacing.gridSize;
|
||||||
|
|
||||||
|
const panelPadding = chromePadding * 2;
|
||||||
const panelBorder = 1 * 2;
|
const panelBorder = 1 * 2;
|
||||||
const innerWidth = width - chromePadding * 2 - panelBorder;
|
|
||||||
const innerHeight = height - headerHeight - chromePadding * 2 - panelBorder;
|
const innerWidth = width - panelPadding - panelBorder;
|
||||||
|
const innerHeight = height - headerHeight - panelPadding - panelBorder;
|
||||||
|
|
||||||
const contentStyle: CSSProperties = {
|
const contentStyle: CSSProperties = {
|
||||||
padding: chromePadding,
|
padding: chromePadding,
|
||||||
@@ -185,7 +220,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flex: '0 0 0',
|
flex: '1 1 0',
|
||||||
|
|
||||||
'&:focus-visible, &:hover': {
|
'&:focus-visible, &:hover': {
|
||||||
// only show menu icon on hover or focused panel
|
// only show menu icon on hover or focused panel
|
||||||
@@ -198,11 +233,16 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
outline: `1px solid ${theme.colors.action.focus}`,
|
outline: `1px solid ${theme.colors.action.focus}`,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
loadingBarContainer: css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
content: css({
|
content: css({
|
||||||
label: 'panel-content',
|
label: 'panel-content',
|
||||||
width: '100%',
|
|
||||||
contain: 'strict',
|
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
contain: 'strict',
|
||||||
}),
|
}),
|
||||||
headerContainer: css({
|
headerContainer: css({
|
||||||
label: 'panel-header',
|
label: 'panel-header',
|
||||||
@@ -210,23 +250,41 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: `0 ${theme.spacing(padding)}`,
|
padding: `0 ${theme.spacing(padding)}`,
|
||||||
}),
|
}),
|
||||||
|
streaming: css({
|
||||||
|
marginRight: 0,
|
||||||
|
color: theme.colors.success.text,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
color: theme.colors.success.text,
|
||||||
|
},
|
||||||
|
}),
|
||||||
title: css({
|
title: css({
|
||||||
|
marginBottom: 0, // override default h6 margin-bottom
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontSize: theme.typography.h6.fontSize,
|
||||||
|
fontWeight: theme.typography.h6.fontWeight,
|
||||||
}),
|
}),
|
||||||
items: css({
|
items: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}),
|
}),
|
||||||
item: css({
|
item: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}),
|
}),
|
||||||
menuItem: css({
|
menuItem: css({
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
}),
|
}),
|
||||||
|
errorContainer: css({
|
||||||
|
label: 'error-container',
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}),
|
||||||
rightAligned: css({
|
rightAligned: css({
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
||||||
|
import { useStyles2 } from '../../themes';
|
||||||
|
import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
message?: string;
|
||||||
|
onClick?: (e: React.SyntheticEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelStatus({ message, onClick }: Props) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={onClick}
|
||||||
|
variant={'destructive'}
|
||||||
|
className={styles.buttonStyles}
|
||||||
|
icon="exclamation-triangle"
|
||||||
|
tooltip={message || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
const { headerHeight, padding } = theme.components.panel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttonStyles: css({
|
||||||
|
label: 'panel-header-state-button',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(padding),
|
||||||
|
width: theme.spacing(headerHeight),
|
||||||
|
height: theme.spacing(headerHeight),
|
||||||
|
borderRadius: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
3134
public/api-spec.json
3134
public/api-spec.json
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ import {
|
|||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
import { profiler } from 'app/core/profiler';
|
import { profiler } from 'app/core/profiler';
|
||||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||||
|
import { InspectTab } from 'app/features/inspector/types';
|
||||||
import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory';
|
import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory';
|
||||||
import { RenderEvent } from 'app/types/events';
|
import { RenderEvent } from 'app/types/events';
|
||||||
|
|
||||||
@@ -45,7 +46,6 @@ import { DashboardModel, PanelModel } from '../state';
|
|||||||
import { loadSnapshotData } from '../utils/loadSnapshotData';
|
import { loadSnapshotData } from '../utils/loadSnapshotData';
|
||||||
|
|
||||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||||
import { PanelHeaderLoadingIndicator } from './PanelHeader/PanelHeaderLoadingIndicator';
|
|
||||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||||
import { liveTimer } from './liveTimer';
|
import { liveTimer } from './liveTimer';
|
||||||
|
|
||||||
@@ -566,6 +566,11 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
|||||||
return !panel.hasTitle();
|
return !panel.hasTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenErrorInspect(e: React.SyntheticEvent, tab: string) {
|
||||||
|
e.stopPropagation();
|
||||||
|
locationService.partial({ inspect: this.props.panel.id, inspectTab: tab });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
|
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
|
||||||
const { errorMessage, data } = this.state;
|
const { errorMessage, data } = this.state;
|
||||||
@@ -581,17 +586,22 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
|||||||
[`panel-alert-state--${alertState}`]: alertState !== undefined,
|
[`panel-alert-state--${alertState}`]: alertState !== undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// for new panel header design
|
|
||||||
const onCancelQuery = () => panel.getQueryRunner().cancelQuery();
|
|
||||||
const title = panel.getDisplayTitle();
|
const title = panel.getDisplayTitle();
|
||||||
const noPadding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
const padding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
||||||
const leftItems = [
|
|
||||||
<PanelHeaderLoadingIndicator state={data.state} onClick={onCancelQuery} key="loading-indicator" />,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (config.featureToggles.newPanelChromeUI) {
|
if (config.featureToggles.newPanelChromeUI) {
|
||||||
return (
|
return (
|
||||||
<PanelChrome width={width} height={height} title={title} leftItems={leftItems} padding={noPadding}>
|
<PanelChrome
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
padding={padding}
|
||||||
|
title={title}
|
||||||
|
loadingState={data.state}
|
||||||
|
status={{
|
||||||
|
message: errorMessage,
|
||||||
|
onClick: (e: React.SyntheticEvent) => this.onOpenErrorInspect(e, InspectTab.Error),
|
||||||
|
}}
|
||||||
|
>
|
||||||
{(innerWidth, innerHeight) => (
|
{(innerWidth, innerHeight) => (
|
||||||
<>
|
<>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
|
|||||||
Reference in New Issue
Block a user