From 75e04b3218793e99c54f1eafc6976432119f5b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Cadilhac?= <76635605+XDukeHD@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:58:40 +0000 Subject: [PATCH 1/8] feat: add file editor modal for editing and saving files feat: implement rename modal for renaming files feat: create server console component for displaying console messages and sending commands feat: develop server files component for managing server files with upload, download, and edit functionalities feat: build server header component to display server status and control actions feat: establish server layout component for organizing server-related views feat: create server overview component to display server statistics and information feat: implement server sidebar for navigation between different server management tabs --- .env | 4 +- config.json | 2 +- .../app/[identifier]/console/page.tsx | 321 +++++++++++++++ .../dashboard/app/CreateFolderModal.tsx | 111 ++++++ .../dashboard/app/FileEditorModal.tsx | 131 +++++++ src/components/dashboard/app/RenameModal.tsx | 105 +++++ .../dashboard/app/ServerConsole.tsx | 112 ++++++ src/components/dashboard/app/ServerFiles.tsx | 368 ++++++++++++++++++ src/components/dashboard/app/ServerHeader.tsx | 130 +++++++ src/components/dashboard/app/ServerLayout.tsx | 109 ++++++ .../dashboard/app/ServerOverview.tsx | 218 +++++++++++ .../dashboard/app/ServerSidebar.tsx | 100 +++++ 12 files changed, 1708 insertions(+), 3 deletions(-) create mode 100644 src/app/dashboard/app/[identifier]/console/page.tsx create mode 100644 src/components/dashboard/app/CreateFolderModal.tsx create mode 100644 src/components/dashboard/app/FileEditorModal.tsx create mode 100644 src/components/dashboard/app/RenameModal.tsx create mode 100644 src/components/dashboard/app/ServerConsole.tsx create mode 100644 src/components/dashboard/app/ServerFiles.tsx create mode 100644 src/components/dashboard/app/ServerHeader.tsx create mode 100644 src/components/dashboard/app/ServerLayout.tsx create mode 100644 src/components/dashboard/app/ServerOverview.tsx create mode 100644 src/components/dashboard/app/ServerSidebar.tsx diff --git a/.env b/.env index db4cc05..ac67a9f 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -API_URL=https://system-api.firehosting.com.br -NEXT_PUBLIC_API_URL=https://system-api.firehosting.com.br +API_URL=https://api-dev.firehosting.com.br +NEXT_PUBLIC_API_URL=https://api-dev.firehosting.com.br NEXT_PUBLIC_SOCKET_URL=https://firehosting-socket.squareweb.app NEXT_PUBLIC_CENTRAL_URL=https://central.firehosting.com.br \ No newline at end of file diff --git a/config.json b/config.json index a1d5d09..50772b9 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "api": { - "baseUrl": "https://system-api.firehosting.com.br", + "baseUrl": "https://api-dev.firehosting.com.br", "authKey": "CjYYooDNiVgzWBJPNlYyIUfoxJRtLozDFiTAoHdQuPAnxFEAuK" } } diff --git a/src/app/dashboard/app/[identifier]/console/page.tsx b/src/app/dashboard/app/[identifier]/console/page.tsx new file mode 100644 index 0000000..a3d6e0e --- /dev/null +++ b/src/app/dashboard/app/[identifier]/console/page.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { useParams } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; +import { useServer } from "@/contexts/ServerContext"; +import ServerLayout from "@/components/dashboard/app/ServerLayout"; +import ServerConsole from "@/components/dashboard/app/ServerConsole"; +import { ServerConsoleWebSocket } from "@/services/ServerConsoleWebSocket"; + +type ServerDetails = { + name: string; + identifier: string; + ip?: string; + status: string; + ram?: string; + cpu?: string; + disk?: string; + location?: string; +}; + +export default function ServerConsolePage() { + const { accessKey } = useAuth(); + const { getServerDetail, sendServerPowerSignal, getServerConsoleCredentials } = useServer(); + const { identifier } = useParams() as { identifier: string }; + + const [server, setServer] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [powerLoading, setPowerLoading] = useState(false); + + const [websocket, setWebsocket] = useState(null); + const [consoleMessages, setConsoleMessages] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [reconnectAttempt, setReconnectAttempt] = useState(0); + + const wsRef = useRef(null); + const commandInputRef = useRef(null); + + // Efeito para carregar os detalhes do servidor + useEffect(() => { + let isMounted = true; + setLoading(true); + setError(null); + + if (!accessKey) return; + + const fetchServerDetails = async () => { + try { + const result = await getServerDetail(identifier); + if (!isMounted) return; + + if (result.success && result.server) { + setServer(result.server); + setError(null); + } else { + setError(result.error || "Erro ao carregar detalhes do servidor"); + } + } catch (err) { + if (!isMounted) return; + setError(err.message || "Erro ao carregar detalhes do servidor"); + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + fetchServerDetails(); + + return () => { + isMounted = false; + }; + }, [accessKey, identifier, getServerDetail]); + + // Efeito para conectar ao WebSocket do console + useEffect(() => { + let isMounted = true; + + if (!accessKey || !server) return; + + const connectWebSocket = async () => { + if (reconnectAttempt >= 10) return; + + setIsConnecting(true); + + try { + const credentialsResult = await getServerConsoleCredentials(identifier); + + if (!isMounted) return; + + if (credentialsResult.success && credentialsResult.socket && credentialsResult.key) { + if (wsRef.current) { + wsRef.current.disconnect(); + } + + const ws = new ServerConsoleWebSocket(); + wsRef.current = ws; + setWebsocket(ws); + + ws.onConnected(() => { + if (!isMounted) return; + setIsConnected(true); + setIsConnecting(false); + setReconnectAttempt(0); + setError(null); + }); + + ws.onDisconnected(() => { + if (!isMounted) return; + setIsConnected(false); + + // Tentar reconectar se desconectar + if (reconnectAttempt < 10) { + setReconnectAttempt(prev => prev + 1); + } + }); + + ws.onMessage((data) => { + if (!isMounted) return; + + if (data.type === 'console') { + setConsoleMessages((prev) => { + if (typeof data.data !== 'string') return prev; + if (prev.length > 0 && prev[prev.length - 1] === data.data) return prev; + return [...prev, data.data]; + }); + } else if (data.type === 'status') { + setServer(prev => prev ? { ...prev, status: data.data } : null); + } + }); + + ws.connect({ + socket: credentialsResult.socket, + key: credentialsResult.key, + expiresAt: credentialsResult.expiresAt || Date.now() + 3600000 + }); + + } else { + if (!isMounted) return; + + // Não exibiremos erro na tela, apenas em console para debug + console.error("Erro ao obter credenciais do console:", credentialsResult.error); + + if (reconnectAttempt < 10) { + setReconnectAttempt(prev => prev + 1); + } else { + setIsConnecting(false); + } + } + } catch (err) { + if (!isMounted) return; + + console.error("Erro ao conectar ao console:", err); + + if (reconnectAttempt < 10) { + setReconnectAttempt(prev => prev + 1); + } else { + setIsConnecting(false); + } + } + }; + + connectWebSocket(); + + return () => { + isMounted = false; + + if (wsRef.current) { + wsRef.current.disconnect(); + wsRef.current = null; + } + setWebsocket(null); + setIsConnected(false); + }; + }, [accessKey, identifier, getServerConsoleCredentials, reconnectAttempt, server]); + + // Tentar reconectar quando o contador de tentativas mudar + useEffect(() => { + if (reconnectAttempt > 0 && reconnectAttempt < 10) { + const timer = setTimeout(() => { + // Reconectar com backoff exponencial + const connectWebSocket = async () => { + if (!accessKey) return; + + setIsConnecting(true); + + try { + const credentialsResult = await getServerConsoleCredentials(identifier); + + if (credentialsResult.success && credentialsResult.socket && credentialsResult.key) { + if (wsRef.current) { + wsRef.current.renewCredentials({ + socket: credentialsResult.socket, + key: credentialsResult.key, + expiresAt: credentialsResult.expiresAt || Date.now() + 3600000 + }); + } else { + const ws = new ServerConsoleWebSocket(); + wsRef.current = ws; + setWebsocket(ws); + + ws.onConnected(() => { + setIsConnected(true); + setIsConnecting(false); + setReconnectAttempt(0); + }); + + ws.onDisconnected(() => { + setIsConnected(false); + setReconnectAttempt(prev => prev < 10 ? prev + 1 : prev); + }); + + ws.onMessage((data) => { + if (data.type === 'console') { + setConsoleMessages((prev) => { + if (typeof data.data !== 'string') return prev; + if (prev.length > 0 && prev[prev.length - 1] === data.data) return prev; + return [...prev, data.data]; + }); + } else if (data.type === 'status') { + setServer(prev => prev ? { ...prev, status: data.data } : null); + } + }); + + ws.connect({ + socket: credentialsResult.socket, + key: credentialsResult.key, + expiresAt: credentialsResult.expiresAt || Date.now() + 3600000 + }); + } + } + } catch (err) { + console.error("Erro ao reconectar ao console:", err); + } + }; + + connectWebSocket(); + }, Math.min(1000 * Math.pow(2, reconnectAttempt - 1), 10000)); // Backoff exponencial com máximo de 10s + + return () => clearTimeout(timer); + } + }, [reconnectAttempt, accessKey, identifier, getServerConsoleCredentials]); + + const sendPowerAction = async (signal) => { + if (!accessKey || powerLoading) return; + + setPowerLoading(true); + + try { + const result = await sendServerPowerSignal(identifier, signal); + + if (!result.success) { + throw new Error(result.message || `Falha ao enviar sinal ${signal}`); + } + + setServer((prev) => + prev ? { ...prev, status: getPendingStatus(signal) } : null, + ); + } catch (err) { + setError(err.message || `Erro ao enviar sinal ${signal}`); + } finally { + setPowerLoading(false); + } + }; + + const getPendingStatus = (signal) => { + switch (signal) { + case "start": + return "starting"; + case "stop": + return "stopping"; + case "restart": + return "restarting"; + case "kill": + return "killing"; + default: + return "pending"; + } + }; + + const sendCommand = (e) => { + e.preventDefault(); + + if ( + !commandInputRef.current || + !commandInputRef.current.value || + !websocket || + !isConnected + ) { + return; + } + + const command = commandInputRef.current.value; + + setConsoleMessages((prev) => [...prev, `[Sistema] > ${command}`]); + + websocket.sendCommand(command); + + commandInputRef.current.value = ""; + }; + + return ( + + + + ); +} diff --git a/src/components/dashboard/app/CreateFolderModal.tsx b/src/components/dashboard/app/CreateFolderModal.tsx new file mode 100644 index 0000000..84f9999 --- /dev/null +++ b/src/components/dashboard/app/CreateFolderModal.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { FaTimes } from "react-icons/fa"; + +interface CreateFolderModalProps { + isOpen: boolean; + onClose: () => void; + onCreateFolder: (name: string) => Promise; +} + +export default function CreateFolderModal({ + isOpen, + onClose, + onCreateFolder +}: CreateFolderModalProps) { + const [folderName, setFolderName] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!folderName.trim()) { + setError("O nome da pasta não pode estar vazio"); + return; + } + + setLoading(true); + setError(null); + + try { + await onCreateFolder(folderName.trim()); + onClose(); + setFolderName(""); + } catch (err: any) { + setError(err.message || "Erro ao criar pasta"); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setFolderName(""); + setError(null); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ Criar Nova Pasta +

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setFolderName(e.target.value)} + className="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-white focus:outline-none focus:border-indigo-500" + disabled={loading} + autoFocus + placeholder="Nova Pasta" + /> +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/dashboard/app/FileEditorModal.tsx b/src/components/dashboard/app/FileEditorModal.tsx new file mode 100644 index 0000000..062eb7a --- /dev/null +++ b/src/components/dashboard/app/FileEditorModal.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { FaTimes, FaSave, FaSpinner } from "react-icons/fa"; + +interface FileEditorModalProps { + isOpen: boolean; + onClose: () => void; + fileName: string; + filePath: string; + onSave: (content: string) => Promise; + onGetContent: () => Promise; +} + +export default function FileEditorModal({ + isOpen, + onClose, + fileName, + filePath, + onSave, + onGetContent +}: FileEditorModalProps) { + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (isOpen && filePath) { + loadFileContent(); + } else { + setContent(""); + setError(null); + } + }, [isOpen, filePath]); + + const loadFileContent = async () => { + setLoading(true); + setError(null); + try { + const fileContent = await onGetContent(); + setContent(fileContent); + } catch (err: any) { + setError(err.message || "Erro ao carregar o conteúdo do arquivo"); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + setError(null); + try { + await onSave(content); + onClose(); + } catch (err: any) { + setError(err.message || "Erro ao salvar o arquivo"); + } finally { + setSaving(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ Editando: {fileName} +

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ {loading ? ( +
+ +
+ ) : ( +