Skip to content

Commit 2ca6f7d

Browse files
committed
[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 <ayanark1515@gmail.com>
1 parent 874b63b commit 2ca6f7d

2 files changed

Lines changed: 97 additions & 13 deletions

File tree

dashboards/src/components/LinksDisplay/LinksDisplay.tsx

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,24 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
import { IconButton, Link as LinkComponent, Menu, MenuItem, Theme, Chip, capitalize, Stack } from '@mui/material';
14+
import {
15+
Box,
16+
ClickAwayListener,
17+
IconButton,
18+
Link as LinkComponent,
19+
Menu,
20+
MenuItem,
21+
MenuList,
22+
Paper,
23+
Popper,
24+
Theme,
25+
Chip,
26+
capitalize,
27+
Stack,
28+
} from '@mui/material';
1529
import LaunchIcon from 'mdi-material-ui/Launch';
1630
import { Link } from '@perses-dev/spec';
17-
import { MouseEvent, ReactElement, useState } from 'react';
31+
import { MouseEvent, ReactElement, useId, useState } from 'react';
1832
import { InfoTooltip } from '@perses-dev/components';
1933
import { useReplaceVariablesInString } from '@perses-dev/plugin-system';
2034

@@ -73,13 +87,19 @@ export function LinksDisplay({ links, variant }: LinksProps): ReactElement | nul
7387
}
7488
}
7589

76-
// Default: show dropdown menu for multiple links
90+
if (variant === 'panel') {
91+
return <PanelLinksDropdown links={links} />;
92+
}
93+
94+
// Dashboard variant: show dropdown menu for multiple links
95+
const menuButtonId = `${variant}-links-button`;
96+
7797
return (
78-
<>
98+
<Box sx={{ display: 'inline-flex' }}>
7999
<InfoTooltip description={`${links.length} links`} enterDelay={100}>
80100
<IconButton
81101
aria-label={`${capitalize(variant)}-links`}
82-
id={`${variant}-links-button`}
102+
id={menuButtonId}
83103
size="small"
84104
onClick={handleOpenMenu}
85105
sx={(theme) => ({ borderRadius: theme.shape.borderRadius, padding: '4px' })}
@@ -96,15 +116,75 @@ export function LinksDisplay({ links, variant }: LinksProps): ReactElement | nul
96116
anchorEl={anchorEl}
97117
open={isMenuOpened}
98118
onClose={handleClose}
119+
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
120+
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
99121
MenuListProps={{
100-
'aria-labelledby': `${variant}-links-button`,
122+
'aria-labelledby': menuButtonId,
101123
}}
102124
>
103125
{links.map((link: Link) => (
104126
<LinkMenuItem key={link.url} link={link} />
105127
))}
106128
</Menu>
107-
</>
129+
</Box>
130+
);
131+
}
132+
133+
function PanelLinksDropdown({ links }: { links: Link[] }): ReactElement {
134+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
135+
const menuId = useId();
136+
const open = Boolean(anchorEl);
137+
138+
const handleToggle = (event: MouseEvent<HTMLButtonElement>): void => {
139+
setAnchorEl(anchorEl ? null : event.currentTarget);
140+
};
141+
142+
const handleClose = (): void => {
143+
setAnchorEl(null);
144+
};
145+
146+
return (
147+
<Box sx={{ display: 'inline-flex', background: (theme) => theme.palette.background.default }}>
148+
<InfoTooltip description={`${links.length} links`} enterDelay={100}>
149+
<IconButton
150+
aria-label="Panel-links"
151+
aria-describedby={open ? menuId : undefined}
152+
size="small"
153+
onClick={handleToggle}
154+
sx={(theme) => ({ borderRadius: theme.shape.borderRadius, padding: '4px' })}
155+
>
156+
<LaunchIcon fontSize="inherit" sx={{ color: (theme: Theme) => theme.palette.text.secondary }} />
157+
</IconButton>
158+
</InfoTooltip>
159+
<Popper
160+
id={menuId}
161+
open={open}
162+
anchorEl={anchorEl}
163+
placement="bottom-end"
164+
// react-grid-layout applies CSS transforms to panels; fixed positioning keeps the menu
165+
// anchored to the link icon instead of using incorrect offset coordinates.
166+
popperOptions={{ strategy: 'fixed' }}
167+
modifiers={[
168+
{
169+
name: 'offset',
170+
options: {
171+
offset: [0, 4],
172+
},
173+
},
174+
]}
175+
sx={{ zIndex: (theme) => theme.zIndex.modal }}
176+
>
177+
<ClickAwayListener onClickAway={handleClose}>
178+
<Paper elevation={8}>
179+
<MenuList autoFocusItem={open}>
180+
{links.map((link: Link) => (
181+
<LinkMenuItem key={link.url} link={link} onNavigate={handleClose} />
182+
))}
183+
</MenuList>
184+
</Paper>
185+
</ClickAwayListener>
186+
</Popper>
187+
</Box>
108188
);
109189
}
110190

@@ -145,12 +225,12 @@ function LinkButton({ link }: { link: Link }): ReactElement {
145225
);
146226
}
147227

148-
function LinkMenuItem({ link }: { link: Link }): ReactElement {
228+
function LinkMenuItem({ link, onNavigate }: { link: Link; onNavigate?: () => void }): ReactElement {
149229
const { url, name, tooltip, targetBlank } = useLink(link);
150230

151231
return (
152232
<InfoTooltip description={tooltip ?? url} enterDelay={100}>
153-
<MenuItem component={LinkComponent} href={url} target={targetBlank ? '_blank' : '_self'}>
233+
<MenuItem component={LinkComponent} href={url} target={targetBlank ? '_blank' : '_self'} onClick={onNavigate}>
154234
{name ?? url}
155235
</MenuItem>
156236
</InfoTooltip>

dashboards/src/components/Panel/PanelActions.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,12 @@ export const PanelActions: React.FC<PanelActionsProps> = ({
109109
return undefined;
110110
}, [descriptionTooltipId, description]);
111111

112-
const linksAction = links && links.length > 0 && <LinksDisplay links={links} variant="panel" />;
113112
const extraActions = editHandlers === undefined && extra;
114113

114+
// Return a new LinksDisplay element on each call. A shared JSX variable reused across
115+
// responsive header branches can bind the menu anchor to a hidden breakpoint layout.
116+
const renderPanelLinks = (): ReactNode => (links?.length ? <LinksDisplay links={links} variant="panel" /> : null);
117+
115118
const queryStateIndicator = useMemo((): ReactNode | undefined => {
116119
const hasData = queryResults.some((q) => q.data);
117120
const isFetching = queryResults.some((q) => q.isFetching);
@@ -271,7 +274,8 @@ export const PanelActions: React.FC<PanelActionsProps> = ({
271274
{divider}
272275
<OnHover>
273276
<OverflowMenu title={title}>
274-
{descriptionAction} {linksAction} {queryStateIndicator} {noticesIndicator} {extraActions} {viewQueryAction}
277+
{descriptionAction} {renderPanelLinks()} {queryStateIndicator} {noticesIndicator} {extraActions}{' '}
278+
{viewQueryAction}
275279
{readActions} {pluginActions} {itemActions}
276280
{editActions}
277281
</OverflowMenu>
@@ -288,7 +292,7 @@ export const PanelActions: React.FC<PanelActionsProps> = ({
288292
})}
289293
>
290294
<OnHover>
291-
{descriptionAction} {linksAction}
295+
{descriptionAction} {renderPanelLinks()}
292296
</OnHover>
293297
{divider} {queryStateIndicator}
294298
{noticesIndicator}
@@ -311,7 +315,7 @@ export const PanelActions: React.FC<PanelActionsProps> = ({
311315
})}
312316
>
313317
<OnHover>
314-
{descriptionAction} {linksAction}
318+
{descriptionAction} {renderPanelLinks()}
315319
</OnHover>
316320
{divider} {queryStateIndicator}
317321
{noticesIndicator}

0 commit comments

Comments
 (0)