diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index b7cd0a09..a67db32f 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -4,6 +4,8 @@ * Displays CLIProxy process status with start/stop/restart controls. * Shows: running state, port, session count, uptime, update availability. * In remote mode: shows remote server info instead of local controls. + * + * Design: Two-state widget (collapsed/expanded) with icon-only control buttons. */ import { useState } from 'react'; @@ -20,10 +22,11 @@ import { Globe, AlertTriangle, Settings, + X, + Download, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'; import { Select, @@ -42,6 +45,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useQuery } from '@tanstack/react-query'; import { api, type CliproxyServerConfig } from '@/lib/api-client'; import { @@ -92,6 +96,53 @@ function formatTimeAgo(timestamp?: number): string { return `${hours}h ago`; } +/** Icon button with tooltip wrapper */ +function IconButton({ + icon: Icon, + tooltip, + onClick, + disabled, + isPending, + className, + variant = 'ghost', +}: { + icon: React.ElementType; + tooltip: string; + onClick: () => void; + disabled?: boolean; + isPending?: boolean; + className?: string; + variant?: 'ghost' | 'outline' | 'destructive-ghost'; +}) { + return ( + + + + + + {tooltip} + + + ); +} + export function ProxyStatusWidget() { const { data: status, isLoading } = useProxyStatus(); const { data: updateCheck } = useCliproxyUpdateCheck(); @@ -101,10 +152,9 @@ export function ProxyStatusWidget() { const restartProxy = useRestartProxy(); const installVersion = useInstallVersion(); - // Version picker state - const [showVersionSettings, setShowVersionSettings] = useState(false); + // Version picker state (expanded section) + const [isExpanded, setIsExpanded] = useState(false); const [selectedVersion, setSelectedVersion] = useState(''); - const [manualVersion, setManualVersion] = useState(''); // Confirmation dialog state for unstable versions const [showUnstableConfirm, setShowUnstableConfirm] = useState(false); @@ -129,6 +179,12 @@ export function ProxyStatusWidget() { installVersion.isPending; const hasUpdate = updateCheck?.hasUpdate ?? false; const isUnstable = updateCheck?.isStable === false; + const currentVersion = updateCheck?.currentVersion; + + // Target version for update/downgrade badge + const targetVersion = isUnstable + ? updateCheck?.maxStableVersion || versionsData?.latestStable + : updateCheck?.latestVersion; // Handle version install (shows confirmation for unstable) const handleInstallVersion = (version: string) => { @@ -207,49 +263,104 @@ export function ProxyStatusWidget() { ); } - // Local mode: show original controls - + // Local mode: Two-state widget (collapsed/expanded) return ( -
-
-
-
+
+ {/* Header row: Status dot, title, version, update badge, icon buttons */} +
+
+ {/* Status indicator */} +
+ CLIProxy Plus + + {/* Version in header */} + {currentVersion && ( + + v{currentVersion} + )} - /> - CLIProxy Plus - {hasUpdate && ( - v${updateCheck?.latestVersion}`} - > - - Update - - )} + + {/* Clickable Update/Downgrade badge */} + {(hasUpdate || isUnstable) && targetVersion && ( + handleInstallVersion(targetVersion)} + title={`Click to ${isUnstable ? 'downgrade' : 'update'}`} + > + {isUnstable ? ( + <> + + {targetVersion} + + ) : ( + <> + + {targetVersion} + + )} + + )} +
+ + {/* Right side: status icon + control buttons when running */} +
+ {isLoading ? ( + + ) : isRunning ? ( + <> + {/* Icon buttons: Restart, Stop, Settings/Close */} + restartProxy.mutate()} + disabled={isActioning} + isPending={restartProxy.isPending} + /> + stopProxy.mutate()} + disabled={isActioning} + isPending={stopProxy.isPending} + variant="destructive-ghost" + /> + setIsExpanded(!isExpanded)} + className={isExpanded ? 'bg-muted' : undefined} + /> + + ) : ( + + )} +
-
- {isLoading ? ( - - ) : isRunning ? ( - - ) : ( - - )} -
-
- - {isRunning && status ? ( - <> + {/* Stats row when running */} + {isRunning && status && (
Port {status.port} {status.sessionCount !== undefined && status.sessionCount > 0 && ( @@ -265,254 +376,138 @@ export function ProxyStatusWidget() { )}
- {/* Control buttons when running: Restart | Update/Downgrade | Stop | Settings */} -
- {/* Restart button - pure restart, no version change */} + )} + + {/* Expanded section: Version Management */} + {isRunning && ( + + + {/* Section header */} +

Version Management

+ + {/* Version picker row */} +
+ {/* Dropdown - full width, no truncation */} + + + {/* Install button */} + +
+ + {/* Stability warning for selected version */} + {selectedVersion && + versionsData?.maxStableVersion && + isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && ( +
+ + Versions above {versionsData.maxStableVersion} have known issues +
+ )} + + {/* Sync time */} + {updateCheck?.checkedAt && ( +
+ Last checked {formatTimeAgo(updateCheck.checkedAt)} +
+ )} +
+
+ )} + + {/* Not running state */} + {!isRunning && ( +
+ Not running - - {/* Update/Downgrade button - version change */} - - - {/* Stop button */} - - - {/* Settings gear - toggle version picker */} -
+ )} - {/* Version Settings (collapsible) */} - - -
- {/* Current version */} -
- Current: - - v{updateCheck?.currentVersion} - {isUnstable && ' (unstable)'} - -
- - {/* Version picker row */} -
- {/* Dropdown */} - - - {/* Manual input */} - { - setManualVersion(e.target.value); - setSelectedVersion(''); - }} - className="h-7 text-xs w-24" - /> - - {/* Install button */} - -
- - {/* Stability warning */} - {(selectedVersion || manualVersion) && - versionsData && - isNewerVersionClient( - manualVersion || selectedVersion, - versionsData.maxStableVersion - ) && ( -
- - - Versions above {versionsData.maxStableVersion} have known stability issues - -
- )} -
-
-
- - ) : ( -
- Not running - -
- )} - - {/* Version sync indicator */} - {updateCheck?.currentVersion && ( -
- - {isUnstable && ( - - )} - - v{updateCheck.currentVersion} - - {isUnstable && ( - (unstable) - )} - - {updateCheck.checkedAt && ( - - Synced {formatTimeAgo(updateCheck.checkedAt)} - - )} -
- )} - - {/* Unstable Version Confirmation Dialog */} - - - - - - Install Unstable Version? - - -

- You are about to install v{pendingInstallVersion}, which is above - the maximum stable version{' '} - v{versionsData?.maxStableVersion || '6.6.80'}. -

-

- This version has known stability issues and may cause unexpected behavior. -

-

Are you sure you want to proceed?

-
-
- - Cancel - - Install Anyway - - -
-
-
+ {/* Unstable Version Confirmation Dialog */} + + + + + + Install Unstable Version? + + +

+ You are about to install v{pendingInstallVersion}, which is above + the maximum stable version{' '} + v{versionsData?.maxStableVersion || '6.6.80'}. +

+

+ This version has known stability issues and may cause unexpected behavior. +

+

Are you sure you want to proceed?

+
+
+ + Cancel + + Install Anyway + + +
+
+
+ ); }