Config editor rename to file manager + fixed up file manager UI
This commit is contained in:
@@ -85,7 +85,7 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ssh/config_editor/ {
|
location /ssh/file_manager/ {
|
||||||
proxy_pass http://127.0.0.1:8084;
|
proxy_pass http://127.0.0.1:8084;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {Homepage} from "@/ui/Homepage/Homepage.tsx"
|
|||||||
import {AppView} from "@/ui/Navigation/AppView.tsx"
|
import {AppView} from "@/ui/Navigation/AppView.tsx"
|
||||||
// import {SSHTunnel} from "@/ui/SSH/Tunnel/SSHTunnel.tsx"
|
// import {SSHTunnel} from "@/ui/SSH/Tunnel/SSHTunnel.tsx"
|
||||||
// import {ConfigEditor} from "@/ui/SSH/Config Editor/ConfigEditor.tsx"
|
// import {ConfigEditor} from "@/ui/SSH/Config Editor/ConfigEditor.tsx"
|
||||||
import {SSHManager} from "@/ui/SSH/Manager/SSHManager.tsx"
|
import {HostManager} from "@/ui/apps/Host Manager/HostManager.tsx"
|
||||||
import {TabProvider, useTabs} from "@/contexts/TabContext"
|
import {TabProvider, useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import {TopNavbar} from "@/ui/Navigation/TopNavbar.tsx";
|
import {TopNavbar} from "@/ui/Navigation/TopNavbar.tsx";
|
||||||
import { AdminSettings } from "@/ui/Admin/AdminSettings";
|
import { AdminSettings } from "@/ui/Admin/AdminSettings";
|
||||||
@@ -202,7 +202,7 @@ function AppContent() {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SSHManager onSelectView={handleSelectView} isTopbarOpen={isTopbarOpen} />
|
<HostManager onSelectView={handleSelectView} isTopbarOpen={isTopbarOpen} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Settings tab */}
|
{/* Admin Settings tab */}
|
||||||
|
|||||||
@@ -82,14 +82,14 @@ sqlite.exec(`
|
|||||||
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
||||||
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
||||||
tunnel_connections TEXT,
|
tunnel_connections TEXT,
|
||||||
enable_config_editor INTEGER NOT NULL DEFAULT 1,
|
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
||||||
default_path TEXT,
|
default_path TEXT,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS config_editor_recent (
|
CREATE TABLE IF NOT EXISTS file_manager_recent (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
host_id INTEGER NOT NULL,
|
host_id INTEGER NOT NULL,
|
||||||
@@ -100,7 +100,7 @@ sqlite.exec(`
|
|||||||
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS config_editor_pinned (
|
CREATE TABLE IF NOT EXISTS file_manager_pinned (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
host_id INTEGER NOT NULL,
|
host_id INTEGER NOT NULL,
|
||||||
@@ -111,7 +111,7 @@ sqlite.exec(`
|
|||||||
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS config_editor_shortcuts (
|
CREATE TABLE IF NOT EXISTS file_manager_shortcuts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
host_id INTEGER NOT NULL,
|
host_id INTEGER NOT NULL,
|
||||||
@@ -179,14 +179,14 @@ const migrateSchema = () => {
|
|||||||
addColumnIfNotExists('ssh_data', 'enable_terminal', 'INTEGER NOT NULL DEFAULT 1');
|
addColumnIfNotExists('ssh_data', 'enable_terminal', 'INTEGER NOT NULL DEFAULT 1');
|
||||||
addColumnIfNotExists('ssh_data', 'enable_tunnel', 'INTEGER NOT NULL DEFAULT 1');
|
addColumnIfNotExists('ssh_data', 'enable_tunnel', 'INTEGER NOT NULL DEFAULT 1');
|
||||||
addColumnIfNotExists('ssh_data', 'tunnel_connections', 'TEXT');
|
addColumnIfNotExists('ssh_data', 'tunnel_connections', 'TEXT');
|
||||||
addColumnIfNotExists('ssh_data', 'enable_config_editor', 'INTEGER NOT NULL DEFAULT 1');
|
addColumnIfNotExists('ssh_data', 'enable_file_manager', 'INTEGER NOT NULL DEFAULT 1');
|
||||||
addColumnIfNotExists('ssh_data', 'default_path', 'TEXT');
|
addColumnIfNotExists('ssh_data', 'default_path', 'TEXT');
|
||||||
addColumnIfNotExists('ssh_data', 'created_at', 'TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP');
|
addColumnIfNotExists('ssh_data', 'created_at', 'TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP');
|
||||||
addColumnIfNotExists('ssh_data', 'updated_at', 'TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP');
|
addColumnIfNotExists('ssh_data', 'updated_at', 'TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP');
|
||||||
|
|
||||||
addColumnIfNotExists('config_editor_recent', 'host_id', 'INTEGER NOT NULL');
|
addColumnIfNotExists('file_manager_recent', 'host_id', 'INTEGER NOT NULL');
|
||||||
addColumnIfNotExists('config_editor_pinned', 'host_id', 'INTEGER NOT NULL');
|
addColumnIfNotExists('file_manager_pinned', 'host_id', 'INTEGER NOT NULL');
|
||||||
addColumnIfNotExists('config_editor_shortcuts', 'host_id', 'INTEGER NOT NULL');
|
addColumnIfNotExists('file_manager_shortcuts', 'host_id', 'INTEGER NOT NULL');
|
||||||
|
|
||||||
logger.success('Schema migration completed');
|
logger.success('Schema migration completed');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ export const sshData = sqliteTable('ssh_data', {
|
|||||||
enableTerminal: integer('enable_terminal', {mode: 'boolean'}).notNull().default(true),
|
enableTerminal: integer('enable_terminal', {mode: 'boolean'}).notNull().default(true),
|
||||||
enableTunnel: integer('enable_tunnel', {mode: 'boolean'}).notNull().default(true),
|
enableTunnel: integer('enable_tunnel', {mode: 'boolean'}).notNull().default(true),
|
||||||
tunnelConnections: text('tunnel_connections'),
|
tunnelConnections: text('tunnel_connections'),
|
||||||
enableConfigEditor: integer('enable_config_editor', {mode: 'boolean'}).notNull().default(true),
|
enableConfigEditor: integer('enable_file_manager', {mode: 'boolean'}).notNull().default(true),
|
||||||
defaultPath: text('default_path'),
|
defaultPath: text('default_path'),
|
||||||
createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
updatedAt: text('updated_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
updatedAt: text('updated_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const configEditorRecent = sqliteTable('config_editor_recent', {
|
export const configEditorRecent = sqliteTable('file_manager_recent', {
|
||||||
id: integer('id').primaryKey({autoIncrement: true}),
|
id: integer('id').primaryKey({autoIncrement: true}),
|
||||||
userId: text('user_id').notNull().references(() => users.id),
|
userId: text('user_id').notNull().references(() => users.id),
|
||||||
hostId: integer('host_id').notNull().references(() => sshData.id),
|
hostId: integer('host_id').notNull().references(() => sshData.id),
|
||||||
@@ -57,7 +57,7 @@ export const configEditorRecent = sqliteTable('config_editor_recent', {
|
|||||||
lastOpened: text('last_opened').notNull().default(sql`CURRENT_TIMESTAMP`),
|
lastOpened: text('last_opened').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const configEditorPinned = sqliteTable('config_editor_pinned', {
|
export const configEditorPinned = sqliteTable('file_manager_pinned', {
|
||||||
id: integer('id').primaryKey({autoIncrement: true}),
|
id: integer('id').primaryKey({autoIncrement: true}),
|
||||||
userId: text('user_id').notNull().references(() => users.id),
|
userId: text('user_id').notNull().references(() => users.id),
|
||||||
hostId: integer('host_id').notNull().references(() => sshData.id),
|
hostId: integer('host_id').notNull().references(() => sshData.id),
|
||||||
@@ -66,7 +66,7 @@ export const configEditorPinned = sqliteTable('config_editor_pinned', {
|
|||||||
pinnedAt: text('pinned_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
pinnedAt: text('pinned_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const configEditorShortcuts = sqliteTable('config_editor_shortcuts', {
|
export const configEditorShortcuts = sqliteTable('file_manager_shortcuts', {
|
||||||
id: integer('id').primaryKey({autoIncrement: true}),
|
id: integer('id').primaryKey({autoIncrement: true}),
|
||||||
userId: text('user_id').notNull().references(() => users.id),
|
userId: text('user_id').notNull().references(() => users.id),
|
||||||
hostId: integer('host_id').notNull().references(() => sshData.id),
|
hostId: integer('host_id').notNull().references(() => sshData.id),
|
||||||
|
|||||||
@@ -406,8 +406,8 @@ router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Respons
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Get recent files (requires JWT)
|
// Route: Get recent files (requires JWT)
|
||||||
// GET /ssh/config_editor/recent
|
// GET /ssh/file_manager/recent
|
||||||
router.get('/config_editor/recent', authenticateJWT, async (req: Request, res: Response) => {
|
router.get('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null;
|
const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null;
|
||||||
|
|
||||||
@@ -438,8 +438,8 @@ router.get('/config_editor/recent', authenticateJWT, async (req: Request, res: R
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Add file to recent (requires JWT)
|
// Route: Add file to recent (requires JWT)
|
||||||
// POST /ssh/config_editor/recent
|
// POST /ssh/file_manager/recent
|
||||||
router.post('/config_editor/recent', authenticateJWT, async (req: Request, res: Response) => {
|
router.post('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const {name, path, hostId} = req.body;
|
const {name, path, hostId} = req.body;
|
||||||
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
||||||
@@ -480,8 +480,8 @@ router.post('/config_editor/recent', authenticateJWT, async (req: Request, res:
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Remove file from recent (requires JWT)
|
// Route: Remove file from recent (requires JWT)
|
||||||
// DELETE /ssh/config_editor/recent
|
// DELETE /ssh/file_manager/recent
|
||||||
router.delete('/config_editor/recent', authenticateJWT, async (req: Request, res: Response) => {
|
router.delete('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const {name, path, hostId} = req.body;
|
const {name, path, hostId} = req.body;
|
||||||
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
||||||
@@ -506,8 +506,8 @@ router.delete('/config_editor/recent', authenticateJWT, async (req: Request, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Get pinned files (requires JWT)
|
// Route: Get pinned files (requires JWT)
|
||||||
// GET /ssh/config_editor/pinned
|
// GET /ssh/file_manager/pinned
|
||||||
router.get('/config_editor/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
router.get('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null;
|
const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null;
|
||||||
|
|
||||||
@@ -538,8 +538,8 @@ router.get('/config_editor/pinned', authenticateJWT, async (req: Request, res: R
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Add file to pinned (requires JWT)
|
// Route: Add file to pinned (requires JWT)
|
||||||
// POST /ssh/config_editor/pinned
|
// POST /ssh/file_manager/pinned
|
||||||
router.post('/config_editor/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
router.post('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const {name, path, hostId} = req.body;
|
const {name, path, hostId} = req.body;
|
||||||
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
||||||
@@ -575,8 +575,8 @@ router.post('/config_editor/pinned', authenticateJWT, async (req: Request, res:
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Remove file from pinned (requires JWT)
|
// Route: Remove file from pinned (requires JWT)
|
||||||
// DELETE /ssh/config_editor/pinned
|
// DELETE /ssh/file_manager/pinned
|
||||||
router.delete('/config_editor/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
router.delete('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const {name, path, hostId} = req.body;
|
const {name, path, hostId} = req.body;
|
||||||
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
||||||
@@ -601,8 +601,8 @@ router.delete('/config_editor/pinned', authenticateJWT, async (req: Request, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Get folder shortcuts (requires JWT)
|
// Route: Get folder shortcuts (requires JWT)
|
||||||
// GET /ssh/config_editor/shortcuts
|
// GET /ssh/file_manager/shortcuts
|
||||||
router.get('/config_editor/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
router.get('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null;
|
const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null;
|
||||||
|
|
||||||
@@ -631,8 +631,8 @@ router.get('/config_editor/shortcuts', authenticateJWT, async (req: Request, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Add folder shortcut (requires JWT)
|
// Route: Add folder shortcut (requires JWT)
|
||||||
// POST /ssh/config_editor/shortcuts
|
// POST /ssh/file_manager/shortcuts
|
||||||
router.post('/config_editor/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
router.post('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const {name, path, hostId} = req.body;
|
const {name, path, hostId} = req.body;
|
||||||
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
||||||
@@ -667,8 +667,8 @@ router.post('/config_editor/shortcuts', authenticateJWT, async (req: Request, re
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Route: Remove folder shortcut (requires JWT)
|
// Route: Remove folder shortcut (requires JWT)
|
||||||
// DELETE /ssh/config_editor/shortcuts
|
// DELETE /ssh/file_manager/shortcuts
|
||||||
router.delete('/config_editor/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
router.delete('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const {name, path, hostId} = req.body;
|
const {name, path, hostId} = req.body;
|
||||||
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
if (!isNonEmptyString(userId) || !name || !path || !hostId) {
|
||||||
|
|||||||
@@ -959,9 +959,9 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => {
|
|||||||
const targetUserId = targetUser[0].id;
|
const targetUserId = targetUser[0].id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.$client.prepare('DELETE FROM config_editor_recent WHERE user_id = ?').run(targetUserId);
|
db.$client.prepare('DELETE FROM file_manager_recent WHERE user_id = ?').run(targetUserId);
|
||||||
db.$client.prepare('DELETE FROM config_editor_pinned WHERE user_id = ?').run(targetUserId);
|
db.$client.prepare('DELETE FROM file_manager_pinned WHERE user_id = ?').run(targetUserId);
|
||||||
db.$client.prepare('DELETE FROM config_editor_shortcuts WHERE user_id = ?').run(targetUserId);
|
db.$client.prepare('DELETE FROM file_manager_shortcuts WHERE user_id = ?').run(targetUserId);
|
||||||
db.$client.prepare('DELETE FROM ssh_data WHERE user_id = ?').run(targetUserId);
|
db.$client.prepare('DELETE FROM ssh_data WHERE user_id = ?').run(targetUserId);
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
logger.error(`Cleanup failed for user ${username}:`, cleanupError);
|
logger.error(`Cleanup failed for user ${username}:`, cleanupError);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function scheduleSessionCleanup(sessionId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/ssh/config_editor/ssh/connect', (req, res) => {
|
app.post('/ssh/file_manager/ssh/connect', (req, res) => {
|
||||||
const {sessionId, ip, port, username, password, sshKey, keyPassword} = req.body;
|
const {sessionId, ip, port, username, password, sshKey, keyPassword} = req.body;
|
||||||
if (!sessionId || !ip || !username || !port) {
|
if (!sessionId || !ip || !username || !port) {
|
||||||
return res.status(400).json({error: 'Missing SSH connection parameters'});
|
return res.status(400).json({error: 'Missing SSH connection parameters'});
|
||||||
@@ -153,19 +153,19 @@ app.post('/ssh/config_editor/ssh/connect', (req, res) => {
|
|||||||
client.connect(config);
|
client.connect(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/ssh/config_editor/ssh/disconnect', (req, res) => {
|
app.post('/ssh/file_manager/ssh/disconnect', (req, res) => {
|
||||||
const {sessionId} = req.body;
|
const {sessionId} = req.body;
|
||||||
cleanupSession(sessionId);
|
cleanupSession(sessionId);
|
||||||
res.json({status: 'success', message: 'SSH connection disconnected'});
|
res.json({status: 'success', message: 'SSH connection disconnected'});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/ssh/config_editor/ssh/status', (req, res) => {
|
app.get('/ssh/file_manager/ssh/status', (req, res) => {
|
||||||
const sessionId = req.query.sessionId as string;
|
const sessionId = req.query.sessionId as string;
|
||||||
const isConnected = !!sshSessions[sessionId]?.isConnected;
|
const isConnected = !!sshSessions[sessionId]?.isConnected;
|
||||||
res.json({status: 'success', connected: isConnected});
|
res.json({status: 'success', connected: isConnected});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/ssh/config_editor/ssh/listFiles', (req, res) => {
|
app.get('/ssh/file_manager/ssh/listFiles', (req, res) => {
|
||||||
const sessionId = req.query.sessionId as string;
|
const sessionId = req.query.sessionId as string;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
const sshPath = decodeURIComponent((req.query.path as string) || '/');
|
const sshPath = decodeURIComponent((req.query.path as string) || '/');
|
||||||
@@ -231,7 +231,7 @@ app.get('/ssh/config_editor/ssh/listFiles', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/ssh/config_editor/ssh/readFile', (req, res) => {
|
app.get('/ssh/file_manager/ssh/readFile', (req, res) => {
|
||||||
const sessionId = req.query.sessionId as string;
|
const sessionId = req.query.sessionId as string;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
const filePath = decodeURIComponent(req.query.path as string);
|
const filePath = decodeURIComponent(req.query.path as string);
|
||||||
@@ -280,7 +280,7 @@ app.get('/ssh/config_editor/ssh/readFile', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/ssh/config_editor/ssh/writeFile', (req, res) => {
|
app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||||
const {sessionId, path: filePath, content} = req.body;
|
const {sessionId, path: filePath, content} = req.body;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
// node ./dist/backend/starter.js
|
// node ./dist/backend/starter.js
|
||||||
|
|
||||||
import './database/database.js'
|
import './database/database.js'
|
||||||
import './ssh/ssh.js';
|
import './ssh/terminal.js';
|
||||||
import './ssh/ssh_tunnel.js';
|
import './ssh/tunnel.js';
|
||||||
import './ssh/config_editor.js';
|
import './ssh/file-manager.ts';
|
||||||
import './ssh/server-stats.js';
|
import './ssh/server-stats.js';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export function AdminSettings({ isTopbarOpen = true }: AdminSettingsProps): Reac
|
|||||||
|
|
||||||
<div className="px-6 py-4 overflow-auto">
|
<div className="px-6 py-4 overflow-auto">
|
||||||
<Tabs defaultValue="registration" className="w-full">
|
<Tabs defaultValue="registration" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-4 mb-6">
|
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
||||||
<TabsTrigger value="registration" className="flex items-center gap-2">
|
<TabsTrigger value="registration" className="flex items-center gap-2">
|
||||||
<Users className="h-4 w-4"/>
|
<Users className="h-4 w-4"/>
|
||||||
General
|
General
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import {TerminalComponent} from "../SSH/Terminal/TerminalComponent.tsx";
|
import {TerminalComponent} from "@/ui/apps/Terminal/TerminalComponent.tsx";
|
||||||
import {Server as ServerView} from "@/ui/SSH/Server/Server.tsx";
|
import {Server as ServerView} from "@/ui/apps/Server/Server.tsx";
|
||||||
import {ConfigEditor} from "@/ui/SSH/Config Editor/ConfigEditor.tsx";
|
import {FileManager} from "@/ui/apps/File Manager/FileManager.tsx";
|
||||||
import {useTabs} from "@/contexts/TabContext.tsx";
|
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
|
||||||
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
|
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
|
||||||
import * as ResizablePrimitive from "react-resizable-panels";
|
import * as ResizablePrimitive from "react-resizable-panels";
|
||||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||||
@@ -168,7 +168,7 @@ export function AppView({ isTopbarOpen = true }: TerminalViewProps): React.React
|
|||||||
embedded
|
embedded
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfigEditor
|
<FileManager
|
||||||
embedded
|
embedded
|
||||||
initialHost={t.hostConfig}
|
initialHost={t.hostConfig}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {Status, StatusIndicator} from "@/components/ui/shadcn-io/status";
|
|||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {ButtonGroup} from "@/components/ui/button-group.tsx";
|
import {ButtonGroup} from "@/components/ui/button-group.tsx";
|
||||||
import {Server, Terminal} from "lucide-react";
|
import {Server, Terminal} from "lucide-react";
|
||||||
import {useTabs} from "@/contexts/TabContext";
|
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
|
||||||
import { getServerStatusById } from "@/ui/SSH/ssh-axios";
|
import { getServerStatusById } from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -86,6 +86,7 @@ export function Host({ host }: HostProps): React.ReactElement {
|
|||||||
<Button variant="outline" className="!px-2 border-1 border-[#303032]" onClick={handleServerClick}>
|
<Button variant="outline" className="!px-2 border-1 border-[#303032]" onClick={handleServerClick}>
|
||||||
<Server/>
|
<Server/>
|
||||||
</Button>
|
</Button>
|
||||||
|
{host.enableTerminal && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="!px-2 border-1 border-[#303032]"
|
className="!px-2 border-1 border-[#303032]"
|
||||||
@@ -93,6 +94,7 @@ export function Host({ host }: HostProps): React.ReactElement {
|
|||||||
>
|
>
|
||||||
<Terminal/>
|
<Terminal/>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
{hasTags && (
|
{hasTags && (
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ import {
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {Card} from "@/components/ui/card.tsx";
|
import {Card} from "@/components/ui/card.tsx";
|
||||||
import {FolderCard} from "@/ui/Navigation/Hosts/FolderCard.tsx";
|
import {FolderCard} from "@/ui/Navigation/Hosts/FolderCard.tsx";
|
||||||
import {getSSHHosts} from "@/ui/SSH/ssh-axios";
|
import {getSSHHosts} from "@/ui/main-axios.ts";
|
||||||
import { useTabs } from "@/contexts/TabContext";
|
import { useTabs } from "@/ui/Navigation/Tabs/TabContext.tsx";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -206,19 +206,20 @@ export function LeftSidebar({
|
|||||||
const fetchHosts = React.useCallback(async () => {
|
const fetchHosts = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const newHosts = await getSSHHosts();
|
const newHosts = await getSSHHosts();
|
||||||
const terminalHosts = newHosts.filter(host => host.enableTerminal);
|
// Show all hosts in sidebar, regardless of terminal setting
|
||||||
|
// Terminal visibility is handled in the UI components
|
||||||
|
|
||||||
const prevHosts = prevHostsRef.current;
|
const prevHosts = prevHostsRef.current;
|
||||||
|
|
||||||
// Create a stable map of existing hosts by ID for comparison
|
// Create a stable map of existing hosts by ID for comparison
|
||||||
const existingHostsMap = new Map(prevHosts.map(h => [h.id, h]));
|
const existingHostsMap = new Map(prevHosts.map(h => [h.id, h]));
|
||||||
const newHostsMap = new Map(terminalHosts.map(h => [h.id, h]));
|
const newHostsMap = new Map(newHosts.map(h => [h.id, h]));
|
||||||
|
|
||||||
// Check if there are any meaningful changes
|
// Check if there are any meaningful changes
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
|
|
||||||
// Check for new hosts, removed hosts, or changed hosts
|
// Check for new hosts, removed hosts, or changed hosts
|
||||||
if (terminalHosts.length !== prevHosts.length) {
|
if (newHosts.length !== prevHosts.length) {
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
} else {
|
} else {
|
||||||
for (const [id, newHost] of newHostsMap) {
|
for (const [id, newHost] of newHostsMap) {
|
||||||
@@ -248,8 +249,8 @@ export function LeftSidebar({
|
|||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
// Use a small delay to batch updates and reduce jittering
|
// Use a small delay to batch updates and reduce jittering
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setHosts(terminalHosts);
|
setHosts(newHosts);
|
||||||
prevHostsRef.current = terminalHosts;
|
prevHostsRef.current = newHosts;
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {useSidebar} from "@/components/ui/sidebar";
|
|||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {ChevronDown, ChevronUpIcon} from "lucide-react";
|
import {ChevronDown, ChevronUpIcon} from "lucide-react";
|
||||||
import {Tab} from "@/ui/Navigation/Tabs/Tab.tsx";
|
import {Tab} from "@/ui/Navigation/Tabs/Tab.tsx";
|
||||||
import {useTabs} from "@/contexts/TabContext";
|
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
|
||||||
|
|
||||||
interface TopNavbarProps {
|
interface TopNavbarProps {
|
||||||
isTopbarOpen: boolean;
|
isTopbarOpen: boolean;
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { ConfigTabList } from "./ConfigTabList.tsx";
|
|
||||||
|
|
||||||
export function ConfigTopbar(props: any): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<ConfigTabList {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
8
src/ui/apps/File Manager/FIleManagerTopNavbar.tsx
Normal file
8
src/ui/apps/File Manager/FIleManagerTopNavbar.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FileManagerTabList } from "./FileManagerTabList.tsx";
|
||||||
|
|
||||||
|
export function FIleManagerTopNavbar(props: any): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<FileManagerTabList {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, {useState, useEffect, useRef} from "react";
|
import React, {useState, useEffect, useRef} from "react";
|
||||||
import {ConfigEditorSidebar} from "@/ui/SSH/Config Editor/ConfigEditorSidebar.tsx";
|
import {FileManagerLeftSidebar} from "@/ui/apps/File Manager/FileManagerLeftSidebar.tsx";
|
||||||
import {ConfigTabList} from "@/ui/SSH/Config Editor/ConfigTabList.tsx";
|
import {FileManagerTabList} from "@/ui/apps/File Manager/FileManagerTabList.tsx";
|
||||||
import {ConfigHomeView} from "@/ui/SSH/Config Editor/ConfigHomeView.tsx";
|
import {FileManagerHomeView} from "@/ui/apps/File Manager/FileManagerHomeView.tsx";
|
||||||
import {ConfigCodeEditor} from "@/ui/SSH/Config Editor/ConfigCodeEditor.tsx";
|
import {FileManagerFileEditor} from "@/ui/apps/File Manager/FileManagerFileEditor.tsx";
|
||||||
import {Button} from '@/components/ui/button.tsx';
|
import {Button} from '@/components/ui/button.tsx';
|
||||||
import {ConfigTopbar} from "@/ui/SSH/Config Editor/ConfigTopbar.tsx";
|
import {FIleManagerTopNavbar} from "@/ui/apps/File Manager/FIleManagerTopNavbar.tsx";
|
||||||
import {cn} from '@/lib/utils.ts';
|
import {cn} from '@/lib/utils.ts';
|
||||||
import {
|
import {
|
||||||
getConfigEditorRecent,
|
getConfigEditorRecent,
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
writeSSHFile,
|
writeSSHFile,
|
||||||
getSSHStatus,
|
getSSHStatus,
|
||||||
connectSSH
|
connectSSH
|
||||||
} from '@/ui/SSH/ssh-axios.ts';
|
} from '@/ui/main-axios.ts';
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@@ -59,7 +59,7 @@ interface SSHHost {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigEditor({onSelectView, embedded = false, initialHost = null}: { onSelectView?: (view: string) => void, embedded?: boolean, initialHost?: SSHHost | null }): React.ReactElement {
|
export function FileManager({onSelectView, embedded = false, initialHost = null}: { onSelectView?: (view: string) => void, embedded?: boolean, initialHost?: SSHHost | null }): React.ReactElement {
|
||||||
const [tabs, setTabs] = useState<Tab[]>([]);
|
const [tabs, setTabs] = useState<Tab[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState<string | number>('home');
|
const [activeTab, setActiveTab] = useState<string | number>('home');
|
||||||
const [recent, setRecent] = useState<any[]>([]);
|
const [recent, setRecent] = useState<any[]>([]);
|
||||||
@@ -451,7 +451,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
|
|||||||
return (
|
return (
|
||||||
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
|
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
|
||||||
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
||||||
<ConfigEditorSidebar
|
<FileManagerLeftSidebar
|
||||||
onSelectView={onSelectView || (() => {})}
|
onSelectView={onSelectView || (() => {})}
|
||||||
onOpenFile={handleOpenFile}
|
onOpenFile={handleOpenFile}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
@@ -482,7 +482,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
|
|||||||
return (
|
return (
|
||||||
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
|
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
|
||||||
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
||||||
<ConfigEditorSidebar
|
<FileManagerLeftSidebar
|
||||||
onSelectView={onSelectView || (() => {})}
|
onSelectView={onSelectView || (() => {})}
|
||||||
onOpenFile={handleOpenFile}
|
onOpenFile={handleOpenFile}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
@@ -498,7 +498,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
|
|||||||
<div
|
<div
|
||||||
className="h-9 w-full bg-[#09090b] border-2 border-[#303032] rounded-md flex items-center overflow-x-auto scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent"
|
className="h-9 w-full bg-[#09090b] border-2 border-[#303032] rounded-md flex items-center overflow-x-auto scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent"
|
||||||
style={{minWidth: 0}}>
|
style={{minWidth: 0}}>
|
||||||
<ConfigTopbar
|
<FIleManagerTopNavbar
|
||||||
tabs={tabs.map(t => ({id: t.id, title: t.title}))}
|
tabs={tabs.map(t => ({id: t.id, title: t.title}))}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
@@ -544,7 +544,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
|
|||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
}}>
|
}}>
|
||||||
{activeTab === 'home' ? (
|
{activeTab === 'home' ? (
|
||||||
<ConfigHomeView
|
<FileManagerHomeView
|
||||||
recent={recent}
|
recent={recent}
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
shortcuts={shortcuts}
|
shortcuts={shortcuts}
|
||||||
@@ -605,7 +605,7 @@ export function ConfigEditor({onSelectView, embedded = false, initialHost = null
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<ConfigCodeEditor
|
<FileManagerFileEditor
|
||||||
content={tab.content}
|
content={tab.content}
|
||||||
fileName={tab.fileName}
|
fileName={tab.fileName}
|
||||||
onContentChange={content => setTabContent(tab.id, content)}
|
onContentChange={content => setTabContent(tab.id, content)}
|
||||||
@@ -11,7 +11,7 @@ interface ConfigCodeEditorProps {
|
|||||||
onContentChange: (value: string) => void;
|
onContentChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigCodeEditor({content, fileName, onContentChange}: ConfigCodeEditorProps) {
|
export function FileManagerFileEditor({content, fileName, onContentChange}: ConfigCodeEditorProps) {
|
||||||
function getLanguageName(filename: string): string {
|
function getLanguageName(filename: string): string {
|
||||||
if (!filename || typeof filename !== 'string') {
|
if (!filename || typeof filename !== 'string') {
|
||||||
return 'text';
|
return 'text';
|
||||||
@@ -31,7 +31,7 @@ interface ConfigHomeViewProps {
|
|||||||
onAddShortcut: (path: string) => void;
|
onAddShortcut: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigHomeView({
|
export function FileManagerHomeView({
|
||||||
recent,
|
recent,
|
||||||
pinned,
|
pinned,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
@@ -177,8 +177,8 @@ export function ConfigHomeView({
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
className="h-8 px-2 bg-[#23232a] border-2 border-[#303032] hover:bg-[#2d2d30] rounded-md"
|
className="h-8 px-2 bg-[#23232a] border-2 !border-[#303032] hover:bg-[#2d2d30] rounded-md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (newShortcut.trim()) {
|
if (newShortcut.trim()) {
|
||||||
onAddShortcut(newShortcut.trim());
|
onAddShortcut(newShortcut.trim());
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
getConfigEditorPinned,
|
getConfigEditorPinned,
|
||||||
addConfigEditorPinned,
|
addConfigEditorPinned,
|
||||||
removeConfigEditorPinned
|
removeConfigEditorPinned
|
||||||
} from '@/ui/SSH/ssh-axios.ts';
|
} from '@/ui/main-axios.ts';
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -37,7 +37,7 @@ interface SSHHost {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
const FileManagerLeftSidebar = forwardRef(function ConfigEditorSidebar(
|
||||||
{onSelectView, onOpenFile, tabs, host}: {
|
{onSelectView, onOpenFile, tabs, host}: {
|
||||||
onSelectView?: (view: string) => void;
|
onSelectView?: (view: string) => void;
|
||||||
onOpenFile: (file: any) => void;
|
onOpenFile: (file: any) => void;
|
||||||
@@ -280,27 +280,21 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
</Button>
|
</Button>
|
||||||
<Input ref={pathInputRef} value={currentPath}
|
<Input ref={pathInputRef} value={currentPath}
|
||||||
onChange={e => setCurrentPath(e.target.value)}
|
onChange={e => setCurrentPath(e.target.value)}
|
||||||
className="flex-1 bg-[#18181b] border border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
|
className="flex-1 bg-[#18181b] border-2 border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-2 border-b-1 border-[#303032] bg-[#18181b]">
|
<div className="px-2 py-2 border-b-1 border-[#303032] bg-[#18181b]">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search files and folders..."
|
placeholder="Search files and folders..."
|
||||||
className="w-full h-7 text-sm bg-[#23232a] border border-[#434345] text-white placeholder:text-muted-foreground rounded"
|
className="w-full h-7 text-sm bg-[#23232a] border-2 border-[#434345] text-white placeholder:text-muted-foreground rounded-md"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={fileSearch}
|
value={fileSearch}
|
||||||
onChange={e => setFileSearch(e.target.value)}
|
onChange={e => setFileSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 w-full h-full bg-[#09090b] border-t-1 border-[#303032]">
|
<div className="flex-1 min-h-0 w-full bg-[#09090b] border-t-1 border-[#303032]">
|
||||||
<ScrollArea className="w-full h-full bg-[#09090b]" style={{
|
<ScrollArea className="h-full w-full bg-[#09090b]">
|
||||||
height: '100%',
|
<div className="p-2 pb-0">
|
||||||
maxHeight: '100%',
|
|
||||||
paddingRight: 8,
|
|
||||||
scrollbarGutter: 'stable',
|
|
||||||
background: '#09090b'
|
|
||||||
}}>
|
|
||||||
<div className="p-2 pr-2">
|
|
||||||
{connectingSSH || filesLoading ? (
|
{connectingSSH || filesLoading ? (
|
||||||
<div className="text-xs text-muted-foreground">Loading...</div>
|
<div className="text-xs text-muted-foreground">Loading...</div>
|
||||||
) : filesError ? (
|
) : filesError ? (
|
||||||
@@ -388,4 +382,4 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export {ConfigEditorSidebar};
|
export {FileManagerLeftSidebar};
|
||||||
@@ -41,7 +41,7 @@ interface ConfigFileSidebarViewerProps {
|
|||||||
currentSSH?: SSHConnection;
|
currentSSH?: SSHConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigFileSidebarViewer({
|
export function FileManagerLeftSidebarFileViewer({
|
||||||
sshConnections,
|
sshConnections,
|
||||||
onAddSSH,
|
onAddSSH,
|
||||||
onConnectSSH,
|
onConnectSSH,
|
||||||
@@ -15,7 +15,7 @@ interface ConfigTabListProps {
|
|||||||
onHomeClick: () => void;
|
onHomeClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigTabList({tabs, activeTab, setActiveTab, closeTab, onHomeClick}: ConfigTabListProps) {
|
export function FileManagerTabList({tabs, activeTab, setActiveTab, closeTab, onHomeClick}: ConfigTabListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center h-full px-[0.5rem] overflow-x-auto">
|
<div className="inline-flex items-center h-full px-[0.5rem] overflow-x-auto">
|
||||||
<Button
|
<Button
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {SSHManagerHostViewer} from "@/ui/SSH/Manager/SSHManagerHostViewer.tsx"
|
import {HostManagerHostViewer} from "@/ui/apps/Host Manager/HostManagerHostViewer.tsx"
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {SSHManagerHostEditor} from "@/ui/SSH/Manager/SSHManagerHostEditor.tsx";
|
import {HostManagerHostEditor} from "@/ui/apps/Host Manager/HostManagerHostEditor.tsx";
|
||||||
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
||||||
|
|
||||||
interface ConfigEditorProps {
|
interface ConfigEditorProps {
|
||||||
@@ -33,7 +33,7 @@ interface SSHHost {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHManager({onSelectView, isTopbarOpen}: ConfigEditorProps): React.ReactElement {
|
export function HostManager({onSelectView, isTopbarOpen}: ConfigEditorProps): React.ReactElement {
|
||||||
const [activeTab, setActiveTab] = useState("host_viewer");
|
const [activeTab, setActiveTab] = useState("host_viewer");
|
||||||
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
||||||
const {state: sidebarState} = useSidebar();
|
const {state: sidebarState} = useSidebar();
|
||||||
@@ -75,7 +75,7 @@ export function SSHManager({onSelectView, isTopbarOpen}: ConfigEditorProps): Rea
|
|||||||
>
|
>
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange}
|
<Tabs value={activeTab} onValueChange={handleTabChange}
|
||||||
className="flex-1 flex flex-col h-full min-h-0">
|
className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<TabsList className="mt-1.5">
|
<TabsList className="bg-[#18181b] border-2 border-[#303032] mt-1.5">
|
||||||
<TabsTrigger value="host_viewer">Host Viewer</TabsTrigger>
|
<TabsTrigger value="host_viewer">Host Viewer</TabsTrigger>
|
||||||
<TabsTrigger value="add_host">
|
<TabsTrigger value="add_host">
|
||||||
{editingHost ? "Edit Host" : "Add Host"}
|
{editingHost ? "Edit Host" : "Add Host"}
|
||||||
@@ -83,12 +83,12 @@ export function SSHManager({onSelectView, isTopbarOpen}: ConfigEditorProps): Rea
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
|
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
||||||
<SSHManagerHostViewer onEditHost={handleEditHost}/>
|
<HostManagerHostViewer onEditHost={handleEditHost}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="add_host" className="flex-1 flex flex-col h-full min-h-0">
|
<TabsContent value="add_host" className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
||||||
<div className="flex flex-col h-full min-h-0">
|
<div className="flex flex-col h-full min-h-0">
|
||||||
<SSHManagerHostEditor
|
<HostManagerHostEditor
|
||||||
editingHost={editingHost}
|
editingHost={editingHost}
|
||||||
onFormSubmit={handleFormSubmit}
|
onFormSubmit={handleFormSubmit}
|
||||||
/>
|
/>
|
||||||
@@ -19,7 +19,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx
|
|||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {Switch} from "@/components/ui/switch.tsx";
|
import {Switch} from "@/components/ui/switch.tsx";
|
||||||
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/SSH/ssh-axios';
|
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -49,7 +49,7 @@ interface SSHManagerHostEditorProps {
|
|||||||
onFormSubmit?: () => void;
|
onFormSubmit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHostEditorProps) {
|
export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHostEditorProps) {
|
||||||
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
||||||
const [folders, setFolders] = useState<string[]>([]);
|
const [folders, setFolders] = useState<string[]>([]);
|
||||||
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
||||||
@@ -396,7 +396,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="terminal">Terminal</TabsTrigger>
|
<TabsTrigger value="terminal">Terminal</TabsTrigger>
|
||||||
<TabsTrigger value="tunnel">Tunnel</TabsTrigger>
|
<TabsTrigger value="tunnel">Tunnel</TabsTrigger>
|
||||||
<TabsTrigger value="config_editor">Config Editor</TabsTrigger>
|
<TabsTrigger value="file_manager">File Manager</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general" className="pt-2">
|
<TabsContent value="general" className="pt-2">
|
||||||
<FormLabel className="mb-3 font-bold">Connection Details</FormLabel>
|
<FormLabel className="mb-3 font-bold">Connection Details</FormLabel>
|
||||||
@@ -986,13 +986,13 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="config_editor">
|
<TabsContent value="file_manager">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableConfigEditor"
|
name="enableConfigEditor"
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Enable Config Editor</FormLabel>
|
<FormLabel>Enable File Manager</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
@@ -1000,7 +1000,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enable/disable host visibility in Config Editor tab.
|
Enable/disable host visibility in File Manager tab.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -1018,7 +1018,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
<Input placeholder="/home" {...field} />
|
<Input placeholder="/home" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Set default directory shown when connected via
|
<FormDescription>Set default directory shown when connected via
|
||||||
Config Editor</FormDescription>
|
File Manager</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -6,7 +6,7 @@ import {ScrollArea} from "@/components/ui/scroll-area";
|
|||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/SSH/ssh-axios";
|
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
|
||||||
import {
|
import {
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -46,7 +46,7 @@ interface SSHManagerHostViewerProps {
|
|||||||
onEditHost?: (host: SSHHost) => void;
|
onEditHost?: (host: SSHHost) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
|
export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
|
||||||
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -5,9 +5,9 @@ import {Separator} from "@/components/ui/separator.tsx";
|
|||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import {Cpu, HardDrive, MemoryStick} from "lucide-react";
|
import {Cpu, HardDrive, MemoryStick} from "lucide-react";
|
||||||
import {SSHTunnel} from "@/ui/SSH/Tunnel/SSHTunnel.tsx";
|
import {Tunnel} from "@/ui/apps/Tunnel/Tunnel.tsx";
|
||||||
import { getServerStatusById, getServerMetricsById, ServerMetrics } from "@/ui/SSH/ssh-axios";
|
import { getServerStatusById, getServerMetricsById, ServerMetrics } from "@/ui/main-axios.ts";
|
||||||
import { useTabs } from "@/contexts/TabContext";
|
import { useTabs } from "@/ui/Navigation/Tabs/TabContext.tsx";
|
||||||
|
|
||||||
interface ServerProps {
|
interface ServerProps {
|
||||||
hostConfig?: any;
|
hostConfig?: any;
|
||||||
@@ -22,6 +22,54 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
const { addTab } = useTabs() as any;
|
const { addTab } = useTabs() as any;
|
||||||
const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
|
const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
|
||||||
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
|
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
|
||||||
|
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
|
||||||
|
|
||||||
|
// Listen for host configuration changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
setCurrentHostConfig(hostConfig);
|
||||||
|
}, [hostConfig]);
|
||||||
|
|
||||||
|
// Always fetch latest host config when component mounts or hostConfig changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchLatestHostConfig = async () => {
|
||||||
|
if (hostConfig?.id) {
|
||||||
|
try {
|
||||||
|
// Import the getSSHHosts function to fetch updated host data
|
||||||
|
const { getSSHHosts } = await import('@/ui/main-axios.ts');
|
||||||
|
const hosts = await getSSHHosts();
|
||||||
|
const updatedHost = hosts.find(h => h.id === hostConfig.id);
|
||||||
|
if (updatedHost) {
|
||||||
|
setCurrentHostConfig(updatedHost);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch latest host config:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch immediately when component mounts or hostConfig changes
|
||||||
|
fetchLatestHostConfig();
|
||||||
|
|
||||||
|
// Also listen for SSH hosts changed event to refresh host config
|
||||||
|
const handleHostsChanged = async () => {
|
||||||
|
if (hostConfig?.id) {
|
||||||
|
try {
|
||||||
|
// Import the getSSHHosts function to fetch updated host data
|
||||||
|
const { getSSHHosts } = await import('@/ui/main-axios.ts');
|
||||||
|
const hosts = await getSSHHosts();
|
||||||
|
const updatedHost = hosts.find(h => h.id === hostConfig.id);
|
||||||
|
if (updatedHost) {
|
||||||
|
setCurrentHostConfig(updatedHost);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh host config:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('ssh-hosts:changed', handleHostsChanged);
|
||||||
|
return () => window.removeEventListener('ssh-hosts:changed', handleHostsChanged);
|
||||||
|
}, [hostConfig?.id]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -29,7 +77,7 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getServerStatusById(hostConfig?.id);
|
const res = await getServerStatusById(currentHostConfig?.id);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setServerStatus(res?.status === 'online' ? 'online' : 'offline');
|
setServerStatus(res?.status === 'online' ? 'online' : 'offline');
|
||||||
}
|
}
|
||||||
@@ -39,16 +87,16 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchMetrics = async () => {
|
const fetchMetrics = async () => {
|
||||||
if (!hostConfig?.id) return;
|
if (!currentHostConfig?.id) return;
|
||||||
try {
|
try {
|
||||||
const data = await getServerMetricsById(hostConfig.id);
|
const data = await getServerMetricsById(currentHostConfig.id);
|
||||||
if (!cancelled) setMetrics(data);
|
if (!cancelled) setMetrics(data);
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setMetrics(null);
|
if (!cancelled) setMetrics(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hostConfig?.id) {
|
if (currentHostConfig?.id) {
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
fetchMetrics();
|
fetchMetrics();
|
||||||
intervalId = window.setInterval(() => {
|
intervalId = window.setInterval(() => {
|
||||||
@@ -61,7 +109,7 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (intervalId) window.clearInterval(intervalId);
|
if (intervalId) window.clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [hostConfig?.id]);
|
}, [currentHostConfig?.id]);
|
||||||
|
|
||||||
const topMarginPx = isTopbarOpen ? 74 : 16;
|
const topMarginPx = isTopbarOpen ? 74 : 16;
|
||||||
const leftMarginPx = sidebarState === 'collapsed' ? 16 : 8;
|
const leftMarginPx = sidebarState === 'collapsed' ? 16 : 8;
|
||||||
@@ -90,29 +138,32 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<h1 className="font-bold text-lg">
|
<h1 className="font-bold text-lg">
|
||||||
{hostConfig.folder} / {title}
|
{currentHostConfig?.folder} / {title}
|
||||||
</h1>
|
</h1>
|
||||||
<Status status={serverStatus} className="!bg-transparent !p-0.75 flex-shrink-0">
|
<Status status={serverStatus} className="!bg-transparent !p-0.75 flex-shrink-0">
|
||||||
<StatusIndicator/>
|
<StatusIndicator/>
|
||||||
</Status>
|
</Status>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
{currentHostConfig?.enableConfigEditor && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
className="font-semibold"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!hostConfig) return;
|
if (!currentHostConfig) return;
|
||||||
const titleBase = hostConfig?.name && hostConfig.name.trim() !== ''
|
const titleBase = currentHostConfig?.name && currentHostConfig.name.trim() !== ''
|
||||||
? hostConfig.name.trim()
|
? currentHostConfig.name.trim()
|
||||||
: `${hostConfig.username}@${hostConfig.ip}`;
|
: `${currentHostConfig.username}@${currentHostConfig.ip}`;
|
||||||
addTab({
|
addTab({
|
||||||
type: 'config',
|
type: 'config',
|
||||||
title: titleBase,
|
title: titleBase,
|
||||||
hostConfig: hostConfig,
|
hostConfig: currentHostConfig,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
File Manager
|
File Manager
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="p-0.25 w-full"/>
|
<Separator className="p-0.25 w-full"/>
|
||||||
@@ -181,9 +232,9 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SSH Tunnels */}
|
{/* SSH Tunnels */}
|
||||||
{(hostConfig?.tunnelConnections && hostConfig.tunnelConnections.length > 0) && (
|
{(currentHostConfig?.tunnelConnections && currentHostConfig.tunnelConnections.length > 0) && (
|
||||||
<div className="rounded-lg border-2 border-[#303032] m-3 bg-[#0e0e10] h-[360px] overflow-hidden flex flex-col min-h-0">
|
<div className="rounded-lg border-2 border-[#303032] m-3 bg-[#0e0e10] h-[360px] overflow-hidden flex flex-col min-h-0">
|
||||||
<SSHTunnel filterHostKey={(hostConfig?.name && hostConfig.name.trim() !== '') ? hostConfig.name : `${hostConfig?.username}@${hostConfig?.ip}`}/>
|
<Tunnel filterHostKey={(currentHostConfig?.name && currentHostConfig.name.trim() !== '') ? currentHostConfig.name : `${currentHostConfig?.username}@${currentHostConfig?.ip}`}/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {useState, useEffect, useCallback} from "react";
|
import React, {useState, useEffect, useCallback} from "react";
|
||||||
import {SSHTunnelViewer} from "@/ui/SSH/Tunnel/SSHTunnelViewer.tsx";
|
import {TunnelViewer} from "@/ui/apps/Tunnel/TunnelViewer.tsx";
|
||||||
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/SSH/ssh-axios";
|
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
interface TunnelConnection {
|
interface TunnelConnection {
|
||||||
sourcePort: number;
|
sourcePort: number;
|
||||||
@@ -48,7 +48,7 @@ interface SSHTunnelProps {
|
|||||||
filterHostKey?: string;
|
filterHostKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHTunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
||||||
// Keep full list for endpoint lookups; keep a separate visible list for UI
|
// Keep full list for endpoint lookups; keep a separate visible list for UI
|
||||||
const [allHosts, setAllHosts] = useState<SSHHost[]>([]);
|
const [allHosts, setAllHosts] = useState<SSHHost[]>([]);
|
||||||
const [visibleHosts, setVisibleHosts] = useState<SSHHost[]>([]);
|
const [visibleHosts, setVisibleHosts] = useState<SSHHost[]>([]);
|
||||||
@@ -197,7 +197,7 @@ export function SSHTunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SSHTunnelViewer
|
<TunnelViewer
|
||||||
hosts={visibleHosts}
|
hosts={visibleHosts}
|
||||||
tunnelStatuses={tunnelStatuses}
|
tunnelStatuses={tunnelStatuses}
|
||||||
tunnelActions={tunnelActions}
|
tunnelActions={tunnelActions}
|
||||||
@@ -79,7 +79,7 @@ interface SSHTunnelObjectProps {
|
|||||||
bare?: boolean; // when true, render without Card wrapper/background
|
bare?: boolean; // when true, render without Card wrapper/background
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHTunnelObject({
|
export function TunnelObject({
|
||||||
host,
|
host,
|
||||||
tunnelStatuses,
|
tunnelStatuses,
|
||||||
tunnelActions,
|
tunnelActions,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {SSHTunnelObject} from "./SSHTunnelObject.tsx";
|
import {TunnelObject} from "./TunnelObject.tsx";
|
||||||
|
|
||||||
interface TunnelConnection {
|
interface TunnelConnection {
|
||||||
sourcePort: number;
|
sourcePort: number;
|
||||||
@@ -46,7 +46,7 @@ interface SSHTunnelViewerProps {
|
|||||||
onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise<any>;
|
onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHTunnelViewer({
|
export function TunnelViewer({
|
||||||
hosts = [],
|
hosts = [],
|
||||||
tunnelStatuses = {},
|
tunnelStatuses = {},
|
||||||
tunnelActions = {},
|
tunnelActions = {},
|
||||||
@@ -74,7 +74,7 @@ export function SSHTunnelViewer({
|
|||||||
<div className="min-h-0 flex-1 overflow-auto pr-1">
|
<div className="min-h-0 flex-1 overflow-auto pr-1">
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
|
||||||
{activeHost.tunnelConnections.map((t, idx) => (
|
{activeHost.tunnelConnections.map((t, idx) => (
|
||||||
<SSHTunnelObject
|
<TunnelObject
|
||||||
key={`tunnel-${activeHost.id}-${t.endpointHost}-${t.sourcePort}-${t.endpointPort}`}
|
key={`tunnel-${activeHost.id}-${t.endpointHost}-${t.sourcePort}-${t.endpointPort}`}
|
||||||
host={{...activeHost, tunnelConnections: [activeHost.tunnelConnections[idx]]}}
|
host={{...activeHost, tunnelConnections: [activeHost.tunnelConnections[idx]]}}
|
||||||
tunnelStatuses={tunnelStatuses}
|
tunnelStatuses={tunnelStatuses}
|
||||||
@@ -129,7 +129,7 @@ const configEditorApi = axios.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const statsApi = axios.create({
|
const statsApi = axios.create({
|
||||||
baseURL: isLocalhost ? 'http://localhost:8085' : '/ssh/stats',
|
baseURL: isLocalhost ? 'http://localhost:8085' : '',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
@@ -364,7 +364,7 @@ export async function cancelTunnel(tunnelName: string): Promise<any> {
|
|||||||
|
|
||||||
export async function getConfigEditorRecent(hostId: number): Promise<ConfigEditorFile[]> {
|
export async function getConfigEditorRecent(hostId: number): Promise<ConfigEditorFile[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/config_editor/recent?hostId=${hostId}`);
|
const response = await sshHostApi.get(`/ssh/file_manager/recent?hostId=${hostId}`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
@@ -379,7 +379,7 @@ export async function addConfigEditorRecent(file: {
|
|||||||
hostId: number
|
hostId: number
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/config_editor/recent', file);
|
const response = await sshHostApi.post('/ssh/file_manager/recent', file);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -394,7 +394,7 @@ export async function removeConfigEditorRecent(file: {
|
|||||||
hostId: number
|
hostId: number
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete('/ssh/config_editor/recent', {data: file});
|
const response = await sshHostApi.delete('/ssh/file_manager/recent', {data: file});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -403,7 +403,7 @@ export async function removeConfigEditorRecent(file: {
|
|||||||
|
|
||||||
export async function getConfigEditorPinned(hostId: number): Promise<ConfigEditorFile[]> {
|
export async function getConfigEditorPinned(hostId: number): Promise<ConfigEditorFile[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/config_editor/pinned?hostId=${hostId}`);
|
const response = await sshHostApi.get(`/ssh/file_manager/pinned?hostId=${hostId}`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
@@ -418,7 +418,7 @@ export async function addConfigEditorPinned(file: {
|
|||||||
hostId: number
|
hostId: number
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/config_editor/pinned', file);
|
const response = await sshHostApi.post('/ssh/file_manager/pinned', file);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -433,7 +433,7 @@ export async function removeConfigEditorPinned(file: {
|
|||||||
hostId: number
|
hostId: number
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete('/ssh/config_editor/pinned', {data: file});
|
const response = await sshHostApi.delete('/ssh/file_manager/pinned', {data: file});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -442,7 +442,7 @@ export async function removeConfigEditorPinned(file: {
|
|||||||
|
|
||||||
export async function getConfigEditorShortcuts(hostId: number): Promise<ConfigEditorShortcut[]> {
|
export async function getConfigEditorShortcuts(hostId: number): Promise<ConfigEditorShortcut[]> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.get(`/ssh/config_editor/shortcuts?hostId=${hostId}`);
|
const response = await sshHostApi.get(`/ssh/file_manager/shortcuts?hostId=${hostId}`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
@@ -457,7 +457,7 @@ export async function addConfigEditorShortcut(shortcut: {
|
|||||||
hostId: number
|
hostId: number
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.post('/ssh/config_editor/shortcuts', shortcut);
|
const response = await sshHostApi.post('/ssh/file_manager/shortcuts', shortcut);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -472,7 +472,7 @@ export async function removeConfigEditorShortcut(shortcut: {
|
|||||||
hostId: number
|
hostId: number
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await sshHostApi.delete('/ssh/config_editor/shortcuts', {data: shortcut});
|
const response = await sshHostApi.delete('/ssh/file_manager/shortcuts', {data: shortcut});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -488,7 +488,7 @@ export async function connectSSH(sessionId: string, config: {
|
|||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await configEditorApi.post('/ssh/config_editor/ssh/connect', {
|
const response = await configEditorApi.post('/ssh/file_manager/ssh/connect', {
|
||||||
sessionId,
|
sessionId,
|
||||||
...config
|
...config
|
||||||
});
|
});
|
||||||
@@ -500,7 +500,7 @@ export async function connectSSH(sessionId: string, config: {
|
|||||||
|
|
||||||
export async function disconnectSSH(sessionId: string): Promise<any> {
|
export async function disconnectSSH(sessionId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await configEditorApi.post('/ssh/config_editor/ssh/disconnect', {sessionId});
|
const response = await configEditorApi.post('/ssh/file_manager/ssh/disconnect', {sessionId});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -509,7 +509,7 @@ export async function disconnectSSH(sessionId: string): Promise<any> {
|
|||||||
|
|
||||||
export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> {
|
export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> {
|
||||||
try {
|
try {
|
||||||
const response = await configEditorApi.get('/ssh/config_editor/ssh/status', {
|
const response = await configEditorApi.get('/ssh/file_manager/ssh/status', {
|
||||||
params: {sessionId}
|
params: {sessionId}
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -520,7 +520,7 @@ export async function getSSHStatus(sessionId: string): Promise<{ connected: bool
|
|||||||
|
|
||||||
export async function listSSHFiles(sessionId: string, path: string): Promise<any[]> {
|
export async function listSSHFiles(sessionId: string, path: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const response = await configEditorApi.get('/ssh/config_editor/ssh/listFiles', {
|
const response = await configEditorApi.get('/ssh/file_manager/ssh/listFiles', {
|
||||||
params: {sessionId, path}
|
params: {sessionId, path}
|
||||||
});
|
});
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
@@ -531,7 +531,7 @@ export async function listSSHFiles(sessionId: string, path: string): Promise<any
|
|||||||
|
|
||||||
export async function readSSHFile(sessionId: string, path: string): Promise<{ content: string; path: string }> {
|
export async function readSSHFile(sessionId: string, path: string): Promise<{ content: string; path: string }> {
|
||||||
try {
|
try {
|
||||||
const response = await configEditorApi.get('/ssh/config_editor/ssh/readFile', {
|
const response = await configEditorApi.get('/ssh/file_manager/ssh/readFile', {
|
||||||
params: {sessionId, path}
|
params: {sessionId, path}
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -542,7 +542,7 @@ export async function readSSHFile(sessionId: string, path: string): Promise<{ co
|
|||||||
|
|
||||||
export async function writeSSHFile(sessionId: string, path: string, content: string): Promise<any> {
|
export async function writeSSHFile(sessionId: string, path: string, content: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await configEditorApi.post('/ssh/config_editor/ssh/writeFile', {
|
const response = await configEditorApi.post('/ssh/file_manager/ssh/writeFile', {
|
||||||
sessionId,
|
sessionId,
|
||||||
path,
|
path,
|
||||||
content
|
content
|
||||||
Reference in New Issue
Block a user