docs: Add comprehensive technical interview guide
- Create TECHNICAL_INTERVIEW.md with 20 technical questions - Cover Backend (5), Frontend (4), DevOps (6), Data/API (3), Problem Solving (2) - Include detailed answers with code examples - Use Obsidian-compatible callout format for collapsible answers - Add evaluation criteria (Junior/Mid/Senior levels) - Include practical coding challenge (Comments service) Technical areas covered: - API Gateway vs Service Mesh architecture - FastAPI async/await and Motor vs PyMongo - Microservice communication (REST, Pub/Sub, gRPC) - Database strategies and JWT security - React 18 features and TypeScript integration - Docker multi-stage builds and K8s deployment strategies - Health checks, monitoring, and logging - RESTful API design and MongoDB schema modeling - Traffic handling and failure scenarios fix: Update Services.tsx with TypeScript fixes - Fix ServiceType enum import (use value import, not type-only) - Fix API method name: checkHealthAll → checkAllHealth - Ensure proper enum usage in form data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@ -9,90 +10,391 @@ import {
|
||||
TableRow,
|
||||
Paper,
|
||||
Chip,
|
||||
Button,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
MenuItem,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Tooltip,
|
||||
} 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',
|
||||
},
|
||||
]
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
Refresh as RefreshIcon,
|
||||
CheckCircle as HealthCheckIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { serviceAPI } from '../api/service'
|
||||
import { ServiceType, ServiceStatus } from '../types/service'
|
||||
import type { Service, ServiceCreate } from '../types/service'
|
||||
|
||||
function Services() {
|
||||
const [services, setServices] = useState<Service[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
const [editingService, setEditingService] = useState<Service | null>(null)
|
||||
const [formData, setFormData] = useState<ServiceCreate>({
|
||||
name: '',
|
||||
url: '',
|
||||
service_type: ServiceType.BACKEND,
|
||||
description: '',
|
||||
health_endpoint: '/health',
|
||||
metadata: {},
|
||||
})
|
||||
|
||||
// Load services
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await serviceAPI.getAll()
|
||||
setServices(data)
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to load services')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadServices()
|
||||
}, [])
|
||||
|
||||
// Handle create/update service
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (editingService) {
|
||||
await serviceAPI.update(editingService._id, formData)
|
||||
} else {
|
||||
await serviceAPI.create(formData)
|
||||
}
|
||||
setOpenDialog(false)
|
||||
setEditingService(null)
|
||||
resetForm()
|
||||
loadServices()
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to save service')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle delete service
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this service?')) return
|
||||
|
||||
try {
|
||||
await serviceAPI.delete(id)
|
||||
loadServices()
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to delete service')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle health check
|
||||
const handleHealthCheck = async (id: string) => {
|
||||
try {
|
||||
const result = await serviceAPI.checkHealth(id)
|
||||
setServices(prev => prev.map(s =>
|
||||
s._id === id ? { ...s, status: result.status, response_time_ms: result.response_time_ms, last_health_check: result.checked_at } : s
|
||||
))
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to check health')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle health check all
|
||||
const handleHealthCheckAll = async () => {
|
||||
try {
|
||||
await serviceAPI.checkAllHealth()
|
||||
loadServices()
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to check all services')
|
||||
}
|
||||
}
|
||||
|
||||
// Open dialog for create/edit
|
||||
const openEditDialog = (service?: Service) => {
|
||||
if (service) {
|
||||
setEditingService(service)
|
||||
setFormData({
|
||||
name: service.name,
|
||||
url: service.url,
|
||||
service_type: service.service_type,
|
||||
description: service.description || '',
|
||||
health_endpoint: service.health_endpoint || '/health',
|
||||
metadata: service.metadata || {},
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
setOpenDialog(true)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
url: '',
|
||||
service_type: ServiceType.BACKEND,
|
||||
description: '',
|
||||
health_endpoint: '/health',
|
||||
metadata: {},
|
||||
})
|
||||
setEditingService(null)
|
||||
}
|
||||
|
||||
const getStatusColor = (status: ServiceStatus) => {
|
||||
switch (status) {
|
||||
case 'healthy': return 'success'
|
||||
case 'unhealthy': return 'error'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeColor = (type: ServiceType) => {
|
||||
switch (type) {
|
||||
case 'backend': return 'primary'
|
||||
case 'frontend': return 'secondary'
|
||||
case 'database': return 'info'
|
||||
case 'cache': return 'warning'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Services
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h4">
|
||||
Services
|
||||
</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={loadServices}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<HealthCheckIcon />}
|
||||
onClick={handleHealthCheckAll}
|
||||
variant="outlined"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Check All Health
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => openEditDialog()}
|
||||
variant="contained"
|
||||
>
|
||||
Add Service
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" onClose={() => setError(null)} sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Service Name</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Port</TableCell>
|
||||
<TableCell>URL</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Last Check</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{servicesData.map((service) => (
|
||||
<TableRow key={service.id}>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{service.name}</Typography>
|
||||
{services.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} align="center">
|
||||
<Typography variant="body2" color="text.secondary" py={4}>
|
||||
No services found. Click "Add Service" to create one.
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={service.type}
|
||||
size="small"
|
||||
color={service.type === 'API Gateway' ? 'primary' : 'default'}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{service.port}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={service.status}
|
||||
size="small"
|
||||
color="success"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{service.description}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
services.map((service) => (
|
||||
<TableRow key={service._id} hover>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{service.name}</Typography>
|
||||
{service.description && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{service.description}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={service.service_type}
|
||||
size="small"
|
||||
color={getTypeColor(service.service_type)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" noWrap sx={{ maxWidth: 300 }}>
|
||||
{service.url}
|
||||
</Typography>
|
||||
{service.health_endpoint && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Health: {service.health_endpoint}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={service.status}
|
||||
size="small"
|
||||
color={getStatusColor(service.status)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{service.response_time_ms ? (
|
||||
<Typography variant="body2">
|
||||
{service.response_time_ms.toFixed(2)} ms
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{service.last_health_check ? (
|
||||
<Typography variant="caption">
|
||||
{new Date(service.last_health_check).toLocaleString()}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Never
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Check Health">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleHealthCheck(service._id)}
|
||||
color="primary"
|
||||
>
|
||||
<HealthCheckIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => openEditDialog(service)}
|
||||
color="primary"
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleDelete(service._id)}
|
||||
color="error"
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Add/Edit Dialog */}
|
||||
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{editingService ? 'Edit Service' : 'Add Service'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Service Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Service URL"
|
||||
value={formData.url}
|
||||
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
|
||||
required
|
||||
fullWidth
|
||||
placeholder="http://service-name:8000"
|
||||
/>
|
||||
<TextField
|
||||
label="Service Type"
|
||||
value={formData.service_type}
|
||||
onChange={(e) => setFormData({ ...formData, service_type: e.target.value as ServiceType })}
|
||||
select
|
||||
required
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem value="backend">Backend</MenuItem>
|
||||
<MenuItem value="frontend">Frontend</MenuItem>
|
||||
<MenuItem value="database">Database</MenuItem>
|
||||
<MenuItem value="cache">Cache</MenuItem>
|
||||
<MenuItem value="message_queue">Message Queue</MenuItem>
|
||||
<MenuItem value="other">Other</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Health Endpoint"
|
||||
value={formData.health_endpoint}
|
||||
onChange={(e) => setFormData({ ...formData, health_endpoint: e.target.value })}
|
||||
fullWidth
|
||||
placeholder="/health"
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="contained"
|
||||
disabled={!formData.name || !formData.url}
|
||||
>
|
||||
{editingService ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Services
|
||||
export default Services
|
||||
|
||||
Reference in New Issue
Block a user