mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user