1- /**
2- * ServerActionMenu - Overflow menu for server actions
3- *
4- * Actions:
5- * - Configure: Edit server inputs
6- * - Refresh: Quick reconnect with existing credentials
7- * - Reconnect: Logout + re-authenticate (OAuth only)
8- * - View Logs: Open log viewer
9- * - View Definition: View server definition JSON
10- * - Uninstall: Remove server
11- */
12-
13- import { useState , useRef , useEffect } from 'react' ;
141import { MoreVertical , Settings , RefreshCw , RotateCcw , FileText , Code , Trash2 , Copy } from 'lucide-react' ;
2+ import {
3+ DropdownMenu ,
4+ DropdownMenuAction ,
5+ DropdownMenuContent ,
6+ DropdownMenuSeparator ,
7+ DropdownMenuTrigger ,
8+ } from '@mcpmux/ui' ;
159
1610export interface ServerActionMenuProps {
1711 serverId : string ;
@@ -31,6 +25,9 @@ export interface ServerActionMenuProps {
3125 onUninstall : ( ) => void ;
3226}
3327
28+ /**
29+ * Overflow menu for per-server actions (configure, logs, uninstall, etc.).
30+ */
3431export function ServerActionMenu ( {
3532 serverId,
3633 serverName : _serverName ,
@@ -47,155 +44,63 @@ export function ServerActionMenu({
4744 onCloneAccount,
4845 onUninstall,
4946} : ServerActionMenuProps ) {
50- const [ isOpen , setIsOpen ] = useState ( false ) ;
51- const menuRef = useRef < HTMLDivElement > ( null ) ;
52- const buttonRef = useRef < HTMLButtonElement > ( null ) ;
53-
54- // Close menu when clicking outside
55- useEffect ( ( ) => {
56- function handleClickOutside ( event : MouseEvent ) {
57- if (
58- menuRef . current &&
59- ! menuRef . current . contains ( event . target as Node ) &&
60- buttonRef . current &&
61- ! buttonRef . current . contains ( event . target as Node )
62- ) {
63- setIsOpen ( false ) ;
64- }
65- }
66-
67- if ( isOpen ) {
68- document . addEventListener ( 'mousedown' , handleClickOutside ) ;
69- return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
70- }
71- } , [ isOpen ] ) ;
72-
73- // Close menu on escape
74- useEffect ( ( ) => {
75- function handleEscape ( event : KeyboardEvent ) {
76- if ( event . key === 'Escape' ) {
77- setIsOpen ( false ) ;
78- }
79- }
80-
81- if ( isOpen ) {
82- document . addEventListener ( 'keydown' , handleEscape ) ;
83- return ( ) => document . removeEventListener ( 'keydown' , handleEscape ) ;
84- }
85- } , [ isOpen ] ) ;
86-
87- const handleAction = ( action : ( ) => void ) => {
88- setIsOpen ( false ) ;
89- action ( ) ;
90- } ;
91-
9247 return (
93- < div className = "relative" >
94- < button
95- ref = { buttonRef }
96- onClick = { ( ) => setIsOpen ( ! isOpen ) }
97- className = "p-2 text-sm rounded-lg bg-[rgb(var(--surface-hover))] border border-[rgb(var(--border))] text-[rgb(var(--foreground))]/70 hover:bg-[rgb(var(--surface-elevated))] hover:text-[rgb(var(--foreground))] transition-colors"
98- title = "More actions"
99- aria-label = "More actions"
100- aria-expanded = { isOpen }
101- aria-haspopup = "menu"
102- data-testid = { `action-menu-${ serverId } ` }
103- >
104- < MoreVertical className = "h-4 w-4" />
105- </ button >
106-
107- { isOpen && (
108- < div
109- ref = { menuRef }
110- className = "absolute right-0 mt-1 w-48 py-1 bg-[rgb(var(--surface-elevated))] border border-[rgb(var(--border))] rounded-lg shadow-lg z-50 animate-in fade-in slide-in-from-top-1 duration-150"
111- role = "menu"
48+ < DropdownMenu >
49+ < DropdownMenuTrigger >
50+ < button
51+ type = "button"
52+ className = "p-2 text-sm rounded-lg bg-[rgb(var(--surface-hover))] border border-[rgb(var(--border))] text-[rgb(var(--foreground))]/70 hover:bg-[rgb(var(--surface-elevated))] hover:text-[rgb(var(--foreground))] transition-colors"
53+ title = "More actions"
54+ aria-label = "More actions"
55+ data-testid = { `action-menu-${ serverId } ` }
11256 >
113- { /* Configure - visible if server has inputs */ }
114- { hasInputs && (
115- < button
116- onClick = { ( ) => handleAction ( onConfigure ) }
117- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
118- role = "menuitem"
119- >
120- < Settings className = "h-4 w-4 text-[rgb(var(--muted))]" />
121- Configure
122- </ button >
123- ) }
124-
125- { /* Refresh - visible when enabled (quick reconnect with existing creds) */ }
126- { isEnabled && (
127- < button
128- onClick = { ( ) => handleAction ( onRefresh ) }
129- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
130- role = "menuitem"
131- >
132- < RefreshCw className = "h-4 w-4 text-[rgb(var(--muted))]" />
133- Refresh
134- </ button >
135- ) }
136-
137- { /* Reconnect - OAuth only (logout + re-auth) */ }
138- { isOAuth && isEnabled && (
139- < button
140- onClick = { ( ) => handleAction ( onReconnect ) }
141- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--warning))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
142- role = "menuitem"
143- >
144- < RotateCcw className = "h-4 w-4" />
145- Reconnect
146- </ button >
147- ) }
148-
149- { /* View Logs - always visible */ }
150- < button
151- onClick = { ( ) => handleAction ( onViewLogs ) }
152- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
153- role = "menuitem"
154- data-testid = { `view-logs-${ serverId } ` }
155- >
156- < FileText className = "h-4 w-4 text-[rgb(var(--muted))]" />
157- View Logs
158- </ button >
159-
160- { /* View Definition - always visible */ }
161- < button
162- onClick = { ( ) => handleAction ( onViewDefinition ) }
163- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
164- role = "menuitem"
165- data-testid = { `view-definition-${ serverId } ` }
166- >
167- < Code className = "h-4 w-4 text-[rgb(var(--muted))]" />
168- View Definition
169- </ button >
170-
171- { /* Add another account - registry/manual installs only, not clones-of-clones */ }
172- { canCloneAccount && onCloneAccount && (
173- < button
174- onClick = { ( ) => handleAction ( onCloneAccount ) }
175- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
176- role = "menuitem"
177- data-testid = { `clone-account-${ serverId } ` }
178- >
179- < Copy className = "h-4 w-4 text-[rgb(var(--muted))]" />
180- Add another account…
181- </ button >
182- ) }
183-
184- { /* Separator */ }
185- < div className = "my-1 border-t border-[rgb(var(--border-subtle))]" />
186-
187- { /* Uninstall - always visible, destructive */ }
188- < button
189- onClick = { ( ) => handleAction ( onUninstall ) }
190- className = "w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--error))] hover:bg-[rgb(var(--error))]/10 transition-colors"
191- role = "menuitem"
192- data-testid = { `uninstall-menu-${ serverId } ` }
193- >
194- < Trash2 className = "h-4 w-4" />
195- Uninstall
196- </ button >
197- </ div >
198- ) }
199- </ div >
57+ < MoreVertical className = "h-4 w-4" />
58+ </ button >
59+ </ DropdownMenuTrigger >
60+ < DropdownMenuContent align = "end" className = "w-48 py-1 p-1" >
61+ { hasInputs && (
62+ < DropdownMenuAction icon = { Settings } label = "Configure" onSelect = { onConfigure } />
63+ ) }
64+ { isEnabled && (
65+ < DropdownMenuAction icon = { RefreshCw } label = "Refresh" onSelect = { onRefresh } />
66+ ) }
67+ { isOAuth && isEnabled && (
68+ < DropdownMenuAction
69+ icon = { RotateCcw }
70+ label = "Reconnect"
71+ onSelect = { onReconnect }
72+ variant = "warning"
73+ />
74+ ) }
75+ < DropdownMenuAction
76+ icon = { FileText }
77+ label = "View Logs"
78+ onSelect = { onViewLogs }
79+ data-testid = { `view-logs-${ serverId } ` }
80+ />
81+ < DropdownMenuAction
82+ icon = { Code }
83+ label = "View Definition"
84+ onSelect = { onViewDefinition }
85+ data-testid = { `view-definition-${ serverId } ` }
86+ />
87+ { canCloneAccount && onCloneAccount && (
88+ < DropdownMenuAction
89+ icon = { Copy }
90+ label = "Add another account…"
91+ onSelect = { onCloneAccount }
92+ data-testid = { `clone-account-${ serverId } ` }
93+ />
94+ ) }
95+ < DropdownMenuSeparator />
96+ < DropdownMenuAction
97+ icon = { Trash2 }
98+ label = "Uninstall"
99+ onSelect = { onUninstall }
100+ variant = "danger"
101+ data-testid = { `uninstall-menu-${ serverId } ` }
102+ />
103+ </ DropdownMenuContent >
104+ </ DropdownMenu >
200105 ) ;
201106}
0 commit comments