Skip to content
34 changes: 29 additions & 5 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,30 @@ const Sidebar = ({
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
const [copiedServerFile, setCopiedServerFile] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const { toast } = useToast();

// Wrapper for onConnect that manages loading state
const handleConnect = useCallback(async () => {
try {
setIsConnecting(true);
await onConnect();
} finally {
setIsConnecting(false);
}
}, [onConnect]);

// Wrapper for restart that manages loading state
const handleRestart = useCallback(async () => {
try {
setIsConnecting(true);
onDisconnect();
await onConnect();
} finally {
setIsConnecting(false);
}
}, [onConnect, onDisconnect]);

// Reusable error reporter for copy actions
const reportError = useCallback(
(error: unknown) => {
Expand Down Expand Up @@ -676,10 +698,8 @@ const Sidebar = ({
<div className="grid grid-cols-2 gap-4">
<Button
data-testid="connect-button"
onClick={() => {
onDisconnect();
onConnect();
}}
onClick={handleRestart}
loading={isConnecting}
>
<RotateCcw className="w-4 h-4 mr-2" />
{transportType === "stdio" ? "Restart" : "Reconnect"}
Expand All @@ -691,7 +711,11 @@ const Sidebar = ({
</div>
)}
{connectionStatus !== "connected" && (
<Button className="w-full" onClick={onConnect}>
<Button
className="w-full"
onClick={handleConnect}
loading={isConnecting}
>
<Play className="w-4 h-4 mr-2" />
Connect
</Button>
Expand Down
64 changes: 62 additions & 2 deletions client/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,77 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
loadingText?: string;
}

const LoadingSpinner = () => (
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
(
{
className,
variant,
size,
asChild = false,
loading,
loadingText = "Loading...",
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : "button";

if (asChild) {
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{children}
</Comp>
);
}

return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
disabled={loading || props.disabled}
ref={ref}
{...props}
/>
>
{loading ? (
<>
<LoadingSpinner />
{loadingText}
</>
) : (
children
)}
</Comp>
);
},
);
Expand Down