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