Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
tmp/
testing/
dist/
node_modules/
.env
Expand Down
1 change: 0 additions & 1 deletion dash/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
Expand Down
19 changes: 9 additions & 10 deletions dash/src/components/applications/live-logs-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { useContainerLogs } from "@/hooks"
import { LogLine } from "@/components/logs/log-line"
import {
Loader2,
Wifi,
Expand Down Expand Up @@ -63,7 +64,7 @@ export const LiveLogsViewer = ({ appId, enabled = true }: LiveLogsViewerProps) =
}, [])

const downloadLogs = () => {
const logText = logs.join("\n")
const logText = logs.map(log => log.line).join("\n")
const blob = new Blob([logText], { type: "text/plain" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
Expand Down Expand Up @@ -176,19 +177,17 @@ export const LiveLogsViewer = ({ appId, enabled = true }: LiveLogsViewerProps) =
<>
<div
ref={logsContainerRef}
className="flex-1 overflow-y-auto bg-slate-950 text-slate-100 font-mono text-xs sm:text-sm p-4 space-y-0.5"
className="flex-1 overflow-y-auto bg-slate-950 text-slate-100 p-4 space-y-0.5"
style={{ height: "calc(100vh - 400px)", minHeight: "400px" }}
>
{logs.map((log, index) => (
<div
<LogLine
key={index}
className="hover:bg-slate-900 px-2 py-0.5 rounded transition-colors"
>
<span className="text-slate-500 select-none mr-3">
{String(index + 1).padStart(4, " ")}
</span>
<span className="whitespace-pre-wrap break-all">{log}</span>
</div>
line={log.line}
index={index}
showLineNumbers={true}
streamType={log.stream}
/>
))}
<div ref={logsEndRef} />
</div>
Expand Down
58 changes: 25 additions & 33 deletions dash/src/components/deployments/deployment-monitor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useEffect, useRef } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Badge } from '@/components/ui/badge';
import { Terminal, X, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react';
import { Terminal, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react';
import { useDeploymentMonitor } from '@/hooks';
import { LogLine } from '@/components/logs/log-line';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';

Expand Down Expand Up @@ -94,17 +94,17 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
const statusInfo = getStatusInfo();

return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent
showCloseButton={false}
className="w-full max-w-[90vw] lg:max-w-[85vw] h-[90vh] p-0 rounded-xl overflow-hidden border border-border bg-background/95 backdrop-blur-xl shadow-2xl flex flex-col gap-0"
<Sheet open={open} onOpenChange={handleClose}>
<SheetContent
side="right"
className="w-full sm:w-[90vw] sm:max-w-[90vw] lg:w-[85vw] lg:max-w-[85vw] p-0 flex flex-col gap-0"
>
{/* Header */}
<DialogHeader className="px-6 py-4 border-b bg-background/80 backdrop-blur flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 shrink-0">
<DialogTitle className="flex items-center gap-3 text-lg">
<SheetHeader className="px-10 py-4 border-b bg-background/80 backdrop-blur flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 shrink-0">
<SheetTitle className="flex items-center gap-3 text-lg">
<Terminal className="h-5 w-5 text-primary" />
<span>Deployment Monitor</span>
</DialogTitle>
</SheetTitle>

<div className="flex items-center gap-3">
{/* Connection Status */}
Expand All @@ -127,15 +127,8 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
<Badge variant="outline" className="font-mono text-xs px-2 py-0.5">
#{deploymentId}
</Badge>

<button
onClick={handleClose}
className="p-1.5 hover:bg-muted rounded-md transition"
>
<X className="h-5 w-5" />
</button>
</div>
</DialogHeader>
</SheetHeader>

{/* Status Bar */}
{status && (
Expand Down Expand Up @@ -194,7 +187,7 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
)}

{/* Logs Viewer */}
<div className="flex-1 bg-black text-green-400 p-4 overflow-auto font-mono text-sm leading-relaxed">
<div className="flex-1 bg-slate-950 p-4 overflow-auto">
{isLoading ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<Loader2 className="h-8 w-8 animate-spin mb-3" />
Expand All @@ -208,22 +201,21 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
) : (
<div className="space-y-0.5">
{logs.map((log, index) => (
<div key={index} className="whitespace-pre-wrap break-words">
{log}
</div>
<LogLine
key={index}
line={log.line}
index={index}
showLineNumbers={false}
streamType={log.stream}
/>
))}
<div ref={bottomRef} />
</div>
)}
</div>

{/* Footer */}
<div className="px-6 py-3 border-t bg-background/80 backdrop-blur flex justify-end shrink-0">
<Button onClick={handleClose} className="px-6">
Close
</Button>
</div>
</DialogContent>
</Dialog>
</SheetContent>
</Sheet>
);
};
2 changes: 2 additions & 0 deletions dash/src/components/logs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { LogLine } from './log-line';
export type { LogLineProps } from './log-line';
89 changes: 89 additions & 0 deletions dash/src/components/logs/log-line.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { memo } from 'react';
import { parseAnsi, type AnsiSegment } from '@/lib/ansi-parser';
import { cn } from '@/lib/utils';

export interface LogLineProps {
line: string;
index: number;
showLineNumbers?: boolean;
streamType?: 'stdout' | 'stderr';
className?: string;
}

/**
* Renders a single log line with ANSI color support and stream type indicators
*/
export const LogLine = memo(({
line,
index,
showLineNumbers = true,
streamType,
className,
}: LogLineProps) => {
const segments = parseAnsi(line);

return (
<div
className={cn(
'hover:bg-slate-900/50 px-2 py-0.5 rounded transition-colors flex items-start gap-2',
className
)}
>
{/* Line Number */}
{showLineNumbers && (
<span className="text-slate-500 select-none font-mono text-sm min-w-[3ch] text-right shrink-0 mt-[2px]">
{String(index + 1).padStart(4, ' ')}
</span>
)}

{/* Stream Type Indicator */}
{streamType && (
<span
className={cn(
'select-none font-mono text-xs px-1.5 py-1.5 rounded shrink-0 mt-[2px]',
streamType === 'stderr'
? 'bg-red-500/20 text-red-400'
: 'bg-blue-500/20 text-blue-400'
)}
title={streamType === 'stderr' ? 'Standard Error' : 'Standard Output'}
>
{streamType === 'stderr' ? 'ERR' : 'OUT'}
</span>
)}

{/* Log Content with ANSI colors */}
<div className="flex-1 whitespace-pre-wrap break-all font-mono text-sm leading-relaxed">
{segments.map((segment: AnsiSegment, i: number) => {
const style: React.CSSProperties = {};

if (segment.styles.color) {
style.color = segment.styles.color;
}
if (segment.styles.backgroundColor) {
style.backgroundColor = segment.styles.backgroundColor;
}
if (segment.styles.bold) {
style.fontWeight = 'bold';
}
if (segment.styles.italic) {
style.fontStyle = 'italic';
}
if (segment.styles.underline) {
style.textDecoration = 'underline';
}
if (segment.styles.dim) {
style.opacity = 0.5;
}

return (
<span className='text-base' key={i} style={style}>
{segment.text}
</span>
);
})}
</div>
</div>
);
});

LogLine.displayName = 'LogLine';
2 changes: 2 additions & 0 deletions dash/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { useDeploymentMonitor } from './use-deployment-monitor';
export type { DeploymentLogEntry } from './use-deployment-monitor';
export { useIsMobile } from './use-mobile';
export { useContainerLogs } from './use-container-logs';
export type { ContainerLogEntry } from './use-container-logs';
export { useProjects } from './use-projects';
export { useProject } from './use-project';
export { useApplications } from './use-applications';
Expand Down
16 changes: 14 additions & 2 deletions dash/src/hooks/use-container-logs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useEffect, useRef, useState, useCallback } from 'react';

export interface ContainerLogEntry {
line: string;
stream?: 'stdout' | 'stderr';
}

interface ContainerLogEvent {
type: 'log' | 'status' | 'error' | 'end';
timestamp: string;
data: {
line?: string;
stream?: 'stdout' | 'stderr';
message?: string;
container?: string;
state?: string;
Expand All @@ -23,7 +29,7 @@ export const useContainerLogs = ({
enabled,
onError,
}: UseContainerLogsOptions) => {
const [logs, setLogs] = useState<string[]>([]);
const [logs, setLogs] = useState<ContainerLogEntry[]>([]);
const [containerState, setContainerState] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [isConnected, setIsConnected] = useState(false);
Expand Down Expand Up @@ -70,7 +76,13 @@ export const useContainerLogs = ({
switch (logEvent.type) {
case 'log': {
if (logEvent.data.line && logEvent.data.line.trim()) {
setLogs((prev) => [...prev, logEvent.data.line!]);
setLogs((prev) => [
...prev,
{
line: logEvent.data.line!,
stream: logEvent.data.stream,
},
]);
}
break;
}
Expand Down
Loading
Loading