diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8f5035c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.qwen + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.qwen/settings.json b/frontend/.qwen/settings.json new file mode 100644 index 0000000..627a679 --- /dev/null +++ b/frontend/.qwen/settings.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(yarn *)", + "Bash(npx *)", + "Bash(npm run *)", + "Bash(type *)", + "Bash(dir)", + "Bash(move *)", + "Bash(findstr *)" + ] + }, + "$version": 3 +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0fca6f0 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4aecc80 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "HellreigN", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@codemirror/lang-sql": "^6.10.0", + "@tailwindcss/vite": "^4.2.2", + "@uiw/react-codemirror": "^4.25.8", + "axios": "^1.13.6", + "framer-motion": "^12.38.0", + "primeicons": "^7.0.0", + "primereact": "^10.9.7", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-icons": "^5.6.0", + "react-router-dom": "^7.13.1", + "recharts": "^3.8.0", + "tailwind": "^4.0.0", + "tailwindcss": "^4.2.2", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx new file mode 100644 index 0000000..625d6a5 --- /dev/null +++ b/frontend/src/app/App.tsx @@ -0,0 +1,16 @@ +import "@/shared/styles/index.css"; +import "primereact/resources/themes/lara-light-cyan/theme.css"; +import "primereact/resources/primereact.min.css"; +import "primeicons/primeicons.css"; +import { PrimeReactProvider } from "primereact/api"; +import { Routing } from "./providers/routing/routing"; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/frontend/src/app/providers/routing/helper/protected.route.tsx b/frontend/src/app/providers/routing/helper/protected.route.tsx new file mode 100644 index 0000000..8ce4d9c --- /dev/null +++ b/frontend/src/app/providers/routing/helper/protected.route.tsx @@ -0,0 +1,12 @@ +import { useAuthStore } from "@/store/auth/auth.store"; +import { Navigate } from "react-router-dom"; + +export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { isAuthenticated } = useAuthStore(); + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +}; diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx new file mode 100644 index 0000000..af4b471 --- /dev/null +++ b/frontend/src/app/providers/routing/routing.tsx @@ -0,0 +1,32 @@ +import { Suspense } from "react"; +import { Routes as ReactRoutes, Route, Navigate } from "react-router-dom"; +import { HomePage } from "@/pages/home.page"; +import { ThemesPage } from "@/pages/themes.page"; +import { AuthPage } from "@/pages/auth.page"; +import { RegisterPage } from "@/pages/register.page"; +import { AddAgentsPage } from "@/pages/add-agents.page"; +import { DefaultLayout } from "@/shared/layouts/DefaultLayout"; + +export const Routing = () => { + return ( + + Загрузка... + + } + > + + }> + } /> + } /> + } /> + } /> + } /> + + } /> + + + + ); +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..c240c26 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,12 @@ +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; +import { ThemeInitialProvider } from "./modules/theme-changer"; +import App from "./app/App"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/frontend/src/modules/agent/types/agent.types.tsx b/frontend/src/modules/agent/types/agent.types.tsx new file mode 100644 index 0000000..4dae3f0 --- /dev/null +++ b/frontend/src/modules/agent/types/agent.types.tsx @@ -0,0 +1,27 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const DeployType = { + Docker: "docker", + Binary: "binary", + Deploy: "deploy", +} as const; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const AuthMethod = { + Key: "key", + Password: "password", +} as const; + +export interface ExtraField { + key: string; + value: string; +} + +export interface SSHAgentConfig { + user: string; + ip: string; + authMethod: string; + sshKey?: string; + password?: string; + extraFields: ExtraField[]; + deployType: string; +} diff --git a/frontend/src/modules/agent/ui/SSHAgentForm.tsx b/frontend/src/modules/agent/ui/SSHAgentForm.tsx new file mode 100644 index 0000000..d53b187 --- /dev/null +++ b/frontend/src/modules/agent/ui/SSHAgentForm.tsx @@ -0,0 +1,496 @@ +import React from "react"; +import { + FiServer, + FiGlobe, + FiKey, + FiLock, + FiPlus, + FiTrash2, + FiSettings, +} from "react-icons/fi"; +import { SiDocker } from "react-icons/si"; +import { FiPackage, FiUploadCloud } from "react-icons/fi"; + +type DeployType = "docker" | "binary" | "deploy"; +type AuthMethod = "key" | "password"; + +interface ExtraField { + key: string; + value: string; +} + +export interface SSHAgentConfig { + user: string; + ip: string; + authMethod: AuthMethod; + sshKey?: string; + password?: string; + extraFields: ExtraField[]; + deployType: DeployType; +} + +interface SSHAgentFormProps { + index: number; + config: SSHAgentConfig; + onChange: (index: number, config: SSHAgentConfig) => void; + onRemove: (index: number) => void; + canRemove: boolean; +} + +const DEPLOY_OPTIONS: { + value: DeployType; + label: string; + icon: React.ReactNode; +}[] = [ + { value: "docker", label: "Docker", icon: }, + { value: "binary", label: "Binary", icon: }, +]; + +const inputBaseStyle: React.CSSProperties = { + width: "100%", + padding: "10px 12px", + border: "1px solid var(--border)", + borderRadius: "8px", + backgroundColor: "var(--input-bg)", + color: "var(--text-primary)", + fontSize: "14px", + transition: "border-color 0.2s, box-shadow 0.2s", +}; + +const labelStyle: React.CSSProperties = { + display: "block", + marginBottom: "8px", + color: "var(--text-secondary)", + fontSize: "14px", + fontWeight: 500, +}; + +export const SSHAgentForm: React.FC = ({ + index, + config, + onChange, + onRemove, + canRemove, +}) => { + const handleChange = (field: keyof SSHAgentConfig, value: unknown) => { + onChange(index, { ...config, [field]: value }); + }; + + const handleExtraFieldChange = ( + fieldIndex: number, + field: keyof ExtraField, + value: string, + ) => { + const newExtraFields = [...config.extraFields]; + newExtraFields[fieldIndex] = { + ...newExtraFields[fieldIndex], + [field]: value, + }; + handleChange("extraFields", newExtraFields); + }; + + const addExtraField = () => { + handleChange("extraFields", [ + ...config.extraFields, + { key: "", value: "" }, + ]); + }; + + const removeExtraField = (fieldIndex: number) => { + const newExtraFields = config.extraFields.filter( + (_, i) => i !== fieldIndex, + ); + handleChange("extraFields", newExtraFields); + }; + + const handleFocus = ( + e: React.FocusEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => { + e.currentTarget.style.borderColor = "var(--border-focus)"; + e.currentTarget.style.boxShadow = `0 0 0 3px var(--border-focus)30`; + }; + + const handleBlur = ( + e: React.FocusEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => { + e.currentTarget.style.borderColor = "var(--border)"; + e.currentTarget.style.boxShadow = "none"; + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+

+ SSH сервер #{index + 1} +

+
+ {canRemove && ( + + )} +
+ +
+ {/* User и IP */} +
+
+ + handleChange("user", e.target.value)} + required + style={inputBaseStyle} + onFocus={handleFocus} + onBlur={handleBlur} + placeholder="username" + /> +
+ +
+ + handleChange("ip", e.target.value)} + required + style={inputBaseStyle} + onFocus={handleFocus} + onBlur={handleBlur} + placeholder="192.168.1.1" + /> +
+
+ + {/* Метод аутентификации */} +
+ +
+ {(["key", "password"] as const).map((method) => ( + + ))} +
+
+ + {/* SSH Key или Password */} + {config.authMethod === "key" ? ( +
+ +