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' ;
1529import LaunchIcon from 'mdi-material-ui/Launch' ;
1630import { Link } from '@perses-dev/spec' ;
17- import { MouseEvent , ReactElement , useState } from 'react' ;
31+ import { MouseEvent , ReactElement , useId , useState } from 'react' ;
1832import { InfoTooltip } from '@perses-dev/components' ;
1933import { 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 >
0 commit comments