VE router dashboard frontend and backend code
This commit is contained in:
commit
d9fd7edad5
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
23
backend/.env
Normal file
23
backend/.env
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
CORS_ORIGIN=http://localhost:5173,http://localhost:3000
|
||||||
|
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3307
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=rootpassword
|
||||||
|
DB_NAME=ve_router_db
|
||||||
|
DB_CONNECTION_LIMIT=10
|
||||||
|
|
||||||
|
# Authentication Configuration
|
||||||
|
JWT_SECRET=your-super-secure-jwt-secret-key
|
||||||
|
JWT_EXPIRES_IN=1d
|
||||||
|
SALT_ROUNDS=10
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FILENAME=app.log
|
||||||
24
backend/.gitignore
vendored
Normal file
24
backend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
45
backend/README.md
Normal file
45
backend/README.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# VE Router Backend
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Install dependencies:
|
||||||
|
\\\ash
|
||||||
|
npm install
|
||||||
|
\\\
|
||||||
|
|
||||||
|
2. Configure environment variables:
|
||||||
|
\\\ash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
\\\
|
||||||
|
|
||||||
|
3. Start development server:
|
||||||
|
\\\ash
|
||||||
|
npm run dev
|
||||||
|
\\\
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
- \
|
||||||
|
pm start\: Start production server
|
||||||
|
- \
|
||||||
|
pm run dev\: Start development server
|
||||||
|
- \
|
||||||
|
pm run build\: Build the project
|
||||||
|
- \
|
||||||
|
pm test\: Run tests
|
||||||
|
- \
|
||||||
|
pm run lint\: Lint code
|
||||||
|
- \
|
||||||
|
pm run format\: Format code
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
\\\
|
||||||
|
src/
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── controllers/ # Request handlers
|
||||||
|
├── middleware/ # Express middleware
|
||||||
|
├── repositories/ # Data access layer
|
||||||
|
├── routes/ # API routes
|
||||||
|
├── services/ # Business logic
|
||||||
|
├── types/ # TypeScript types
|
||||||
|
└── utils/ # Utility functions
|
||||||
|
\\\
|
||||||
267
backend/backend-setup.ps1
Normal file
267
backend/backend-setup.ps1
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
# setup.ps1
|
||||||
|
$projectName = "ve-router-backend"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
$green = "`e[0;32m"
|
||||||
|
$blue = "`e[0;34m"
|
||||||
|
$nc = "`e[0m" # No Color
|
||||||
|
|
||||||
|
Write-Host "$blue Creating project structure for $projectName... $nc"
|
||||||
|
|
||||||
|
# Create project directory
|
||||||
|
New-Item -ItemType Directory -Force -Name $projectName
|
||||||
|
Set-Location -Path $projectName
|
||||||
|
|
||||||
|
# Create package.json
|
||||||
|
Write-Host "$blue Initializing package.json... $nc"
|
||||||
|
$packageJson = @"
|
||||||
|
{
|
||||||
|
"name": "ve-router-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Router Management System Backend",
|
||||||
|
"main": "dist/app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/app.js",
|
||||||
|
"dev": "nodemon src/app.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mysql2": "^3.2.0",
|
||||||
|
"winston": "^3.8.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.13",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/jest": "^29.5.0",
|
||||||
|
"@types/node": "^18.15.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"eslint": "^8.37.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"nodemon": "^2.0.22",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
$packageJson | Out-File -FilePath "package.json" -Encoding UTF8
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
Write-Host "$blue Creating directory structure... $nc"
|
||||||
|
$dirs = @("src/config", "src/controllers", "src/middleware", "src/repositories", "src/routes", "src/services", "src/types", "src/utils", "tests/integration", "tests/unit/services")
|
||||||
|
$dirs | ForEach-Object { New-Item -ItemType Directory -Force -Path $_ }
|
||||||
|
|
||||||
|
# Create TypeScript configuration
|
||||||
|
Write-Host "$blue Creating TypeScript configuration... $nc"
|
||||||
|
$tsconfig = @"
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2018",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
$tsconfig | Out-File -FilePath "tsconfig.json" -Encoding UTF8
|
||||||
|
|
||||||
|
# Create environment files
|
||||||
|
Write-Host "$blue Creating environment files... $nc"
|
||||||
|
$envExample = @"
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
DB_NAME=ve_router_db
|
||||||
|
LOG_LEVEL=info
|
||||||
|
JWT_SECRET=your_jwt_secret
|
||||||
|
CORS_ORIGIN=http://localhost:3000
|
||||||
|
"
|
||||||
|
$envExample | Out-File -FilePath ".env.example" -Encoding UTF8
|
||||||
|
Copy-Item -Path ".env.example" -Destination ".env"
|
||||||
|
|
||||||
|
# Create .gitignore
|
||||||
|
Write-Host "$blue Creating .gitignore... $nc"
|
||||||
|
$gitignore = @"
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
"
|
||||||
|
$gitignore | Out-File -FilePath ".gitignore" -Encoding UTF8
|
||||||
|
|
||||||
|
# Create source files
|
||||||
|
Write-Host "$blue Creating source files... $nc"
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
$srcConfig = @"
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
env: process.env.NODE_ENV || 'development',
|
||||||
|
port: parseInt(process.env.PORT || '3000', 10),
|
||||||
|
db: {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || 've_router_db'
|
||||||
|
},
|
||||||
|
logLevel: process.env.LOG_LEVEL || 'info',
|
||||||
|
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
|
||||||
|
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||||
|
};
|
||||||
|
"@
|
||||||
|
$srcConfig | Out-File -FilePath "src/config/config.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
$srcDb = @"
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: config.db.host,
|
||||||
|
user: config.db.user,
|
||||||
|
password: config.db.password,
|
||||||
|
database: config.db.database,
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
export default pool;
|
||||||
|
"@
|
||||||
|
$srcDb | Out-File -FilePath "src/config/db.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# Types
|
||||||
|
$srcTypes = @"
|
||||||
|
export interface Study {
|
||||||
|
siuid: string;
|
||||||
|
patientId: string;
|
||||||
|
accessionNumber: string;
|
||||||
|
patientName: string;
|
||||||
|
studyDate: string;
|
||||||
|
modality: string;
|
||||||
|
studyDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VM {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
||||||
|
|
||||||
|
export interface RouterData {
|
||||||
|
id: number;
|
||||||
|
slNo: number;
|
||||||
|
routerId: string;
|
||||||
|
facility: string;
|
||||||
|
routerAlias: string;
|
||||||
|
lastSeen: string;
|
||||||
|
diskStatus: string;
|
||||||
|
diskUsage: number;
|
||||||
|
freeDisk: number;
|
||||||
|
totalDisk: number;
|
||||||
|
routerActivity: {
|
||||||
|
studies: Study[];
|
||||||
|
};
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: string;
|
||||||
|
appStatus: string;
|
||||||
|
vms: VM[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
$srcTypes | Out-File -FilePath "src/types/index.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# Create barrel files for each directory
|
||||||
|
Write-Host "$blue Creating barrel files... $nc"
|
||||||
|
$directories = @("controllers", "middleware", "repositories", "routes", "services", "utils")
|
||||||
|
foreach ($dir in $directories) {
|
||||||
|
"export * from './$dir';" | Out-File -FilePath "src/$dir/index.ts" -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create README.md
|
||||||
|
Write-Host "$blue Creating README.md... $nc"
|
||||||
|
$readme = @"
|
||||||
|
# VE Router Backend
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Install dependencies:
|
||||||
|
\`\`\`bash
|
||||||
|
npm install
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Configure environment variables:
|
||||||
|
\`\`\`bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. Start development server:
|
||||||
|
\`\`\`bash
|
||||||
|
npm run dev
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
- \`npm start\`: Start production server
|
||||||
|
- \`npm run dev\`: Start development server
|
||||||
|
- \`npm run build\`: Build the project
|
||||||
|
- \`npm test\`: Run tests
|
||||||
|
- \`npm run lint\`: Lint code
|
||||||
|
- \`npm run format\`: Format code
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
\`\`\`
|
||||||
|
src/
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── controllers/ # Request handlers
|
||||||
|
├── middleware/ # Express middleware
|
||||||
|
├── repositories/ # Data access layer
|
||||||
|
├── routes/ # API routes
|
||||||
|
├── services/ # Business logic
|
||||||
|
├── types/ # TypeScript types
|
||||||
|
└── utils/ # Utility functions
|
||||||
|
\`\`\`
|
||||||
|
"@
|
||||||
|
$readme | Out-File -FilePath "README.md" -Encoding UTF8
|
||||||
|
|
||||||
|
# Initialize git repository
|
||||||
|
Write-Host "$blue Initializing git repository... $nc"
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
Write-Host "$blue Installing dependencies... $nc"
|
||||||
|
npm install
|
||||||
|
|
||||||
|
Write-Host "$green Project setup complete! $nc"
|
||||||
|
Write-Host "$green To get started: $nc"
|
||||||
|
Write-Host "1. cd $projectName"
|
||||||
|
Write-Host "2. Edit .env with your configuration"
|
||||||
|
Write-Host "3. npm run dev"
|
||||||
49
backend/barrel-setup.ps1
Normal file
49
backend/barrel-setup.ps1
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# For utils directory
|
||||||
|
$utilsContent = @"
|
||||||
|
export * from './logger';
|
||||||
|
export * from './validators';
|
||||||
|
export * from './formatters';
|
||||||
|
// Add more utility exports as needed
|
||||||
|
"@
|
||||||
|
$utilsContent | Out-File -FilePath "src/utils/index.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# For services directory
|
||||||
|
$servicesContent = @"
|
||||||
|
export * from './RouterService';
|
||||||
|
export * from './UserService';
|
||||||
|
// Add more service exports as needed
|
||||||
|
"@
|
||||||
|
$servicesContent | Out-File -FilePath "src/services/index.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# For controllers directory
|
||||||
|
$controllersContent = @"
|
||||||
|
export * from './RouterController';
|
||||||
|
export * from './UserController';
|
||||||
|
// Add more controller exports as needed
|
||||||
|
"@
|
||||||
|
$controllersContent | Out-File -FilePath "src/controllers/index.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# For repositories directory
|
||||||
|
$repositoriesContent = @"
|
||||||
|
export * from './RouterRepository';
|
||||||
|
export * from './UserRepository';
|
||||||
|
// Add more repository exports as needed
|
||||||
|
"@
|
||||||
|
$repositoriesContent | Out-File -FilePath "src/repositories/index.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# For middleware directory
|
||||||
|
$middlewareContent = @"
|
||||||
|
export * from './auth';
|
||||||
|
export * from './errorHandler';
|
||||||
|
export * from './validate';
|
||||||
|
// Add more middleware exports as needed
|
||||||
|
"@
|
||||||
|
$middlewareContent | Out-File -FilePath "src/middleware/index.ts" -Encoding UTF8
|
||||||
|
|
||||||
|
# For routes directory
|
||||||
|
$routesContent = @"
|
||||||
|
export * from './router.routes';
|
||||||
|
export * from './user.routes';
|
||||||
|
// Add more route exports as needed
|
||||||
|
"@
|
||||||
|
$routesContent | Out-File -FilePath "src/routes/index.ts" -Encoding UTF8
|
||||||
16
backend/dockerfile
Normal file
16
backend/dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# ve-router-backend/Dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
4
backend/dockerignore
Normal file
4
backend/dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
3607
backend/package-lock.json
generated
Normal file
3607
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
backend/package.json
Normal file
33
backend/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "ve-router-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Router Management System Backend",
|
||||||
|
"main": "dist/app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/app.js",
|
||||||
|
"dev": "nodemon --exec ts-node src/app.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mysql2": "^3.2.0",
|
||||||
|
"ve-router-backend": "file:",
|
||||||
|
"winston": "^3.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.13",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^18.15.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"eslint": "^8.37.0",
|
||||||
|
"nodemon": "^2.0.22",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
61
backend/src/app.ts
Normal file
61
backend/src/app.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// src/app.ts
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import config from './config/config';
|
||||||
|
import routes from './routes';
|
||||||
|
import { errorHandler } from './middleware';
|
||||||
|
import logger from './utils/logger';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Debug middleware to log all incoming requests
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
logger.debug(`Incoming request: ${req.method} ${req.originalUrl}`);
|
||||||
|
logger.debug('Request body:', req.body);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// CORS configuration
|
||||||
|
app.use(cors({
|
||||||
|
origin: ['http://localhost:5173', 'http://localhost:3000'],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
logger.info(`Registering routes with prefix: ${config.server.apiPrefix}`);
|
||||||
|
app.use(config.server.apiPrefix, routes);
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
// 404 handler - Debug version
|
||||||
|
app.use((req, res) => {
|
||||||
|
logger.warn('404 Not Found:', {
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
originalUrl: req.originalUrl,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(404).json({
|
||||||
|
error: 'Not Found',
|
||||||
|
path: req.path,
|
||||||
|
originalUrl: req.originalUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = config.server.port || 3000;
|
||||||
|
app.listen(port, () => {
|
||||||
|
logger.info(`Server running on port ${port} in ${config.env} mode`);
|
||||||
|
logger.info(`API endpoints available at http://localhost:${port}${config.server.apiPrefix}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
38
backend/src/config/config.ts
Normal file
38
backend/src/config/config.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// src/config/config.ts
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
// Initialize dotenv at the very start
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
env: process.env.NODE_ENV || 'development',
|
||||||
|
server: {
|
||||||
|
port: parseInt(process.env.PORT || '3000', 10),
|
||||||
|
// Update this to include your frontend URL
|
||||||
|
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173', 'http://localhost:3000'],
|
||||||
|
apiPrefix: '/api/v1'
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306', 10),
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '', // Make sure this is getting the password
|
||||||
|
database: process.env.DB_NAME || 've_router_db',
|
||||||
|
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10', 10)
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
filename: process.env.LOG_FILENAME || 'app.log'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this for debugging
|
||||||
|
console.log('Database Config:', {
|
||||||
|
host: config.db.host,
|
||||||
|
user: config.db.user,
|
||||||
|
database: config.db.database,
|
||||||
|
// Don't log the password
|
||||||
|
});
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
||||||
31
backend/src/config/db.ts
Normal file
31
backend/src/config/db.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// src/config/db.ts
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import config from './config';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: config.db.host,
|
||||||
|
user: config.db.user,
|
||||||
|
password: config.db.password, // Make sure this is being passed
|
||||||
|
database: config.db.database,
|
||||||
|
port: config.db.port,
|
||||||
|
connectionLimit: config.db.connectionLimit,
|
||||||
|
waitForConnections: true,
|
||||||
|
queueLimit: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
pool.getConnection()
|
||||||
|
.then(connection => {
|
||||||
|
logger.info('Database connected successfully');
|
||||||
|
connection.release();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
logger.error('Database connection failed:', {
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
errno: error.errno
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default pool;
|
||||||
2
backend/src/config/index.ts
Normal file
2
backend/src/config/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as config } from './config';
|
||||||
|
export { default as db } from './db';
|
||||||
210
backend/src/controllers/DicomStudyController.ts
Normal file
210
backend/src/controllers/DicomStudyController.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
// src/controllers/DicomStudyController.ts
|
||||||
|
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { DicomStudyService } from '../services/DicomStudyService';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
interface ApiError {
|
||||||
|
message: string;
|
||||||
|
code?: string;
|
||||||
|
stack?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DicomStudyController {
|
||||||
|
private service: DicomStudyService;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.service = new DicomStudyService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown, message: string): ApiError {
|
||||||
|
const apiError: ApiError = {
|
||||||
|
message: message
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
logger.error(`${message}: ${error.message}`);
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
apiError.message = error.message;
|
||||||
|
apiError.stack = error.stack;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error(`${message}: Unknown error type`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiError;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllStudies = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const studies = await this.service.getAllStudies();
|
||||||
|
res.json(studies);
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, 'Failed to fetch studies');
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getStudyById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid study ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = await this.service.getStudyById(id);
|
||||||
|
if (!study) {
|
||||||
|
return res.status(404).json({ error: 'Study not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(study);
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, `Failed to fetch study ${req.params.id}`);
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getStudiesByRouterId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const routerId = req.params.routerId;
|
||||||
|
if (!routerId || typeof routerId !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Invalid router ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const studies = await this.service.getStudiesByRouterId(routerId);
|
||||||
|
res.json(studies);
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, `Failed to fetch studies for router ${req.params.routerId}`);
|
||||||
|
// If router not found, return 404
|
||||||
|
if (error instanceof Error && error.message.includes('Invalid router_id')) {
|
||||||
|
return res.status(404).json({ error: 'Router not found' });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createStudy = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const requiredFields = [
|
||||||
|
'router_id',
|
||||||
|
'study_instance_uid',
|
||||||
|
'patient_id',
|
||||||
|
'patient_name',
|
||||||
|
'accession_number',
|
||||||
|
'study_date',
|
||||||
|
'modality',
|
||||||
|
'series_instance_uid',
|
||||||
|
'study_status_code',
|
||||||
|
'association_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
const missingFields = requiredFields.filter(field => !req.body[field]);
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required fields',
|
||||||
|
missingFields
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate router_id format if needed
|
||||||
|
if (typeof req.body.router_id !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid router_id format',
|
||||||
|
details: 'router_id must be a string'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = await this.service.createStudy(req.body);
|
||||||
|
res.status(201).json(study);
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, 'Failed to create study');
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message.includes('Missing required field')) {
|
||||||
|
return res.status(400).json({ error: apiError });
|
||||||
|
}
|
||||||
|
if (error.message.includes('Invalid router_id')) {
|
||||||
|
return res.status(404).json({ error: 'Router not found' });
|
||||||
|
}
|
||||||
|
if (error.message.includes('Invalid study status code')) {
|
||||||
|
return res.status(400).json({ error: apiError });
|
||||||
|
}
|
||||||
|
if (error.message.includes('Invalid study date format')) {
|
||||||
|
return res.status(400).json({ error: apiError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateStudy = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid study ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = await this.service.updateStudy(id, req.body);
|
||||||
|
if (!study) {
|
||||||
|
return res.status(404).json({ error: 'Study not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(study);
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, `Failed to update study ${req.params.id}`);
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteStudy = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid study ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await this.service.deleteStudy(id);
|
||||||
|
if (!success) {
|
||||||
|
return res.status(404).json({ error: 'Study not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, `Failed to delete study ${req.params.id}`);
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
searchStudies = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { startDate, endDate, modality, patientName } = req.query;
|
||||||
|
|
||||||
|
if (startDate && typeof startDate !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid startDate format',
|
||||||
|
expected: 'YYYY-MM-DD'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (endDate && typeof endDate !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid endDate format',
|
||||||
|
expected: 'YYYY-MM-DD'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const studies = await this.service.searchStudies({
|
||||||
|
startDate: startDate as string,
|
||||||
|
endDate: endDate as string,
|
||||||
|
modality: modality as string,
|
||||||
|
patientName: patientName as string
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(studies);
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = this.handleError(error, 'Failed to search studies');
|
||||||
|
res.status(500).json({ error: apiError });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
135
backend/src/controllers/RouterController.ts
Normal file
135
backend/src/controllers/RouterController.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// src/controllers/RouterController.ts
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { RouterService } from '../services/RouterService';
|
||||||
|
import { Pool } from 'mysql2/promise';
|
||||||
|
import { RouterData, VMUpdate, VMUpdateRequest } from '../types';
|
||||||
|
|
||||||
|
|
||||||
|
export class RouterController {
|
||||||
|
private service: RouterService;
|
||||||
|
|
||||||
|
constructor(pool: Pool) {
|
||||||
|
this.service = new RouterService(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/controllers/RouterController.ts
|
||||||
|
// src/controllers/RouterController.ts
|
||||||
|
updateRouterVMs = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const routerId = req.query.router_id as string;
|
||||||
|
const { vms } = req.body;
|
||||||
|
|
||||||
|
// Add debugging logs
|
||||||
|
console.log('Received request:');
|
||||||
|
console.log('Router ID:', routerId);
|
||||||
|
console.log('VMs data:', vms);
|
||||||
|
|
||||||
|
if (!routerId) {
|
||||||
|
return res.status(400).json({ error: 'router_id is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(vms)) {
|
||||||
|
return res.status(400).json({ error: 'VMs must be an array' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedVMs = await this.service.updateRouterVMs(routerId, vms);
|
||||||
|
res.json(updatedVMs);
|
||||||
|
} catch (err) {
|
||||||
|
// Type cast the error
|
||||||
|
const error = err as Error;
|
||||||
|
|
||||||
|
// Enhanced error logging
|
||||||
|
console.error('Error in updateRouterVMs:', {
|
||||||
|
message: error?.message || 'Unknown error',
|
||||||
|
stack: error?.stack,
|
||||||
|
type: error?.constructor.name
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to update VMs',
|
||||||
|
details: error?.message || 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
getAllRouters = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const routers = await this.service.getAllRouters();
|
||||||
|
res.json(routers);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error && typeof error === 'object' && 'message' in error) {
|
||||||
|
const e = error as { message: string }; // Type assertion here
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'An unexpected error occurred' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
getRouterById = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const router = await this.service.getRouterById(id);
|
||||||
|
|
||||||
|
if (!router) {
|
||||||
|
return res.status(404).json({ error: 'Router not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(router);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch router' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createRouter = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const router = await this.service.createRouter(req.body);
|
||||||
|
res.status(201).json(router);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to create router' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateRouter = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const router = await this.service.updateRouter(id, req.body);
|
||||||
|
|
||||||
|
if (!router) {
|
||||||
|
return res.status(404).json({ error: 'Router not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(router);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to update router' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRouter = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const success = await this.service.deleteRouter(id);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return res.status(404).json({ error: 'Router not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to delete router' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoutersByFacility = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const facility = req.params.facility;
|
||||||
|
const routers = await this.service.getRoutersByFacility(facility);
|
||||||
|
res.json(routers);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch routers by facility' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
3
backend/src/controllers/index.ts
Normal file
3
backend/src/controllers/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './RouterController';
|
||||||
|
export * from './DicomStudyController';
|
||||||
|
// Add more controller exports as needed
|
||||||
22
backend/src/middleware/errorHandler.ts
Normal file
22
backend/src/middleware/errorHandler.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import config from '../config/config';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export const errorHandler = (
|
||||||
|
err: Error,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
logger.error('Error:', {
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
path: req.path,
|
||||||
|
method: req.method
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: config.env === 'development' ? err.message : undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
2
backend/src/middleware/index.ts
Normal file
2
backend/src/middleware/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './errorHandler';
|
||||||
|
// Add more middleware exports as needed
|
||||||
249
backend/src/repositories/DicomStudyRepository.ts
Normal file
249
backend/src/repositories/DicomStudyRepository.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
// src/repositories/DicomStudyRepository.ts
|
||||||
|
|
||||||
|
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DBDicomStudy, DicomStudySearchParams } from '../types/dicom';
|
||||||
|
import pool from '../config/db';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export class DicomStudyRepository {
|
||||||
|
private async getRouterStringId(numericId: number): Promise<string> {
|
||||||
|
try {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
'SELECT router_id FROM routers WHERE id = ?',
|
||||||
|
[numericId]
|
||||||
|
);
|
||||||
|
const rows = result as any[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error(`Router not found with id: ${numericId}`);
|
||||||
|
}
|
||||||
|
return rows[0].router_id;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting router string ID:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRouterNumericId(routerId: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
'SELECT id FROM routers WHERE router_id = ?',
|
||||||
|
[routerId]
|
||||||
|
);
|
||||||
|
const rows = result as any[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error(`Router not found with router_id: ${routerId}`);
|
||||||
|
}
|
||||||
|
return rows[0].id;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting router numeric ID:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async mapDBStudyToDicomStudy(dbStudy: DBDicomStudy): Promise<DicomStudy> {
|
||||||
|
const routerStringId = await this.getRouterStringId(dbStudy.router_id);
|
||||||
|
return {
|
||||||
|
id: dbStudy.id,
|
||||||
|
router_id: routerStringId,
|
||||||
|
study_instance_uid: dbStudy.study_instance_uid,
|
||||||
|
patient_id: dbStudy.patient_id,
|
||||||
|
patient_name: dbStudy.patient_name,
|
||||||
|
accession_number: dbStudy.accession_number,
|
||||||
|
study_date: dbStudy.study_date,
|
||||||
|
modality: dbStudy.modality,
|
||||||
|
study_description: dbStudy.study_description,
|
||||||
|
series_instance_uid: dbStudy.series_instance_uid,
|
||||||
|
procedure_code: dbStudy.procedure_code,
|
||||||
|
referring_physician_name: dbStudy.referring_physician_name,
|
||||||
|
study_status_code: dbStudy.study_status_code,
|
||||||
|
association_id: dbStudy.association_id,
|
||||||
|
created_at: dbStudy.created_at,
|
||||||
|
updated_at: dbStudy.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
|
||||||
|
try {
|
||||||
|
// Convert string router_id to numeric id for database
|
||||||
|
const numericRouterId = await this.getRouterNumericId(studyData.router_id);
|
||||||
|
|
||||||
|
const [result] = await pool.query(
|
||||||
|
`INSERT INTO dicom_study_overview (
|
||||||
|
router_id, study_instance_uid, patient_id, patient_name,
|
||||||
|
accession_number, study_date, modality, study_description,
|
||||||
|
series_instance_uid, procedure_code, referring_physician_name,
|
||||||
|
study_status_code, association_id
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
numericRouterId,
|
||||||
|
studyData.study_instance_uid,
|
||||||
|
studyData.patient_id,
|
||||||
|
studyData.patient_name,
|
||||||
|
studyData.accession_number,
|
||||||
|
studyData.study_date,
|
||||||
|
studyData.modality,
|
||||||
|
studyData.study_description,
|
||||||
|
studyData.series_instance_uid,
|
||||||
|
studyData.procedure_code,
|
||||||
|
studyData.referring_physician_name,
|
||||||
|
studyData.study_status_code,
|
||||||
|
studyData.association_id
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the created study with the correct router_id format
|
||||||
|
const insertId = (result as any).insertId;
|
||||||
|
return await this.findById(insertId) as DicomStudy;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error creating DICOM study:', error);
|
||||||
|
throw new Error('Failed to create DICOM study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<DicomStudy[]> {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(`
|
||||||
|
SELECT d.*, r.router_id as router_string_id
|
||||||
|
FROM dicom_study_overview d
|
||||||
|
JOIN routers r ON d.router_id = r.id
|
||||||
|
ORDER BY d.created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return Promise.all((rows as DBDicomStudy[]).map(row => this.mapDBStudyToDicomStudy(row)));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching all DICOM studies:', error);
|
||||||
|
throw new Error('Failed to fetch DICOM studies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: number): Promise<DicomStudy | null> {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(`
|
||||||
|
SELECT d.*, r.router_id as router_string_id
|
||||||
|
FROM dicom_study_overview d
|
||||||
|
JOIN routers r ON d.router_id = r.id
|
||||||
|
WHERE d.id = ?
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
const results = rows as DBDicomStudy[];
|
||||||
|
if (results.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.mapDBStudyToDicomStudy(results[0]);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching DICOM study by ID:', error);
|
||||||
|
throw new Error('Failed to fetch DICOM study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByRouterId(routerId: string): Promise<DicomStudy[]> {
|
||||||
|
try {
|
||||||
|
const numericRouterId = await this.getRouterNumericId(routerId);
|
||||||
|
const [rows] = await pool.query(`
|
||||||
|
SELECT d.*, r.router_id as router_string_id
|
||||||
|
FROM dicom_study_overview d
|
||||||
|
JOIN routers r ON d.router_id = r.id
|
||||||
|
WHERE d.router_id = ?
|
||||||
|
ORDER BY d.created_at DESC
|
||||||
|
`, [numericRouterId]);
|
||||||
|
|
||||||
|
return Promise.all((rows as DBDicomStudy[]).map(row => this.mapDBStudyToDicomStudy(row)));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching DICOM studies by router ID:', error);
|
||||||
|
throw new Error('Failed to fetch DICOM studies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, studyData: UpdateDicomStudyDTO): Promise<DicomStudy | null> {
|
||||||
|
try {
|
||||||
|
// First check if study exists
|
||||||
|
const existingStudy = await this.findById(id);
|
||||||
|
if (!existingStudy) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build update query dynamically based on provided fields
|
||||||
|
const updateFields: string[] = [];
|
||||||
|
const updateValues: any[] = [];
|
||||||
|
|
||||||
|
Object.entries(studyData).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
updateFields.push(`${key} = ?`);
|
||||||
|
updateValues.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updateFields.length > 0) {
|
||||||
|
// Add updated_at timestamp
|
||||||
|
updateFields.push('updated_at = CURRENT_TIMESTAMP');
|
||||||
|
|
||||||
|
// Add id for WHERE clause
|
||||||
|
updateValues.push(id);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
UPDATE dicom_study_overview
|
||||||
|
SET ${updateFields.join(', ')}
|
||||||
|
WHERE id = ?
|
||||||
|
`, updateValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated study
|
||||||
|
return await this.findById(id);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating DICOM study:', error);
|
||||||
|
throw new Error('Failed to update DICOM study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
'DELETE FROM dicom_study_overview WHERE id = ?',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return (result as any).affectedRows > 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting DICOM study:', error);
|
||||||
|
throw new Error('Failed to delete DICOM study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(params: DicomStudySearchParams): Promise<DicomStudy[]> {
|
||||||
|
try {
|
||||||
|
let query = `
|
||||||
|
SELECT d.*, r.router_id as router_string_id
|
||||||
|
FROM dicom_study_overview d
|
||||||
|
JOIN routers r ON d.router_id = r.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const values: any[] = [];
|
||||||
|
|
||||||
|
if (params.startDate) {
|
||||||
|
query += ' AND d.study_date >= ?';
|
||||||
|
values.push(params.startDate);
|
||||||
|
}
|
||||||
|
if (params.endDate) {
|
||||||
|
query += ' AND d.study_date <= ?';
|
||||||
|
values.push(params.endDate);
|
||||||
|
}
|
||||||
|
if (params.modality) {
|
||||||
|
query += ' AND d.modality = ?';
|
||||||
|
values.push(params.modality);
|
||||||
|
}
|
||||||
|
if (params.patientName) {
|
||||||
|
query += ' AND d.patient_name LIKE ?';
|
||||||
|
values.push(`%${params.patientName}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sorting
|
||||||
|
query += ' ORDER BY d.created_at DESC';
|
||||||
|
|
||||||
|
const [rows] = await pool.query(query, values);
|
||||||
|
return Promise.all((rows as DBDicomStudy[]).map(row => this.mapDBStudyToDicomStudy(row)));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error searching DICOM studies:', error);
|
||||||
|
throw new Error('Failed to search DICOM studies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
260
backend/src/repositories/RouterRepository.ts
Normal file
260
backend/src/repositories/RouterRepository.ts
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
// src/repositories/RouterRepository.ts
|
||||||
|
import { RouterData, Study, VM,VMUpdate } from '../types';
|
||||||
|
import pool from '../config/db';
|
||||||
|
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import { Pool } from 'mysql2/promise';
|
||||||
|
|
||||||
|
|
||||||
|
export class RouterRepository {
|
||||||
|
constructor(private pool: Pool) {} // Modified constructor
|
||||||
|
|
||||||
|
// src/repositories/RouterRepository.ts
|
||||||
|
// src/repositories/RouterRepository.ts
|
||||||
|
async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
||||||
|
console.log('Repository: Starting VM update for router:', routerId);
|
||||||
|
const connection = await this.pool.getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.beginTransaction();
|
||||||
|
console.log('Started transaction');
|
||||||
|
|
||||||
|
// First verify router exists
|
||||||
|
const [routers] = await connection.query(
|
||||||
|
'SELECT id FROM routers WHERE router_id = ?',
|
||||||
|
[routerId]
|
||||||
|
);
|
||||||
|
console.log('Router query result:', routers);
|
||||||
|
|
||||||
|
if (!Array.isArray(routers) || routers.length === 0) {
|
||||||
|
throw new Error(`Router ${routerId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log each VM update
|
||||||
|
for (const vm of vms) {
|
||||||
|
console.log(`Processing VM ${vm.vm_number}`);
|
||||||
|
const [result]: any = await connection.query(
|
||||||
|
`UPDATE vm_details
|
||||||
|
SET status_code = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE router_id = ? AND vm_number = ?`,
|
||||||
|
[vm.status_code, routerId, vm.vm_number]
|
||||||
|
);
|
||||||
|
console.log('Update result:', result);
|
||||||
|
|
||||||
|
if (!result.affectedRows) {
|
||||||
|
console.log('No rows affected, attempting insert');
|
||||||
|
await connection.query(
|
||||||
|
`INSERT INTO vm_details (router_id, vm_number, status_code)
|
||||||
|
VALUES (?, ?, ?)`,
|
||||||
|
[routerId, vm.vm_number, vm.status_code]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.commit();
|
||||||
|
console.log('Transaction committed');
|
||||||
|
|
||||||
|
const [updatedVMs] = await connection.query(
|
||||||
|
`SELECT * FROM vm_details WHERE router_id = ? ORDER BY vm_number`,
|
||||||
|
[routerId]
|
||||||
|
);
|
||||||
|
return updatedVMs;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Repository error:', err);
|
||||||
|
await connection.rollback();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
console.log('Connection released');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRouterStudies(routerId: number): Promise<Study[]> {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT
|
||||||
|
study_instance_uid as siuid,
|
||||||
|
patient_id as patientId,
|
||||||
|
accession_number as accessionNumber,
|
||||||
|
patient_name as patientName,
|
||||||
|
DATE_FORMAT(study_date, '%Y-%m-%d') as studyDate,
|
||||||
|
modality,
|
||||||
|
study_description as studyDescription
|
||||||
|
FROM dicom_study_overview
|
||||||
|
WHERE router_id = ?
|
||||||
|
ORDER BY study_date DESC`,
|
||||||
|
[routerId]
|
||||||
|
);
|
||||||
|
return rows as Study[];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error fetching studies for router ${routerId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRouterVMs(routerId: number): Promise<VM[]> {
|
||||||
|
try {
|
||||||
|
// First get the router_id string from routers table
|
||||||
|
const [routerRow] = await pool.query<RowDataPacket[]>(
|
||||||
|
'SELECT router_id FROM routers WHERE id = ?',
|
||||||
|
[routerId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!routerRow || !routerRow[0]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const routerIdString = routerRow[0].router_id;
|
||||||
|
|
||||||
|
// Then use this to query vm_details
|
||||||
|
const [rows] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT
|
||||||
|
id,
|
||||||
|
status_code as status
|
||||||
|
FROM vm_details
|
||||||
|
WHERE router_id = ?`,
|
||||||
|
[routerIdString]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`VMs for router ${routerId} (${routerIdString}):`, rows);
|
||||||
|
return rows as VM[];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error fetching VMs for router ${routerId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> {
|
||||||
|
try {
|
||||||
|
const studies = await this.getRouterStudies(dbRouter.id);
|
||||||
|
const vms = await this.getRouterVMs(dbRouter.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dbRouter.id,
|
||||||
|
slNo: index + 1,
|
||||||
|
routerId: dbRouter.router_id,
|
||||||
|
facility: dbRouter.facility,
|
||||||
|
routerAlias: dbRouter.router_alias,
|
||||||
|
lastSeen: new Date(dbRouter.last_seen).toISOString(),
|
||||||
|
diskStatus: dbRouter.disk_status_code,
|
||||||
|
diskUsage: parseFloat(dbRouter.disk_usage),
|
||||||
|
freeDisk: parseInt(dbRouter.free_disk),
|
||||||
|
totalDisk: parseInt(dbRouter.total_disk),
|
||||||
|
routerActivity: {
|
||||||
|
studies
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: dbRouter.vpn_status_code,
|
||||||
|
appStatus: dbRouter.disk_status_code,
|
||||||
|
vms
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error transforming router ${dbRouter.id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<RouterData[]> {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query<RowDataPacket[]>(
|
||||||
|
'SELECT * FROM routers ORDER BY created_at DESC'
|
||||||
|
);
|
||||||
|
|
||||||
|
const routers = await Promise.all(
|
||||||
|
rows.map((row, index) => this.transformDatabaseRouter(row, index))
|
||||||
|
);
|
||||||
|
|
||||||
|
return routers;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in RouterRepository.findAll:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: number): Promise<RouterData | null> {
|
||||||
|
const [rows] = await pool.query<RowDataPacket[]>(
|
||||||
|
'SELECT * FROM routers WHERE id = ?',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rows.length) return null;
|
||||||
|
return this.transformDatabaseRouter(rows[0], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByFacility(facility: string): Promise<RouterData[]> {
|
||||||
|
const [rows] = await pool.query<RowDataPacket[]>(
|
||||||
|
'SELECT * FROM routers WHERE facility = ? ORDER BY created_at DESC',
|
||||||
|
[facility]
|
||||||
|
);
|
||||||
|
|
||||||
|
const routers = await Promise.all(
|
||||||
|
rows.map((row, index) => this.transformDatabaseRouter(row, index))
|
||||||
|
);
|
||||||
|
|
||||||
|
return routers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(router: Partial<RouterData>): Promise<RouterData> {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
`INSERT INTO routers (
|
||||||
|
router_id, facility, router_alias, last_seen,
|
||||||
|
vpn_status_code, disk_status_code, license_status,
|
||||||
|
free_disk, total_disk, disk_usage
|
||||||
|
) VALUES (?, ?, ?, NOW(), ?, ?, 'inactive', ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
router.routerId,
|
||||||
|
router.facility,
|
||||||
|
router.routerAlias,
|
||||||
|
'unknown',
|
||||||
|
router.diskStatus || 'unknown',
|
||||||
|
router.freeDisk,
|
||||||
|
router.totalDisk,
|
||||||
|
router.diskUsage || 0
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.findById(result.insertId) as Promise<RouterData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, router: Partial<RouterData>): Promise<RouterData | null> {
|
||||||
|
const updates: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (router.routerId) updates.router_id = router.routerId;
|
||||||
|
if (router.facility) updates.facility = router.facility;
|
||||||
|
if (router.routerAlias) updates.router_alias = router.routerAlias;
|
||||||
|
if (router.diskStatus) updates.disk_status_code = router.diskStatus;
|
||||||
|
|
||||||
|
if (router.freeDisk !== undefined || router.totalDisk !== undefined) {
|
||||||
|
const existingRouter = await this.findById(id);
|
||||||
|
if (!existingRouter) return null;
|
||||||
|
|
||||||
|
updates.free_disk = router.freeDisk ?? existingRouter.freeDisk;
|
||||||
|
updates.total_disk = router.totalDisk ?? existingRouter.totalDisk;
|
||||||
|
updates.disk_usage = ((updates.total_disk - updates.free_disk) / updates.total_disk) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
const setClauses = Object.keys(updates)
|
||||||
|
.map(key => `${key} = ?`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE routers SET ${setClauses}, updated_at = NOW() WHERE id = ?`,
|
||||||
|
[...Object.values(updates), id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<boolean> {
|
||||||
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
|
'DELETE FROM routers WHERE id = ?',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return result.affectedRows > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
backend/src/repositories/index.ts
Normal file
3
backend/src/repositories/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './RouterRepository';
|
||||||
|
export * from './DicomStudyRepository';
|
||||||
|
// Add more repository exports as needed
|
||||||
36
backend/src/routes/dicom.routes.ts
Normal file
36
backend/src/routes/dicom.routes.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// src/routes/dicom.routes.ts
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import { DicomStudyController } from '../controllers/DicomStudyController';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const dicomStudyController = new DicomStudyController();
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
logger.info('Initializing DICOM routes');
|
||||||
|
|
||||||
|
// Add debug middleware for DICOM routes
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
logger.debug(`DICOM route hit: ${req.method} ${req.originalUrl}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test route to verify DICOM routing is working
|
||||||
|
router.get('/test', (req, res) => {
|
||||||
|
res.json({ message: 'DICOM routes are working' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', (req, res, next) => {
|
||||||
|
logger.debug('POST / route hit with body:', req.body);
|
||||||
|
dicomStudyController.createStudy(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/', dicomStudyController.getAllStudies);
|
||||||
|
router.get('/search', dicomStudyController.searchStudies);
|
||||||
|
router.get('/router/:routerId', dicomStudyController.getStudiesByRouterId);
|
||||||
|
router.get('/:id', dicomStudyController.getStudyById);
|
||||||
|
router.put('/:id', dicomStudyController.updateStudy);
|
||||||
|
router.delete('/:id', dicomStudyController.deleteStudy);
|
||||||
|
|
||||||
|
export default router;
|
||||||
23
backend/src/routes/index.ts
Normal file
23
backend/src/routes/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// src/routes/index.ts
|
||||||
|
import { Router } from 'express';
|
||||||
|
import routerRoutes from './router.routes';
|
||||||
|
import dicomRoutes from './dicom.routes';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Add debug logging
|
||||||
|
logger.info('Registering routes:');
|
||||||
|
logger.info('- /routers -> router routes');
|
||||||
|
logger.info('- /studies -> dicom routes');
|
||||||
|
|
||||||
|
router.use('/routers', routerRoutes);
|
||||||
|
router.use('/studies', dicomRoutes);
|
||||||
|
|
||||||
|
// Debug middleware to log incoming requests
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
logger.debug(`Route hit: ${req.method} ${req.originalUrl}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
34
backend/src/routes/router.routes.ts
Normal file
34
backend/src/routes/router.routes.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// src/routes/router.routes.ts
|
||||||
|
import { Router } from 'express';
|
||||||
|
import pool from '../config/db'; // If using default export
|
||||||
|
import { RouterController } from '../controllers/RouterController';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const controller = new RouterController(pool);
|
||||||
|
|
||||||
|
router.put('/vms', async (req, res, next) => {
|
||||||
|
console.log('Route handler: /vms endpoint hit');
|
||||||
|
console.log('Query params:', req.query);
|
||||||
|
console.log('Request body:', req.body);
|
||||||
|
try {
|
||||||
|
await controller.updateRouterVMs(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Route error:', err);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/', controller.getAllRouters);
|
||||||
|
router.get('/:id', controller.getRouterById);
|
||||||
|
router.post('/', controller.createRouter);
|
||||||
|
router.put('/:id', controller.updateRouter);
|
||||||
|
router.delete('/:id', controller.deleteRouter);
|
||||||
|
router.get('/facility/:facility', controller.getRoutersByFacility);
|
||||||
|
|
||||||
|
// Add this new route
|
||||||
|
router.put('/vms', controller.updateRouterVMs); // New route for VM updates
|
||||||
|
|
||||||
|
export default router;
|
||||||
154
backend/src/services/DicomStudyService.ts
Normal file
154
backend/src/services/DicomStudyService.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DicomStudySearchParams } from '../types/dicom';
|
||||||
|
import { DicomStudyRepository } from '../repositories/DicomStudyRepository';
|
||||||
|
import pool from '../config/db';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export class DicomStudyService {
|
||||||
|
private repository: DicomStudyRepository;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.repository = new DicomStudyRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isValidStatusCode(statusCode: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT 1 FROM status_type WHERE code = ? AND category_id = (SELECT id FROM status_category WHERE name = ?)',
|
||||||
|
[statusCode, 'DICOM_STUDY']
|
||||||
|
);
|
||||||
|
return (rows as any[]).length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error checking status code:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createStudy(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
|
||||||
|
// Validate required fields
|
||||||
|
const requiredFields = [
|
||||||
|
'router_id',
|
||||||
|
'study_instance_uid',
|
||||||
|
'patient_id',
|
||||||
|
'patient_name',
|
||||||
|
'accession_number',
|
||||||
|
'study_date',
|
||||||
|
'modality',
|
||||||
|
'series_instance_uid',
|
||||||
|
'study_status_code',
|
||||||
|
'association_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
if (!studyData[field as keyof CreateDicomStudyDTO]) {
|
||||||
|
throw new Error(`Missing required field: ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status code
|
||||||
|
const isValidStatus = await this.isValidStatusCode(studyData.study_status_code);
|
||||||
|
if (!isValidStatus) {
|
||||||
|
throw new Error(`Invalid study status code: ${studyData.study_status_code}. Must be one of: NEW, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, ON_HOLD`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format
|
||||||
|
if (!this.isValidDate(studyData.study_date)) {
|
||||||
|
throw new Error('Invalid study date format. Use YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Creating new study', { studyData });
|
||||||
|
return await this.repository.create(studyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidDate(dateString: string): boolean {
|
||||||
|
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
if (!regex.test(dateString)) return false;
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date instanceof Date && !isNaN(date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllStudies(): Promise<DicomStudy[]> {
|
||||||
|
try {
|
||||||
|
logger.info('Fetching all studies');
|
||||||
|
return await this.repository.findAll();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in getAllStudies:', error);
|
||||||
|
throw new Error('Failed to fetch studies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStudyById(id: number): Promise<DicomStudy | null> {
|
||||||
|
try {
|
||||||
|
logger.info(`Fetching study by id: ${id}`);
|
||||||
|
const study = await this.repository.findById(id);
|
||||||
|
|
||||||
|
if (!study) {
|
||||||
|
logger.warn(`Study not found with id: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return study;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error in getStudyById: ${id}`, error);
|
||||||
|
throw new Error('Failed to fetch study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStudiesByRouterId(routerId: string): Promise<DicomStudy[]> {
|
||||||
|
try {
|
||||||
|
logger.info(`Fetching studies for router: ${routerId}`);
|
||||||
|
return await this.repository.findByRouterId(routerId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error in getStudiesByRouterId: ${routerId}`, error);
|
||||||
|
throw new Error('Failed to fetch studies for router');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStudy(id: number, studyData: UpdateDicomStudyDTO): Promise<DicomStudy | null> {
|
||||||
|
try {
|
||||||
|
const existingStudy = await this.repository.findById(id);
|
||||||
|
if (!existingStudy) {
|
||||||
|
logger.warn(`Study not found for update: ${id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Updating study: ${id}`, { studyData });
|
||||||
|
return await this.repository.update(id, studyData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error in updateStudy: ${id}`, error);
|
||||||
|
throw new Error('Failed to update study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStudy(id: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const existingStudy = await this.repository.findById(id);
|
||||||
|
if (!existingStudy) {
|
||||||
|
logger.warn(`Study not found for deletion: ${id}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Deleting study: ${id}`);
|
||||||
|
return await this.repository.delete(id);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error in deleteStudy: ${id}`, error);
|
||||||
|
throw new Error('Failed to delete study');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchStudies(params: DicomStudySearchParams): Promise<DicomStudy[]> {
|
||||||
|
try {
|
||||||
|
if (params.startDate && !this.isValidDate(params.startDate)) {
|
||||||
|
throw new Error('Invalid start date format. Use YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
if (params.endDate && !this.isValidDate(params.endDate)) {
|
||||||
|
throw new Error('Invalid end date format. Use YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Searching studies with params:', params);
|
||||||
|
return await this.repository.search(params);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in searchStudies:', error);
|
||||||
|
throw new Error('Failed to search studies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
backend/src/services/RouterService.ts
Normal file
49
backend/src/services/RouterService.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// src/services/RouterService.ts
|
||||||
|
import { RouterRepository } from '../repositories/RouterRepository';
|
||||||
|
import { RouterData,VMUpdate} from '../types';
|
||||||
|
import { Pool } from 'mysql2/promise';
|
||||||
|
|
||||||
|
|
||||||
|
export class RouterService {
|
||||||
|
private repository: RouterRepository;
|
||||||
|
|
||||||
|
constructor(pool: Pool) {
|
||||||
|
this.repository = new RouterRepository(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
||||||
|
console.log('Service: Updating VMs for router:', routerId);
|
||||||
|
try {
|
||||||
|
return await this.repository.updateVMs(routerId, vms);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Service error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllRouters(): Promise<RouterData[]> {
|
||||||
|
return this.repository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRouterById(id: number): Promise<RouterData | null> {
|
||||||
|
return this.repository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
||||||
|
return this.repository.findByFacility(facility);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRouter(router: Partial<RouterData>): Promise<RouterData> {
|
||||||
|
return this.repository.create(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRouter(id: number, router: Partial<RouterData>): Promise<RouterData | null> {
|
||||||
|
return this.repository.update(id, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRouter(id: number): Promise<boolean> {
|
||||||
|
return this.repository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
backend/src/services/index.ts
Normal file
3
backend/src/services/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './RouterService';
|
||||||
|
export * from './DicomStudyService';
|
||||||
|
// Add more service exports as needed
|
||||||
74
backend/src/types/dicom.ts
Normal file
74
backend/src/types/dicom.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
export interface DicomStudy {
|
||||||
|
id: number;
|
||||||
|
router_id: string; // External interface - always string
|
||||||
|
study_instance_uid: string;
|
||||||
|
patient_id: string;
|
||||||
|
patient_name: string;
|
||||||
|
accession_number: string;
|
||||||
|
study_date: string;
|
||||||
|
modality: string;
|
||||||
|
study_description?: string;
|
||||||
|
series_instance_uid: string;
|
||||||
|
procedure_code?: string;
|
||||||
|
referring_physician_name?: string;
|
||||||
|
study_status_code: string;
|
||||||
|
association_id: string;
|
||||||
|
created_at?: Date;
|
||||||
|
updated_at?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDicomStudyDTO {
|
||||||
|
router_id: string; // External interface - always string
|
||||||
|
study_instance_uid: string;
|
||||||
|
patient_id: string;
|
||||||
|
patient_name: string;
|
||||||
|
accession_number: string;
|
||||||
|
study_date: string;
|
||||||
|
modality: string;
|
||||||
|
study_description?: string;
|
||||||
|
series_instance_uid: string;
|
||||||
|
procedure_code?: string;
|
||||||
|
referring_physician_name?: string;
|
||||||
|
study_status_code: string;
|
||||||
|
association_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBDicomStudy {
|
||||||
|
id: number;
|
||||||
|
router_id: number; // Database uses numeric ID
|
||||||
|
study_instance_uid: string;
|
||||||
|
patient_id: string;
|
||||||
|
patient_name: string;
|
||||||
|
accession_number: string;
|
||||||
|
study_date: string;
|
||||||
|
modality: string;
|
||||||
|
study_description?: string;
|
||||||
|
series_instance_uid: string;
|
||||||
|
procedure_code?: string;
|
||||||
|
referring_physician_name?: string;
|
||||||
|
study_status_code: string;
|
||||||
|
association_id: string;
|
||||||
|
created_at?: Date;
|
||||||
|
updated_at?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDicomStudyDTO {
|
||||||
|
study_instance_uid?: string;
|
||||||
|
patient_id?: string;
|
||||||
|
patient_name?: string;
|
||||||
|
accession_number?: string;
|
||||||
|
study_date?: string;
|
||||||
|
modality?: string;
|
||||||
|
study_description?: string;
|
||||||
|
series_instance_uid?: string;
|
||||||
|
procedure_code?: string;
|
||||||
|
referring_physician_name?: string;
|
||||||
|
study_status_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DicomStudySearchParams {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
modality?: string;
|
||||||
|
patientName?: string;
|
||||||
|
}
|
||||||
46
backend/src/types/index.ts
Normal file
46
backend/src/types/index.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// src/types/index.ts
|
||||||
|
export interface RouterData {
|
||||||
|
id: number; // maps to backend 'id'
|
||||||
|
slNo: number; // maps to backend 'slNo'
|
||||||
|
routerId: string; // maps to backend 'router_id'
|
||||||
|
facility: string; // maps to backend 'facility'
|
||||||
|
routerAlias: string; // maps to backend 'router_alias'
|
||||||
|
lastSeen: string; // maps to backend 'last_seen'
|
||||||
|
diskStatus: string; // maps to backend 'disk_status_code'
|
||||||
|
diskUsage: number; // maps to backend 'disk_usage'
|
||||||
|
freeDisk: number; // maps to backend 'free_disk'
|
||||||
|
totalDisk: number; // maps to backend 'total_disk'
|
||||||
|
routerActivity: {
|
||||||
|
studies: Study[];
|
||||||
|
};
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: string; // maps to backend 'vpn_status_code'
|
||||||
|
appStatus: string; // maps to backend 'disk_status_code'
|
||||||
|
vms: VM[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Study {
|
||||||
|
siuid: string;
|
||||||
|
patientId: string;
|
||||||
|
accessionNumber: string;
|
||||||
|
patientName: string;
|
||||||
|
studyDate: string;
|
||||||
|
modality: string;
|
||||||
|
studyDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VM {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add these new interfaces
|
||||||
|
export interface VMUpdate {
|
||||||
|
vm_number: number;
|
||||||
|
status_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VMUpdateRequest {
|
||||||
|
vms: VMUpdate[];
|
||||||
|
}
|
||||||
2
backend/src/utils/index.ts
Normal file
2
backend/src/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './logger';
|
||||||
|
// Add more utility exports as needed
|
||||||
27
backend/src/utils/logger.ts
Normal file
27
backend/src/utils/logger.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config/config';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: config.logger.level || 'info',
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.json()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.simple()
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: 'error.log',
|
||||||
|
level: 'error'
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: 'combined.log'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default logger;
|
||||||
26
backend/tsconfig.json
Normal file
26
backend/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2018",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es2018", "esnext.asynciterable"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["*"]
|
||||||
|
},
|
||||||
|
"allowJs": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
60
docker-compose.yml
Normal file
60
docker-compose.yml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./router-dashboard
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=http://localhost:3001/api/v1
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
volumes:
|
||||||
|
- ./router-dashboard:/app
|
||||||
|
- /app/node_modules
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./ve-router-backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- DB_HOST=host.docker.internal
|
||||||
|
- DB_PORT=3307
|
||||||
|
- DB_USER=ve_router_user
|
||||||
|
- DB_PASSWORD=ve_router_password
|
||||||
|
- DB_NAME=ve_router_db
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
volumes:
|
||||||
|
- ./ve-router-backend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: rootpassword
|
||||||
|
MYSQL_DATABASE: ve_router_db
|
||||||
|
MYSQL_USER: ve_router_user
|
||||||
|
MYSQL_PASSWORD: ve_router_password
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
# Correct paths for init scripts
|
||||||
|
- ./router-dashboard/sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql
|
||||||
|
- ./router-dashboard/sql/seed_data.sql:/docker-entrypoint-initdb.d/02-seed_data.sql
|
||||||
|
ports:
|
||||||
|
- "3307:3306"
|
||||||
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ve_router_user", "-pve_router_password"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
34
dockerfile
Normal file
34
dockerfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Dockerfile
|
||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine as build
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built assets from build stage
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/package*.json ./
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
2
frontend/.env
Normal file
2
frontend/.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=http://localhost:3001/api/v1
|
||||||
|
VITE_NODE_ENV=development
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
frontend/README.md
Normal file
50
frontend/README.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 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/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
15
frontend/dockerfile
Normal file
15
frontend/dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV VITE_API_URL=http://localhost:3001/api/v1
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host"]
|
||||||
28
frontend/eslint.config.js
Normal file
28
frontend/eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4238
frontend/package-lock.json
generated
Normal file
4238
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
frontend/package.json
Normal file
34
frontend/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "router-dashboard",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"router-dashboard": "file:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
|
"@types/react": "^18.2.37",
|
||||||
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^8.53.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.4",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.3.5",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
198
frontend/settings-json/router-client-config.json
Normal file
198
frontend/settings-json/router-client-config.json
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
{
|
||||||
|
"dicom": {
|
||||||
|
"local": {
|
||||||
|
"aet": "ABEL_3",
|
||||||
|
"port": 104,
|
||||||
|
"file_directory": "/dicom_images",
|
||||||
|
"wait_time": 2,
|
||||||
|
"receiver_wait_time": 5000
|
||||||
|
},
|
||||||
|
"association": {
|
||||||
|
"acse_timeout": 5,
|
||||||
|
"dimse_timeout": 1000,
|
||||||
|
"network_timeout": 1000,
|
||||||
|
"retry": {
|
||||||
|
"attempts": 3,
|
||||||
|
"interval": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query_retrieve": {
|
||||||
|
"cfind_level": "STUDY",
|
||||||
|
"cmove_level": "STUDY"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rabbitmq": {
|
||||||
|
"local": {
|
||||||
|
"hostname": "router-rabbitmq",
|
||||||
|
"port": 5672,
|
||||||
|
"credentials": {
|
||||||
|
"username": "vitalengine",
|
||||||
|
"password": "vitalengine"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"durable": true,
|
||||||
|
"auto_delete": false,
|
||||||
|
"exchange_type": "direct",
|
||||||
|
"heartbeat": 50
|
||||||
|
},
|
||||||
|
"exchanges": {
|
||||||
|
"image_uploader": {
|
||||||
|
"name": "dicom.image.uploader",
|
||||||
|
"queue": "dicom.image.uploader.queue",
|
||||||
|
"routing_key": "image_uploader_routing_key"
|
||||||
|
},
|
||||||
|
"image_sender": {
|
||||||
|
"name": "dicom.image.sender",
|
||||||
|
"queue": "dicom.image.sender.queue"
|
||||||
|
},
|
||||||
|
"rds_status": {
|
||||||
|
"name": "dicom.image.status.rds",
|
||||||
|
"queue": "dicom.image.status.rds.queue",
|
||||||
|
"routing_key": "rds_status_routing_key"
|
||||||
|
},
|
||||||
|
"presigned_s3": {
|
||||||
|
"name": "presigned.s3.client",
|
||||||
|
"queue": "presigned.s3.client.queue",
|
||||||
|
"routing_key": "presigned_s3_client_routing_key"
|
||||||
|
},
|
||||||
|
"query_retrieve": {
|
||||||
|
"request": {
|
||||||
|
"name": "dicom.qr.request",
|
||||||
|
"queue": "dicom.qr.request.queue"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"name": "dicom.qr.response",
|
||||||
|
"queue": "dicom.qr.response.queue",
|
||||||
|
"routing_key": "dicom.qr.response.routingkey"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"hostname": "10.8.0.1",
|
||||||
|
"port": 5672,
|
||||||
|
"credentials": {
|
||||||
|
"username": "vitalengine",
|
||||||
|
"password": "vitalengine"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"durable": true,
|
||||||
|
"auto_delete": false,
|
||||||
|
"exchange_type": "direct",
|
||||||
|
"heartbeat": 50,
|
||||||
|
"retry": {
|
||||||
|
"attempts": 3,
|
||||||
|
"interval": 10
|
||||||
|
},
|
||||||
|
"consumer_threads": 5
|
||||||
|
},
|
||||||
|
"exchanges": {
|
||||||
|
"dicom_incoming": {
|
||||||
|
"name": "dicom.incoming.file",
|
||||||
|
"queue": "dicom.incoming.file.queue",
|
||||||
|
"routing_key": "dicom.incoming.file.routingkey"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"vpn": {
|
||||||
|
"ip": "10.8.0.254"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aws": {
|
||||||
|
"presigned_url_endpoint": "https://qa-develop.dev.vitalengine.com/dicom-cstore/rest/aws-pre-signed-url?aet="
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"scp_connections": {
|
||||||
|
"pacs_nodes": [
|
||||||
|
{
|
||||||
|
"node_id": "PACS_01",
|
||||||
|
"active": true,
|
||||||
|
"aet_config": {
|
||||||
|
"local_aet": "ABEL_3",
|
||||||
|
"remote_aet": "REMOTE_PACS",
|
||||||
|
"remote_host": "192.168.1.100",
|
||||||
|
"remote_port": 104
|
||||||
|
},
|
||||||
|
"connection_settings": {
|
||||||
|
"max_pdu_length": 16384,
|
||||||
|
"timeout": 30,
|
||||||
|
"max_associations": 5,
|
||||||
|
"implicit_vr_big_endian": false,
|
||||||
|
"implicit_vr_little_endian": true,
|
||||||
|
"explicit_vr_little_endian": true
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"storage_commitment": true,
|
||||||
|
"query_retrieve": true,
|
||||||
|
"verification": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"peer_connections": {
|
||||||
|
"peer_nodes": [
|
||||||
|
{
|
||||||
|
"node_id": "PEER_01",
|
||||||
|
"active": true,
|
||||||
|
"aet_config": {
|
||||||
|
"local_aet": "ABEL_3",
|
||||||
|
"remote_aet": "PEER_ROUTER_1",
|
||||||
|
"remote_host": "10.8.0.15",
|
||||||
|
"remote_port": 104
|
||||||
|
},
|
||||||
|
"connection_settings": {
|
||||||
|
"max_pdu_length": 16384,
|
||||||
|
"timeout": 30,
|
||||||
|
"max_associations": 2
|
||||||
|
},
|
||||||
|
"transfer_settings": {
|
||||||
|
"auto_forward": true,
|
||||||
|
"compression": "none",
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_id": "PEER_02",
|
||||||
|
"active": false,
|
||||||
|
"aet_config": {
|
||||||
|
"local_aet": "ABEL_3",
|
||||||
|
"remote_aet": "PEER_ROUTER_2",
|
||||||
|
"remote_host": "10.8.0.16",
|
||||||
|
"remote_port": 104
|
||||||
|
},
|
||||||
|
"connection_settings": {
|
||||||
|
"max_pdu_length": 16384,
|
||||||
|
"timeout": 30,
|
||||||
|
"max_associations": 2
|
||||||
|
},
|
||||||
|
"transfer_settings": {
|
||||||
|
"auto_forward": false,
|
||||||
|
"compression": "none",
|
||||||
|
"priority": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"global_peer_settings": {
|
||||||
|
"max_total_associations": 10,
|
||||||
|
"retry_interval": 300,
|
||||||
|
"max_retry_attempts": 3,
|
||||||
|
"keep_alive_interval": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network_settings": {
|
||||||
|
"vpn": {
|
||||||
|
"enabled": true,
|
||||||
|
"network": "10.8.0.0/24",
|
||||||
|
"gateway": "10.8.0.1"
|
||||||
|
},
|
||||||
|
"allowed_subnets": [
|
||||||
|
"192.168.1.0/24",
|
||||||
|
"10.8.0.0/24"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
135
frontend/settings-json/router-server-config.json
Normal file
135
frontend/settings-json/router-server-config.json
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"aws": {
|
||||||
|
"environment": {
|
||||||
|
"production": false,
|
||||||
|
"regions": {
|
||||||
|
"prod": "us-west-2",
|
||||||
|
"nonprod": "us-east-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sqs": {
|
||||||
|
"queue_settings": {
|
||||||
|
"max_messages": 10,
|
||||||
|
"visibility_timeout": 5,
|
||||||
|
"wait_time_seconds": 1
|
||||||
|
},
|
||||||
|
"queues": {
|
||||||
|
"dicom_router": {
|
||||||
|
"prod": {
|
||||||
|
"name": "prod-ve-dcmsend-dicom-router-sqs",
|
||||||
|
"url": "https://queue.amazonaws.com/677720207704/prod-ve-dcmsend-dicom-router-sqs"
|
||||||
|
},
|
||||||
|
"nonprod": {
|
||||||
|
"name": "dev-dcmsend-dicom-router-queue",
|
||||||
|
"url": "https://sqs.us-east-1.amazonaws.com/63377856061"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"client_settings": {
|
||||||
|
"prod": {
|
||||||
|
"name": "router-client-settings-queue",
|
||||||
|
"url": "https://queue.amazonaws.com/677720207704/router-client-settings-queue"
|
||||||
|
},
|
||||||
|
"nonprod": {
|
||||||
|
"name": "dev-router-client-settings-queue",
|
||||||
|
"url": "https://sqs.us-east-1.amazonaws.com/633778560614/dev-router-client-settings-queue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incoming_file_list": {
|
||||||
|
"prod": {
|
||||||
|
"url": "https://sqs.us-west-2.amazonaws.com/67"
|
||||||
|
},
|
||||||
|
"nonprod": {
|
||||||
|
"url": "https://sqs.us-east-1.amazonaws.com/6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s3_upload_process": {
|
||||||
|
"prod": {
|
||||||
|
"url": "https://sqs.us-west-2.amazonaws.com/67"
|
||||||
|
},
|
||||||
|
"nonprod": {
|
||||||
|
"url": "https://sqs.us-east-1.amazonaws.com/633778560614/dicom-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s3": {
|
||||||
|
"credentials": {
|
||||||
|
"temporary_duration": 21600
|
||||||
|
},
|
||||||
|
"buckets": {
|
||||||
|
"dicom_router": {
|
||||||
|
"prod": {
|
||||||
|
"name": "prod-ve-dcmsend-dicom-router-metadata",
|
||||||
|
"role_arn": "arn:aws:iam::677720207704:role/s3uploaderrole"
|
||||||
|
},
|
||||||
|
"nonprod": {
|
||||||
|
"name": "dev-dicom-router-metadata",
|
||||||
|
"role_arn": "arn:aws:iam::633778560614:role/s3uploaderrole"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"client_settings": {
|
||||||
|
"prod": {
|
||||||
|
"name": "router-client-settings"
|
||||||
|
},
|
||||||
|
"nonprod": {
|
||||||
|
"name": "dev-router-client-settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edge_endpoints": {
|
||||||
|
"prod": "https://app.fffs.com",
|
||||||
|
"nonprod": "https://qa-develop.dev.VA.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rabbitmq": {
|
||||||
|
"connection": {
|
||||||
|
"hostname": "localhost",
|
||||||
|
"port": 5672,
|
||||||
|
"credentials": {
|
||||||
|
"username": "va",
|
||||||
|
"password": "aa"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"durable": true,
|
||||||
|
"auto_delete": false,
|
||||||
|
"exchange_type": "direct",
|
||||||
|
"heartbeat": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exchanges": {
|
||||||
|
"image_sender": {
|
||||||
|
"name": "dicom.image.sender",
|
||||||
|
"queue": "dicom.image.sender.queue"
|
||||||
|
},
|
||||||
|
"rds_status": {
|
||||||
|
"name": "dicom.image.status.rds",
|
||||||
|
"queue": "dicom.image.status.rds.queue",
|
||||||
|
"routing_key": "rds_status_routing_key"
|
||||||
|
},
|
||||||
|
"presigned_s3": {
|
||||||
|
"name": "presigned.s3.client",
|
||||||
|
"queue": "presigned.s3.client.queue",
|
||||||
|
"routing_key": "presigned_s3_client_routing_key"
|
||||||
|
},
|
||||||
|
"client_settings": {
|
||||||
|
"name": "client.settings.response",
|
||||||
|
"queue": "client.settings.response.queue",
|
||||||
|
"routing_key": "client_settings_response_routing_key"
|
||||||
|
},
|
||||||
|
"dicom_incoming": {
|
||||||
|
"name": "dicom.incoming.file",
|
||||||
|
"queue": "dicom.incoming.file.queue",
|
||||||
|
"routing_key": "dicom.incoming.file.routingkey"
|
||||||
|
},
|
||||||
|
"s3_upload": {
|
||||||
|
"name": "s3.upload.sqs.publish",
|
||||||
|
"queue": "s3.upload.sqs.publish.queue",
|
||||||
|
"routing_key": "s3.upload.sqs.publish.routingkey"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"base_url": "https://router-server.dev-api.fd.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/App.css
Normal file
10
frontend/src/App.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DashboardLayout from './components/dashboard/DashboardLayout';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<DashboardLayout />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
7
frontend/src/App.tsx
Normal file
7
frontend/src/App.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import DashboardLayout from './components/dashboard/DashboardLayout';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <DashboardLayout />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
frontend/src/assets/images/ve-logo-tm.svg
Normal file
1
frontend/src/assets/images/ve-logo-tm.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
117
frontend/src/components/dashboard/Dashboard.tsx
Normal file
117
frontend/src/components/dashboard/Dashboard.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// src/components/dashboard/Dashboard.tsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import RouterTable from './RouterTable';
|
||||||
|
import { RouterData } from '../../types';
|
||||||
|
import { apiService } from '../../services/api.service';
|
||||||
|
|
||||||
|
interface DashboardSummary {
|
||||||
|
totalRouters: number;
|
||||||
|
activeRouters: number;
|
||||||
|
criticalRouters: number;
|
||||||
|
diskWarnings: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const [routers, setRouters] = useState<RouterData[]>([]);
|
||||||
|
const [summary, setSummary] = useState<DashboardSummary>({
|
||||||
|
totalRouters: 0,
|
||||||
|
activeRouters: 0,
|
||||||
|
criticalRouters: 0,
|
||||||
|
diskWarnings: 0
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRouters = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await apiService.getAllRouters();
|
||||||
|
setRouters(data);
|
||||||
|
|
||||||
|
// Calculate summary
|
||||||
|
const summaryData = {
|
||||||
|
totalRouters: data.length,
|
||||||
|
activeRouters: data.filter(router =>
|
||||||
|
router.systemStatus.vpnStatus === 'VPN_CONNECTED'
|
||||||
|
).length,
|
||||||
|
criticalRouters: data.filter(router =>
|
||||||
|
router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
|
||||||
|
router.diskStatus === 'DISK_CRITICAL'
|
||||||
|
).length,
|
||||||
|
diskWarnings: data.filter(router =>
|
||||||
|
router.diskUsage >= 80
|
||||||
|
).length
|
||||||
|
};
|
||||||
|
|
||||||
|
setSummary(summaryData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching router data:', error);
|
||||||
|
setError('Failed to load router data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchRouters();
|
||||||
|
|
||||||
|
// Optional: Set up polling for real-time updates
|
||||||
|
const interval = setInterval(fetchRouters, 30000); // Update every 30 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Total Routers</h3>
|
||||||
|
<p className="text-2xl font-semibold mt-1">{summary.totalRouters}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Active Routers</h3>
|
||||||
|
<p className="text-2xl font-semibold text-green-600 mt-1">{summary.activeRouters}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Critical Status</h3>
|
||||||
|
<p className="text-2xl font-semibold text-red-600 mt-1">{summary.criticalRouters}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Disk Usage Alert</h3>
|
||||||
|
<p className="text-2xl font-semibold text-yellow-600 mt-1">{summary.diskWarnings}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Router Table */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-4 border-b border-gray-200">
|
||||||
|
<h2 className="text-lg font-semibold">Router Status Overview</h2>
|
||||||
|
</div>
|
||||||
|
<RouterTable routers={routers} onRefresh={() => {}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
229
frontend/src/components/dashboard/DashboardLayout.tsx
Normal file
229
frontend/src/components/dashboard/DashboardLayout.tsx
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
// src/components/dashboard/DashboardLayout.tsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Navbar from './Navbar';
|
||||||
|
import Header from './Header';
|
||||||
|
import RouterTable from './RouterTable';
|
||||||
|
import RouterManagement from './pages/RouterManagement';
|
||||||
|
import { RouterData } from '../../types';
|
||||||
|
import { apiService } from '../../services/api.service';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
||||||
|
|
||||||
|
const DashboardLayout: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('dashboard');
|
||||||
|
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
||||||
|
const [routers, setRouters] = useState<RouterData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [user] = useState<User>({
|
||||||
|
name: 'John Doe',
|
||||||
|
role: 'Administrator'
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRouters = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
let data = await apiService.getAllRouters();
|
||||||
|
|
||||||
|
// Apply filter based on activeFilter
|
||||||
|
if (activeFilter !== 'all') {
|
||||||
|
data = data.filter(router => {
|
||||||
|
switch (activeFilter) {
|
||||||
|
case 'active':
|
||||||
|
return router.routerActivity?.studies &&
|
||||||
|
router.routerActivity.studies.length > 0;
|
||||||
|
case 'critical':
|
||||||
|
return router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
|
||||||
|
router.systemStatus.appStatus === 'DISK_CRITICAL' ||
|
||||||
|
router.diskStatus === 'DISK_CRITICAL';
|
||||||
|
case 'diskAlert':
|
||||||
|
return router.diskUsage > 80;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setRouters(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching routers:', err);
|
||||||
|
setError('Failed to fetch router data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchRouters();
|
||||||
|
const interval = setInterval(fetchRouters, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [activeFilter]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
console.log('Logging out...');
|
||||||
|
// Add your logout logic here
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSummary = (routerData: RouterData[]) => {
|
||||||
|
return {
|
||||||
|
total: routerData.length,
|
||||||
|
active: routerData.filter(r =>
|
||||||
|
r.routerActivity?.studies &&
|
||||||
|
r.routerActivity.studies.length > 0
|
||||||
|
).length,
|
||||||
|
critical: routerData.filter(r =>
|
||||||
|
r.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
|
||||||
|
r.systemStatus.appStatus === 'DISK_CRITICAL' ||
|
||||||
|
r.diskStatus === 'DISK_CRITICAL'
|
||||||
|
).length,
|
||||||
|
diskAlert: routerData.filter(r => r.diskUsage > 80).length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'dashboard': {
|
||||||
|
const summary = getSummary(routers);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
className={`bg-white p-4 rounded-lg shadow ${
|
||||||
|
activeFilter === 'all' ? 'ring-2 ring-blue-500' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveFilter('all')}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Total Routers</h3>
|
||||||
|
<p className="text-2xl font-semibold mt-1">{summary.total}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`bg-white p-4 rounded-lg shadow cursor-pointer hover:bg-gray-50 ${
|
||||||
|
activeFilter === 'active' ? 'ring-2 ring-blue-500' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveFilter('active')}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Active Routers</h3>
|
||||||
|
<p className="text-2xl font-semibold text-green-600 mt-1">{summary.active}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`bg-white p-4 rounded-lg shadow cursor-pointer hover:bg-gray-50 ${
|
||||||
|
activeFilter === 'critical' ? 'ring-2 ring-blue-500' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveFilter('critical')}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Critical Status</h3>
|
||||||
|
<p className="text-2xl font-semibold text-red-600 mt-1">{summary.critical}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`bg-white p-4 rounded-lg shadow cursor-pointer hover:bg-gray-50 ${
|
||||||
|
activeFilter === 'diskAlert' ? 'ring-2 ring-blue-500' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveFilter('diskAlert')}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Disk Usage Alert</h3>
|
||||||
|
<p className="text-2xl font-semibold text-yellow-600 mt-1">
|
||||||
|
{summary.diskAlert}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Label */}
|
||||||
|
{activeFilter !== 'all' && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
|
<span>Showing:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{activeFilter === 'active' && 'Active Routers (with Studies)'}
|
||||||
|
{activeFilter === 'critical' && 'Routers with Critical Status'}
|
||||||
|
{activeFilter === 'diskAlert' && 'Routers with High Disk Usage'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveFilter('all')}
|
||||||
|
className="text-blue-500 hover:text-blue-700 ml-2"
|
||||||
|
>
|
||||||
|
Clear Filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Router Table */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<RouterTable
|
||||||
|
routers={routers} // Make sure routers is initialized as an empty array if undefined
|
||||||
|
filter={activeFilter}
|
||||||
|
onFilterChange={setActiveFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'routers':
|
||||||
|
return <RouterManagement />;
|
||||||
|
|
||||||
|
case 'users':
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">User Management</h2>
|
||||||
|
<p>User management content goes here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'settings':
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Settings</h2>
|
||||||
|
<p>Settings content goes here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100">
|
||||||
|
<Header user={user} onLogout={handleLogout} />
|
||||||
|
<div className="flex h-[calc(100vh-4rem)]">
|
||||||
|
<Navbar activeTab={activeTab} onTabChange={setActiveTab} />
|
||||||
|
<main className="flex-1 overflow-auto">
|
||||||
|
{renderContent()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardLayout;
|
||||||
49
frontend/src/components/dashboard/Header.tsx
Normal file
49
frontend/src/components/dashboard/Header.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// src/components/dashboard/Header.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { LogOut } from 'lucide-react';
|
||||||
|
import companyLogo from '../../assets/images/ve-logo-tm.svg';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
user: User;
|
||||||
|
onLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
|
||||||
|
return (
|
||||||
|
<header className="bg-white shadow-sm">
|
||||||
|
<div className="h-16 px-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Replace the gray circle with the logo */}
|
||||||
|
<img
|
||||||
|
src={companyLogo}
|
||||||
|
alt="Company Logo"
|
||||||
|
className="h-10 w-auto object-contain"
|
||||||
|
// If you want the logo to maintain its aspect ratio but fit within certain dimensions:
|
||||||
|
// style={{ maxWidth: '40px', maxHeight: '40px' }}
|
||||||
|
/>
|
||||||
|
<h1 className="text-xl font-bold">Router Management and Reporting System</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-medium">{user.name}</div>
|
||||||
|
<div className="text-xs text-gray-500">{user.role}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onLogout}
|
||||||
|
className="px-3 py-1 text-sm flex items-center gap-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded"
|
||||||
|
>
|
||||||
|
<LogOut size={16} />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
173
frontend/src/components/dashboard/Navbar.tsx
Normal file
173
frontend/src/components/dashboard/Navbar.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Router as RouterIcon,
|
||||||
|
Users,
|
||||||
|
Settings,
|
||||||
|
Pin,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
type LucideIcon
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
activeTab: string;
|
||||||
|
onTabChange: (tab: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const [isPinned, setIsPinned] = useState(true);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const tabs: Tab[] = [
|
||||||
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
|
{ id: 'routers', label: 'Router Management', icon: RouterIcon },
|
||||||
|
{ id: 'users', label: 'User Management', icon: Users },
|
||||||
|
{ id: 'settings', label: 'Settings', icon: Settings }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (!isPinned) {
|
||||||
|
setIsCollapsed(false);
|
||||||
|
}
|
||||||
|
setIsHovered(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (!isPinned) {
|
||||||
|
setIsCollapsed(true);
|
||||||
|
}
|
||||||
|
setIsHovered(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePin = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsPinned(!isPinned);
|
||||||
|
if (!isPinned) {
|
||||||
|
setIsCollapsed(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCollapse = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsCollapsed(!isCollapsed);
|
||||||
|
if (!isCollapsed) {
|
||||||
|
setIsPinned(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative bg-gray-900 text-white h-full flex flex-col transition-all duration-300 ease-in-out ${
|
||||||
|
isCollapsed ? 'w-16' : 'w-64'
|
||||||
|
}`}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{/* Header with title and controls */}
|
||||||
|
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold">Router Management</h2>
|
||||||
|
<h2 className="text-sm font-semibold">System</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`flex items-center gap-2 ${isCollapsed ? 'w-full justify-center' : 'justify-end'}`}>
|
||||||
|
{(isHovered || !isCollapsed) && (
|
||||||
|
<button
|
||||||
|
onClick={togglePin}
|
||||||
|
className={`p-1 rounded hover:bg-gray-700 transition-colors ${
|
||||||
|
isPinned ? 'text-blue-400' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
title={isPinned ? 'Unpin sidebar' : 'Pin sidebar'}
|
||||||
|
>
|
||||||
|
<Pin
|
||||||
|
size={16}
|
||||||
|
className={`transform transition-transform ${isPinned ? 'rotate-45' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={toggleCollapse}
|
||||||
|
className="p-1 rounded hover:bg-gray-700 transition-colors"
|
||||||
|
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Items */}
|
||||||
|
<nav className="flex-1 p-2">
|
||||||
|
{tabs.map(tab => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => onTabChange(tab.id)}
|
||||||
|
className={`
|
||||||
|
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
|
||||||
|
transition-colors duration-200 group
|
||||||
|
${activeTab === tab.id
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-gray-800'}
|
||||||
|
`}
|
||||||
|
title={isCollapsed ? tab.label : undefined}
|
||||||
|
>
|
||||||
|
<Icon size={20} />
|
||||||
|
{!isCollapsed && <span>{tab.label}</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Expanded overlay when collapsed and hovered */}
|
||||||
|
{isHovered && isCollapsed && !isPinned && (
|
||||||
|
<div
|
||||||
|
className="absolute left-16 top-0 bg-gray-900 text-white h-full w-64 shadow-lg z-50 overflow-hidden"
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<div className="p-4 border-b border-gray-700">
|
||||||
|
<h2 className="text-sm font-semibold">Router Management</h2>
|
||||||
|
<h2 className="text-sm font-semibold">System</h2>
|
||||||
|
</div>
|
||||||
|
<nav className="p-2">
|
||||||
|
{tabs.map(tab => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => onTabChange(tab.id)}
|
||||||
|
className={`
|
||||||
|
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
|
||||||
|
transition-colors duration-200
|
||||||
|
${activeTab === tab.id
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-gray-800'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Icon size={20} />
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
40
frontend/src/components/dashboard/RouterManagement.tsx
Normal file
40
frontend/src/components/dashboard/RouterManagement.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// src/components/dashboard/pages/RouterManagement.tsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { RouterData, FilterType } from '../../../types';
|
||||||
|
import { RouterTable } from '../RouterTable';
|
||||||
|
|
||||||
|
export const RouterManagement: React.FC = () => {
|
||||||
|
const [filter, setFilter] = useState<FilterType>('all');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-6">Router Management</h2>
|
||||||
|
|
||||||
|
{/* Filter Controls */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<select
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value as FilterType)}
|
||||||
|
className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="all">All Routers</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="critical">Critical</option>
|
||||||
|
<option value="diskAlert">Disk Alert</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Router Table Component */}
|
||||||
|
<RouterTable
|
||||||
|
filter={filter}
|
||||||
|
onFilterChange={setFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RouterManagement;
|
||||||
145
frontend/src/components/dashboard/RouterTable.tsx
Normal file
145
frontend/src/components/dashboard/RouterTable.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// src/components/dashboard/RouterTable.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Search, ArrowUpDown, GripHorizontal, X } from 'lucide-react';
|
||||||
|
import { RouterData, FilterType } from '../../types';
|
||||||
|
import { useRouterTableLogic } from './RouterTableLogic';
|
||||||
|
import { RouterTableRow } from './RouterTableRow';
|
||||||
|
|
||||||
|
interface RouterTableProps {
|
||||||
|
routers?: RouterData[];
|
||||||
|
filter: FilterType;
|
||||||
|
onFilterChange: (filter: FilterType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RouterTable: React.FC<RouterTableProps> = ({
|
||||||
|
routers = [],
|
||||||
|
filter,
|
||||||
|
onFilterChange
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
routers: filteredRouters,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
expandedRows,
|
||||||
|
timeLeft,
|
||||||
|
handleSort,
|
||||||
|
toggleRowExpansion,
|
||||||
|
handleExpandedContentHover,
|
||||||
|
totalRouters
|
||||||
|
} = useRouterTableLogic(routers);
|
||||||
|
|
||||||
|
const getLocalTimeZone = () => {
|
||||||
|
try {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
.split('/')
|
||||||
|
.pop()
|
||||||
|
?.replace('_', ' ') || '';
|
||||||
|
} catch {
|
||||||
|
return new Date()
|
||||||
|
.toLocaleTimeString('en-us', { timeZoneName: 'short' })
|
||||||
|
.split(' ')[2];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnHeaders = [
|
||||||
|
{ key: 'slNo', label: 'Sl#', sortable: true },
|
||||||
|
{ key: 'routerId', label: 'Router ID', sortable: true },
|
||||||
|
{ key: 'routerAlias', label: 'Router Alias', sortable: true },
|
||||||
|
{ key: 'facility', label: 'Facility', sortable: true },
|
||||||
|
{ key: 'activity', label: 'Activity', sortable: false },
|
||||||
|
{ key: 'systemStatus', label: 'System Status', sortable: false },
|
||||||
|
{ key: 'lastSeen', label: `Last Seen (${getLocalTimeZone()})`, sortable: true },
|
||||||
|
{ key: 'diskUsage', label: 'Disk Usage', sortable: true }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="mb-4 relative">
|
||||||
|
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by Router ID, Alias, or Facility..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-10 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 border-b border-gray-200">
|
||||||
|
{columnHeaders.map(column => (
|
||||||
|
<th
|
||||||
|
key={column.key}
|
||||||
|
className="px-4 py-2 text-left"
|
||||||
|
onClick={() => column.sortable && handleSort(column.key as keyof RouterData)}
|
||||||
|
>
|
||||||
|
<div className={`flex items-center gap-2 ${column.sortable ? 'cursor-pointer hover:text-gray-700' : ''}`}>
|
||||||
|
<GripHorizontal size={16} className="text-gray-400" />
|
||||||
|
<span>{column.label}</span>
|
||||||
|
{column.sortable && <ArrowUpDown size={14} className="text-gray-400" />}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredRouters.length > 0 ? (
|
||||||
|
filteredRouters.map(router => (
|
||||||
|
<RouterTableRow
|
||||||
|
key={router.id}
|
||||||
|
router={router}
|
||||||
|
expandedRows={expandedRows}
|
||||||
|
timeLeft={timeLeft}
|
||||||
|
onToggleExpansion={toggleRowExpansion}
|
||||||
|
onExpandedContentHover={handleExpandedContentHover}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columnHeaders.length} className="px-4 py-8 text-center border-b">
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-2">
|
||||||
|
{searchTerm ? (
|
||||||
|
<>
|
||||||
|
<p className="text-gray-600">No results found for "{searchTerm}"</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
className="text-blue-500 hover:text-blue-700 text-sm flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
Clear search
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-600">No routers available</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results Summary */}
|
||||||
|
{filteredRouters.length > 0 && (
|
||||||
|
<div className="mt-4 text-sm text-gray-500">
|
||||||
|
Showing {filteredRouters.length} of {totalRouters} routers
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RouterTable;
|
||||||
185
frontend/src/components/dashboard/RouterTableLogic.tsx
Normal file
185
frontend/src/components/dashboard/RouterTableLogic.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// src/components/dashboard/RouterTableLogic.tsx
|
||||||
|
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
|
import { RouterData, FilterType } from '../../types';
|
||||||
|
|
||||||
|
interface TimeoutInfo {
|
||||||
|
timeoutId: NodeJS.Timeout;
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoHideTimeouts {
|
||||||
|
[key: string]: TimeoutInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRouterTableLogic = (initialRouters: RouterData[] = []) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
|
||||||
|
const [sortConfig, setSortConfig] = useState<{ key: keyof RouterData; direction: 'asc' | 'desc' } | null>(null);
|
||||||
|
const [timeLeft, setTimeLeft] = useState<{ [key: string]: number }>({});
|
||||||
|
const [autoHideTimeouts, setAutoHideTimeouts] = useState<AutoHideTimeouts>({});
|
||||||
|
|
||||||
|
const AUTO_HIDE_DELAY = 10000;
|
||||||
|
|
||||||
|
// Update countdown timer
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTimeLeft(prev => {
|
||||||
|
const now = Date.now();
|
||||||
|
const updated = { ...prev };
|
||||||
|
Object.entries(autoHideTimeouts).forEach(([key, info]) => {
|
||||||
|
if (info) {
|
||||||
|
const remaining = Math.max(0, AUTO_HIDE_DELAY - (now - info.startTime));
|
||||||
|
updated[key] = (remaining / AUTO_HIDE_DELAY) * 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
Object.values(autoHideTimeouts).forEach(info => {
|
||||||
|
if (info?.timeoutId) {
|
||||||
|
clearTimeout(info.timeoutId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [autoHideTimeouts]);
|
||||||
|
|
||||||
|
// Filter and sort data
|
||||||
|
const filteredRouters = useMemo(() => {
|
||||||
|
const routers = Array.isArray(initialRouters) ? initialRouters : [];
|
||||||
|
let processed = [...routers];
|
||||||
|
|
||||||
|
if (searchTerm.trim()) {
|
||||||
|
const lowerSearchTerm = searchTerm.toLowerCase().trim();
|
||||||
|
processed = processed.filter(router => {
|
||||||
|
const searchableFields = ['routerId', 'routerAlias', 'facility'];
|
||||||
|
return searchableFields.some(field =>
|
||||||
|
String(router[field as keyof RouterData] || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(lowerSearchTerm)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortConfig) {
|
||||||
|
processed.sort((a, b) => {
|
||||||
|
const aValue = a[sortConfig.key];
|
||||||
|
const bValue = b[sortConfig.key];
|
||||||
|
|
||||||
|
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||||
|
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}, [initialRouters, searchTerm, sortConfig]);
|
||||||
|
|
||||||
|
const handleSort = useCallback((key: keyof RouterData) => {
|
||||||
|
setSortConfig(current => ({
|
||||||
|
key,
|
||||||
|
direction: current?.key === key && current.direction === 'asc' ? 'desc' : 'asc'
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleRowExpansion = useCallback((rowId: number, section: 'activity' | 'status' | 'disk') => {
|
||||||
|
const key = `${rowId}-${section}`;
|
||||||
|
|
||||||
|
setExpandedRows(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
|
||||||
|
if (next.has(key)) {
|
||||||
|
next.delete(key);
|
||||||
|
const existingTimeout = autoHideTimeouts[key]?.timeoutId;
|
||||||
|
if (existingTimeout) {
|
||||||
|
clearTimeout(existingTimeout);
|
||||||
|
setAutoHideTimeouts(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setTimeLeft(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next.add(key);
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setExpandedRows(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(key);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setAutoHideTimeouts(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setTimeLeft(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, AUTO_HIDE_DELAY);
|
||||||
|
|
||||||
|
setAutoHideTimeouts(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: { timeoutId, startTime: Date.now() }
|
||||||
|
}));
|
||||||
|
setTimeLeft(prev => ({ ...prev, [key]: 100 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [autoHideTimeouts]);
|
||||||
|
|
||||||
|
const handleExpandedContentHover = useCallback((rowId: number, section: 'activity' | 'status' | 'disk') => {
|
||||||
|
const key = `${rowId}-${section}`;
|
||||||
|
const existingTimeout = autoHideTimeouts[key]?.timeoutId;
|
||||||
|
|
||||||
|
if (existingTimeout) {
|
||||||
|
clearTimeout(existingTimeout);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setExpandedRows(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(key);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setAutoHideTimeouts(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setTimeLeft(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, AUTO_HIDE_DELAY);
|
||||||
|
|
||||||
|
setAutoHideTimeouts(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: { timeoutId, startTime: Date.now() }
|
||||||
|
}));
|
||||||
|
setTimeLeft(prev => ({ ...prev, [key]: 100 }));
|
||||||
|
}
|
||||||
|
}, [autoHideTimeouts]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
routers: filteredRouters,
|
||||||
|
totalRouters: initialRouters.length,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
expandedRows,
|
||||||
|
timeLeft,
|
||||||
|
handleSort,
|
||||||
|
toggleRowExpansion,
|
||||||
|
handleExpandedContentHover
|
||||||
|
};
|
||||||
|
};
|
||||||
234
frontend/src/components/dashboard/RouterTableRow.tsx
Normal file
234
frontend/src/components/dashboard/RouterTableRow.tsx
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
// src/components/dashboard/RouterTableRow.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { ChevronRight, ChevronDown } from 'lucide-react';
|
||||||
|
import { RouterData } from '../../types';
|
||||||
|
import { STATUS_COLORS, formatStatus, getStatusColor } from '../../utils/statusHelpers';
|
||||||
|
|
||||||
|
interface RouterTableRowProps {
|
||||||
|
router: RouterData;
|
||||||
|
expandedRows: Set<string>;
|
||||||
|
timeLeft: { [key: string]: number };
|
||||||
|
onToggleExpansion: (id: number, section: 'activity' | 'status' | 'disk') => void;
|
||||||
|
onExpandedContentHover: (id: number, section: 'activity' | 'status' | 'disk') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
||||||
|
router,
|
||||||
|
expandedRows,
|
||||||
|
timeLeft,
|
||||||
|
onToggleExpansion,
|
||||||
|
onExpandedContentHover
|
||||||
|
}) => {
|
||||||
|
const formatLocalTime = (isoString: string) => {
|
||||||
|
return new Date(isoString).toLocaleString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDiskSpace = (bytes: number) => {
|
||||||
|
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsageColor = (usage: number) => {
|
||||||
|
if (usage >= 90) return 'bg-red-500';
|
||||||
|
if (usage >= 70) return 'bg-yellow-500';
|
||||||
|
return 'bg-green-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderActivityPanel = () => (
|
||||||
|
<div
|
||||||
|
className="bg-gray-50 p-4 relative"
|
||||||
|
onMouseEnter={() => onExpandedContentHover(router.id, 'activity')}
|
||||||
|
>
|
||||||
|
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
|
||||||
|
<div
|
||||||
|
className="h-1 bg-blue-500 transition-all duration-200"
|
||||||
|
style={{ width: `${timeLeft[`${router.id}-activity`] || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold mb-3">Recent Studies</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{router.routerActivity.studies.length > 0 ? (
|
||||||
|
router.routerActivity.studies.map((study, idx) => (
|
||||||
|
<div key={idx} className="bg-white p-3 rounded shadow-sm">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div><span className="font-medium">SIUID:</span> {study.siuid}</div>
|
||||||
|
<div><span className="font-medium">Patient:</span> {study.patientName}</div>
|
||||||
|
<div><span className="font-medium">Accession:</span> {study.accessionNumber}</div>
|
||||||
|
<div><span className="font-medium">Date:</span> {study.studyDate}</div>
|
||||||
|
<div><span className="font-medium">Modality:</span> {study.modality}</div>
|
||||||
|
{study.studyDescription && (
|
||||||
|
<div><span className="font-medium">Description:</span> {study.studyDescription}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="col-span-3 text-center text-gray-500">No recent studies</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStatusPanel = () => (
|
||||||
|
<div
|
||||||
|
className="bg-gray-50 p-4 relative"
|
||||||
|
onMouseEnter={() => onExpandedContentHover(router.id, 'status')}
|
||||||
|
>
|
||||||
|
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
|
||||||
|
<div
|
||||||
|
className="h-1 bg-blue-500 transition-all duration-200"
|
||||||
|
style={{ width: `${timeLeft[`${router.id}-status`] || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">VPN Status</h4>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
|
||||||
|
{formatStatus(router.systemStatus.vpnStatus)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">App Status</h4>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.diskStatus)}`}>
|
||||||
|
{formatStatus(router.diskStatus)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">VM Status</h4>
|
||||||
|
{router.systemStatus.vms.length > 0 ? (
|
||||||
|
router.systemStatus.vms.map((vm, idx) => (
|
||||||
|
<div key={idx} className="mb-1">
|
||||||
|
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(vm.status)}`}>
|
||||||
|
VM {vm.id}: {formatStatus(vm.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500">No VMs configured</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDiskPanel = () => (
|
||||||
|
<div
|
||||||
|
className="bg-gray-50 p-4 relative"
|
||||||
|
onMouseEnter={() => onExpandedContentHover(router.id, 'disk')}
|
||||||
|
>
|
||||||
|
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
|
||||||
|
<div
|
||||||
|
className="h-1 bg-blue-500 transition-all duration-200"
|
||||||
|
style={{ width: `${timeLeft[`${router.id}-disk`] || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Free Disk Space</h4>
|
||||||
|
<span className="text-lg">{formatDiskSpace(router.freeDisk)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Total Disk Space</h4>
|
||||||
|
<span className="text-lg">{formatDiskSpace(router.totalDisk)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Used Disk Space</h4>
|
||||||
|
<span className="text-lg">{formatDiskSpace(router.totalDisk - router.freeDisk)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<tr className="border-b hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-2">{router.slNo}</td>
|
||||||
|
<td className="px-4 py-2">{router.routerId}</td>
|
||||||
|
<td className="px-4 py-2">{router.routerAlias}</td>
|
||||||
|
<td className="px-4 py-2">{router.facility}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleExpansion(router.id, 'activity')}
|
||||||
|
className="flex items-center gap-1 text-blue-500 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
{expandedRows.has(`${router.id}-activity`) ? (
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
)}
|
||||||
|
View Activity ({router.routerActivity.studies.length} studies)
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleExpansion(router.id, 'status')}
|
||||||
|
className="flex items-center gap-1 text-blue-500 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
{expandedRows.has(`${router.id}-status`) ? (
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
)}
|
||||||
|
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
|
||||||
|
{formatStatus(router.systemStatus.vpnStatus)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">{formatLocalTime(router.lastSeen)}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleExpansion(router.id, 'disk')}
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex-1 flex items-center gap-2">
|
||||||
|
<div className="w-24 bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full ${getUsageColor(router.diskUsage)}`}
|
||||||
|
style={{ width: `${router.diskUsage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{router.diskUsage}%</span>
|
||||||
|
</div>
|
||||||
|
{expandedRows.has(`${router.id}-disk`) ? (
|
||||||
|
<ChevronDown size={16} className="text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} className="text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* Expandable Panels */}
|
||||||
|
{expandedRows.has(`${router.id}-activity`) && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8}>
|
||||||
|
{renderActivityPanel()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{expandedRows.has(`${router.id}-status`) && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8}>
|
||||||
|
{renderStatusPanel()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{expandedRows.has(`${router.id}-disk`) && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8}>
|
||||||
|
{renderDiskPanel()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RouterTableRow;
|
||||||
200
frontend/src/components/dashboard/pages/RouterManagement.tsx
Normal file
200
frontend/src/components/dashboard/pages/RouterManagement.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Settings,
|
||||||
|
RefreshCw,
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
FileSpreadsheet
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const RouterManagement: React.FC = () => {
|
||||||
|
const [selectedRouter, setSelectedRouter] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-semibold">Router Management</h2>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2">
|
||||||
|
<Plus size={20} />
|
||||||
|
Add New Router
|
||||||
|
</button>
|
||||||
|
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
|
||||||
|
<RefreshCw size={20} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="grid grid-cols-4 gap-6 p-6">
|
||||||
|
{/* Configuration Section */}
|
||||||
|
<div className="col-span-3">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{/* Router Configuration */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Router Configuration</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Router ID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter Router ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Router Alias
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter Router Alias"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Facility
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter Facility Name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
License Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter License Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Network Settings */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Network Settings</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
IP Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter IP Address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Subnet Mask
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter Subnet Mask"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Gateway
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter Gateway"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
DNS Server
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter DNS Server"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions Panel */}
|
||||||
|
<div className="col-span-1">
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Actions</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
|
||||||
|
<Settings size={20} />
|
||||||
|
Configure Router
|
||||||
|
</button>
|
||||||
|
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
|
||||||
|
<Upload size={20} />
|
||||||
|
Upload Config
|
||||||
|
</button>
|
||||||
|
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
|
||||||
|
<Download size={20} />
|
||||||
|
Download Config
|
||||||
|
</button>
|
||||||
|
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
|
||||||
|
<FileSpreadsheet size={20} />
|
||||||
|
Export Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Section */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<h4 className="font-medium mb-2">Current Status</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-600">Connection:</span>
|
||||||
|
<span className="px-2 py-1 rounded-full text-sm bg-green-100 text-green-800">
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-600">License:</span>
|
||||||
|
<span className="px-2 py-1 rounded-full text-sm bg-blue-100 text-blue-800">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-600">Last Updated:</span>
|
||||||
|
<span className="text-sm">2 mins ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<div className="border-t border-gray-200 p-6">
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RouterManagement;
|
||||||
12
frontend/src/config/env.ts
Normal file
12
frontend/src/config/env.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// src/config/env.ts
|
||||||
|
interface Config {
|
||||||
|
apiUrl: string;
|
||||||
|
environment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
|
||||||
|
environment: import.meta.env.VITE_NODE_ENV || 'development',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
136
frontend/src/data/mockData.ts
Normal file
136
frontend/src/data/mockData.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { RouterData } from '../types';
|
||||||
|
|
||||||
|
export const STATUS_COLORS = {
|
||||||
|
Normal: 'bg-green-100 text-green-800',
|
||||||
|
Warning: 'bg-yellow-100 text-yellow-800',
|
||||||
|
Critical: 'bg-red-100 text-red-800',
|
||||||
|
Connected: 'bg-green-100 text-green-800',
|
||||||
|
Running: 'bg-green-100 text-green-800'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsageColor = (usage: number): string => {
|
||||||
|
if (usage > 90) return 'bg-red-500';
|
||||||
|
if (usage > 70) return 'bg-yellow-500';
|
||||||
|
return 'bg-green-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MOCK_ROUTERS: RouterData[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
slNo: 1,
|
||||||
|
routerId: 'RTR001',
|
||||||
|
facility: 'City Hospital',
|
||||||
|
routerAlias: 'Main-Router-1',
|
||||||
|
lastSeen: '2024-03-07T14:30:00Z',
|
||||||
|
diskStatus: 'Normal',
|
||||||
|
diskUsage: 45,
|
||||||
|
// ... existing fields ...
|
||||||
|
freeDisk: 107374182400, // 100 GB in bytes
|
||||||
|
totalDisk: 1099511627776, // 1 TB in bytes
|
||||||
|
routerActivity: {
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
siuid: 'SI123456',
|
||||||
|
patientId: 'P001',
|
||||||
|
accessionNumber: 'ACC001',
|
||||||
|
patientName: 'John Smith',
|
||||||
|
studyDate: '2024-03-07',
|
||||||
|
modality: 'CT',
|
||||||
|
studyDescription: 'Chest CT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
siuid: 'SI123457',
|
||||||
|
patientId: 'P002',
|
||||||
|
accessionNumber: 'ACC002',
|
||||||
|
patientName: 'Sarah Johnson',
|
||||||
|
studyDate: '2024-03-07',
|
||||||
|
modality: 'MRI',
|
||||||
|
studyDescription: 'Brain MRI'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: 'Connected',
|
||||||
|
appStatus: 'Running',
|
||||||
|
vms: [
|
||||||
|
{ id: 1, status: 'Running' },
|
||||||
|
{ id: 2, status: 'Running' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
slNo: 2,
|
||||||
|
routerId: 'RTR002',
|
||||||
|
facility: 'Medical Center',
|
||||||
|
routerAlias: 'Emergency-Router',
|
||||||
|
lastSeen: '2024-03-07T14:25:00Z',
|
||||||
|
diskStatus: 'Critical',
|
||||||
|
diskUsage: 92,
|
||||||
|
freeDisk: 107374182400, // 100 GB in bytes
|
||||||
|
totalDisk: 1099511627776, // 1 TB in bytes
|
||||||
|
routerActivity: {
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
siuid: 'SI123458',
|
||||||
|
patientId: 'P003',
|
||||||
|
accessionNumber: 'ACC003',
|
||||||
|
patientName: 'Michael Brown',
|
||||||
|
studyDate: '2024-03-07',
|
||||||
|
modality: 'XR',
|
||||||
|
studyDescription: 'Chest X-Ray'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: 'Critical',
|
||||||
|
appStatus: 'Critical',
|
||||||
|
vms: [
|
||||||
|
{ id: 1, status: 'Critical' },
|
||||||
|
{ id: 2, status: 'Running' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
slNo: 3,
|
||||||
|
routerId: 'RTR003',
|
||||||
|
facility: 'Imaging Center',
|
||||||
|
routerAlias: 'Radiology-Router',
|
||||||
|
lastSeen: '2024-03-07T14:20:00Z',
|
||||||
|
diskStatus: 'Warning',
|
||||||
|
diskUsage: 78,
|
||||||
|
freeDisk: 107374182400, // 100 GB in bytes
|
||||||
|
totalDisk: 1099511627776, // 1 TB in bytes
|
||||||
|
routerActivity: {
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
siuid: 'SI123459',
|
||||||
|
patientId: 'P004',
|
||||||
|
accessionNumber: 'ACC004',
|
||||||
|
patientName: 'Emily Davis',
|
||||||
|
studyDate: '2024-03-07',
|
||||||
|
modality: 'US',
|
||||||
|
studyDescription: 'Abdominal Ultrasound'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
siuid: 'SI123460',
|
||||||
|
patientId: 'P005',
|
||||||
|
accessionNumber: 'ACC005',
|
||||||
|
patientName: 'Robert Wilson',
|
||||||
|
studyDate: '2024-03-07',
|
||||||
|
modality: 'MG',
|
||||||
|
studyDescription: 'Mammogram'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: 'Connected',
|
||||||
|
appStatus: 'Warning',
|
||||||
|
vms: [
|
||||||
|
{ id: 1, status: 'Running' },
|
||||||
|
{ id: 2, status: 'Warning' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
0
frontend/src/hooks
Normal file
0
frontend/src/hooks
Normal file
4
frontend/src/index.css
Normal file
4
frontend/src/index.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
11
frontend/src/main.tsx
Normal file
11
frontend/src/main.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// File: src/main.tsx
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
228
frontend/src/services/api.service.ts
Normal file
228
frontend/src/services/api.service.ts
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
// router-dashboard/src/services/api.service.ts
|
||||||
|
import { RouterData, FilterType, BackendRouter } from '../types';
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:3000/api/v1';
|
||||||
|
|
||||||
|
// Default request options for all API calls
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
credentials: 'include' as const,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to log API responses in development
|
||||||
|
const logResponse = (prefix: string, data: any) => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(`${prefix}:`, data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
async getAllRouters(filter: FilterType = 'all'): Promise<RouterData[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/routers?filter=${filter}`, {
|
||||||
|
method: 'GET',
|
||||||
|
...DEFAULT_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
logResponse('Raw API response', data);
|
||||||
|
const transformed = this.transformData(data);
|
||||||
|
logResponse('Transformed routers', transformed);
|
||||||
|
return transformed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching routers:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private transformData(data: any[]): RouterData[] {
|
||||||
|
try {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
console.error('Expected array of routers, got:', typeof data);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.map(router => {
|
||||||
|
console.log('Processing router:', router); // Debug log raw data
|
||||||
|
|
||||||
|
const transformed: RouterData = {
|
||||||
|
id: router.id,
|
||||||
|
slNo: router.slNo,
|
||||||
|
routerId: router.routerId, // Changed from router.router_id
|
||||||
|
facility: router.facility,
|
||||||
|
routerAlias: router.routerAlias, // Changed from router.router_alias
|
||||||
|
lastSeen: router.lastSeen, // Changed from router.last_seen
|
||||||
|
diskStatus: router.diskStatus, // Changed from router.disk_status_code
|
||||||
|
diskUsage: router.diskUsage || 0, // Changed from router.disk_usage
|
||||||
|
freeDisk: router.freeDisk || 0, // Changed from router.free_disk
|
||||||
|
totalDisk: router.totalDisk || 0, // Changed from router.total_disk
|
||||||
|
routerActivity: {
|
||||||
|
studies: Array.isArray(router.routerActivity?.studies)
|
||||||
|
? router.routerActivity.studies.map((study: any) => ({
|
||||||
|
siuid: study.siuid,
|
||||||
|
patientId: study.patientId,
|
||||||
|
accessionNumber: study.accessionNumber,
|
||||||
|
patientName: study.patientName,
|
||||||
|
studyDate: study.studyDate,
|
||||||
|
modality: study.modality,
|
||||||
|
studyDescription: study.studyDescription
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: router.systemStatus?.vpnStatus || 'unknown',
|
||||||
|
appStatus: router.systemStatus?.appStatus || 'unknown',
|
||||||
|
vms: Array.isArray(router.systemStatus?.vms)
|
||||||
|
? router.systemStatus.vms.map((vm: any) => ({
|
||||||
|
id: vm.id,
|
||||||
|
status: vm.status
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Transformed router:', transformed); // Debug transformed data
|
||||||
|
return transformed;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error transforming data:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRouterById(id: number): Promise<RouterData | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
||||||
|
...DEFAULT_OPTIONS
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch router');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const transformed = this.transformData([data]);
|
||||||
|
return transformed[0] || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching router:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRouter(routerData: Partial<RouterData>): Promise<RouterData | null> {
|
||||||
|
try {
|
||||||
|
// Transform frontend model to backend model
|
||||||
|
const backendData = {
|
||||||
|
router_id: routerData.routerId,
|
||||||
|
facility: routerData.facility,
|
||||||
|
router_alias: routerData.routerAlias,
|
||||||
|
disk_status_code: routerData.diskStatus,
|
||||||
|
disk_usage: routerData.diskUsage,
|
||||||
|
free_disk: routerData.freeDisk,
|
||||||
|
total_disk: routerData.totalDisk,
|
||||||
|
vpn_status_code: routerData.systemStatus?.vpnStatus,
|
||||||
|
last_seen: new Date().toISOString(), // Set current timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/routers`, {
|
||||||
|
method: 'POST',
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
body: JSON.stringify(backendData),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create router');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const transformed = this.transformData([data]);
|
||||||
|
return transformed[0] || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating router:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRouter(id: number, routerData: Partial<RouterData>): Promise<RouterData | null> {
|
||||||
|
try {
|
||||||
|
// Transform frontend model to backend model
|
||||||
|
const backendData = {
|
||||||
|
...(routerData.routerId && { router_id: routerData.routerId }),
|
||||||
|
...(routerData.facility && { facility: routerData.facility }),
|
||||||
|
...(routerData.routerAlias && { router_alias: routerData.routerAlias }),
|
||||||
|
...(routerData.diskStatus && { disk_status_code: routerData.diskStatus }),
|
||||||
|
...(routerData.diskUsage !== undefined && { disk_usage: routerData.diskUsage }),
|
||||||
|
...(routerData.freeDisk !== undefined && { free_disk: routerData.freeDisk }),
|
||||||
|
...(routerData.totalDisk !== undefined && { total_disk: routerData.totalDisk }),
|
||||||
|
...(routerData.systemStatus?.vpnStatus && { vpn_status_code: routerData.systemStatus.vpnStatus }),
|
||||||
|
last_seen: new Date().toISOString() // Update timestamp on changes
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
body: JSON.stringify(backendData),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update router');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const transformed = this.transformData([data]);
|
||||||
|
return transformed[0] || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating router:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRouter(id: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
...DEFAULT_OPTIONS
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete router');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting router:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
|
||||||
|
{ ...DEFAULT_OPTIONS }
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch routers by facility');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return this.transformData(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching routers by facility:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkApiStatus(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/routers`, {
|
||||||
|
method: 'GET',
|
||||||
|
...DEFAULT_OPTIONS
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Status Check Failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiService = new ApiService();
|
||||||
2
frontend/src/services/index.ts
Normal file
2
frontend/src/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// router-dashboard/src/services/index.ts
|
||||||
|
export * from './api.service';
|
||||||
36
frontend/src/types/backend.ts
Normal file
36
frontend/src/types/backend.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// router-dashboard/src/types/backend.ts
|
||||||
|
export interface BackendStudy {
|
||||||
|
siuid: string;
|
||||||
|
patient_id: string;
|
||||||
|
accession_number: string;
|
||||||
|
patient_name: string;
|
||||||
|
study_date: string;
|
||||||
|
modality: string;
|
||||||
|
study_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendVM {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
status_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendRouter {
|
||||||
|
id: number;
|
||||||
|
slNo: number;
|
||||||
|
router_id: string;
|
||||||
|
facility: string;
|
||||||
|
router_alias: string;
|
||||||
|
last_seen: string;
|
||||||
|
disk_status_code: string;
|
||||||
|
disk_usage: number;
|
||||||
|
free_disk: number;
|
||||||
|
total_disk: number;
|
||||||
|
routerActivity?: {
|
||||||
|
studies: BackendStudy[];
|
||||||
|
};
|
||||||
|
vpn_status_code: string;
|
||||||
|
system_status?: {
|
||||||
|
vms: BackendVM[];
|
||||||
|
};
|
||||||
|
}
|
||||||
41
frontend/src/types/index.ts
Normal file
41
frontend/src/types/index.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// router-dashboard/src/types/index.ts
|
||||||
|
export interface Study {
|
||||||
|
siuid: string;
|
||||||
|
patientId: string;
|
||||||
|
accessionNumber: string;
|
||||||
|
patientName: string;
|
||||||
|
studyDate: string;
|
||||||
|
modality: string;
|
||||||
|
studyDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VM {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
||||||
|
|
||||||
|
export interface RouterData {
|
||||||
|
id: number;
|
||||||
|
slNo: number;
|
||||||
|
routerId: string;
|
||||||
|
facility: string;
|
||||||
|
routerAlias: string;
|
||||||
|
lastSeen: string;
|
||||||
|
diskStatus: string;
|
||||||
|
diskUsage: number;
|
||||||
|
freeDisk: number;
|
||||||
|
totalDisk: number;
|
||||||
|
routerActivity: {
|
||||||
|
studies: Study[];
|
||||||
|
};
|
||||||
|
systemStatus: {
|
||||||
|
vpnStatus: string;
|
||||||
|
appStatus: string;
|
||||||
|
vms: VM[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export everything from backend types
|
||||||
|
export * from './backend';
|
||||||
30
frontend/src/utils/statusHelpers.ts
Normal file
30
frontend/src/utils/statusHelpers.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// src/utils/statusHelpers.ts
|
||||||
|
|
||||||
|
// Define all possible status values
|
||||||
|
export type StatusType =
|
||||||
|
| 'RUNNING'
|
||||||
|
| 'STOPPED'
|
||||||
|
| 'WARNING'
|
||||||
|
| 'CONNECTED'
|
||||||
|
| 'DISCONNECTED'
|
||||||
|
| 'ERROR'
|
||||||
|
| 'UNKNOWN';
|
||||||
|
|
||||||
|
export const STATUS_COLORS: Record<StatusType | string, string> = {
|
||||||
|
'RUNNING': 'bg-green-100 text-green-700',
|
||||||
|
'CONNECTED': 'bg-green-100 text-green-700',
|
||||||
|
'STOPPED': 'bg-red-100 text-red-700',
|
||||||
|
'DISCONNECTED': 'bg-red-100 text-red-700',
|
||||||
|
'WARNING': 'bg-yellow-100 text-yellow-700',
|
||||||
|
'ERROR': 'bg-red-100 text-red-700',
|
||||||
|
'UNKNOWN': 'bg-gray-100 text-gray-700'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatStatus = (status: string): string => {
|
||||||
|
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this helper function
|
||||||
|
export const getStatusColor = (status: string): string => {
|
||||||
|
return STATUS_COLORS[status] || STATUS_COLORS['UNKNOWN'];
|
||||||
|
};
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
12
frontend/tailwind.config.js
Normal file
12
frontend/tailwind.config.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// File: tailwind.config.js
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
26
frontend/tsconfig.app.json
Normal file
26
frontend/tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
21
frontend/tsconfig.json
Normal file
21
frontend/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
frontend/vite.config.ts
Normal file
7
frontend/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
135
sql/init.sql
Normal file
135
sql/init.sql
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
-- Create and use database
|
||||||
|
CREATE DATABASE IF NOT EXISTS ve_router_db;
|
||||||
|
USE ve_router_db;
|
||||||
|
|
||||||
|
-- Router table
|
||||||
|
CREATE TABLE IF NOT EXISTS routers (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
router_id VARCHAR(10) UNIQUE NOT NULL, -- Unique router identifier
|
||||||
|
facility VARCHAR(50) NOT NULL,
|
||||||
|
router_alias VARCHAR(50) NOT NULL,
|
||||||
|
last_seen TIMESTAMP NOT NULL,
|
||||||
|
vpn_status_code VARCHAR(50) NOT NULL,
|
||||||
|
disk_status_code VARCHAR(50) NOT NULL,
|
||||||
|
license_status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'inactive',
|
||||||
|
free_disk BIGINT NOT NULL CHECK (free_disk >= 0),
|
||||||
|
total_disk BIGINT NOT NULL CHECK (total_disk > 0),
|
||||||
|
disk_usage DECIMAL(5,2) NOT NULL CHECK (disk_usage BETWEEN 0 AND 100),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Users and Authentication tables
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('admin', 'operator', 'viewer') NOT NULL DEFAULT 'viewer',
|
||||||
|
status ENUM('active', 'locked', 'disabled') NOT NULL DEFAULT 'active',
|
||||||
|
failed_login_attempts INT NOT NULL DEFAULT 0,
|
||||||
|
last_login TIMESTAMP NULL,
|
||||||
|
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT valid_email CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
||||||
|
CONSTRAINT valid_username CHECK (username REGEXP '^[A-Za-z0-9_-]{3,50}$')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- User-Router access permissions
|
||||||
|
CREATE TABLE IF NOT EXISTS user_router_access (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
router_id INT NOT NULL,
|
||||||
|
can_view BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
can_manage BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY unique_user_router_access (user_id, router_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Session management
|
||||||
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
session_token VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
refresh_token VARCHAR(255) NOT NULL,
|
||||||
|
ip_address VARCHAR(45) NOT NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
expires_at TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL 24 HOUR),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT unique_session_token UNIQUE(session_token),
|
||||||
|
CONSTRAINT unique_refresh_token UNIQUE(refresh_token)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- System status table
|
||||||
|
CREATE TABLE IF NOT EXISTS system_status (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
router_id INT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Container status table
|
||||||
|
CREATE TABLE IF NOT EXISTS container_status (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
system_status_id INT NOT NULL,
|
||||||
|
container_number INT NOT NULL CHECK (container_number BETWEEN 1 AND 10),
|
||||||
|
status_code VARCHAR(50) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- VM details table
|
||||||
|
CREATE TABLE IF NOT EXISTS vm_details (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
router_id INT NOT NULL,
|
||||||
|
vm_number INT NOT NULL CHECK (vm_number > 0),
|
||||||
|
status_code VARCHAR(50) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT unique_vm_per_router UNIQUE(router_id, vm_number)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- DICOM study overview table with router_id as a string reference
|
||||||
|
CREATE TABLE IF NOT EXISTS dicom_study_overview (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
router_id VARCHAR(10) NOT NULL, -- Matching VARCHAR(10) with the routers table
|
||||||
|
study_instance_uid VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
patient_id VARCHAR(50) NOT NULL,
|
||||||
|
patient_name VARCHAR(100) NOT NULL,
|
||||||
|
accession_number VARCHAR(50) NOT NULL,
|
||||||
|
study_date DATE NOT NULL,
|
||||||
|
modality VARCHAR(20) NOT NULL,
|
||||||
|
study_description VARCHAR(255),
|
||||||
|
series_instance_uid VARCHAR(100) NOT NULL,
|
||||||
|
procedure_code VARCHAR(50),
|
||||||
|
referring_physician_name VARCHAR(100),
|
||||||
|
study_status_code VARCHAR(50) NOT NULL DEFAULT 'NEW', -- Default value, ensure 'NEW' exists in status_type
|
||||||
|
association_id VARCHAR(50) NOT NULL DEFAULT 'NEW',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- History and Audit Tables (No changes required here)
|
||||||
|
CREATE TABLE IF NOT EXISTS router_status_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
router_id INT NOT NULL,
|
||||||
|
vpn_status_code VARCHAR(50) NOT NULL,
|
||||||
|
disk_status_code VARCHAR(50) NOT NULL,
|
||||||
|
license_status ENUM('active', 'inactive', 'suspended') NOT NULL,
|
||||||
|
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Status category table
|
||||||
|
CREATE TABLE IF NOT EXISTS status_category (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- Additional history and settings tables as needed...
|
||||||
267
sql/seed_data.sql
Normal file
267
sql/seed_data.sql
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
CREATE PROCEDURE seed_complete_router_system()
|
||||||
|
BEGIN
|
||||||
|
-- Disable foreign key checks and start fresh
|
||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
-- Conditionally clear existing data, only if the table exists
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'auth_log') THEN
|
||||||
|
TRUNCATE TABLE auth_log;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_sessions') THEN
|
||||||
|
TRUNCATE TABLE user_sessions;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_router_access') THEN
|
||||||
|
TRUNCATE TABLE user_router_access;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'users') THEN
|
||||||
|
TRUNCATE TABLE users;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'container_status_history') THEN
|
||||||
|
TRUNCATE TABLE container_status_history;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_status_history') THEN
|
||||||
|
TRUNCATE TABLE router_status_history;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'container_status') THEN
|
||||||
|
TRUNCATE TABLE container_status;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vm_details') THEN
|
||||||
|
TRUNCATE TABLE vm_details;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'dicom_study_overview') THEN
|
||||||
|
TRUNCATE TABLE dicom_study_overview;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'system_status') THEN
|
||||||
|
TRUNCATE TABLE system_status;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_settings_history') THEN
|
||||||
|
TRUNCATE TABLE router_settings_history;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_settings') THEN
|
||||||
|
TRUNCATE TABLE router_settings;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'routers') THEN
|
||||||
|
TRUNCATE TABLE routers;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'status_type') THEN
|
||||||
|
TRUNCATE TABLE status_type;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'status_category') THEN
|
||||||
|
TRUNCATE TABLE status_category;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Re-enable foreign key checks
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
|
|
||||||
|
-- Insert status categories
|
||||||
|
INSERT INTO status_category (name, description)
|
||||||
|
VALUES
|
||||||
|
('Network', 'Network related statuses'),
|
||||||
|
('Disk', 'Disk related statuses'),
|
||||||
|
('VPN', 'VPN connection statuses'),
|
||||||
|
('License', 'License statuses'),
|
||||||
|
('Container', 'Container related statuses')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Insert status types
|
||||||
|
INSERT INTO status_type (category_id, name, code, description, severity)
|
||||||
|
VALUES
|
||||||
|
(1, 'Online', 'NET_ONLINE', 'System is online', 1),
|
||||||
|
(1, 'Offline', 'NET_OFFLINE', 'System is offline', 5),
|
||||||
|
(2, 'Normal', 'DISK_NORMAL', 'Disk usage is normal', 1),
|
||||||
|
(2, 'Warning', 'DISK_WARNING', 'Disk usage is high', 3),
|
||||||
|
(2, 'Critical', 'DISK_CRITICAL', 'Disk usage is critical', 5),
|
||||||
|
(3, 'Connected', 'VPN_CONNECTED', 'VPN is connected', 1),
|
||||||
|
(3, 'Disconnected', 'VPN_DISCONNECTED', 'VPN is disconnected', 5),
|
||||||
|
(5, 'Running', 'CONTAINER_RUNNING', 'Container is running', 1),
|
||||||
|
(5, 'Stopped', 'CONTAINER_STOPPED', 'Container is stopped', 5)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Insert routers
|
||||||
|
INSERT INTO routers (router_id, facility, router_alias, last_seen, vpn_status_code, disk_status_code, license_status, free_disk, total_disk, disk_usage)
|
||||||
|
VALUES
|
||||||
|
('RTR001', 'Main Hospital', 'MAIN_RAD', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'active', 500000000000, 1000000000000, 50.00),
|
||||||
|
('RTR002', 'Emergency Center', 'ER_RAD', NOW(), 'VPN_CONNECTED', 'DISK_WARNING', 'active', 400000000000, 1000000000000, 60.00),
|
||||||
|
('RTR003', 'Imaging Center', 'IMG_CENTER', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'active', 600000000000, 1000000000000, 40.00)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Store router IDs for later use
|
||||||
|
SET @router1_id = (SELECT id FROM routers WHERE router_id = 'RTR001');
|
||||||
|
SET @router2_id = (SELECT id FROM routers WHERE router_id = 'RTR002');
|
||||||
|
SET @router3_id = (SELECT id FROM routers WHERE router_id = 'RTR003');
|
||||||
|
|
||||||
|
-- Insert system status
|
||||||
|
INSERT INTO system_status (router_id)
|
||||||
|
VALUES
|
||||||
|
(@router1_id),
|
||||||
|
(@router2_id),
|
||||||
|
(@router3_id)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Insert container status
|
||||||
|
INSERT INTO container_status (system_status_id, container_number, status_code)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 'CONTAINER_RUNNING'),
|
||||||
|
(1, 2, 'CONTAINER_RUNNING'),
|
||||||
|
(2, 1, 'CONTAINER_RUNNING'),
|
||||||
|
(2, 2, 'CONTAINER_STOPPED'),
|
||||||
|
(3, 1, 'CONTAINER_RUNNING')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Insert VM details
|
||||||
|
INSERT INTO vm_details (router_id, vm_number, status_code)
|
||||||
|
VALUES
|
||||||
|
(@router1_id, 1, 'NET_ONLINE'),
|
||||||
|
(@router2_id, 1, 'NET_ONLINE'),
|
||||||
|
(@router3_id, 1, 'NET_ONLINE')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Insert DICOM studies
|
||||||
|
INSERT INTO dicom_study_overview (
|
||||||
|
router_id,
|
||||||
|
study_instance_uid,
|
||||||
|
patient_id,
|
||||||
|
patient_name,
|
||||||
|
accession_number,
|
||||||
|
study_date,
|
||||||
|
modality,
|
||||||
|
study_description,
|
||||||
|
series_instance_uid,
|
||||||
|
procedure_code,
|
||||||
|
referring_physician_name
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(@router1_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.134', 'P1', 'John Doe', 'ACC1234', '2024-03-15', 'CT', 'Chest CT', '1.2.840.113619.2.55.3.283116435.276.1543707219.135', 'CT001', 'Dr. Smith'),
|
||||||
|
(@router2_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.136', 'P2', 'Jane Doe', 'ACC1235', '2024-03-15', 'MR', 'Brain MRI', '1.2.840.113619.2.55.3.283116435.276.1543707219.137', 'MR001', 'Dr. Johnson')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- Insert router settings for each router (calls to upsert_router_settings are disabled)
|
||||||
|
-- Main Hospital Router
|
||||||
|
-- CALL upsert_router_settings(
|
||||||
|
-- @router1_id,
|
||||||
|
-- 'client',
|
||||||
|
-- '{
|
||||||
|
-- "dicom": {
|
||||||
|
-- "local": {
|
||||||
|
-- "aet": "MAIN_RAD",
|
||||||
|
-- "port": 104,
|
||||||
|
-- "file_directory": "/dicom_images",
|
||||||
|
-- "wait_time": 2,
|
||||||
|
-- "receiver_wait_time": 5000
|
||||||
|
-- },
|
||||||
|
-- "association": {
|
||||||
|
-- "acse_timeout": 5,
|
||||||
|
-- "dimse_timeout": 1000,
|
||||||
|
-- "network_timeout": 1000,
|
||||||
|
-- "retry": {
|
||||||
|
-- "attempts": 3,
|
||||||
|
-- "interval": 10
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- },
|
||||||
|
-- "rabbitmq": {
|
||||||
|
-- "local": {
|
||||||
|
-- "hostname": "router-rabbitmq",
|
||||||
|
-- "port": 5672,
|
||||||
|
-- "credentials": {
|
||||||
|
-- "username": "vitalengine",
|
||||||
|
-- "password": "vitalengine"
|
||||||
|
-- },
|
||||||
|
-- "settings": {
|
||||||
|
-- "durable": true,
|
||||||
|
-- "auto_delete": false,
|
||||||
|
-- "exchange_type": "direct",
|
||||||
|
-- "heartbeat": 50
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- },
|
||||||
|
-- "scp_connections": {
|
||||||
|
-- "pacs_nodes": [
|
||||||
|
-- {
|
||||||
|
-- "host": "pacsmain.example.com",
|
||||||
|
-- "port": 104
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- "host": "pacsbackup.example.com",
|
||||||
|
-- "port": 104
|
||||||
|
-- }
|
||||||
|
-- ]
|
||||||
|
-- }
|
||||||
|
-- }',
|
||||||
|
-- 'system',
|
||||||
|
-- 'Initial client configuration for Main Hospital'
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- Emergency Center Router
|
||||||
|
-- CALL upsert_router_settings(
|
||||||
|
-- @router2_id,
|
||||||
|
-- 'client',
|
||||||
|
-- '{
|
||||||
|
-- "dicom": {
|
||||||
|
-- "local": {
|
||||||
|
-- "aet": "ER_RAD",
|
||||||
|
-- "port": 104,
|
||||||
|
-- "file_directory": "/dicom_images",
|
||||||
|
-- "wait_time": 2,
|
||||||
|
-- "receiver_wait_time": 5000
|
||||||
|
-- },
|
||||||
|
-- "association": {
|
||||||
|
-- "acse_timeout": 5,
|
||||||
|
-- "dimse_timeout": 1000,
|
||||||
|
-- "network_timeout": 1000,
|
||||||
|
-- "retry": {
|
||||||
|
-- "attempts": 3,
|
||||||
|
-- "interval": 10
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- },
|
||||||
|
-- "rabbitmq": {
|
||||||
|
-- "local": {
|
||||||
|
-- "hostname": "router-rabbitmq",
|
||||||
|
-- "port": 5672,
|
||||||
|
-- "credentials": {
|
||||||
|
-- "username": "vitalengine",
|
||||||
|
-- "password": "vitalengine"
|
||||||
|
-- },
|
||||||
|
-- "settings": {
|
||||||
|
-- "durable": true,
|
||||||
|
-- "auto_delete": false,
|
||||||
|
-- "exchange_type": "direct",
|
||||||
|
-- "heartbeat": 50
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- },
|
||||||
|
-- "scp_connections": {
|
||||||
|
-- "pacs_nodes": [
|
||||||
|
-- {
|
||||||
|
-- "host": "pacsemergency.example.com",
|
||||||
|
-- "port": 104
|
||||||
|
-- }
|
||||||
|
-- ]
|
||||||
|
-- }
|
||||||
|
-- }',
|
||||||
|
-- 'system',
|
||||||
|
-- 'Initial client configuration for Emergency Center'
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- Insert settings for other routers as needed...
|
||||||
|
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
25
start.bat
Normal file
25
start.bat
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@echo off
|
||||||
|
echo Stopping containers...
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
echo Removing old volume...
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
echo Starting services...
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo Waiting for MySQL to be ready...
|
||||||
|
REM Replace timeout with ping for Windows
|
||||||
|
ping -n 30 127.0.0.1 > nul
|
||||||
|
|
||||||
|
echo Checking MySQL status...
|
||||||
|
docker-compose exec mysql mysqladmin -u ve_router_user -pve_router_password ping
|
||||||
|
|
||||||
|
IF %ERRORLEVEL% EQU 0 (
|
||||||
|
echo MySQL is up and running!
|
||||||
|
echo Showing tables in ve_router_db:
|
||||||
|
docker-compose exec mysql mysql -u ve_router_user -pve_router_password -h mysql ve_router_db -e "SHOW TABLES;"
|
||||||
|
) ELSE (
|
||||||
|
echo MySQL is not responding. Please check the logs:
|
||||||
|
docker-compose logs mysql
|
||||||
|
)
|
||||||
18
start.ps1
Normal file
18
start.ps1
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# init-db.ps1
|
||||||
|
Write-Host "Stopping containers..."
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
Write-Host "Removing old volume..."
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
Write-Host "Starting services..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
Write-Host "Waiting for MySQL to be ready..."
|
||||||
|
Start-Sleep -Seconds 30
|
||||||
|
|
||||||
|
Write-Host "Checking MySQL status..."
|
||||||
|
docker-compose logs mysql
|
||||||
|
|
||||||
|
Write-Host "Attempting to connect to MySQL..."
|
||||||
|
docker-compose exec mysql mysql -u ve_router_user -pve_router_password -h localhost ve_router_db -e "SHOW TABLES;"
|
||||||
Loading…
x
Reference in New Issue
Block a user