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:
parent
88a8cba6b0
commit
3f1908464d
@ -18,7 +18,7 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
name: IconName;
|
||||
/** Icon size */
|
||||
size?: IconSize;
|
||||
/** Type od the icon - mono or default */
|
||||
/** Type of the icon - mono or default */
|
||||
iconType?: IconType;
|
||||
/** Tooltip content to display on hover */
|
||||
tooltip?: PopoverContent;
|
||||
|
@ -30,7 +30,7 @@ const getStyles = (width?: string, height?: string) => (_: GrafanaTheme2) => {
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
'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 { useInterval } from 'react-use';
|
||||
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { PanelChrome, PanelChromeProps } from '@grafana/ui';
|
||||
|
||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
||||
@ -64,26 +65,21 @@ export const Examples = () => {
|
||||
<DashboardStoryCanvas>
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<VerticalGroup spacing="md">
|
||||
{renderPanel('Default panel with error state indicator', {
|
||||
{renderPanel('Default panel with error status', {
|
||||
title: 'Default title',
|
||||
leftItems: [
|
||||
<PanelChrome.ErrorIndicator
|
||||
key="errorIndicator"
|
||||
error="Error text"
|
||||
onClick={action('ErrorIndicator: onClick fired')}
|
||||
/>,
|
||||
],
|
||||
status: {
|
||||
message: 'Error text',
|
||||
onClick: action('ErrorIndicator: onClick fired'),
|
||||
},
|
||||
})}
|
||||
{renderPanel('No padding with error state indicator', {
|
||||
{renderPanel('No padding with error state', {
|
||||
padding: 'none',
|
||||
title: 'Default title',
|
||||
leftItems: [
|
||||
<PanelChrome.ErrorIndicator
|
||||
key="errorIndicator"
|
||||
error="Error text"
|
||||
onClick={action('ErrorIndicator: onClick fired')}
|
||||
/>,
|
||||
],
|
||||
loadingState: LoadingState.Error,
|
||||
})}
|
||||
{renderPanel('Default panel with streaming state', {
|
||||
title: 'Default title',
|
||||
loadingState: LoadingState.Streaming,
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup spacing="md">
|
||||
@ -93,22 +89,21 @@ export const Examples = () => {
|
||||
})}
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="md">
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<VerticalGroup spacing="md">
|
||||
{renderPanel('No title and loading indicator', {
|
||||
title: '',
|
||||
{renderPanel('Default panel with deprecated error indicator', {
|
||||
title: 'Default title',
|
||||
leftItems: [
|
||||
<PanelChrome.LoadingIndicator
|
||||
loading={loading}
|
||||
onCancel={() => setLoading(false)}
|
||||
key="loading-indicator"
|
||||
<PanelChrome.ErrorIndicator
|
||||
key="errorIndicator"
|
||||
error="Error text"
|
||||
onClick={action('ErrorIndicator: onClick fired')}
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup spacing="md">
|
||||
{renderPanel('Very long title', {
|
||||
title: 'Very long title that should get ellipsis when there is no more space',
|
||||
{renderPanel('No padding with deprecated loading indicator', {
|
||||
padding: 'none',
|
||||
title: 'Default title',
|
||||
leftItems: [
|
||||
<PanelChrome.LoadingIndicator
|
||||
loading={loading}
|
||||
@ -128,7 +123,9 @@ export const Basic: ComponentStory<typeof PanelChrome> = (args: PanelChromeProps
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@ -221,6 +218,7 @@ Basic.args = {
|
||||
title: 'Very long title that should get ellipsis when there is no more space',
|
||||
titleItems,
|
||||
menu,
|
||||
loadingState: LoadingState.Loading,
|
||||
};
|
||||
|
||||
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' });
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
it('renders panel with a fixed header if prop hoverHeader is false', () => {
|
||||
setup({ title: 'Test Panel Header', hoverHeader: false });
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
it.skip('renders panel with a fixed header if prop hoverHeader is false', () => {
|
||||
// setup({ title: 'Test Panel Header', hoverHeader: false });
|
||||
// expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders panel with a header if prop menu', () => {
|
||||
|
@ -1,15 +1,24 @@
|
||||
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 { 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 { PopoverContent, Tooltip } from '../Tooltip';
|
||||
|
||||
import { PanelStatus } from './PanelStatus';
|
||||
|
||||
interface Status {
|
||||
message?: string;
|
||||
onClick?: (e: React.SyntheticEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -31,16 +40,16 @@ export interface PanelChromeProps {
|
||||
padding?: PanelPadding;
|
||||
title?: string;
|
||||
titleItems?: PanelChromeInfoState[];
|
||||
menu?: React.ReactElement;
|
||||
/** dragClass, hoverHeader, loadingState, and states not yet implemented */
|
||||
menu?: ReactElement;
|
||||
/** dragClass, hoverHeader not yet implemented */
|
||||
// dragClass?: string;
|
||||
hoverHeader?: boolean;
|
||||
// loadingState?: LoadingState;
|
||||
// states?: ReactNode[];
|
||||
/** @deprecated in favor of prop states
|
||||
loadingState?: LoadingState;
|
||||
status?: Status;
|
||||
/** @deprecated in favor of props
|
||||
* status for errors and loadingState for loading and streaming
|
||||
* which will serve the same purpose
|
||||
* of showing the panel state in the top right corner
|
||||
* of itself or its header
|
||||
* of showing/interacting with the panel's data state
|
||||
* */
|
||||
leftItems?: ReactNode[];
|
||||
}
|
||||
@ -53,7 +62,7 @@ export type PanelPadding = 'none' | 'md';
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const PanelChrome: React.FC<PanelChromeProps> = ({
|
||||
export function PanelChrome({
|
||||
width,
|
||||
height,
|
||||
children,
|
||||
@ -63,14 +72,18 @@ export const PanelChrome: React.FC<PanelChromeProps> = ({
|
||||
menu,
|
||||
// dragClass,
|
||||
hoverHeader = false,
|
||||
// loadingState,
|
||||
// states = [],
|
||||
loadingState,
|
||||
status,
|
||||
leftItems = [],
|
||||
}) => {
|
||||
}: PanelChromeProps) {
|
||||
const theme = useTheme2();
|
||||
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 headerStyles: CSSProperties = {
|
||||
@ -82,72 +95,91 @@ export const PanelChrome: React.FC<PanelChromeProps> = ({
|
||||
};
|
||||
const containerStyles: CSSProperties = { width, height };
|
||||
|
||||
const handleMenuOpen = () => {};
|
||||
|
||||
const hasHeader = title || titleItems.length > 0 || menu;
|
||||
const isUsingDeprecatedLeftItems = isEmpty(status) && !loadingState;
|
||||
const showLoading = loadingState === LoadingState.Loading && !isUsingDeprecatedLeftItems;
|
||||
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 (
|
||||
<div className={styles.container} style={containerStyles}>
|
||||
{hasHeader && !hoverHeader && (
|
||||
<div className={styles.headerContainer} style={headerStyles} data-testid="header-container">
|
||||
{title && (
|
||||
<div title={title} className={styles.title}>
|
||||
{title}
|
||||
<div className={styles.loadingBarContainer}>
|
||||
{showLoading ? <LoadingBar width={'28%'} height={'2px'} /> : null}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{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"
|
||||
onClick={handleMenuOpen}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{leftItems.length > 0 && (
|
||||
<div className={cx(styles.rightAligned, styles.items)}>{itemsRenderer(leftItems, (item) => item)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderStatus()}
|
||||
</div>
|
||||
|
||||
<div className={styles.content} style={contentStyle}>
|
||||
{children(innerWidth, innerHeight)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const itemsRenderer = (items: ReactNode[], renderer: (items: ReactNode[]) => ReactNode): ReactNode => {
|
||||
const toRender = React.Children.toArray(items).filter(Boolean);
|
||||
return toRender.length > 0 ? renderer(toRender) : null;
|
||||
};
|
||||
|
||||
const getHeaderHeight = (theme: GrafanaTheme2, title: string, items: ReactNode[]) => {
|
||||
if (title.length > 0 || items.length > 0) {
|
||||
const getHeaderHeight = (theme: GrafanaTheme2, hasHeader: boolean) => {
|
||||
if (hasHeader) {
|
||||
return theme.spacing.gridSize * theme.components.panel.headerHeight;
|
||||
}
|
||||
return 0;
|
||||
@ -161,9 +193,12 @@ const getContentStyle = (
|
||||
height: number
|
||||
) => {
|
||||
const chromePadding = (padding === 'md' ? theme.components.panel.padding : 0) * theme.spacing.gridSize;
|
||||
|
||||
const panelPadding = chromePadding * 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 = {
|
||||
padding: chromePadding,
|
||||
@ -185,7 +220,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '0 0 0',
|
||||
flex: '1 1 0',
|
||||
|
||||
'&:focus-visible, &:hover': {
|
||||
// only show menu icon on hover or focused panel
|
||||
@ -198,11 +233,16 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
outline: `1px solid ${theme.colors.action.focus}`,
|
||||
},
|
||||
}),
|
||||
loadingBarContainer: css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
content: css({
|
||||
label: 'panel-content',
|
||||
width: '100%',
|
||||
contain: 'strict',
|
||||
flexGrow: 1,
|
||||
contain: 'strict',
|
||||
}),
|
||||
headerContainer: css({
|
||||
label: 'panel-header',
|
||||
@ -210,23 +250,41 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
alignItems: 'center',
|
||||
padding: `0 ${theme.spacing(padding)}`,
|
||||
}),
|
||||
streaming: css({
|
||||
marginRight: 0,
|
||||
color: theme.colors.success.text,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.success.text,
|
||||
},
|
||||
}),
|
||||
title: css({
|
||||
marginBottom: 0, // override default h6 margin-bottom
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: theme.typography.h6.fontWeight,
|
||||
}),
|
||||
items: css({
|
||||
display: 'flex',
|
||||
}),
|
||||
item: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
menuItem: css({
|
||||
visibility: 'hidden',
|
||||
}),
|
||||
errorContainer: css({
|
||||
label: 'error-container',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
rightAligned: css({
|
||||
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 { profiler } from 'app/core/profiler';
|
||||
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 { RenderEvent } from 'app/types/events';
|
||||
|
||||
@ -45,7 +46,6 @@ import { DashboardModel, PanelModel } from '../state';
|
||||
import { loadSnapshotData } from '../utils/loadSnapshotData';
|
||||
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { PanelHeaderLoadingIndicator } from './PanelHeader/PanelHeaderLoadingIndicator';
|
||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { liveTimer } from './liveTimer';
|
||||
|
||||
@ -566,6 +566,11 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
return !panel.hasTitle();
|
||||
}
|
||||
|
||||
onOpenErrorInspect(e: React.SyntheticEvent, tab: string) {
|
||||
e.stopPropagation();
|
||||
locationService.partial({ inspect: this.props.panel.id, inspectTab: tab });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
|
||||
const { errorMessage, data } = this.state;
|
||||
@ -581,17 +586,22 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
[`panel-alert-state--${alertState}`]: alertState !== undefined,
|
||||
});
|
||||
|
||||
// for new panel header design
|
||||
const onCancelQuery = () => panel.getQueryRunner().cancelQuery();
|
||||
const title = panel.getDisplayTitle();
|
||||
const noPadding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
||||
const leftItems = [
|
||||
<PanelHeaderLoadingIndicator state={data.state} onClick={onCancelQuery} key="loading-indicator" />,
|
||||
];
|
||||
const padding: PanelPadding = plugin.noPadding ? 'none' : 'md';
|
||||
|
||||
if (config.featureToggles.newPanelChromeUI) {
|
||||
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) => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
|
Loading…
Reference in New Issue
Block a user