From 2ca6f7db6937634ff6845a4d4a1e6ea669c7b4bf Mon Sep 17 00:00:00 2001 From: Ayana-Rukasar Date: Thu, 11 Jun 2026 16:49:37 +0530 Subject: [PATCH] [BUGFIX] fix panel links menu positioning Panel link dropdowns appeared at arbitrary screen positions inside react-grid-layout panels that use CSS transforms. Render a fresh LinksDisplay instance per header breakpoint via renderPanelLinks() and use Popper with strategy: fixed so the menu stays anchored below the link icon. Fixes perses/perses#3654 Signed-off-by: Ayana-Rukasar --- .../components/LinksDisplay/LinksDisplay.tsx | 98 +++++++++++++++++-- .../src/components/Panel/PanelActions.tsx | 12 ++- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/dashboards/src/components/LinksDisplay/LinksDisplay.tsx b/dashboards/src/components/LinksDisplay/LinksDisplay.tsx index dcd06a9e..16f6e93f 100644 --- a/dashboards/src/components/LinksDisplay/LinksDisplay.tsx +++ b/dashboards/src/components/LinksDisplay/LinksDisplay.tsx @@ -11,10 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { IconButton, Link as LinkComponent, Menu, MenuItem, Theme, Chip, capitalize, Stack } from '@mui/material'; +import { + Box, + ClickAwayListener, + IconButton, + Link as LinkComponent, + Menu, + MenuItem, + MenuList, + Paper, + Popper, + Theme, + Chip, + capitalize, + Stack, +} from '@mui/material'; import LaunchIcon from 'mdi-material-ui/Launch'; import { Link } from '@perses-dev/spec'; -import { MouseEvent, ReactElement, useState } from 'react'; +import { MouseEvent, ReactElement, useId, useState } from 'react'; import { InfoTooltip } from '@perses-dev/components'; import { useReplaceVariablesInString } from '@perses-dev/plugin-system'; @@ -73,13 +87,19 @@ export function LinksDisplay({ links, variant }: LinksProps): ReactElement | nul } } - // Default: show dropdown menu for multiple links + if (variant === 'panel') { + return ; + } + + // Dashboard variant: show dropdown menu for multiple links + const menuButtonId = `${variant}-links-button`; + return ( - <> + ({ borderRadius: theme.shape.borderRadius, padding: '4px' })} @@ -96,15 +116,75 @@ export function LinksDisplay({ links, variant }: LinksProps): ReactElement | nul anchorEl={anchorEl} open={isMenuOpened} onClose={handleClose} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} MenuListProps={{ - 'aria-labelledby': `${variant}-links-button`, + 'aria-labelledby': menuButtonId, }} > {links.map((link: Link) => ( ))} - + + ); +} + +function PanelLinksDropdown({ links }: { links: Link[] }): ReactElement { + const [anchorEl, setAnchorEl] = useState(null); + const menuId = useId(); + const open = Boolean(anchorEl); + + const handleToggle = (event: MouseEvent): void => { + setAnchorEl(anchorEl ? null : event.currentTarget); + }; + + const handleClose = (): void => { + setAnchorEl(null); + }; + + return ( + theme.palette.background.default }}> + + ({ borderRadius: theme.shape.borderRadius, padding: '4px' })} + > + theme.palette.text.secondary }} /> + + + theme.zIndex.modal }} + > + + + + {links.map((link: Link) => ( + + ))} + + + + + ); } @@ -145,12 +225,12 @@ function LinkButton({ link }: { link: Link }): ReactElement { ); } -function LinkMenuItem({ link }: { link: Link }): ReactElement { +function LinkMenuItem({ link, onNavigate }: { link: Link; onNavigate?: () => void }): ReactElement { const { url, name, tooltip, targetBlank } = useLink(link); return ( - + {name ?? url} diff --git a/dashboards/src/components/Panel/PanelActions.tsx b/dashboards/src/components/Panel/PanelActions.tsx index d4a1c885..6e7f4a9c 100644 --- a/dashboards/src/components/Panel/PanelActions.tsx +++ b/dashboards/src/components/Panel/PanelActions.tsx @@ -109,9 +109,12 @@ export const PanelActions: React.FC = ({ return undefined; }, [descriptionTooltipId, description]); - const linksAction = links && links.length > 0 && ; const extraActions = editHandlers === undefined && extra; + // Return a new LinksDisplay element on each call. A shared JSX variable reused across + // responsive header branches can bind the menu anchor to a hidden breakpoint layout. + const renderPanelLinks = (): ReactNode => (links?.length ? : null); + const queryStateIndicator = useMemo((): ReactNode | undefined => { const hasData = queryResults.some((q) => q.data); const isFetching = queryResults.some((q) => q.isFetching); @@ -271,7 +274,8 @@ export const PanelActions: React.FC = ({ {divider} - {descriptionAction} {linksAction} {queryStateIndicator} {noticesIndicator} {extraActions} {viewQueryAction} + {descriptionAction} {renderPanelLinks()} {queryStateIndicator} {noticesIndicator} {extraActions}{' '} + {viewQueryAction} {readActions} {pluginActions} {itemActions} {editActions} @@ -288,7 +292,7 @@ export const PanelActions: React.FC = ({ })} > - {descriptionAction} {linksAction} + {descriptionAction} {renderPanelLinks()} {divider} {queryStateIndicator} {noticesIndicator} @@ -311,7 +315,7 @@ export const PanelActions: React.FC = ({ })} > - {descriptionAction} {linksAction} + {descriptionAction} {renderPanelLinks()} {divider} {queryStateIndicator} {noticesIndicator}