Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
194 lines
10 KiB
TypeScript
194 lines
10 KiB
TypeScript
import React from 'react';
|
|
import { PortForwardingType } from '../domain/models';
|
|
import { AppLogo } from './AppLogo';
|
|
|
|
// SVG Icon components from public folder
|
|
const CloudIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
<img src="/cloud.svg" alt="cloud" className={className} />
|
|
);
|
|
|
|
const FirewallIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
<img src="/firewall.svg" alt="firewall" className={className} />
|
|
);
|
|
|
|
const ServerIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
<img src="/server.svg" alt="server" className={className} />
|
|
);
|
|
|
|
// Animated diagram component for port forwarding visualization
|
|
interface TrafficDiagramProps {
|
|
type: PortForwardingType;
|
|
isAnimating?: boolean;
|
|
/** Which role to highlight: 'app' | 'ssh-server' | 'target' | undefined (all visible) */
|
|
highlightRole?: 'app' | 'ssh-server' | 'target';
|
|
}
|
|
|
|
// AppLogo is now imported from ./AppLogo to share accent color theming
|
|
|
|
// Animated line component
|
|
const AnimatedLine: React.FC<{
|
|
x1: number; y1: number; x2: number; y2: number;
|
|
isAnimating: boolean;
|
|
reverse?: boolean;
|
|
isBlocked?: boolean;
|
|
}> = ({ x1, y1, x2, y2, isAnimating, reverse = false, isBlocked = false }) => {
|
|
if (isBlocked) {
|
|
return <line x1={x1} y1={y1} x2={x2} y2={y2} className="stroke-destructive" strokeWidth="2.5" />;
|
|
}
|
|
return (
|
|
<>
|
|
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="hsl(var(--border))" strokeWidth="2" strokeDasharray="6 4" />
|
|
<line
|
|
x1={x1} y1={y1} x2={x2} y2={y2}
|
|
className="stroke-primary"
|
|
strokeWidth="3"
|
|
strokeDasharray="12 12"
|
|
strokeLinecap="round"
|
|
>
|
|
{isAnimating && (
|
|
<animate
|
|
attributeName="stroke-dashoffset"
|
|
values={reverse ? "0;24" : "24;0"}
|
|
dur="0.6s"
|
|
repeatCount="indefinite"
|
|
/>
|
|
)}
|
|
</line>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const TrafficDiagram: React.FC<TrafficDiagramProps> = ({ type, isAnimating = true, highlightRole }) => {
|
|
// Helper to determine opacity based on highlight role
|
|
const getOpacity = (role: 'app' | 'ssh-server' | 'target' | 'firewall') => {
|
|
if (!highlightRole) return 'opacity-100';
|
|
if (role === 'firewall') return 'opacity-30'; // Firewall always dimmed when highlighting
|
|
return role === highlightRole ? 'opacity-100' : 'opacity-30';
|
|
};
|
|
|
|
return (
|
|
<div className="relative w-full h-48 flex items-center justify-center overflow-hidden rounded-xl bg-secondary/60 border border-border/50">
|
|
{/* ========== LOCAL FORWARDING ========== */}
|
|
{type === 'local' && (
|
|
<div className="relative w-[300px] max-w-full aspect-[300/170]">
|
|
{/* App Logo - left (same line as firewall) */}
|
|
<div className={`absolute left-6 top-5 z-10 transition-opacity duration-300 ${getOpacity('app')}`}>
|
|
<AppLogo className="h-12 w-12" />
|
|
</div>
|
|
|
|
{/* Firewall - center top (same line as app) */}
|
|
<div className={`absolute left-1/2 -translate-x-1/2 top-4 z-10 transition-opacity duration-300 ${getOpacity('firewall')}`}>
|
|
<FirewallIcon className="h-14 w-14" />
|
|
</div>
|
|
|
|
{/* Target servers - right (in red border box) */}
|
|
<div className={`absolute right-4 top-2 z-10 transition-opacity duration-300 ${getOpacity('target')}`}>
|
|
<div className="p-2 border-2 border-destructive/60 rounded-lg space-y-2">
|
|
<ServerIcon className="h-8 w-8" />
|
|
<ServerIcon className="h-8 w-8" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* SSH Server - bottom center */}
|
|
<div className={`absolute left-1/2 -translate-x-1/2 bottom-4 z-10 transition-opacity duration-300 ${getOpacity('ssh-server')}`}>
|
|
<ServerIcon className="h-10 w-10" />
|
|
</div>
|
|
|
|
{/* SVG Lines */}
|
|
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 300 170" preserveAspectRatio="none" style={{ zIndex: 1 }}>
|
|
{/* App to Firewall - blocked red line (with gap) */}
|
|
<AnimatedLine x1={78} y1={44} x2={122} y2={44} isAnimating={false} isBlocked />
|
|
{/* App to SSH Server - blue animated (with gap) */}
|
|
<AnimatedLine x1={76} y1={64} x2={126} y2={110} isAnimating={isAnimating} />
|
|
{/* SSH Server to targets - blue animated (with gap) */}
|
|
<AnimatedLine x1={174} y1={112} x2={242} y2={28} isAnimating={isAnimating} />
|
|
<AnimatedLine x1={178} y1={120} x2={242} y2={68} isAnimating={isAnimating} />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
|
|
{/* ========== REMOTE FORWARDING ========== */}
|
|
{type === 'remote' && (
|
|
<div className="relative w-[300px] max-w-full aspect-[300/170]">
|
|
{/* Left Server - the remote SSH server where port will be opened */}
|
|
<div className={`absolute left-6 top-5 z-10 transition-opacity duration-300 ${getOpacity('ssh-server')}`}>
|
|
<ServerIcon className="h-10 w-10" />
|
|
</div>
|
|
|
|
{/* Firewall - center top */}
|
|
<div className={`absolute left-1/2 -translate-x-1/2 top-4 z-10 transition-opacity duration-300 ${getOpacity('firewall')}`}>
|
|
<FirewallIcon className="h-14 w-14" />
|
|
</div>
|
|
|
|
{/* Right Server - the destination where traffic will be forwarded */}
|
|
<div className={`absolute right-6 top-5 z-10 transition-opacity duration-300 ${getOpacity('target')}`}>
|
|
<ServerIcon className="h-10 w-10" />
|
|
</div>
|
|
|
|
{/* App Logo - bottom center (Netcatty client) */}
|
|
<div className={`absolute left-1/2 -translate-x-1/2 bottom-4 z-10 transition-opacity duration-300 ${getOpacity('app')}`}>
|
|
<AppLogo className="h-12 w-12" />
|
|
</div>
|
|
|
|
{/* SVG Lines */}
|
|
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 300 170" preserveAspectRatio="none" style={{ zIndex: 1 }}>
|
|
{/* Left server to Firewall - blocked (with gap) */}
|
|
<AnimatedLine x1={70} y1={40} x2={122} y2={40} isAnimating={false} isBlocked />
|
|
{/* Left server to App - blue animated (with gap) */}
|
|
<AnimatedLine x1={68} y1={58} x2={124} y2={128} isAnimating={isAnimating} />
|
|
{/* Right server to App - blue animated (with gap) */}
|
|
<AnimatedLine x1={250} y1={58} x2={176} y2={128} isAnimating={isAnimating} reverse />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
|
|
{/* ========== DYNAMIC FORWARDING ========== */}
|
|
{type === 'dynamic' && (
|
|
<div className="relative w-[300px] max-w-full aspect-[300/170]">
|
|
{/* App Logo - left (same line as firewall) */}
|
|
<div className={`absolute left-6 top-5 z-10 transition-opacity duration-300 ${getOpacity('app')}`}>
|
|
<AppLogo className="h-12 w-12" />
|
|
</div>
|
|
|
|
{/* Firewall - center top (same line as app) */}
|
|
<div className={`absolute left-1/2 -translate-x-1/2 top-4 z-10 transition-opacity duration-300 ${getOpacity('firewall')}`}>
|
|
<FirewallIcon className="h-14 w-14" />
|
|
</div>
|
|
|
|
{/* Cloud targets - right (in red border box) */}
|
|
<div className={`absolute right-4 top-2 z-10 transition-opacity duration-300 ${getOpacity('target')}`}>
|
|
<div className="p-2 border-2 border-destructive/60 rounded-lg space-y-1">
|
|
<CloudIcon className="h-7 w-7" />
|
|
<CloudIcon className="h-7 w-7" />
|
|
<CloudIcon className="h-7 w-7" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* SSH Server - bottom center */}
|
|
<div className={`absolute left-1/2 -translate-x-1/2 bottom-4 z-10 transition-opacity duration-300 ${getOpacity('ssh-server')}`}>
|
|
<ServerIcon className="h-10 w-10" />
|
|
</div>
|
|
|
|
{/* Cloud target - bottom right */}
|
|
<div className={`absolute right-8 bottom-6 z-10 transition-opacity duration-300 ${getOpacity('target')}`}>
|
|
<CloudIcon className="h-8 w-8" />
|
|
</div>
|
|
|
|
{/* SVG Lines */}
|
|
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 300 170" preserveAspectRatio="none" style={{ zIndex: 1 }}>
|
|
{/* App to Firewall - blocked (with gap) */}
|
|
<AnimatedLine x1={78} y1={44} x2={122} y2={44} isAnimating={false} isBlocked />
|
|
{/* App to SSH Server - blue animated (with gap) */}
|
|
<AnimatedLine x1={76} y1={64} x2={126} y2={110} isAnimating={isAnimating} />
|
|
{/* SSH Server to clouds - blue animated (with gap) */}
|
|
<AnimatedLine x1={174} y1={112} x2={246} y2={26} isAnimating={isAnimating} />
|
|
<AnimatedLine x1={178} y1={120} x2={246} y2={58} isAnimating={isAnimating} />
|
|
<AnimatedLine x1={178} y1={128} x2={246} y2={90} isAnimating={isAnimating} />
|
|
<AnimatedLine x1={174} y1={128} x2={234} y2={130} isAnimating={isAnimating} />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|