diff --git a/console/frontend/Dockerfile b/console/frontend/Dockerfile new file mode 100644 index 0000000..2d7f71f --- /dev/null +++ b/console/frontend/Dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM node:18-alpine as builder + +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine + +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/console/frontend/index.html b/console/frontend/index.html new file mode 100644 index 0000000..9e817c5 --- /dev/null +++ b/console/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Console - Microservices Dashboard + + +
+ + + \ No newline at end of file diff --git a/console/frontend/nginx.conf b/console/frontend/nginx.conf new file mode 100644 index 0000000..9db5c76 --- /dev/null +++ b/console/frontend/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_pass http://console-backend:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/console/frontend/package.json b/console/frontend/package.json new file mode 100644 index 0000000..8085dec --- /dev/null +++ b/console/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "console-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/material": "^5.15.2", + "@mui/icons-material": "^5.15.2", + "axios": "^1.6.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.1" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} \ No newline at end of file diff --git a/console/frontend/src/App.tsx b/console/frontend/src/App.tsx new file mode 100644 index 0000000..7a5285e --- /dev/null +++ b/console/frontend/src/App.tsx @@ -0,0 +1,19 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Services from './pages/Services' +import Users from './pages/Users' + +function App() { + return ( + + }> + } /> + } /> + } /> + + + ) +} + +export default App \ No newline at end of file diff --git a/console/frontend/src/components/Layout.tsx b/console/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..5c93367 --- /dev/null +++ b/console/frontend/src/components/Layout.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react' +import { Outlet, Link as RouterLink } from 'react-router-dom' +import { + AppBar, + Box, + Drawer, + IconButton, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Toolbar, + Typography, +} from '@mui/material' +import { + Menu as MenuIcon, + Dashboard as DashboardIcon, + Cloud as CloudIcon, + People as PeopleIcon, +} from '@mui/icons-material' + +const drawerWidth = 240 + +const menuItems = [ + { text: 'Dashboard', icon: , path: '/' }, + { text: 'Services', icon: , path: '/services' }, + { text: 'Users', icon: , path: '/users' }, +] + +function Layout() { + const [open, setOpen] = useState(true) + + const handleDrawerToggle = () => { + setOpen(!open) + } + + return ( + + theme.zIndex.drawer + 1 }} + > + + + + + + Microservices Console + + + + + + + + {menuItems.map((item) => ( + + + {item.icon} + + + + ))} + + + + + + + + + ) +} + +export default Layout \ No newline at end of file diff --git a/console/frontend/src/main.tsx b/console/frontend/src/main.tsx new file mode 100644 index 0000000..e59a55f --- /dev/null +++ b/console/frontend/src/main.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { ThemeProvider, createTheme } from '@mui/material/styles' +import CssBaseline from '@mui/material/CssBaseline' +import App from './App' + +const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1976d2', + }, + secondary: { + main: '#dc004e', + }, + }, +}) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + , +) \ No newline at end of file diff --git a/console/frontend/src/pages/Dashboard.tsx b/console/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..86eb4e8 --- /dev/null +++ b/console/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import { + Grid, + Paper, + Typography, + Box, + Card, + CardContent, + Chip, +} from '@mui/material' +import { + CheckCircle as CheckCircleIcon, + Error as ErrorIcon +} from '@mui/icons-material' +import axios from 'axios' + +interface ServiceStatus { + name: string + status: 'healthy' | 'unhealthy' + endpoint: string + lastChecked: string +} + +function Dashboard() { + const [services, setServices] = useState([]) + const [stats, setStats] = useState({ + totalServices: 0, + healthyServices: 0, + unhealthyServices: 0, + }) + + useEffect(() => { + checkServices() + const interval = setInterval(checkServices, 10000) + return () => clearInterval(interval) + }, []) + + const checkServices = async () => { + const serviceChecks = [ + { name: 'Console Backend', endpoint: '/api/health' }, + { name: 'Users Service', endpoint: '/api/users/health' }, + ] + + const results = await Promise.all( + serviceChecks.map(async (service) => { + try { + await axios.get(service.endpoint) + return { + ...service, + status: 'healthy' as const, + lastChecked: new Date().toLocaleTimeString(), + } + } catch { + return { + ...service, + status: 'unhealthy' as const, + lastChecked: new Date().toLocaleTimeString(), + } + } + }) + ) + + setServices(results) + + const healthy = results.filter(s => s.status === 'healthy').length + setStats({ + totalServices: results.length, + healthyServices: healthy, + unhealthyServices: results.length - healthy, + }) + } + + return ( + + + Dashboard + + + + + + + + Total Services + + + {stats.totalServices} + + + + + + + + + Healthy Services + + + {stats.healthyServices} + + + + + + + + + Unhealthy Services + + + {stats.unhealthyServices} + + + + + + + + + Service Status + + + {services.map((service) => ( + + + + + + {service.name} + + {service.endpoint} + + + Last checked: {service.lastChecked} + + + : } + /> + + + + + ))} + + + + ) +} + +export default Dashboard \ No newline at end of file diff --git a/console/frontend/src/pages/Services.tsx b/console/frontend/src/pages/Services.tsx new file mode 100644 index 0000000..f492f7d --- /dev/null +++ b/console/frontend/src/pages/Services.tsx @@ -0,0 +1,98 @@ +import { + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, +} from '@mui/material' + +const servicesData = [ + { + id: 1, + name: 'Console', + type: 'API Gateway', + port: 8011, + status: 'Running', + description: 'Central orchestrator and API gateway', + }, + { + id: 2, + name: 'Users', + type: 'Microservice', + port: 8001, + status: 'Running', + description: 'User management service', + }, + { + id: 3, + name: 'MongoDB', + type: 'Database', + port: 27017, + status: 'Running', + description: 'Document database for persistence', + }, + { + id: 4, + name: 'Redis', + type: 'Cache', + port: 6379, + status: 'Running', + description: 'In-memory cache and pub/sub', + }, +] + +function Services() { + return ( + + + Services + + + + + + + Service Name + Type + Port + Status + Description + + + + {servicesData.map((service) => ( + + + {service.name} + + + + + {service.port} + + + + {service.description} + + ))} + +
+
+
+ ) +} + +export default Services \ No newline at end of file diff --git a/console/frontend/src/pages/Users.tsx b/console/frontend/src/pages/Users.tsx new file mode 100644 index 0000000..07c4e4e --- /dev/null +++ b/console/frontend/src/pages/Users.tsx @@ -0,0 +1,208 @@ +import { useState, useEffect } from 'react' +import { + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + IconButton, + TextField, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Stack, +} from '@mui/material' +import { + Add as AddIcon, + Edit as EditIcon, + Delete as DeleteIcon, +} from '@mui/icons-material' +import axios from 'axios' + +interface User { + _id: string + username: string + email: string + full_name?: string + created_at: string +} + +function Users() { + const [users, setUsers] = useState([]) + const [openDialog, setOpenDialog] = useState(false) + const [editingUser, setEditingUser] = useState(null) + const [formData, setFormData] = useState({ + username: '', + email: '', + full_name: '', + }) + + useEffect(() => { + fetchUsers() + }, []) + + const fetchUsers = async () => { + try { + const response = await axios.get('/api/users/') + setUsers(response.data) + } catch (error) { + console.error('Failed to fetch users:', error) + } + } + + const handleOpenDialog = (user?: User) => { + if (user) { + setEditingUser(user) + setFormData({ + username: user.username, + email: user.email, + full_name: user.full_name || '', + }) + } else { + setEditingUser(null) + setFormData({ + username: '', + email: '', + full_name: '', + }) + } + setOpenDialog(true) + } + + const handleCloseDialog = () => { + setOpenDialog(false) + setEditingUser(null) + setFormData({ + username: '', + email: '', + full_name: '', + }) + } + + const handleSubmit = async () => { + try { + if (editingUser) { + await axios.put(`/api/users/${editingUser._id}`, formData) + } else { + await axios.post('/api/users/', formData) + } + fetchUsers() + handleCloseDialog() + } catch (error) { + console.error('Failed to save user:', error) + } + } + + const handleDelete = async (id: string) => { + if (confirm('Are you sure you want to delete this user?')) { + try { + await axios.delete(`/api/users/${id}`) + fetchUsers() + } catch (error) { + console.error('Failed to delete user:', error) + } + } + } + + return ( + + + + Users + + + + + + + + + Username + Email + Full Name + Created At + Actions + + + + {users.map((user) => ( + + {user.username} + {user.email} + {user.full_name || '-'} + + {new Date(user.created_at).toLocaleDateString()} + + + handleOpenDialog(user)} + > + + + handleDelete(user._id)} + > + + + + + ))} + +
+
+ + + + {editingUser ? 'Edit User' : 'Add New User'} + + + + setFormData({ ...formData, username: e.target.value })} + fullWidth + required + /> + setFormData({ ...formData, email: e.target.value })} + fullWidth + required + /> + setFormData({ ...formData, full_name: e.target.value })} + fullWidth + /> + + + + + + + +
+ ) +} + +export default Users \ No newline at end of file diff --git a/console/frontend/tsconfig.json b/console/frontend/tsconfig.json new file mode 100644 index 0000000..7a7611e --- /dev/null +++ b/console/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/console/frontend/tsconfig.node.json b/console/frontend/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/console/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/console/frontend/vite.config.ts b/console/frontend/vite.config.ts new file mode 100644 index 0000000..62f7534 --- /dev/null +++ b/console/frontend/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 3000, + proxy: { + '/api': { + target: 'http://console-backend:8000', + changeOrigin: true + } + } + } +}) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index bcafb57..14ee06f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,19 @@ version: '3.8' services: + console-frontend: + build: + context: ./console/frontend + dockerfile: Dockerfile + container_name: site11_console_frontend + ports: + - "3000:80" + networks: + - site11_network + restart: unless-stopped + depends_on: + - console-backend + console-backend: build: context: ./console/backend