@grafana/ui
Version:
Grafana Components Library
449 lines (446 loc) • 15.2 kB
JavaScript
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { cx, css } from '@emotion/css';
import * as React from 'react';
import { useId, useState } from 'react';
import { useToggle, useMeasure } from 'react-use';
import { LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { useTheme2, useStyles2 } from '../../themes/ThemeContext.mjs';
import { getFocusStyles } from '../../themes/mixins.mjs';
import { DelayRender } from '../../utils/DelayRender.mjs';
import { usePointerDistance } from '../../utils/usePointerDistance.mjs';
import { useElementSelection } from '../ElementSelectionContext/ElementSelectionContext.mjs';
import { Icon } from '../Icon/Icon.mjs';
import { LoadingBar } from '../LoadingBar/LoadingBar.mjs';
import { Text } from '../Text/Text.mjs';
import { Tooltip } from '../Tooltip/Tooltip.mjs';
import { HoverWidget } from './HoverWidget.mjs';
import { PanelDescription } from './PanelDescription.mjs';
import { PanelMenu } from './PanelMenu.mjs';
import { PanelStatus } from './PanelStatus.mjs';
import { TitleItem } from './TitleItem.mjs';
"use strict";
function PanelChrome({
width,
height,
children,
padding = "md",
title = "",
description = "",
displayMode = "default",
titleItems,
menu,
dragClass,
dragClassCancel,
hoverHeader = false,
hoverHeaderOffset,
loadingState,
statusMessage,
statusMessageOnClick,
leftItems,
actions,
selectionId,
onCancelQuery,
onOpenMenu,
collapsible = false,
collapsed,
onToggleCollapse,
onFocus,
onMouseMove,
onMouseEnter,
onDragStart,
showMenuAlways = false
}) {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const panelContentId = useId();
const panelTitleId = useId().replace(/:/g, "_");
const { isSelected, onSelect, isSelectable } = useElementSelection(selectionId);
const pointerDistance = usePointerDistance();
const hasHeader = !hoverHeader;
const [isOpen, toggleOpen] = useToggle(true);
const [selectableHighlight, setSelectableHighlight] = useState(false);
const onHeaderEnter = React.useCallback(() => setSelectableHighlight(true), []);
const onHeaderLeave = React.useCallback(() => setSelectableHighlight(false), []);
if (collapsed === void 0) {
collapsed = !isOpen;
}
const showOnHoverClass = showMenuAlways ? "always-show" : "show-on-hover";
const isPanelTransparent = displayMode === "transparent";
const headerHeight = getHeaderHeight(theme, hasHeader);
const { contentStyle, innerWidth, innerHeight } = getContentStyle(
padding,
theme,
headerHeight,
collapsed,
height,
width
);
const headerStyles = {
height: headerHeight,
cursor: dragClass ? "move" : "auto"
};
const containerStyles = { width, height: collapsed ? void 0 : height };
const [ref, { width: loadingBarWidth }] = useMeasure();
if (leftItems) {
actions = leftItems;
}
const testid = typeof title === "string" ? selectors.components.Panels.Panel.title(title) : "Panel";
const onPointerUp = React.useCallback(
(evt) => {
if (pointerDistance.check(evt) || dragClassCancel && evt.target instanceof Element && evt.target.closest(`.${dragClassCancel}`)) {
return;
}
setTimeout(() => onSelect == null ? void 0 : onSelect(evt));
},
[dragClassCancel, onSelect, pointerDistance]
);
const onPointerDown = React.useCallback(
(evt) => {
evt.stopPropagation();
pointerDistance.set(evt);
onDragStart == null ? void 0 : onDragStart(evt);
},
[pointerDistance, onDragStart]
);
const onContentPointerDown = React.useCallback(
(evt) => {
if (evt.target instanceof Element && evt.target.closest("button,a,canvas,svg")) {
return;
}
onSelect == null ? void 0 : onSelect(evt);
},
[onSelect]
);
const headerContent = /* @__PURE__ */ jsxs(Fragment, { children: [
!collapsible && title && /* @__PURE__ */ jsx("div", { className: styles.title, children: /* @__PURE__ */ jsx(
Text,
{
element: "h2",
variant: "h6",
truncate: true,
title: typeof title === "string" ? title : void 0,
id: panelTitleId,
children: title
}
) }),
collapsible && /* @__PURE__ */ jsx("div", { className: styles.title, children: /* @__PURE__ */ jsx(Text, { element: "h2", variant: "h6", children: /* @__PURE__ */ jsxs(
"button",
{
type: "button",
className: styles.clearButtonStyles,
onClick: () => {
toggleOpen();
if (onToggleCollapse) {
onToggleCollapse(!collapsed);
}
},
"aria-expanded": !collapsed,
"aria-controls": !collapsed ? panelContentId : void 0,
children: [
/* @__PURE__ */ jsx(
Icon,
{
name: !collapsed ? "angle-down" : "angle-right",
"aria-hidden": !!title,
"aria-label": !title ? t("grafana-ui.panel-chrome.aria-label-toggle-collapse", "toggle collapse panel") : void 0
}
),
/* @__PURE__ */ jsx(Text, { variant: "h6", truncate: true, id: panelTitleId, children: title })
]
}
) }) }),
/* @__PURE__ */ jsxs("div", { className: cx(styles.titleItems, dragClassCancel), "data-testid": "title-items-container", children: [
/* @__PURE__ */ jsx(PanelDescription, { description, className: dragClassCancel }),
titleItems
] }),
loadingState === LoadingState.Streaming && /* @__PURE__ */ jsx(
Tooltip,
{
content: onCancelQuery ? t("grafana-ui.panel-chrome.tooltip-stop-streaming", "Stop streaming") : t("grafana-ui.panel-chrome.tooltip-streaming", "Streaming"),
children: /* @__PURE__ */ jsx(TitleItem, { className: dragClassCancel, "data-testid": "panel-streaming", onClick: onCancelQuery, children: /* @__PURE__ */ jsx(Icon, { name: "circle-mono", size: "md", className: styles.streaming }) })
}
),
loadingState === LoadingState.Loading && onCancelQuery && /* @__PURE__ */ jsx(DelayRender, { delay: 2e3, children: /* @__PURE__ */ jsx(Tooltip, { content: t("grafana-ui.panel-chrome.tooltip-cancel", "Cancel query"), children: /* @__PURE__ */ jsx(
TitleItem,
{
className: cx(dragClassCancel, styles.pointer),
"data-testid": "panel-cancel-query",
onClick: onCancelQuery,
children: /* @__PURE__ */ jsx(Icon, { name: "sync-slash", size: "md" })
}
) }) }),
/* @__PURE__ */ jsx("div", { className: styles.rightAligned, children: actions && /* @__PURE__ */ jsx("div", { className: styles.rightActions, children: itemsRenderer(actions, (item) => item) }) })
] });
return (
// tabIndex={0} is needed for keyboard accessibility in the plot area
/* @__PURE__ */ jsxs(
"section",
{
className: cx(
styles.container,
isPanelTransparent && styles.transparentContainer,
isSelected && "dashboard-selected-element",
!isSelected && isSelectable && selectableHighlight && "dashboard-selectable-element"
),
style: containerStyles,
"aria-labelledby": !!title ? panelTitleId : void 0,
"data-testid": testid,
tabIndex: 0,
onFocus,
onMouseMove,
onMouseEnter,
ref,
children: [
/* @__PURE__ */ jsx("div", { className: styles.loadingBarContainer, children: loadingState === LoadingState.Loading ? /* @__PURE__ */ jsx(
LoadingBar,
{
width: loadingBarWidth,
ariaLabel: t("grafana-ui.panel-chrome.ariaLabel-panel-loading", "Panel loading bar")
}
) : null }),
hoverHeader && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
HoverWidget,
{
menu,
title: typeof title === "string" ? title : void 0,
offset: hoverHeaderOffset,
dragClass,
onOpenMenu,
children: headerContent
}
),
statusMessage && /* @__PURE__ */ jsx("div", { className: styles.errorContainerFloating, children: /* @__PURE__ */ jsx(
PanelStatus,
{
message: statusMessage,
onClick: statusMessageOnClick,
ariaLabel: t("grafana-ui.panel-chrome.ariaLabel-panel-status", "Panel status")
}
) })
] }),
hasHeader && /* @__PURE__ */ jsxs(
"div",
{
className: cx(styles.headerContainer, dragClass),
style: headerStyles,
"data-testid": selectors.components.Panels.Panel.headerContainer,
onPointerDown,
onMouseEnter: isSelectable ? onHeaderEnter : void 0,
onMouseLeave: isSelectable ? onHeaderLeave : void 0,
onPointerUp,
children: [
statusMessage && /* @__PURE__ */ jsx("div", { className: dragClassCancel, children: /* @__PURE__ */ jsx(
PanelStatus,
{
message: statusMessage,
onClick: statusMessageOnClick,
ariaLabel: t("grafana-ui.panel-chrome.ariaLabel-panel-status", "Panel status")
}
) }),
headerContent,
menu && /* @__PURE__ */ jsx(
PanelMenu,
{
menu,
title: typeof title === "string" ? title : void 0,
placement: "bottom-end",
menuButtonClass: cx(styles.menuItem, dragClassCancel, showOnHoverClass),
onOpenMenu
}
)
]
}
),
!collapsed && /* @__PURE__ */ jsx(
"div",
{
id: panelContentId,
"data-testid": selectors.components.Panels.Panel.content,
className: cx(styles.content, height === void 0 && styles.containNone),
style: contentStyle,
onPointerDown: onContentPointerDown,
children: typeof children === "function" ? children(innerWidth, innerHeight) : children
}
)
]
}
)
);
}
const itemsRenderer = (items, renderer) => {
const toRender = React.Children.toArray(items).filter(Boolean);
return toRender.length > 0 ? renderer(toRender) : null;
};
const getHeaderHeight = (theme, hasHeader) => {
if (hasHeader) {
return theme.spacing.gridSize * theme.components.panel.headerHeight;
}
return 0;
};
const getContentStyle = (padding, theme, headerHeight, collapsed, height, width) => {
const chromePadding = (padding === "md" ? theme.components.panel.padding : 0) * theme.spacing.gridSize;
const panelPadding = chromePadding * 2;
const panelBorder = 1 * 2;
let innerWidth = 0;
if (width) {
innerWidth = width - panelPadding - panelBorder;
}
let innerHeight = 0;
if (height) {
innerHeight = height - headerHeight - panelPadding - panelBorder;
}
if (collapsed) {
innerHeight = headerHeight;
}
const contentStyle = {
padding: chromePadding
};
return { contentStyle, innerWidth, innerHeight };
};
const getStyles = (theme) => {
const { background, borderColor, padding } = theme.components.panel;
return {
container: css({
label: "panel-container",
backgroundColor: background,
border: `1px solid ${borderColor}`,
position: "relative",
borderRadius: theme.shape.radius.default,
height: "100%",
display: "flex",
flexDirection: "column",
".always-show": {
background: "none",
"&:focus-visible, &:hover": {
background: theme.colors.secondary.shade
}
},
".show-on-hover": {
opacity: "0",
visibility: "hidden"
},
"&:focus-visible, &:hover": {
// only show menu icon on hover or focused panel
".show-on-hover": {
opacity: "1",
visibility: "visible"
}
},
"&:focus-visible": getFocusStyles(theme),
// The not:(:focus) clause is so that this rule is only applied when decendants are focused (important otherwise the hover header is visible when panel is clicked).
"&:focus-within:not(:focus)": {
".show-on-hover": {
visibility: "visible",
opacity: "1"
}
}
}),
transparentContainer: css({
label: "panel-transparent-container",
backgroundColor: "transparent",
border: "1px solid transparent",
boxSizing: "border-box",
"&:hover": {
border: `1px solid ${borderColor}`
}
}),
loadingBarContainer: css({
label: "panel-loading-bar-container",
position: "absolute",
top: 0,
width: "100%",
// this is to force the loading bar container to create a new stacking context
// otherwise, in webkit browsers on windows/linux, the aliasing of panel text changes when the loading bar is shown
// see https://github.com/grafana/grafana/issues/88104
zIndex: 1
}),
containNone: css({
contain: "none"
}),
content: css({
label: "panel-content",
flexGrow: 1,
contain: "size layout"
}),
headerContainer: css({
label: "panel-header",
display: "flex",
alignItems: "center"
}),
pointer: css({
cursor: "pointer"
}),
streaming: css({
label: "panel-streaming",
marginRight: 0,
color: theme.colors.success.text,
"&:hover": {
color: theme.colors.success.text
}
}),
title: css({
label: "panel-title",
display: "flex",
padding: theme.spacing(0, padding),
minWidth: 0,
"& > h2": {
minWidth: 0
}
}),
items: css({
display: "flex"
}),
item: css({
display: "flex",
justifyContent: "center",
alignItems: "center"
}),
hiddenMenu: css({
visibility: "hidden"
}),
menuItem: css({
label: "panel-menu",
border: "none",
background: theme.colors.secondary.main,
"&:hover": {
background: theme.colors.secondary.shade
}
}),
errorContainerFloating: css({
label: "error-container",
position: "absolute",
left: 0,
top: 0,
zIndex: 1
}),
rightActions: css({
display: "flex",
padding: theme.spacing(0, padding),
gap: theme.spacing(1)
}),
rightAligned: css({
label: "right-aligned-container",
marginLeft: "auto",
display: "flex",
alignItems: "center"
}),
titleItems: css({
display: "flex",
height: "100%"
}),
clearButtonStyles: css({
alignItems: "center",
display: "flex",
gap: theme.spacing(0.5),
background: "transparent",
border: "none",
padding: 0,
maxWidth: "100%"
})
};
};
export { PanelChrome };
//# sourceMappingURL=PanelChrome.mjs.map