UNPKG

@grafana/ui

Version:
449 lines (446 loc) • 15.2 kB
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