Add frontend/src/components/PortCard.tsx
This commit is contained in:
parent
d8e4386426
commit
606004a443
1 changed files with 117 additions and 0 deletions
117
frontend/src/components/PortCard.tsx
Normal file
117
frontend/src/components/PortCard.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { PortStatus } from '../types';
|
||||||
|
|
||||||
|
interface PortCardProps {
|
||||||
|
port: PortStatus;
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: (portIndex: number) => void;
|
||||||
|
onStartRecording: (portIndex: number) => void;
|
||||||
|
onStopRecording: (portIndex: number) => void;
|
||||||
|
onOpenConfig: (portIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(seconds: number): string {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateFilePath(path: string, maxLength: number = 50): string {
|
||||||
|
if (path.length <= maxLength) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return '...' + path.slice(-(maxLength - 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PortCard({
|
||||||
|
port,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
onStartRecording,
|
||||||
|
onStopRecording,
|
||||||
|
onOpenConfig,
|
||||||
|
}: PortCardProps) {
|
||||||
|
const recordingBadgeClass = port.is_recording
|
||||||
|
? 'bg-red-500 text-white text-xs px-2 py-1 rounded'
|
||||||
|
: 'bg-gray-500 text-white text-xs px-2 py-1 rounded';
|
||||||
|
|
||||||
|
const recordingStatus = port.is_recording ? 'RECORDING' : 'IDLE';
|
||||||
|
|
||||||
|
const cardClass = `bg-gray-800 border border-gray-700 rounded-lg p-4 cursor-pointer hover:border-blue-500 transition-colors ${
|
||||||
|
isSelected ? 'border-blue-500 ring-2 ring-blue-500' : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cardClass} onClick={() => onSelect(port.port_index)}>
|
||||||
|
{/* Header: Port number and recording status badge */}
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-lg font-semibold text-white">Port {port.port_index}</h3>
|
||||||
|
<span className={recordingBadgeClass}>{recordingStatus}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metrics row */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-3 text-sm text-gray-300">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400">Frames:</span> {port.frame_count}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400">FPS:</span> {port.fps.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400">Bitrate:</span> {port.bitrate_mbps.toFixed(2)} Mbps
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400">Uptime:</span> {formatUptime(port.uptime_seconds)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current file path */}
|
||||||
|
<div className="mb-3 text-sm">
|
||||||
|
<span className="text-gray-400">File:</span>
|
||||||
|
<div className="text-gray-300 truncate font-mono text-xs mt-1">
|
||||||
|
{truncateFilePath(port.current_file)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Codec info */}
|
||||||
|
<div className="mb-4 text-sm text-gray-300">
|
||||||
|
<span className="text-gray-400">Codec:</span> {port.codec}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Control buttons */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!port.is_recording ? (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartRecording(port.port_index);
|
||||||
|
}}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm transition-colors"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStopRecording(port.port_index);
|
||||||
|
}}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition-colors"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenConfig(port.port_index);
|
||||||
|
}}
|
||||||
|
className="bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded text-sm transition-colors"
|
||||||
|
>
|
||||||
|
Config
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue