Initial scaffold: full-stack mRemotify monorepo
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>
This commit is contained in:
81
frontend/src/components/Tabs/SessionTabs.tsx
Normal file
81
frontend/src/components/Tabs/SessionTabs.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
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>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user