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
2 changes: 1 addition & 1 deletion api/api/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"version": "v1",
"status": "active",
"release_date": "2025-09-10T17:02:59.771205+05:30",
"release_date": "2025-09-11T20:17:18.854289411+05:30",
"end_of_life": "0001-01-01T00:00:00Z",
"changes": [
"Initial API version"
Expand Down
4 changes: 2 additions & 2 deletions view/app/containers/[id]/components/OverviewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ export function OverviewTab({ container }: OverviewTabProps) {
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{container?.ports?.map((port) => (
<Badge key={`${port.private_port}-${port.public_port}`} variant="outline">
{container?.ports?.map((port, index) => (
<Badge key={`${port.private_port}-${port.public_port}-${index}`} variant="outline">
{port.public_port} → {port.private_port} ({port.type})
</Badge>
))}
Expand Down
61 changes: 61 additions & 0 deletions view/app/containers/[id]/components/Terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';
import React, { useEffect, useMemo, useRef } from 'react';
import '@xterm/xterm/css/xterm.css';
import { v4 as uuidv4 } from 'uuid';
import { useTerminal } from '@/app/terminal/utils/useTerminal';
import { useContainerReady } from '@/app/terminal/utils/isContainerReady';
import { useWebSocket } from '@/hooks/socket-provider';

type TerminalProps = {
containerId: string;
};

export const Terminal: React.FC<TerminalProps> = ({ containerId }) => {
const terminalRef = useRef<HTMLDivElement | null>(null);
const sessionId = useMemo(() => `container-${containerId}-${uuidv4()}`, [containerId]);
const { sendJsonMessage, isReady } = useWebSocket();

const { terminalRef: termRef, initializeTerminal, terminalInstance } = useTerminal(
true,
0,
0,
true,
sessionId
);

const isMounted = useContainerReady(true, termRef as React.RefObject<HTMLDivElement>);

useEffect(() => {
if (isMounted) {
initializeTerminal();
}
}, [isMounted, initializeTerminal]);

const hasSentInitRef = useRef(false);
useEffect(() => {
if (!hasSentInitRef.current && terminalInstance && isReady) {
hasSentInitRef.current = true;
// TODO: optimize this such that backend handles this instead of client.
const cmd = `if docker ps >/dev/null 2>&1; then D=docker; else D='sudo -n docker'; fi; $D exec -it ${containerId} /bin/bash || $D exec -it ${containerId} /bin/sh\r`;
setTimeout(() => {
sendJsonMessage({ action: 'terminal', data: { value: cmd, terminalId: sessionId } });
}, 150);
}
}, [terminalInstance, isReady, sendJsonMessage, containerId, sessionId]);

return (
<div
ref={(el) => {
terminalRef.current = el;
// @ts-ignore
if (termRef) termRef.current = el;
}}
className="relative"
style={{ height: '60vh', minHeight: 300, backgroundColor: '#1e1e1e' }}
/>
);
};

export default Terminal;


16 changes: 15 additions & 1 deletion view/app/containers/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useEffect, useState } from 'react';
import { OverviewTab } from './components/OverviewTab';
import { LogsTab } from './components/LogsTab';
import { DetailsTab } from './components/DetailsTab';
import { Terminal as TerminalComponent } from './components/Terminal';
import ContainerDetailsLoading from './components/ContainerDetailsLoading';
import { DeleteDialog } from '@/components/ui/delete-dialog';
import { Images } from './components/images';
Expand Down Expand Up @@ -169,7 +170,7 @@ export default function ContainerDetailsPage() {

<div className="space-y-4">
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="overview">
<Info className="mr-2 h-4 w-4" />
{t('containers.overview')}
Expand All @@ -178,6 +179,10 @@ export default function ContainerDetailsPage() {
<Layers className="mr-2 h-4 w-4" />
{t('containers.images.title')}
</TabsTrigger>
<TabsTrigger value="terminal" disabled={container.status !== 'running'}>
<Terminal className="mr-2 h-4 w-4" />
{t('terminal.title')}
</TabsTrigger>
<TabsTrigger value="logs">
<Terminal className="mr-2 h-4 w-4" />
{t('containers.logs')}
Expand All @@ -196,6 +201,15 @@ export default function ContainerDetailsPage() {
<TabsContent value="details" className="mt-4">
<DetailsTab container={container} />
</TabsContent>
<TabsContent value="terminal" className="mt-4">
{container.status === 'running' ? (
<TerminalComponent containerId={containerId} />
) : (
<div className="flex items-center justify-center h-48 text-muted-foreground">
Start the container to use the terminal
</div>
)}
</TabsContent>
<TabsContent value="images" className="mt-4">
{container.image ? (
<Images containerId={containerId} imagePrefix={container.image + '*'} />
Expand Down
Loading