refactor(frontend): migrate Bash, Log, and Deploy feedback dialogs (#952)

- modal.tsx Modal now accepts a showClose prop that passes through to
  DialogContent, so callers that render their own close affordance
  (DeployFeedbackModal) can suppress the Radix close button
- DeployFeedbackModal -> Modal with showClose=false. Custom header,
  scroll body, embedded terminal, and footer stay in place; Modal
  provides the consistent overlay/blur chrome. DialogTitle is still
  imported from the dialog primitive purely as an sr-only
  accessibility wrapper, since this status panel doesn't fit the §10
  ModalHeader chrome
- BashExecModal -> Modal + ModalHeader. Kicker BASH · {CONTAINER}.
  The xterm container sits in the body
- LogViewer -> Modal + ModalHeader. Kicker LOGS · {CONTAINER}. The
  scroll region sits in the body
This commit is contained in:
Anso
2026-05-06 20:19:34 -04:00
committed by GitHub
parent 9ab7819b24
commit eb2d10af71
4 changed files with 67 additions and 59 deletions
+19 -22
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
import { Modal, ModalHeader } from './ui/modal';
import { Terminal as TerminalIcon } from 'lucide-react';
import { loadXtermModules, type Terminal, type FitAddon, type XtermModules } from '@/lib/xtermLoader';
import { buildXtermMinimalTheme } from '@/lib/terminalTheme';
@@ -214,31 +214,28 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl h-[600px] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TerminalIcon className="w-5 h-5" />
Bash: {containerName}
<Modal open={isOpen} onOpenChange={handleClose} className="max-w-4xl h-[600px] flex flex-col">
<ModalHeader
kicker={`BASH · ${containerName.toUpperCase()}`}
title={
<span className="flex items-center gap-2">
<TerminalIcon className="w-5 h-5" strokeWidth={1.5} />
Bash session
{isConnected && (
<span className="ml-2 text-xs bg-success/20 text-success px-2 py-0.5 rounded-full">
Connected
</span>
)}
</DialogTitle>
<DialogDescription className="sr-only">
Interactive bash terminal session for {containerName}
</DialogDescription>
</DialogHeader>
{/* Styling wrapper - padding and rounded corners go here */}
<div className="flex-1 rounded-lg bg-black p-1 min-h-0" style={{ overflow: 'hidden' }}>
{/* Clean xterm container - NO padding, NO overflow-hidden, explicit dimensions */}
<div
ref={terminalRef}
style={{ width: '100%', height: '100%' }}
/>
</div>
</DialogContent>
</Dialog>
</span>
}
description={`Interactive bash terminal session for ${containerName}`}
/>
<div className="flex-1 rounded-lg bg-black p-1 min-h-0 mx-6 mb-6" style={{ overflow: 'hidden' }}>
<div
ref={terminalRef}
style={{ width: '100%', height: '100%' }}
/>
</div>
</Modal>
);
}
+12 -11
View File
@@ -7,11 +7,8 @@ import {
Minimize2,
Terminal as TerminalIcon,
} from 'lucide-react';
import {
Dialog,
DialogContent,
DialogTitle,
} from '@/components/ui/dialog';
import { Modal } from '@/components/ui/modal';
import { DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { StructuredLogRow } from '@/components/log-rendering/StructuredLogRow';
import TerminalComponent from '@/components/Terminal';
@@ -156,11 +153,15 @@ export function DeployFeedbackModal({ isMinimized, onMinimize }: DeployFeedbackM
const verbLabel = VERB_LABELS[action];
return (
<Dialog open={isDialogOpen} onOpenChange={handleOpenChange}>
<DialogContent
showClose={false}
<Modal
open={isDialogOpen}
onOpenChange={handleOpenChange}
showClose={false}
className="max-w-[640px] max-h-[70vh] flex flex-col"
>
<div
data-testid="deploy-feedback-modal"
className="max-w-[640px] p-0 gap-0 max-h-[70vh] flex flex-col"
className="flex flex-col flex-1 min-h-0"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
@@ -273,8 +274,8 @@ export function DeployFeedbackModal({ isMinimized, onMinimize }: DeployFeedbackM
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</Modal>
);
}
+32 -25
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useRef } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Modal, ModalHeader } from "@/components/ui/modal";
import { Loader2, Terminal } from "lucide-react";
interface LogViewerProps {
@@ -56,30 +56,37 @@ export function LogViewer({ containerId, containerName, isOpen, onClose }: LogVi
}, [isOpen, containerId]);
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-4xl h-[80vh] flex flex-col bg-background border-border">
<DialogHeader className="flex flex-row items-center gap-2 pb-2 border-b">
<Terminal className="w-5 h-5" />
<DialogTitle className="flex-1 text-left font-mono text-sm">
{containerName} {isConnected ? <span className="text-success text-xs ml-2">(connected)</span> : <Loader2 className="inline w-3 h-3 ml-2 animate-spin" />}
</DialogTitle>
</DialogHeader>
<Modal open={isOpen} onOpenChange={(open) => !open && onClose()} className="max-w-4xl h-[80vh] flex flex-col">
<ModalHeader
kicker={`LOGS · ${containerName.toUpperCase()}`}
title={
<span className="flex items-center gap-2">
<Terminal className="w-5 h-5" strokeWidth={1.5} />
Container logs
{isConnected ? (
<span className="text-success text-xs ml-2">(connected)</span>
) : (
<Loader2 className="inline w-4 h-4 ml-2 animate-spin" />
)}
</span>
}
description={`Live log stream for ${containerName}`}
/>
<div
ref={scrollRef}
className="flex-1 w-full bg-[var(--terminal-bg)] text-success p-4 rounded-md overflow-y-auto font-mono text-xs mt-2"
>
{logs.length === 0 && !isConnected ? (
<div className="text-muted-foreground">Connecting to container stream...</div>
) : (
logs.map((log, i) => (
<div key={i} className="break-all whitespace-pre-wrap leading-tight mb-1">
{log}
</div>
))
)}
</div>
</DialogContent>
</Dialog>
<div
ref={scrollRef}
className="flex-1 w-full bg-[var(--terminal-bg)] text-success p-4 overflow-y-auto font-mono text-xs mx-6 mb-6 rounded-md"
>
{logs.length === 0 && !isConnected ? (
<div className="text-muted-foreground">Connecting to container stream...</div>
) : (
logs.map((log, i) => (
<div key={i} className="break-all whitespace-pre-wrap leading-tight mb-1">
{log}
</div>
))
)}
</div>
</Modal>
);
}
+4 -1
View File
@@ -34,12 +34,15 @@ interface ModalProps {
children: React.ReactNode;
size?: ModalSize;
className?: string;
/** Pass through to DialogContent — set to false when the modal renders its own close affordance. */
showClose?: boolean;
}
export function Modal({ open, onOpenChange, children, size = 'md', className }: ModalProps) {
export function Modal({ open, onOpenChange, children, size = 'md', className, showClose }: ModalProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showClose={showClose}
className={cn(
'p-0 gap-0 overflow-hidden grid-cols-1',
SIZE_CLASS[size],