Sets up the complete mRemotify project — a browser-based remote connection manager — with a working pnpm workspace monorepo: Frontend (React + TypeScript + Vite + Ant Design 5): - Login page with JWT auth - Resizable sidebar with drag-and-drop connection tree (folders + connections) - Tabbed session area (SSH via xterm.js, RDP via guacamole-common-js) - Connection CRUD modal with SSH/RDP-specific fields - Zustand store for auth, tree data, and open sessions Backend (Fastify + TypeScript + Prisma + PostgreSQL): - JWT authentication (login + /me endpoint) - Full CRUD REST API for folders (self-referencing) and connections - AES-256-CBC password encryption at rest - WebSocket proxy for SSH sessions (ssh2 <-> xterm.js) - WebSocket proxy for RDP sessions (guacd TCP handshake + bidirectional relay) - Admin user seeding on first start Infrastructure: - Docker Compose: postgres (healthcheck) + guacd + backend + frontend/nginx - nginx: serves SPA, proxies /api and /ws (with WebSocket upgrade) to backend - .env.example with all required variables documented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
import React from 'react';
|
|
import { Tabs, Typography } from 'antd';
|
|
import { CodeOutlined, WindowsOutlined } from '@ant-design/icons';
|
|
import { useStore } from '../../store';
|
|
import { SshTab } from './SshTab';
|
|
import { RdpTab } from './RdpTab';
|
|
import { Session } from '../../types';
|
|
|
|
function tabLabel(session: Session) {
|
|
const icon =
|
|
session.connection.protocol === 'rdp' ? (
|
|
<WindowsOutlined />
|
|
) : (
|
|
<CodeOutlined />
|
|
);
|
|
return (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
|
{icon}
|
|
{session.connection.name}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export const SessionTabs: React.FC = () => {
|
|
const sessions = useStore((s) => s.sessions);
|
|
const activeSessionId = useStore((s) => s.activeSessionId);
|
|
const closeSession = useStore((s) => s.closeSession);
|
|
const setActiveSession = useStore((s) => s.setActiveSession);
|
|
|
|
if (sessions.length === 0) {
|
|
return (
|
|
<div
|
|
style={{
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#bfbfbf',
|
|
userSelect: 'none',
|
|
}}
|
|
>
|
|
<Typography.Title level={4} type="secondary" style={{ marginBottom: 8 }}>
|
|
No open sessions
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">
|
|
Double-click a connection in the sidebar to start a session.
|
|
</Typography.Text>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tabs
|
|
type="editable-card"
|
|
hideAdd
|
|
activeKey={activeSessionId ?? undefined}
|
|
onChange={setActiveSession}
|
|
onEdit={(key, action) => {
|
|
if (action === 'remove') closeSession(key as string);
|
|
}}
|
|
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
|
tabBarStyle={{ margin: 0, flexShrink: 0 }}
|
|
items={sessions.map((session) => ({
|
|
key: session.id,
|
|
label: tabLabel(session),
|
|
closable: true,
|
|
style: { height: '100%', padding: 0 },
|
|
children: (
|
|
<div style={{ height: '100%', overflow: 'hidden' }}>
|
|
{session.connection.protocol === 'ssh' ? (
|
|
<SshTab session={session} />
|
|
) : (
|
|
<RdpTab session={session} />
|
|
)}
|
|
</div>
|
|
),
|
|
}))}
|
|
/>
|
|
);
|
|
};
|