First commit
This commit is contained in:
741
archive/old_public/css/style.css
Normal file
741
archive/old_public/css/style.css
Normal file
@@ -0,0 +1,741 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
height: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.toolbar-info .separator {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.project-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.project-selector label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.view-switcher-group {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-switcher {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background-color: white;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-view:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.btn-view:hover:not(.active) {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-view.active {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
.zoom-input {
|
||||
width: 60px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-input:focus {
|
||||
outline: none;
|
||||
border-color: #4A90E2;
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.zoom-input:hover {
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
.zoom-unit {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #357ABD;
|
||||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Main Content Area with Split Panes */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.canvas-pane {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
#canvasWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
height: 6px;
|
||||
background-color: #e0e0e0;
|
||||
cursor: ns-resize;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background-color: #4A90E2;
|
||||
}
|
||||
|
||||
.resize-handle.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
min-height: 150px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.table-pane.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 200px;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context-menu.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.context-menu ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.context-menu li {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.context-menu li:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.context-menu li.divider {
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
margin: 5px 0;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.context-menu li.divider:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.context-menu li.menu-header {
|
||||
padding: 8px 20px 4px 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.context-menu li.menu-header:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.context-menu li.spacing-control {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.context-menu li.spacing-control:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.context-menu .spacing-label {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.context-menu .spacing-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.context-menu .spacing-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.context-menu .spacing-btn:hover {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
.context-menu .submenu-indicator {
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-content.modal-large {
|
||||
max-width: 800px;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.port-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.port-button {
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.port-button:hover:not(.used) {
|
||||
border-color: #4A90E2;
|
||||
background-color: #f0f7ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.port-button.used {
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
|
||||
.port-button.selected {
|
||||
background-color: #4A90E2;
|
||||
color: white;
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c0c0c0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a0a0a0;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-section {
|
||||
margin-bottom: 25px;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
textarea.form-input {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-group input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Device Type Grid */
|
||||
.device-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.device-type-card {
|
||||
background-color: #f9f9f9;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.device-type-card:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #4A90E2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.device-type-card .device-type-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.device-type-card .device-type-ports {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Preview Box */
|
||||
.preview-box {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.preview-box strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.preview-names {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
color: #4A90E2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Projects List */
|
||||
.projects-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background-color: #f9f9f9;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
|
||||
.project-card.active {
|
||||
background-color: #e3f2fd;
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.project-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #fff;
|
||||
color: #666;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #fff;
|
||||
color: #d32f2f;
|
||||
border-color: #d32f2f;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#tableContent {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ag-Grid customization */
|
||||
.ag-theme-alpine {
|
||||
--ag-header-height: 40px;
|
||||
--ag-row-height: 35px;
|
||||
--ag-font-size: 13px;
|
||||
--ag-header-foreground-color: #333;
|
||||
--ag-header-background-color: #f5f5f5;
|
||||
--ag-odd-row-background-color: #fafafa;
|
||||
--ag-row-hover-color: #f0f7ff;
|
||||
--ag-selected-row-background-color: #e3f2fd;
|
||||
}
|
||||
3
archive/old_public/favicon.svg
Normal file
3
archive/old_public/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<text y="80" font-size="80">🗄️</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 115 B |
193
archive/old_public/index.html
Normal file
193
archive/old_public/index.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Datacenter Designer</title>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<!-- ag-Grid -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@31/styles/ag-grid.css"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@31/styles/ag-theme-alpine.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top Toolbar -->
|
||||
<div class="toolbar">
|
||||
<div class="project-selector">
|
||||
<select id="projectSelect" class="form-select">
|
||||
<option value="1">Loading...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="view-switcher-group">
|
||||
<div class="view-switcher">
|
||||
<button id="physicalViewBtn" class="btn-view active" title="Physical layout with racks">Physical</button>
|
||||
<button id="logicalViewBtn" class="btn-view" title="Logical topology without racks">Logical</button>
|
||||
</div>
|
||||
<div class="view-switcher">
|
||||
<button id="racksTableBtn" class="btn-view" title="Toggle racks table view">Racks</button>
|
||||
<button id="devicesTableBtn" class="btn-view" title="Toggle devices table view">Devices</button>
|
||||
<button id="connectionsTableBtn" class="btn-view" title="Toggle connections table view">Connections</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-spacer"></div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="exportProjectBtn" class="btn-sm" title="Export project to JSON file">Export Project</button>
|
||||
<button id="importProjectBtn" class="btn-sm" title="Import project from JSON file">Import Project</button>
|
||||
<input type="file" id="importProjectInput" accept=".json" style="display: none;">
|
||||
</div>
|
||||
<div class="toolbar-info">
|
||||
<input type="number" id="zoomInput" class="zoom-input" min="10" max="300" step="10" value="100" title="Enter zoom percentage">
|
||||
<span class="zoom-unit">%</span>
|
||||
<button id="fitViewBtn" class="btn-sm" title="Fit all racks in view">Fit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area with Split Panes -->
|
||||
<div class="main-content">
|
||||
<!-- Canvas Container (Top Pane) -->
|
||||
<div id="canvasPane" class="canvas-pane">
|
||||
<div id="canvasWrapper"></div>
|
||||
</div>
|
||||
|
||||
<!-- Resize Handle -->
|
||||
<div id="resizeHandle" class="resize-handle hidden"></div>
|
||||
|
||||
<!-- Table Container (Bottom Pane) -->
|
||||
<div id="tablePane" class="table-pane hidden">
|
||||
<div class="table-toolbar">
|
||||
<button id="addTableRowBtn" class="btn btn-primary">+ Add Row</button>
|
||||
<button id="deleteTableRowBtn" class="btn btn-secondary">Delete Selected</button>
|
||||
<div style="flex: 1;"></div>
|
||||
<button id="exportExcelBtn" class="btn btn-success" title="Export all tables to Excel (.xlsx)">Export to Excel</button>
|
||||
</div>
|
||||
<div id="tableContent" class="ag-theme-alpine"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div id="contextMenu" class="context-menu hidden">
|
||||
<ul id="contextMenuList"></ul>
|
||||
</div>
|
||||
|
||||
<!-- Port Selection Modal -->
|
||||
<div id="portModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="portModalTitle">Select Port</h3>
|
||||
<button class="modal-close" id="portModalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="portList" class="port-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Rack Modal -->
|
||||
<div id="addRackModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Add Rack(s)</h3>
|
||||
<button class="modal-close" id="addRackModalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="rackCount">Number of racks:</label>
|
||||
<input type="number" id="rackCount" min="1" max="20" value="3" class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rackPrefix">Name prefix:</label>
|
||||
<input type="text" id="rackPrefix" value="RACK" class="form-input" placeholder="RACK">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Position:</label>
|
||||
<div class="radio-group">
|
||||
<label>
|
||||
<input type="radio" name="rowPosition" value="continue" checked>
|
||||
Continue row
|
||||
<select id="continueRowSelect" class="form-select" style="margin-left: 8px; min-width: 80px; display: inline-block;">
|
||||
<option value="0">1</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><input type="radio" name="rowPosition" value="below"> New row below</label>
|
||||
<label><input type="radio" name="rowPosition" value="above"> New row above</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-box">
|
||||
<strong>Preview:</strong>
|
||||
<div id="rackNamePreview" class="preview-names">RACK01, RACK02, RACK03</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button id="createRacksBtn" class="btn btn-primary">Create</button>
|
||||
<button id="cancelRacksBtn" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Device Modal -->
|
||||
<div id="addDeviceModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Add Device</h3>
|
||||
<button class="modal-close" id="addDeviceModalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Select device type:</label>
|
||||
<div id="deviceTypeList" class="device-type-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manage Projects Modal -->
|
||||
<div id="manageProjectsModal" class="modal hidden">
|
||||
<div class="modal-content modal-large">
|
||||
<div class="modal-header">
|
||||
<h3>Manage Projects</h3>
|
||||
<button class="modal-close" id="manageProjectsModalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-section">
|
||||
<button id="newProjectBtnFromManage" class="btn btn-primary" style="margin-bottom: 20px;">+ New Project</button>
|
||||
</div>
|
||||
<div id="projectsList" class="projects-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New/Edit Project Modal -->
|
||||
<div id="projectFormModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="projectFormTitle">New Project</h3>
|
||||
<button class="modal-close" id="projectFormModalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="projectName">Project name:</label>
|
||||
<input type="text" id="projectName" class="form-input" placeholder="My Datacenter Project">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="projectDescription">Description (optional):</label>
|
||||
<textarea id="projectDescription" class="form-input" rows="3" placeholder="Project description..."></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="saveProjectBtn" class="btn btn-primary">Save</button>
|
||||
<button id="cancelProjectBtn" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/ag-grid-community@31/dist/ag-grid-community.min.js"></script>
|
||||
<script src="https://unpkg.com/konva@9/konva.min.js"></script>
|
||||
<script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
|
||||
<script src="js/app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
1719
archive/old_public/js/app.js
Normal file
1719
archive/old_public/js/app.js
Normal file
File diff suppressed because it is too large
Load Diff
901
archive/old_public/js/connection-manager.js
Normal file
901
archive/old_public/js/connection-manager.js
Normal file
@@ -0,0 +1,901 @@
|
||||
export class ConnectionManager {
|
||||
constructor(layer, api, deviceManager, rackManager) {
|
||||
this.layer = layer;
|
||||
this.api = api;
|
||||
this.deviceManager = deviceManager;
|
||||
this.rackManager = rackManager;
|
||||
this.connections = new Map();
|
||||
this.connectionLayer = new Konva.Layer();
|
||||
this.pendingConnection = null;
|
||||
this.tempLine = null;
|
||||
this.currentView = 'physical'; // Track current view
|
||||
this.selectedConnection = null; // Track selected connection for keyboard deletion
|
||||
|
||||
// Set up keyboard event listener for Delete key
|
||||
this.setupKeyboardListeners();
|
||||
}
|
||||
|
||||
setupKeyboardListeners() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
if (this.selectedConnection) {
|
||||
e.preventDefault();
|
||||
const conn = this.connections.get(this.selectedConnection);
|
||||
if (conn) {
|
||||
if (confirm('Delete this connection?')) {
|
||||
this.deleteConnection(this.selectedConnection, conn.shape, conn.handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
// Deselect connection on Escape
|
||||
this.deselectConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectConnection(connectionId, line, handles) {
|
||||
// Deselect previous connection
|
||||
this.deselectConnection();
|
||||
|
||||
// Select this connection
|
||||
this.selectedConnection = connectionId;
|
||||
line.stroke('#FF6B6B'); // Red highlight for selected
|
||||
line.strokeWidth(3);
|
||||
handles.forEach(h => h.opacity(1));
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
|
||||
deselectConnection() {
|
||||
if (this.selectedConnection) {
|
||||
const conn = this.connections.get(this.selectedConnection);
|
||||
if (conn) {
|
||||
conn.shape.stroke('#000000');
|
||||
conn.shape.strokeWidth(1);
|
||||
conn.handles.forEach(h => h.opacity(0));
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
this.selectedConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
getConnectionLayer() {
|
||||
return this.connectionLayer;
|
||||
}
|
||||
|
||||
setCurrentView(viewType) {
|
||||
this.currentView = viewType;
|
||||
// Reload all connections to use waypoints for the new view
|
||||
this.reloadConnectionsForView();
|
||||
}
|
||||
|
||||
async reloadConnectionsForView() {
|
||||
// Clear existing connections from layer
|
||||
this.connections.forEach(conn => {
|
||||
conn.shape.destroy();
|
||||
conn.handles.forEach(h => h.destroy());
|
||||
});
|
||||
this.connections.clear();
|
||||
|
||||
// Reload connections with view-specific waypoints
|
||||
try {
|
||||
const connections = await this.api.getConnections();
|
||||
connections.forEach(connData => {
|
||||
this.createConnectionLine(connData);
|
||||
});
|
||||
this.connectionLayer.batchDraw();
|
||||
} catch (err) {
|
||||
console.error('Failed to reload connections:', err);
|
||||
}
|
||||
}
|
||||
|
||||
isConnectionMode() {
|
||||
return this.pendingConnection !== null;
|
||||
}
|
||||
|
||||
async loadConnections() {
|
||||
try {
|
||||
const connections = await this.api.getConnections();
|
||||
connections.forEach(connData => {
|
||||
this.createConnectionLine(connData);
|
||||
});
|
||||
this.connectionLayer.batchDraw();
|
||||
} catch (err) {
|
||||
console.error('Failed to load connections:', err);
|
||||
}
|
||||
}
|
||||
|
||||
createConnectionLine(connData) {
|
||||
const sourceDevice = this.deviceManager.getDeviceShape(connData.source_device_id);
|
||||
const targetDevice = this.deviceManager.getDeviceShape(connData.target_device_id);
|
||||
|
||||
if (!sourceDevice || !targetDevice) {
|
||||
console.error('Device shapes not found for connection:', connData);
|
||||
return;
|
||||
}
|
||||
|
||||
const conn = this.drawConnection(sourceDevice, targetDevice, connData);
|
||||
this.connections.set(connData.id, conn);
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
// Alias for table-manager compatibility
|
||||
createConnectionShape(connData) {
|
||||
return this.createConnectionLine(connData);
|
||||
}
|
||||
|
||||
drawConnection(sourceShape, targetShape, connData) {
|
||||
const deviceWidth = this.deviceManager.deviceWidth;
|
||||
const deviceHeight = this.deviceManager.deviceHeight;
|
||||
|
||||
const sourcePos = this.getDeviceAbsolutePosition(sourceShape);
|
||||
const targetPos = this.getDeviceAbsolutePosition(targetShape);
|
||||
|
||||
// Calculate edge points based on relative positions
|
||||
const { sourcePoint, targetPoint } = this.getEdgeConnectionPoints(
|
||||
sourcePos, targetPos, deviceWidth, deviceHeight
|
||||
);
|
||||
|
||||
// Load waypoints based on current view
|
||||
let points;
|
||||
let viewWaypoints = null;
|
||||
|
||||
// Get view-specific waypoints
|
||||
if (this.currentView === 'physical' && connData.waypoints_physical) {
|
||||
viewWaypoints = typeof connData.waypoints_physical === 'string'
|
||||
? JSON.parse(connData.waypoints_physical)
|
||||
: connData.waypoints_physical;
|
||||
} else if (this.currentView === 'logical' && connData.waypoints_logical) {
|
||||
viewWaypoints = typeof connData.waypoints_logical === 'string'
|
||||
? JSON.parse(connData.waypoints_logical)
|
||||
: connData.waypoints_logical;
|
||||
} else if (connData.waypoints) {
|
||||
// Fallback to legacy waypoints column
|
||||
viewWaypoints = typeof connData.waypoints === 'string'
|
||||
? JSON.parse(connData.waypoints)
|
||||
: connData.waypoints;
|
||||
}
|
||||
|
||||
if (viewWaypoints && viewWaypoints.length > 0) {
|
||||
// Use saved waypoints for this view
|
||||
const hasEdgePoints = viewWaypoints[0].isEdge !== undefined;
|
||||
|
||||
if (hasEdgePoints) {
|
||||
// Use saved edge points and middle waypoints
|
||||
const edgeStart = viewWaypoints.find((p, i) => p.isEdge && i === 0) || { x: sourcePoint.x, y: sourcePoint.y };
|
||||
const edgeEnd = viewWaypoints.find((p, i) => p.isEdge && i === viewWaypoints.length - 1) || { x: targetPoint.x, y: targetPoint.y };
|
||||
const middleWaypoints = viewWaypoints.filter(p => !p.isEdge);
|
||||
|
||||
points = this.rebuildOrthogonalPath(edgeStart, middleWaypoints, edgeEnd);
|
||||
} else {
|
||||
// Old format: saved waypoints are only middle points
|
||||
points = this.rebuildOrthogonalPath(sourcePoint, viewWaypoints, targetPoint);
|
||||
}
|
||||
} else {
|
||||
// No waypoints for this view - create default path
|
||||
points = this.createSimpleOrthogonalPath(sourcePoint, targetPoint);
|
||||
}
|
||||
|
||||
// Create the line
|
||||
const line = new Konva.Line({
|
||||
points: points,
|
||||
stroke: '#000000',
|
||||
strokeWidth: 1,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
id: `connection-${connData.id}`,
|
||||
opacity: 0.8,
|
||||
hitStrokeWidth: 10
|
||||
});
|
||||
|
||||
// Add line to layer FIRST so it's drawn underneath handles
|
||||
this.connectionLayer.add(line);
|
||||
|
||||
// Create draggable handles for ALL points (including start and end)
|
||||
const handles = [];
|
||||
const numPoints = points.length / 2;
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const handle = new Konva.Circle({
|
||||
x: points[i * 2],
|
||||
y: points[i * 2 + 1],
|
||||
radius: 4,
|
||||
fill: '#4A90E2',
|
||||
stroke: '#fff',
|
||||
strokeWidth: 1,
|
||||
draggable: true,
|
||||
opacity: 0, // Hidden by default
|
||||
name: `handle-${i}`
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
// Add handle AFTER line, so handles are on top
|
||||
this.connectionLayer.add(handle);
|
||||
}
|
||||
|
||||
// Click to select connection
|
||||
line.on('click', (e) => {
|
||||
e.cancelBubble = true;
|
||||
this.selectConnection(connData.id, line, handles);
|
||||
});
|
||||
|
||||
// Hover behavior: show handles and highlight line
|
||||
line.on('mouseenter', () => {
|
||||
if (this.selectedConnection !== connData.id) {
|
||||
line.stroke('#4A90E2'); // Blue highlight
|
||||
line.strokeWidth(2);
|
||||
handles.forEach(h => h.opacity(1));
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
});
|
||||
|
||||
line.on('mouseleave', () => {
|
||||
// Only hide if no handle is being dragged and not selected
|
||||
const isDragging = handles.some(h => h.isDragging());
|
||||
if (!isDragging && this.selectedConnection !== connData.id) {
|
||||
line.stroke('#000000');
|
||||
line.strokeWidth(1);
|
||||
handles.forEach(h => h.opacity(0));
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
});
|
||||
|
||||
// Simplified handle event listeners
|
||||
handles.forEach((handle, idx) => {
|
||||
const isEdgeHandle = (idx === 0 || idx === handles.length - 1);
|
||||
|
||||
handle.on('mouseenter', () => {
|
||||
// Keep line highlighted and handles visible when hovering over handles
|
||||
line.stroke('#4A90E2');
|
||||
line.strokeWidth(2);
|
||||
handles.forEach(h => h.opacity(1));
|
||||
this.connectionLayer.batchDraw();
|
||||
});
|
||||
|
||||
handle.on('mousedown', (e) => e.cancelBubble = true);
|
||||
|
||||
handle.on('dragmove', () => {
|
||||
this.updateLineFromHandles(line, handles);
|
||||
});
|
||||
|
||||
handle.on('dragend', () => {
|
||||
// Snap edge handles to nearest device edge
|
||||
if (isEdgeHandle) {
|
||||
const snappedPos = this.snapToNearestDeviceEdge(handle.x(), handle.y());
|
||||
handle.position(snappedPos);
|
||||
this.updateLineFromHandles(line, handles);
|
||||
}
|
||||
this.saveWaypoints(connData.id, handles);
|
||||
|
||||
// After drag, check if mouse is still over line or handles to keep handles visible
|
||||
const stage = this.connectionLayer.getStage();
|
||||
const pointerPos = stage.getPointerPosition();
|
||||
if (pointerPos) {
|
||||
const shape = this.connectionLayer.getIntersection(pointerPos);
|
||||
const isOverLineOrHandle = shape === line || handles.includes(shape);
|
||||
if (!isOverLineOrHandle) {
|
||||
// Mouse not over line or handles, hide handles and reset line style
|
||||
line.stroke('#000000');
|
||||
line.strokeWidth(1);
|
||||
handles.forEach(h => h.opacity(0));
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Double-click on line to add waypoint
|
||||
line.on('dblclick', (e) => {
|
||||
const stage = this.connectionLayer.getStage();
|
||||
const pointerPos = stage.getPointerPosition();
|
||||
const scale = stage.scaleX();
|
||||
const stagePos = stage.position();
|
||||
|
||||
// Convert to world coordinates
|
||||
const worldX = (pointerPos.x - stagePos.x) / scale;
|
||||
const worldY = (pointerPos.y - stagePos.y) / scale;
|
||||
|
||||
// Create new waypoint at double-click position
|
||||
this.addWaypointAtPosition(line, handles, worldX, worldY, connData);
|
||||
});
|
||||
|
||||
// Right-click to delete
|
||||
line.on('contextmenu', (e) => {
|
||||
e.evt.preventDefault();
|
||||
this.showConnectionContextMenu(e, connData, line, handles);
|
||||
});
|
||||
|
||||
// Update line when devices move
|
||||
const updateLine = () => {
|
||||
const newSourcePos = this.getDeviceAbsolutePosition(sourceShape);
|
||||
const newTargetPos = this.getDeviceAbsolutePosition(targetShape);
|
||||
|
||||
const { sourcePoint: newSourcePoint, targetPoint: newTargetPoint } =
|
||||
this.getEdgeConnectionPoints(newSourcePos, newTargetPos, this.deviceManager.deviceWidth, this.deviceManager.deviceHeight);
|
||||
|
||||
// Update first and last handle positions (start and end)
|
||||
handles[0].position(newSourcePoint);
|
||||
handles[handles.length - 1].position(newTargetPoint);
|
||||
|
||||
// Get current middle waypoints
|
||||
const waypoints = handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
|
||||
|
||||
// Rebuild path with new endpoints and current waypoints
|
||||
const newPoints = this.rebuildOrthogonalPath(newSourcePoint, waypoints, newTargetPoint);
|
||||
|
||||
line.points(newPoints);
|
||||
|
||||
this.connectionLayer.batchDraw();
|
||||
};
|
||||
|
||||
// Attach update listeners based on current view
|
||||
if (this.currentView === 'logical') {
|
||||
// In logical view, devices are on main layer - attach listeners to devices
|
||||
sourceShape.on('dragmove.connection', updateLine);
|
||||
targetShape.on('dragmove.connection', updateLine);
|
||||
} else {
|
||||
// In physical view, devices are in racks - attach listeners to racks
|
||||
const sourceRack = sourceShape.getParent().getParent();
|
||||
const targetRack = targetShape.getParent().getParent();
|
||||
|
||||
if (sourceRack) sourceRack.on('dragmove.connection', updateLine);
|
||||
if (targetRack) targetRack.on('dragmove.connection', updateLine);
|
||||
}
|
||||
|
||||
// Line already added earlier (before handles, for correct z-order)
|
||||
|
||||
return { data: connData, shape: line, handles: handles, sourceShape, targetShape };
|
||||
}
|
||||
|
||||
addWaypointAtPosition(line, handles, worldX, worldY, connData) {
|
||||
const conn = this.connections.get(connData.id);
|
||||
if (!conn) return;
|
||||
|
||||
// Get current points
|
||||
const startPoint = { x: handles[0].x(), y: handles[0].y() };
|
||||
const endPoint = { x: handles[handles.length - 1].x(), y: handles[handles.length - 1].y() };
|
||||
const waypoints = handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
|
||||
|
||||
// Add new waypoint
|
||||
waypoints.push({ x: worldX, y: worldY });
|
||||
|
||||
// Recreate handles
|
||||
this.recreateHandles(line, handles, startPoint, waypoints, endPoint, connData);
|
||||
|
||||
// Show handles and highlight line (we just added a waypoint)
|
||||
line.stroke('#4A90E2');
|
||||
line.strokeWidth(2);
|
||||
handles.forEach(h => h.opacity(1));
|
||||
|
||||
// Save
|
||||
this.saveWaypoints(connData.id, handles);
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
|
||||
recreateHandles(line, handles, startPoint, waypoints, endPoint, connData) {
|
||||
// Destroy old handles
|
||||
handles.forEach(h => h.destroy());
|
||||
handles.length = 0;
|
||||
|
||||
// Rebuild path
|
||||
const newPoints = this.rebuildOrthogonalPath(startPoint, waypoints, endPoint);
|
||||
line.points(newPoints);
|
||||
|
||||
// Create new handles
|
||||
const allPoints = [startPoint, ...waypoints, endPoint];
|
||||
|
||||
allPoints.forEach((pt, i) => {
|
||||
const isEdgeHandle = (i === 0 || i === allPoints.length - 1);
|
||||
|
||||
const handle = new Konva.Circle({
|
||||
x: pt.x,
|
||||
y: pt.y,
|
||||
radius: 4,
|
||||
fill: '#4A90E2',
|
||||
stroke: '#fff',
|
||||
strokeWidth: 1,
|
||||
draggable: true,
|
||||
opacity: 0, // Hidden by default
|
||||
name: `handle-${i}`
|
||||
});
|
||||
|
||||
// Attach event listeners
|
||||
handle.on('mouseenter', () => {
|
||||
// Keep line highlighted and handles visible when hovering over handles
|
||||
line.stroke('#4A90E2');
|
||||
line.strokeWidth(2);
|
||||
handles.forEach(h => h.opacity(1));
|
||||
this.connectionLayer.batchDraw();
|
||||
});
|
||||
|
||||
handle.on('mousedown', (e) => e.cancelBubble = true);
|
||||
|
||||
handle.on('dragmove', () => {
|
||||
this.updateLineFromHandles(line, handles);
|
||||
});
|
||||
|
||||
handle.on('dragend', () => {
|
||||
if (isEdgeHandle) {
|
||||
const snappedPos = this.snapToNearestDeviceEdge(handle.x(), handle.y());
|
||||
handle.position(snappedPos);
|
||||
this.updateLineFromHandles(line, handles);
|
||||
}
|
||||
this.saveWaypoints(connData.id, handles);
|
||||
|
||||
// After drag, check if mouse is still over line or handles to keep handles visible
|
||||
const stage = this.connectionLayer.getStage();
|
||||
const pointerPos = stage.getPointerPosition();
|
||||
if (pointerPos) {
|
||||
const shape = this.connectionLayer.getIntersection(pointerPos);
|
||||
const isOverLineOrHandle = shape === line || handles.includes(shape);
|
||||
if (!isOverLineOrHandle) {
|
||||
// Mouse not over line or handles, hide handles and reset line style
|
||||
line.stroke('#000000');
|
||||
line.strokeWidth(1);
|
||||
handles.forEach(h => h.opacity(0));
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
this.connectionLayer.add(handle);
|
||||
});
|
||||
|
||||
// Update connection reference
|
||||
const conn = this.connections.get(connData.id);
|
||||
if (conn) conn.handles = handles;
|
||||
}
|
||||
|
||||
async saveWaypoints(connectionId, handles) {
|
||||
try {
|
||||
// Save ALL waypoints including edge points
|
||||
const allPoints = handles.map((h, i) => ({
|
||||
x: h.x(),
|
||||
y: h.y(),
|
||||
isEdge: i === 0 || i === handles.length - 1
|
||||
}));
|
||||
// Save to view-specific column
|
||||
await this.api.updateConnectionWaypoints(connectionId, allPoints, this.currentView);
|
||||
} catch (err) {
|
||||
console.error('Failed to save waypoints:', err);
|
||||
}
|
||||
}
|
||||
|
||||
createSimpleOrthogonalPath(start, end) {
|
||||
// Create a proper orthogonal path with right angles only
|
||||
const dx = Math.abs(end.x - start.x);
|
||||
const dy = Math.abs(end.y - start.y);
|
||||
|
||||
// If points are already aligned (same x or y), draw straight line
|
||||
if (dx === 0 || dy === 0) {
|
||||
return [start.x, start.y, end.x, end.y];
|
||||
}
|
||||
|
||||
// Create L-shaped path based on which direction is dominant
|
||||
if (dx > dy) {
|
||||
// Horizontal first, then vertical
|
||||
return [
|
||||
start.x, start.y,
|
||||
end.x, start.y,
|
||||
end.x, end.y
|
||||
];
|
||||
} else {
|
||||
// Vertical first, then horizontal
|
||||
return [
|
||||
start.x, start.y,
|
||||
start.x, end.y,
|
||||
end.x, end.y
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
snapToNearestDeviceEdge(x, y) {
|
||||
const deviceWidth = this.deviceManager.deviceWidth;
|
||||
const deviceHeight = this.deviceManager.deviceHeight;
|
||||
|
||||
let nearestEdge = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
// Get all devices from deviceManager
|
||||
this.deviceManager.devices.forEach((device, deviceId) => {
|
||||
const deviceShape = device.shape;
|
||||
if (!deviceShape) return;
|
||||
|
||||
const devicePos = this.getDeviceAbsolutePosition(deviceShape);
|
||||
|
||||
// Calculate all 4 edges
|
||||
const edges = [
|
||||
{
|
||||
type: 'left',
|
||||
x: devicePos.x,
|
||||
y: Math.max(devicePos.y, Math.min(devicePos.y + deviceHeight, y)),
|
||||
line: { x1: devicePos.x, y1: devicePos.y, x2: devicePos.x, y2: devicePos.y + deviceHeight }
|
||||
},
|
||||
{
|
||||
type: 'right',
|
||||
x: devicePos.x + deviceWidth,
|
||||
y: Math.max(devicePos.y, Math.min(devicePos.y + deviceHeight, y)),
|
||||
line: { x1: devicePos.x + deviceWidth, y1: devicePos.y, x2: devicePos.x + deviceWidth, y2: devicePos.y + deviceHeight }
|
||||
},
|
||||
{
|
||||
type: 'top',
|
||||
x: Math.max(devicePos.x, Math.min(devicePos.x + deviceWidth, x)),
|
||||
y: devicePos.y,
|
||||
line: { x1: devicePos.x, y1: devicePos.y, x2: devicePos.x + deviceWidth, y2: devicePos.y }
|
||||
},
|
||||
{
|
||||
type: 'bottom',
|
||||
x: Math.max(devicePos.x, Math.min(devicePos.x + deviceWidth, x)),
|
||||
y: devicePos.y + deviceHeight,
|
||||
line: { x1: devicePos.x, y1: devicePos.y + deviceHeight, x2: devicePos.x + deviceWidth, y2: devicePos.y + deviceHeight }
|
||||
}
|
||||
];
|
||||
|
||||
// Find nearest edge point
|
||||
edges.forEach(edge => {
|
||||
const dist = Math.sqrt(Math.pow(edge.x - x, 2) + Math.pow(edge.y - y, 2));
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
nearestEdge = { x: edge.x, y: edge.y };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Return snapped position or original if no devices found
|
||||
return nearestEdge || { x, y };
|
||||
}
|
||||
|
||||
getEdgeConnectionPoints(sourcePos, targetPos, deviceWidth, deviceHeight) {
|
||||
// Calculate center points
|
||||
const sourceCenterX = sourcePos.x + deviceWidth / 2;
|
||||
const sourceCenterY = sourcePos.y + deviceHeight / 2;
|
||||
const targetCenterX = targetPos.x + deviceWidth / 2;
|
||||
const targetCenterY = targetPos.y + deviceHeight / 2;
|
||||
|
||||
const dx = targetCenterX - sourceCenterX;
|
||||
const dy = targetCenterY - sourceCenterY;
|
||||
|
||||
let sourcePoint, targetPoint;
|
||||
|
||||
// Determine which edges to connect based on relative positions
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
// Primarily horizontal - use left/right edges
|
||||
if (dx > 0) {
|
||||
// Source right edge, target left edge
|
||||
sourcePoint = { x: sourcePos.x + deviceWidth, y: sourceCenterY };
|
||||
targetPoint = { x: targetPos.x, y: targetCenterY };
|
||||
} else {
|
||||
// Source left edge, target right edge
|
||||
sourcePoint = { x: sourcePos.x, y: sourceCenterY };
|
||||
targetPoint = { x: targetPos.x + deviceWidth, y: targetCenterY };
|
||||
}
|
||||
} else {
|
||||
// Primarily vertical - use top/bottom edges
|
||||
if (dy > 0) {
|
||||
// Source bottom edge, target top edge
|
||||
sourcePoint = { x: sourceCenterX, y: sourcePos.y + deviceHeight };
|
||||
targetPoint = { x: targetCenterX, y: targetPos.y };
|
||||
} else {
|
||||
// Source top edge, target bottom edge
|
||||
sourcePoint = { x: sourceCenterX, y: sourcePos.y };
|
||||
targetPoint = { x: targetCenterX, y: targetPos.y + deviceHeight };
|
||||
}
|
||||
}
|
||||
|
||||
return { sourcePoint, targetPoint };
|
||||
}
|
||||
|
||||
updateLineFromHandles(line, handles) {
|
||||
if (handles.length < 2) return;
|
||||
|
||||
const startPoint = { x: handles[0].x(), y: handles[0].y() };
|
||||
const endPoint = { x: handles[handles.length - 1].x(), y: handles[handles.length - 1].y() };
|
||||
const waypoints = handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
|
||||
|
||||
const newPoints = this.rebuildOrthogonalPath(startPoint, waypoints, endPoint);
|
||||
line.points(newPoints);
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
|
||||
rebuildOrthogonalPath(start, waypoints, end) {
|
||||
// Simple orthogonal path: just connect all points with right angles
|
||||
const points = [start.x, start.y];
|
||||
|
||||
let prev = start;
|
||||
const allPoints = [...waypoints, end];
|
||||
|
||||
allPoints.forEach(curr => {
|
||||
const dx = Math.abs(curr.x - prev.x);
|
||||
const dy = Math.abs(curr.y - prev.y);
|
||||
|
||||
// Add corner point if needed
|
||||
if (dx > 0 && dy > 0) {
|
||||
// Choose direction based on larger distance
|
||||
if (dx > dy) {
|
||||
points.push(curr.x, prev.y); // Horizontal first
|
||||
} else {
|
||||
points.push(prev.x, curr.y); // Vertical first
|
||||
}
|
||||
}
|
||||
|
||||
points.push(curr.x, curr.y);
|
||||
prev = curr;
|
||||
});
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
getDeviceAbsolutePosition(deviceShape) {
|
||||
if (this.currentView === 'logical') {
|
||||
// In logical view, devices are on main layer with absolute positioning
|
||||
return {
|
||||
x: deviceShape.x(),
|
||||
y: deviceShape.y()
|
||||
};
|
||||
} else {
|
||||
// In physical view, devices are in rack containers with relative positioning
|
||||
const parent = deviceShape.getParent();
|
||||
if (!parent || parent === this.layer) {
|
||||
// Device is on main layer (shouldn't happen in physical view, but handle it)
|
||||
return {
|
||||
x: deviceShape.x(),
|
||||
y: deviceShape.y()
|
||||
};
|
||||
}
|
||||
const rack = parent.getParent();
|
||||
return {
|
||||
x: rack.x() + deviceShape.x(),
|
||||
y: rack.y() + deviceShape.y()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updateAllConnections() {
|
||||
// Update all connection paths
|
||||
this.connections.forEach(conn => {
|
||||
if (!conn.sourceShape || !conn.targetShape) return;
|
||||
|
||||
const deviceWidth = this.deviceManager.deviceWidth;
|
||||
const deviceHeight = this.deviceManager.deviceHeight;
|
||||
|
||||
const sourcePos = this.getDeviceAbsolutePosition(conn.sourceShape);
|
||||
const targetPos = this.getDeviceAbsolutePosition(conn.targetShape);
|
||||
|
||||
const { sourcePoint, targetPoint } = this.getEdgeConnectionPoints(
|
||||
sourcePos, targetPos, deviceWidth, deviceHeight
|
||||
);
|
||||
|
||||
// Update first and last handle positions (start and end)
|
||||
conn.handles[0].position(sourcePoint);
|
||||
conn.handles[conn.handles.length - 1].position(targetPoint);
|
||||
|
||||
// Get middle waypoint positions from handles
|
||||
const waypoints = conn.handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
|
||||
|
||||
// Rebuild path
|
||||
const points = this.rebuildOrthogonalPath(sourcePoint, waypoints, targetPoint);
|
||||
|
||||
conn.shape.points(points);
|
||||
});
|
||||
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
|
||||
startConnection(deviceId, deviceShape) {
|
||||
// Cancel any existing connection mode
|
||||
if (this.tempLine) {
|
||||
this.tempLine.destroy();
|
||||
this.tempLine = null;
|
||||
}
|
||||
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (!deviceData) return;
|
||||
|
||||
const deviceWidth = this.deviceManager.deviceWidth;
|
||||
const deviceHeight = this.deviceManager.deviceHeight;
|
||||
const deviceAbsPos = this.getDeviceAbsolutePosition(deviceShape);
|
||||
|
||||
// Start from device center (will be adjusted to edge when connection completes)
|
||||
const startPoint = {
|
||||
x: deviceAbsPos.x + deviceWidth / 2,
|
||||
y: deviceAbsPos.y + deviceHeight / 2
|
||||
};
|
||||
|
||||
// Create temporary line
|
||||
this.tempLine = new Konva.Line({
|
||||
points: [startPoint.x, startPoint.y, startPoint.x, startPoint.y],
|
||||
stroke: '#000000',
|
||||
strokeWidth: 1,
|
||||
dash: [10, 5],
|
||||
listening: false
|
||||
});
|
||||
|
||||
this.connectionLayer.add(this.tempLine);
|
||||
this.tempLine.moveToTop();
|
||||
|
||||
this.pendingConnection = {
|
||||
sourceDeviceId: deviceId,
|
||||
sourceDeviceShape: deviceShape,
|
||||
sourceDeviceData: deviceData,
|
||||
startPoint: startPoint,
|
||||
deviceAbsPos: deviceAbsPos
|
||||
};
|
||||
|
||||
// Update temp line on mouse move
|
||||
const stage = this.connectionLayer.getStage();
|
||||
stage.on('mousemove.connection', () => {
|
||||
if (this.tempLine && this.pendingConnection) {
|
||||
const pos = stage.getPointerPosition();
|
||||
|
||||
const scale = stage.scaleX();
|
||||
const stagePos = stage.position();
|
||||
const worldX = (pos.x - stagePos.x) / scale;
|
||||
const worldY = (pos.y - stagePos.y) / scale;
|
||||
|
||||
// Calculate which edge to use based on cursor position
|
||||
const dx = worldX - (this.pendingConnection.deviceAbsPos.x + deviceWidth / 2);
|
||||
const dy = worldY - (this.pendingConnection.deviceAbsPos.y + deviceHeight / 2);
|
||||
|
||||
let edgePoint;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
// Use left or right edge
|
||||
if (dx > 0) {
|
||||
edgePoint = {
|
||||
x: this.pendingConnection.deviceAbsPos.x + deviceWidth,
|
||||
y: this.pendingConnection.deviceAbsPos.y + deviceHeight / 2
|
||||
};
|
||||
} else {
|
||||
edgePoint = {
|
||||
x: this.pendingConnection.deviceAbsPos.x,
|
||||
y: this.pendingConnection.deviceAbsPos.y + deviceHeight / 2
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Use top or bottom edge
|
||||
if (dy > 0) {
|
||||
edgePoint = {
|
||||
x: this.pendingConnection.deviceAbsPos.x + deviceWidth / 2,
|
||||
y: this.pendingConnection.deviceAbsPos.y + deviceHeight
|
||||
};
|
||||
} else {
|
||||
edgePoint = {
|
||||
x: this.pendingConnection.deviceAbsPos.x + deviceWidth / 2,
|
||||
y: this.pendingConnection.deviceAbsPos.y
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.tempLine.points([edgePoint.x, edgePoint.y, worldX, worldY]);
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async completeConnection(targetDeviceId, targetDeviceShape) {
|
||||
if (!this.pendingConnection) return;
|
||||
|
||||
const sourceDeviceId = this.pendingConnection.sourceDeviceId;
|
||||
|
||||
if (sourceDeviceId === targetDeviceId) {
|
||||
this.cancelConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceUsedPorts = await this.api.getUsedPorts(sourceDeviceId);
|
||||
const targetUsedPorts = await this.api.getUsedPorts(targetDeviceId);
|
||||
|
||||
const sourcePort = this.getNextAvailablePort(this.pendingConnection.sourceDeviceData, sourceUsedPorts);
|
||||
const targetData = this.deviceManager.getDeviceData(targetDeviceId);
|
||||
const targetPort = this.getNextAvailablePort(targetData, targetUsedPorts);
|
||||
|
||||
if (sourcePort === null || targetPort === null) {
|
||||
this.cancelConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
const connData = await this.api.createConnection(
|
||||
sourceDeviceId,
|
||||
sourcePort,
|
||||
targetDeviceId,
|
||||
targetPort
|
||||
);
|
||||
|
||||
const connections = await this.api.getConnections();
|
||||
const newConn = connections.find(c => c.id === connData.id);
|
||||
|
||||
if (newConn) {
|
||||
this.createConnectionLine(newConn);
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to create connection:', err);
|
||||
}
|
||||
|
||||
this.cancelConnection();
|
||||
}
|
||||
|
||||
getNextAvailablePort(deviceData, usedPorts) {
|
||||
const portsCount = deviceData.ports_count || 24;
|
||||
for (let port = 1; port <= portsCount; port++) {
|
||||
if (!usedPorts.includes(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
cancelConnection() {
|
||||
if (this.tempLine) {
|
||||
this.tempLine.destroy();
|
||||
this.tempLine = null;
|
||||
}
|
||||
|
||||
const stage = this.connectionLayer.getStage();
|
||||
if (stage) {
|
||||
stage.off('mousemove.connection');
|
||||
}
|
||||
|
||||
this.pendingConnection = null;
|
||||
this.connectionLayer.batchDraw();
|
||||
}
|
||||
|
||||
async deleteConnection(connId, line, handles) {
|
||||
try {
|
||||
await this.api.deleteConnection(connId);
|
||||
line.destroy();
|
||||
handles.forEach(h => h.destroy());
|
||||
this.connections.delete(connId);
|
||||
|
||||
// Clear selection if this was the selected connection
|
||||
if (this.selectedConnection === connId) {
|
||||
this.selectedConnection = null;
|
||||
}
|
||||
|
||||
this.connectionLayer.batchDraw();
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete connection:', err);
|
||||
}
|
||||
}
|
||||
|
||||
showConnectionContextMenu(e, connData, line, handles) {
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
const contextMenuList = document.getElementById('contextMenuList');
|
||||
|
||||
const sourceDevice = this.deviceManager.getDeviceData(connData.source_device_id);
|
||||
const targetDevice = this.deviceManager.getDeviceData(connData.target_device_id);
|
||||
|
||||
contextMenuList.innerHTML = `
|
||||
<li class="menu-header">Connection</li>
|
||||
<li style="cursor: default; padding: 8px 20px; font-size: 12px; color: #666;">
|
||||
${sourceDevice.name}:${connData.source_port} ↔ ${targetDevice.name}:${connData.target_port}
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li data-action="delete">Delete Connection</li>
|
||||
`;
|
||||
|
||||
contextMenu.style.left = `${e.evt.pageX}px`;
|
||||
contextMenu.style.top = `${e.evt.pageY}px`;
|
||||
contextMenu.classList.remove('hidden');
|
||||
|
||||
const handleAction = (evt) => {
|
||||
const action = evt.target.dataset.action;
|
||||
if (action === 'delete') {
|
||||
if (confirm('Delete this connection?')) {
|
||||
this.deleteConnection(connData.id, line, handles);
|
||||
}
|
||||
}
|
||||
contextMenu.classList.add('hidden');
|
||||
contextMenuList.removeEventListener('click', handleAction);
|
||||
};
|
||||
|
||||
contextMenuList.addEventListener('click', handleAction);
|
||||
}
|
||||
}
|
||||
513
archive/old_public/js/device-manager.js
Normal file
513
archive/old_public/js/device-manager.js
Normal file
@@ -0,0 +1,513 @@
|
||||
export class DeviceManager {
|
||||
constructor(layer, api, rackManager) {
|
||||
this.layer = layer;
|
||||
this.api = api;
|
||||
this.rackManager = rackManager;
|
||||
this.devices = new Map();
|
||||
this.deviceTypes = [];
|
||||
this.deviceHeight = 30;
|
||||
this.deviceSpacing = 5;
|
||||
this.deviceWidth = 500; // Physical view width
|
||||
this.currentView = 'physical'; // Track current view
|
||||
}
|
||||
|
||||
async loadDeviceTypes() {
|
||||
try {
|
||||
this.deviceTypes = await this.api.getDeviceTypes();
|
||||
} catch (err) {
|
||||
console.error('Failed to load device types:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDevices() {
|
||||
try {
|
||||
const devices = await this.api.getDevices();
|
||||
devices.forEach(deviceData => {
|
||||
this.createDeviceShape(deviceData);
|
||||
});
|
||||
this.layer.batchDraw();
|
||||
} catch (err) {
|
||||
console.error('Failed to load devices:', err);
|
||||
}
|
||||
}
|
||||
|
||||
createDeviceShape(deviceData) {
|
||||
const rackShape = this.rackManager.getRackShape(deviceData.rack_id);
|
||||
if (!rackShape) {
|
||||
console.error('Rack not found for device:', deviceData);
|
||||
return;
|
||||
}
|
||||
|
||||
const devicesContainer = rackShape.findOne('.devices-container');
|
||||
|
||||
// Convert slot position (1-42) to visual Y position
|
||||
// Slot 1 (U1) is at the bottom, slot 42 (U42) is at the top
|
||||
const rackData = this.rackManager.getRackData(deviceData.rack_id);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const maxSlots = 42;
|
||||
|
||||
// Calculate device height based on rack_units
|
||||
const rackUnits = deviceData.rack_units || 1;
|
||||
const deviceHeight = (this.deviceHeight * rackUnits) + (this.deviceSpacing * (rackUnits - 1));
|
||||
|
||||
// Calculate Y position using helper method
|
||||
const y = this.calculateDeviceY(deviceData.position, rackUnits, rackHeight);
|
||||
|
||||
const group = new Konva.Group({
|
||||
x: 10,
|
||||
y: y,
|
||||
draggable: !this.rackManager.racksLocked, // Draggable when racks are unlocked
|
||||
id: `device-${deviceData.id}`
|
||||
});
|
||||
|
||||
// Device rectangle
|
||||
const rect = new Konva.Rect({
|
||||
width: this.deviceWidth,
|
||||
height: deviceHeight,
|
||||
fill: deviceData.color || '#4A90E2',
|
||||
stroke: '#333',
|
||||
strokeWidth: 1,
|
||||
cornerRadius: 4,
|
||||
name: 'device-rect'
|
||||
});
|
||||
|
||||
// Device name
|
||||
const text = new Konva.Text({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.deviceWidth,
|
||||
height: deviceHeight,
|
||||
text: deviceData.name,
|
||||
fontSize: 14,
|
||||
fontStyle: 'bold',
|
||||
fill: '#fff',
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
padding: 5,
|
||||
name: 'device-text'
|
||||
});
|
||||
|
||||
// Make name clickable for renaming
|
||||
text.on('click', (e) => {
|
||||
e.cancelBubble = true; // Prevent group drag
|
||||
window.dispatchEvent(new CustomEvent('rename-device', {
|
||||
detail: { deviceId: deviceData.id, deviceData, deviceShape: group }
|
||||
}));
|
||||
});
|
||||
|
||||
text.on('mouseenter', () => {
|
||||
document.body.style.cursor = 'text';
|
||||
text.fontStyle('bold italic');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
text.on('mouseleave', () => {
|
||||
document.body.style.cursor = 'default';
|
||||
text.fontStyle('bold');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
group.add(rect);
|
||||
group.add(text);
|
||||
|
||||
// Drag and drop between racks
|
||||
group.on('dragstart', () => {
|
||||
// Store original parent and position
|
||||
group.setAttr('originalParent', group.getParent());
|
||||
group.setAttr('originalPosition', group.position());
|
||||
|
||||
// Move to main layer to be on top of everything
|
||||
const absolutePos = group.getAbsolutePosition();
|
||||
group.moveTo(this.layer);
|
||||
group.setAbsolutePosition(absolutePos);
|
||||
group.moveToTop();
|
||||
group.opacity(0.7);
|
||||
});
|
||||
|
||||
group.on('dragend', async () => {
|
||||
group.opacity(1);
|
||||
await this.handleDeviceDrop(deviceData.id, group);
|
||||
});
|
||||
|
||||
// Right-click context menu
|
||||
group.on('contextmenu', (e) => {
|
||||
e.evt.preventDefault();
|
||||
this.showDeviceContextMenu(e, deviceData, group);
|
||||
});
|
||||
|
||||
devicesContainer.add(group);
|
||||
this.devices.set(deviceData.id, { data: deviceData, shape: group });
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
async addDevice(deviceTypeId, rackId, position, name) {
|
||||
try {
|
||||
// Generate unique name if needed
|
||||
const uniqueName = this.generateUniqueName(name);
|
||||
|
||||
const response = await this.api.createDevice(deviceTypeId, rackId, position, uniqueName);
|
||||
|
||||
// Reload devices to get full data
|
||||
const devices = await this.api.getDevices();
|
||||
const newDevice = devices.find(d => d.id === response.id);
|
||||
|
||||
if (newDevice) {
|
||||
this.createDeviceShape(newDevice);
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
return newDevice;
|
||||
} catch (err) {
|
||||
console.error('Failed to add device:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDevice(deviceId, group) {
|
||||
try {
|
||||
await this.api.deleteDevice(deviceId);
|
||||
group.destroy();
|
||||
this.devices.delete(deviceId);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete device:', err);
|
||||
}
|
||||
}
|
||||
|
||||
showDeviceContextMenu(e, deviceData, group) {
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
const contextMenuList = document.getElementById('contextMenuList');
|
||||
|
||||
contextMenuList.innerHTML = `
|
||||
<li data-action="connect">Create Connection</li>
|
||||
<li class="divider"></li>
|
||||
<li data-action="delete">Delete Device</li>
|
||||
`;
|
||||
|
||||
contextMenu.style.left = `${e.evt.pageX}px`;
|
||||
contextMenu.style.top = `${e.evt.pageY}px`;
|
||||
contextMenu.classList.remove('hidden');
|
||||
|
||||
const handleAction = async (evt) => {
|
||||
const action = evt.target.dataset.action;
|
||||
if (action === 'delete') {
|
||||
if (confirm(`Delete device ${deviceData.name}?`)) {
|
||||
this.deleteDevice(deviceData.id, group);
|
||||
}
|
||||
} else if (action === 'connect') {
|
||||
// Trigger connection creation
|
||||
window.dispatchEvent(new CustomEvent('create-connection', {
|
||||
detail: { deviceData, deviceShape: group }
|
||||
}));
|
||||
}
|
||||
contextMenu.classList.add('hidden');
|
||||
contextMenuList.removeEventListener('click', handleAction);
|
||||
};
|
||||
|
||||
contextMenuList.addEventListener('click', handleAction);
|
||||
}
|
||||
|
||||
getNextDevicePosition(rackId) {
|
||||
// Find the lowest available slot (1-42)
|
||||
// U1 is at the bottom, so we fill from bottom to top
|
||||
const usedSlots = new Set();
|
||||
this.devices.forEach(device => {
|
||||
if (device.data.rack_id === rackId) {
|
||||
usedSlots.add(device.data.position);
|
||||
}
|
||||
});
|
||||
|
||||
// Find first available slot starting from U1 (bottom)
|
||||
for (let slot = 1; slot <= 42; slot++) {
|
||||
if (!usedSlots.has(slot)) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
// If all slots are full, return next slot (will overflow)
|
||||
return 43;
|
||||
}
|
||||
|
||||
getDeviceShape(deviceId) {
|
||||
const device = this.devices.get(deviceId);
|
||||
return device ? device.shape : null;
|
||||
}
|
||||
|
||||
getDeviceData(deviceId) {
|
||||
const device = this.devices.get(deviceId);
|
||||
return device ? device.data : null;
|
||||
}
|
||||
|
||||
getAllDevices() {
|
||||
return Array.from(this.devices.values()).map(d => d.data);
|
||||
}
|
||||
|
||||
// Calculate Y position for a device at a given slot with given rack units
|
||||
calculateDeviceY(position, rackUnits = 1, rackHeight = null) {
|
||||
const maxSlots = 42;
|
||||
|
||||
// Use same margin as left/right (10px)
|
||||
const topMargin = 10;
|
||||
|
||||
// Device at position X with N rack units occupies slots X (bottom) to X+N-1 (top)
|
||||
const topSlot = position + (rackUnits - 1);
|
||||
const visualPosition = maxSlots - topSlot;
|
||||
|
||||
return topMargin + (visualPosition * (this.deviceHeight + this.deviceSpacing));
|
||||
}
|
||||
|
||||
// Check if a device at a given position with given rack_units conflicts with other devices
|
||||
// Returns null if no conflict, or a descriptive error message if there is a conflict
|
||||
checkSlotConflict(rackId, position, rackUnits, excludeDeviceId = null) {
|
||||
const slotsOccupied = [];
|
||||
for (let i = 0; i < rackUnits; i++) {
|
||||
slotsOccupied.push(position + i);
|
||||
}
|
||||
|
||||
// Check all devices in the same rack
|
||||
const devicesInRack = Array.from(this.devices.values())
|
||||
.filter(d => d.data.rack_id === rackId && d.data.id !== excludeDeviceId);
|
||||
|
||||
for (const device of devicesInRack) {
|
||||
const deviceRackUnits = device.data.rack_units || 1;
|
||||
const deviceSlotsOccupied = [];
|
||||
for (let i = 0; i < deviceRackUnits; i++) {
|
||||
deviceSlotsOccupied.push(device.data.position + i);
|
||||
}
|
||||
|
||||
// Check for overlap
|
||||
const overlap = slotsOccupied.some(slot => deviceSlotsOccupied.includes(slot));
|
||||
if (overlap) {
|
||||
const conflictSlots = slotsOccupied.filter(slot => deviceSlotsOccupied.includes(slot));
|
||||
return `Device "${device.data.name}" already occupies slot(s) U${conflictSlots.join(', U')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No conflict
|
||||
}
|
||||
|
||||
// Check if a device name already exists (case-insensitive)
|
||||
isDeviceNameTaken(name, excludeDeviceId = null) {
|
||||
const nameLower = name.toLowerCase();
|
||||
return Array.from(this.devices.values()).some(device => {
|
||||
if (excludeDeviceId && device.data.id === excludeDeviceId) {
|
||||
return false; // Exclude the device being renamed
|
||||
}
|
||||
return device.data.name.toLowerCase() === nameLower;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a unique device name by adding _XX suffix
|
||||
generateUniqueName(baseName) {
|
||||
// Remove any existing _XX suffix from the base name
|
||||
const cleanBaseName = baseName.replace(/_\d+$/, '');
|
||||
|
||||
// If the clean name is available, use it
|
||||
if (!this.isDeviceNameTaken(cleanBaseName)) {
|
||||
return cleanBaseName;
|
||||
}
|
||||
|
||||
// Find the highest existing number suffix
|
||||
let maxNumber = 0;
|
||||
const pattern = new RegExp(`^${cleanBaseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}_?(\\d+)$`, 'i');
|
||||
|
||||
Array.from(this.devices.values()).forEach(device => {
|
||||
const match = device.data.name.match(pattern);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]) || 0;
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generate next number with padding
|
||||
const nextNumber = (maxNumber + 1).toString().padStart(2, '0');
|
||||
return `${cleanBaseName}_${nextNumber}`;
|
||||
}
|
||||
|
||||
async handleDeviceDrop(deviceId, deviceShape) {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (!device) return;
|
||||
|
||||
// Get device's center point for more accurate drop detection
|
||||
const absolutePos = deviceShape.getAbsolutePosition();
|
||||
const deviceCenterX = absolutePos.x + (this.deviceWidth / 2);
|
||||
const deviceCenterY = absolutePos.y + (this.deviceHeight / 2);
|
||||
|
||||
// Find which rack the device is over
|
||||
let targetRack = null;
|
||||
let targetRackId = null;
|
||||
|
||||
this.rackManager.racks.forEach((rack, rackId) => {
|
||||
const rackPos = rack.shape.getAbsolutePosition();
|
||||
const rackWidth = rack.data.width || this.rackManager.rackWidth;
|
||||
const rackHeight = rack.data.height || this.rackManager.rackHeight;
|
||||
|
||||
// Check if device center is within rack bounds
|
||||
if (deviceCenterX >= rackPos.x && deviceCenterX <= rackPos.x + rackWidth &&
|
||||
deviceCenterY >= rackPos.y && deviceCenterY <= rackPos.y + rackHeight) {
|
||||
targetRack = rack;
|
||||
targetRackId = rackId;
|
||||
}
|
||||
});
|
||||
|
||||
// If not over any rack, return device to original position
|
||||
if (!targetRack) {
|
||||
const originalParent = deviceShape.getAttr('originalParent');
|
||||
const originalPosition = deviceShape.getAttr('originalPosition');
|
||||
|
||||
if (originalParent) {
|
||||
deviceShape.moveTo(originalParent);
|
||||
deviceShape.position(originalPosition);
|
||||
}
|
||||
this.layer.batchDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRackId = device.data.rack_id;
|
||||
|
||||
// Calculate position within target rack
|
||||
const rackShape = targetRack.shape;
|
||||
const rackAbsolutePos = rackShape.getAbsolutePosition();
|
||||
const relativeY = absolutePos.y - rackAbsolutePos.y;
|
||||
|
||||
// Convert visual Y to slot position (1-42, where U1 is at bottom)
|
||||
const maxSlots = 42;
|
||||
const visualPosition = Math.round((relativeY - 10) / (this.deviceHeight + this.deviceSpacing));
|
||||
let newPosition = maxSlots - visualPosition; // Invert: bottom (high Y) = low slot number
|
||||
newPosition = Math.max(1, Math.min(42, newPosition)); // Clamp to 1-42
|
||||
|
||||
// Get devices in target rack and check for conflicts
|
||||
const devicesInTargetRack = Array.from(this.devices.values())
|
||||
.filter(d => d.data.rack_id === targetRackId && d.data.id !== deviceId)
|
||||
.sort((a, b) => a.data.position - b.data.position);
|
||||
|
||||
// Find available position
|
||||
let finalPosition = newPosition;
|
||||
const occupiedPositions = new Set(devicesInTargetRack.map(d => d.data.position));
|
||||
|
||||
while (occupiedPositions.has(finalPosition)) {
|
||||
finalPosition++;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update device in database
|
||||
await this.api.request(`/api/devices/${deviceId}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: targetRackId, position: finalPosition })
|
||||
});
|
||||
|
||||
// Update local data
|
||||
device.data.rack_id = targetRackId;
|
||||
device.data.position = finalPosition;
|
||||
|
||||
// Move device to new rack's devices-container
|
||||
const newDevicesContainer = rackShape.findOne('.devices-container');
|
||||
deviceShape.moveTo(newDevicesContainer);
|
||||
|
||||
// Reposition device using helper method
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(targetRackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.calculateDeviceY(finalPosition, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
|
||||
// Compact positions in original rack if different
|
||||
if (originalRackId !== targetRackId) {
|
||||
this.compactRackDevices(originalRackId);
|
||||
}
|
||||
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to move device:', err);
|
||||
// Revert to original position
|
||||
const originalParent = deviceShape.getAttr('originalParent');
|
||||
const originalPosition = deviceShape.getAttr('originalPosition');
|
||||
|
||||
if (originalParent) {
|
||||
deviceShape.moveTo(originalParent);
|
||||
deviceShape.position(originalPosition);
|
||||
}
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
async compactRackDevices(rackId) {
|
||||
// Get all devices in this rack, sorted by position (1-42)
|
||||
const devicesInRack = Array.from(this.devices.values())
|
||||
.filter(d => d.data.rack_id === rackId)
|
||||
.sort((a, b) => a.data.position - b.data.position);
|
||||
|
||||
// Reassign positions to be sequential starting from 1 (U1 = bottom)
|
||||
const updatePromises = [];
|
||||
const maxSlots = 42;
|
||||
|
||||
devicesInRack.forEach((device, index) => {
|
||||
const newSlot = index + 1; // Slots start at 1
|
||||
|
||||
if (device.data.position !== newSlot) {
|
||||
device.data.position = newSlot;
|
||||
|
||||
// Update visual position using helper method
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.calculateDeviceY(newSlot, rackUnits, rackHeight);
|
||||
device.shape.position({ x: 10, y: newY });
|
||||
|
||||
// Update database
|
||||
updatePromises.push(
|
||||
this.api.request(`/api/devices/${device.data.id}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: rackId, position: newSlot })
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
|
||||
updateDevicesDraggability(draggable) {
|
||||
this.devices.forEach(device => {
|
||||
device.shape.draggable(draggable);
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentView(viewType) {
|
||||
this.currentView = viewType;
|
||||
|
||||
// Set device width based on view
|
||||
if (viewType === 'logical') {
|
||||
this.deviceWidth = 200; // Narrower in logical view
|
||||
} else {
|
||||
this.deviceWidth = 500; // Normal width in physical view
|
||||
}
|
||||
|
||||
// Resize all existing devices
|
||||
this.devices.forEach(device => {
|
||||
const rect = device.shape.findOne('.device-rect');
|
||||
const text = device.shape.findOne('.device-text');
|
||||
|
||||
if (rect) {
|
||||
rect.width(this.deviceWidth);
|
||||
}
|
||||
if (text) {
|
||||
text.width(this.deviceWidth);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
488
archive/old_public/js/rack-manager.js
Normal file
488
archive/old_public/js/rack-manager.js
Normal file
@@ -0,0 +1,488 @@
|
||||
export class RackManager {
|
||||
constructor(layer, api, deviceManager) {
|
||||
this.layer = layer;
|
||||
this.api = api;
|
||||
this.deviceManager = deviceManager;
|
||||
this.racks = new Map();
|
||||
this.rackPrefix = 'RACK';
|
||||
this.rackWidth = 520; // Fits 500px wide devices with margins
|
||||
this.rackHeight = 1510; // Fits 42 devices (42 * 35px + margins)
|
||||
this.rackSpacing = 80;
|
||||
this.gridSize = 600; // Default: rack width + spacing
|
||||
this.gridVertical = 1610; // Default: rack height + spacing (1510 + 100)
|
||||
this.racksLocked = true; // Start with racks locked
|
||||
this.nextX = 0; // Start at grid origin
|
||||
this.nextY = 0; // Start at grid origin
|
||||
// Note: loadSpacing() will be called after project ID is set
|
||||
}
|
||||
|
||||
loadSpacing() {
|
||||
const projectId = this.api.currentProjectId;
|
||||
const savedGridSize = localStorage.getItem(`gridSize_${projectId}`);
|
||||
const savedGridVertical = localStorage.getItem(`gridVertical_${projectId}`);
|
||||
|
||||
if (savedGridSize) {
|
||||
this.gridSize = parseInt(savedGridSize);
|
||||
this.rackSpacing = this.gridSize - this.rackWidth;
|
||||
} else {
|
||||
this.gridSize = 600; // Default: rack width + spacing
|
||||
}
|
||||
|
||||
if (savedGridVertical) {
|
||||
this.gridVertical = parseInt(savedGridVertical);
|
||||
} else {
|
||||
this.gridVertical = 1610; // Default: rack height + spacing (fits 42 devices)
|
||||
}
|
||||
}
|
||||
|
||||
saveSpacing() {
|
||||
const projectId = this.api.currentProjectId;
|
||||
localStorage.setItem(`gridSize_${projectId}`, this.gridSize.toString());
|
||||
localStorage.setItem(`gridVertical_${projectId}`, this.gridVertical.toString());
|
||||
}
|
||||
|
||||
async toggleRacksLock() {
|
||||
this.racksLocked = !this.racksLocked;
|
||||
this.racks.forEach(rack => {
|
||||
rack.shape.draggable(!this.racksLocked);
|
||||
});
|
||||
|
||||
// Update device draggability
|
||||
if (this.deviceManager) {
|
||||
this.deviceManager.updateDevicesDraggability(!this.racksLocked);
|
||||
}
|
||||
|
||||
// If locking, compact the grid (remove empty columns from the left)
|
||||
if (this.racksLocked) {
|
||||
await this.compactGrid();
|
||||
}
|
||||
|
||||
return this.racksLocked;
|
||||
}
|
||||
|
||||
async compactGrid() {
|
||||
if (this.racks.size === 0) return;
|
||||
|
||||
// Get all rack positions and calculate their grid coordinates
|
||||
const rackPositions = [];
|
||||
this.racks.forEach((rack, id) => {
|
||||
const gridX = Math.round(rack.data.x / this.gridSize);
|
||||
const gridY = Math.round(rack.data.y / this.gridVertical);
|
||||
rackPositions.push({ id, rack, gridX, gridY });
|
||||
});
|
||||
|
||||
// Find the minimum grid X (leftmost column that has racks)
|
||||
const minGridX = Math.min(...rackPositions.map(r => r.gridX));
|
||||
|
||||
// If minGridX is 0, grid is already compact
|
||||
if (minGridX === 0) return;
|
||||
|
||||
// Shift all racks left by minGridX columns
|
||||
const updatePromises = [];
|
||||
|
||||
for (const rackPos of rackPositions) {
|
||||
const newGridX = rackPos.gridX - minGridX;
|
||||
const newX = newGridX * this.gridSize;
|
||||
const newY = rackPos.gridY * this.gridVertical;
|
||||
|
||||
// Update visual position
|
||||
rackPos.rack.shape.position({ x: newX, y: newY });
|
||||
|
||||
// Update data
|
||||
rackPos.rack.data.x = newX;
|
||||
rackPos.rack.data.y = newY;
|
||||
|
||||
// Queue database update
|
||||
updatePromises.push(this.api.updateRackPosition(rackPos.id, newX, newY));
|
||||
}
|
||||
|
||||
// Redraw once
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Wait for all updates
|
||||
await Promise.all(updatePromises);
|
||||
}
|
||||
|
||||
snapToGrid(value, gridSize) {
|
||||
return Math.round(value / gridSize) * gridSize;
|
||||
}
|
||||
|
||||
async loadRacks() {
|
||||
try {
|
||||
const racks = await this.api.getRacks();
|
||||
racks.forEach(rackData => {
|
||||
this.createRackShape(rackData);
|
||||
});
|
||||
this.layer.batchDraw();
|
||||
} catch (err) {
|
||||
console.error('Failed to load racks:', err);
|
||||
}
|
||||
}
|
||||
|
||||
createRackShape(rackData) {
|
||||
const group = new Konva.Group({
|
||||
x: rackData.x,
|
||||
y: rackData.y,
|
||||
draggable: !this.racksLocked, // Locked by default
|
||||
id: `rack-${rackData.id}`
|
||||
});
|
||||
|
||||
// Rack background
|
||||
const rect = new Konva.Rect({
|
||||
width: rackData.width || this.rackWidth,
|
||||
height: rackData.height || this.rackHeight,
|
||||
fill: '#ffffff',
|
||||
stroke: '#333',
|
||||
strokeWidth: 2,
|
||||
shadowColor: 'black',
|
||||
shadowBlur: 5,
|
||||
shadowOpacity: 0.1,
|
||||
shadowOffset: { x: 2, y: 2 }
|
||||
});
|
||||
|
||||
// Rack name label (clickable)
|
||||
const nameLabel = new Konva.Text({
|
||||
x: 0,
|
||||
y: -30,
|
||||
width: rackData.width || this.rackWidth,
|
||||
text: rackData.name,
|
||||
fontSize: 16,
|
||||
fontStyle: 'bold',
|
||||
fill: '#333',
|
||||
align: 'center',
|
||||
name: 'rack-name'
|
||||
});
|
||||
|
||||
// Make name clickable
|
||||
nameLabel.on('click', () => {
|
||||
window.dispatchEvent(new CustomEvent('rename-rack', {
|
||||
detail: { rackId: rackData.id, rackData, rackShape: group }
|
||||
}));
|
||||
});
|
||||
|
||||
nameLabel.on('mouseenter', () => {
|
||||
document.body.style.cursor = 'pointer';
|
||||
nameLabel.fill('#4A90E2');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
nameLabel.on('mouseleave', () => {
|
||||
document.body.style.cursor = 'default';
|
||||
nameLabel.fill('#333');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
// Container for devices
|
||||
const devicesLayer = new Konva.Group({
|
||||
name: 'devices-container'
|
||||
});
|
||||
|
||||
group.add(rect);
|
||||
group.add(nameLabel);
|
||||
group.add(devicesLayer);
|
||||
|
||||
// Grid snapping during drag
|
||||
group.on('dragmove', () => {
|
||||
const x = this.snapToGrid(group.x(), this.gridSize);
|
||||
const y = this.snapToGrid(group.y(), this.gridVertical);
|
||||
group.position({ x, y });
|
||||
});
|
||||
|
||||
// Drag end - update position in DB with smart positioning
|
||||
group.on('dragend', async () => {
|
||||
try {
|
||||
const newX = this.snapToGrid(group.x(), this.gridSize);
|
||||
const newY = this.snapToGrid(group.y(), this.gridVertical);
|
||||
|
||||
// Check if position is occupied by another rack
|
||||
await this.handleRackPlacement(rackData.id, newX, newY);
|
||||
} catch (err) {
|
||||
console.error('Failed to update rack position:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click context menu
|
||||
group.on('contextmenu', (e) => {
|
||||
e.evt.preventDefault();
|
||||
this.showContextMenu(e, rackData, group);
|
||||
});
|
||||
|
||||
this.layer.add(group);
|
||||
this.racks.set(rackData.id, { data: rackData, shape: group });
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
async addRack() {
|
||||
try {
|
||||
const nextName = await this.api.getNextRackName(this.rackPrefix);
|
||||
|
||||
const rackData = await this.api.createRack(nextName, this.nextX, this.nextY);
|
||||
|
||||
this.createRackShape(rackData);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Update next position (using grid sizes)
|
||||
this.nextX += this.gridSize;
|
||||
if (this.nextX > 1200) {
|
||||
this.nextX = 0;
|
||||
this.nextY += this.gridVertical;
|
||||
}
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
return rackData;
|
||||
} catch (err) {
|
||||
console.error('Failed to add rack:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRack(rackId, group) {
|
||||
try {
|
||||
await this.api.deleteRack(rackId);
|
||||
group.destroy();
|
||||
this.racks.delete(rackId);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete rack:', err);
|
||||
}
|
||||
}
|
||||
|
||||
showContextMenu(e, rackData, group) {
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
const contextMenuList = document.getElementById('contextMenuList');
|
||||
|
||||
const lockText = this.racksLocked ? 'Unlock All Racks' : 'Lock All Racks';
|
||||
|
||||
// Build device types list with header
|
||||
let deviceTypesHTML = '<li class="menu-header">Add device:</li>';
|
||||
if (this.deviceManager && this.deviceManager.deviceTypes) {
|
||||
this.deviceManager.deviceTypes.forEach(type => {
|
||||
deviceTypesHTML += `<li data-action="add-device" data-device-type-id="${type.id}" data-device-type-name="${type.name}">${type.name}</li>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Build unlock/management options
|
||||
let managementHTML = `<li data-action="toggle-lock">${lockText}</li>`;
|
||||
|
||||
// Show delete and spacing controls only when unlocked
|
||||
if (!this.racksLocked) {
|
||||
const horizontalSpacing = this.gridSize - this.rackWidth;
|
||||
const verticalSpacing = this.gridVertical - this.rackHeight;
|
||||
|
||||
managementHTML += `
|
||||
<li data-action="delete">Delete Rack</li>
|
||||
<li class="divider"></li>
|
||||
<li class="spacing-control">
|
||||
<span class="spacing-label">Horizontal spacing: ${horizontalSpacing}px</span>
|
||||
<div class="spacing-buttons">
|
||||
<button class="spacing-btn" data-action="h-spacing-decrease">−</button>
|
||||
<button class="spacing-btn" data-action="h-spacing-increase">+</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="spacing-control">
|
||||
<span class="spacing-label">Vertical spacing: ${verticalSpacing}px</span>
|
||||
<div class="spacing-buttons">
|
||||
<button class="spacing-btn" data-action="v-spacing-decrease">−</button>
|
||||
<button class="spacing-btn" data-action="v-spacing-increase">+</button>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
contextMenuList.innerHTML = `
|
||||
${deviceTypesHTML}
|
||||
<li class="divider"></li>
|
||||
${managementHTML}
|
||||
`;
|
||||
|
||||
contextMenu.style.left = `${e.evt.pageX}px`;
|
||||
contextMenu.style.top = `${e.evt.pageY}px`;
|
||||
contextMenu.classList.remove('hidden');
|
||||
|
||||
const handleAction = async (evt) => {
|
||||
const action = evt.target.dataset.action;
|
||||
|
||||
// For spacing buttons, prevent default and stop propagation
|
||||
if (action && action.includes('spacing')) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
if (action === 'add-device') {
|
||||
const deviceTypeId = parseInt(evt.target.dataset.deviceTypeId);
|
||||
const deviceTypeName = evt.target.dataset.deviceTypeName;
|
||||
const deviceName = prompt(`Enter name for ${deviceTypeName}:`, deviceTypeName);
|
||||
|
||||
if (deviceName) {
|
||||
try {
|
||||
// Check if name will be auto-numbered
|
||||
const uniqueName = this.deviceManager.generateUniqueName(deviceName);
|
||||
if (uniqueName !== deviceName) {
|
||||
const proceed = confirm(`Device name "${deviceName}" is already in use.\n\nDevice will be named "${uniqueName}" instead.\n\nContinue?`);
|
||||
if (!proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const position = this.deviceManager.getNextDevicePosition(rackData.id);
|
||||
await this.deviceManager.addDevice(deviceTypeId, rackData.id, position, deviceName);
|
||||
} catch (err) {
|
||||
alert('Failed to add device: ' + err.message);
|
||||
}
|
||||
}
|
||||
} else if (action === 'delete') {
|
||||
if (confirm(`Delete rack ${rackData.name}?`)) {
|
||||
this.deleteRack(rackData.id, group);
|
||||
}
|
||||
} else if (action === 'toggle-lock') {
|
||||
const isLocked = await this.toggleRacksLock();
|
||||
const statusText = isLocked ? 'Racks locked (grid compacted)' : 'Racks unlocked';
|
||||
// Close and reopen menu to refresh the lock state
|
||||
contextMenu.classList.add('hidden');
|
||||
setTimeout(() => {
|
||||
this.showContextMenu(e, rackData, group);
|
||||
}, 10);
|
||||
return; // Don't close menu handler
|
||||
} else if (action === 'h-spacing-increase') {
|
||||
await this.adjustSpacing('horizontal', 10);
|
||||
return; // Don't close menu
|
||||
} else if (action === 'h-spacing-decrease') {
|
||||
await this.adjustSpacing('horizontal', -10);
|
||||
return; // Don't close menu
|
||||
} else if (action === 'v-spacing-increase') {
|
||||
await this.adjustSpacing('vertical', 50);
|
||||
return; // Don't close menu
|
||||
} else if (action === 'v-spacing-decrease') {
|
||||
await this.adjustSpacing('vertical', -50);
|
||||
return; // Don't close menu
|
||||
}
|
||||
|
||||
contextMenu.classList.add('hidden');
|
||||
contextMenuList.removeEventListener('click', handleAction);
|
||||
};
|
||||
|
||||
contextMenuList.addEventListener('click', handleAction);
|
||||
}
|
||||
|
||||
getRackShape(rackId) {
|
||||
const rack = this.racks.get(rackId);
|
||||
return rack ? rack.shape : null;
|
||||
}
|
||||
|
||||
getRackData(rackId) {
|
||||
const rack = this.racks.get(rackId);
|
||||
return rack ? rack.data : null;
|
||||
}
|
||||
|
||||
async handleRackPlacement(movedRackId, newX, newY) {
|
||||
// Get all racks in the same row (same Y coordinate)
|
||||
const racksInRow = [];
|
||||
this.racks.forEach((rack, id) => {
|
||||
if (id !== movedRackId && rack.data.y === newY) {
|
||||
racksInRow.push({ id, rack, x: rack.data.x });
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by X position
|
||||
racksInRow.sort((a, b) => a.x - b.x);
|
||||
|
||||
// Check if new position is occupied
|
||||
const occupiedRack = racksInRow.find(r => r.x === newX);
|
||||
|
||||
if (occupiedRack) {
|
||||
// Position is occupied - shift all racks at and to the right of this position
|
||||
const racksToShift = racksInRow.filter(r => r.x >= newX);
|
||||
|
||||
// Shift each rack one grid position to the right
|
||||
for (const rackInfo of racksToShift) {
|
||||
const newRackX = rackInfo.x + this.gridSize;
|
||||
|
||||
// Update visual position
|
||||
rackInfo.rack.shape.position({ x: newRackX, y: newY });
|
||||
|
||||
// Update data
|
||||
rackInfo.rack.data.x = newRackX;
|
||||
rackInfo.rack.data.y = newY;
|
||||
|
||||
// Update in database
|
||||
await this.api.updateRackPosition(rackInfo.id, newRackX, newY);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the moved rack
|
||||
const movedRack = this.racks.get(movedRackId);
|
||||
if (movedRack) {
|
||||
movedRack.shape.position({ x: newX, y: newY });
|
||||
movedRack.data.x = newX;
|
||||
movedRack.data.y = newY;
|
||||
await this.api.updateRackPosition(movedRackId, newX, newY);
|
||||
}
|
||||
|
||||
// Redraw
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
|
||||
async adjustSpacing(direction, delta) {
|
||||
// Calculate grid coordinates for all racks BEFORE changing spacing
|
||||
const rackGridPositions = new Map();
|
||||
|
||||
this.racks.forEach((rack, id) => {
|
||||
const gridX = Math.round(rack.data.x / this.gridSize);
|
||||
const gridY = Math.round(rack.data.y / this.gridVertical);
|
||||
rackGridPositions.set(id, { gridX, gridY });
|
||||
});
|
||||
|
||||
// Adjust spacing (this updates the grid references)
|
||||
if (direction === 'horizontal') {
|
||||
const newSpacing = (this.gridSize - this.rackWidth) + delta;
|
||||
if (newSpacing < 10) return; // Minimum spacing
|
||||
this.gridSize = this.rackWidth + newSpacing;
|
||||
this.rackSpacing = newSpacing; // Update the spacing value
|
||||
} else {
|
||||
const newSpacing = (this.gridVertical - this.rackHeight) + delta;
|
||||
if (newSpacing < 10) return; // Minimum spacing
|
||||
this.gridVertical = this.rackHeight + newSpacing;
|
||||
}
|
||||
|
||||
// Batch all position updates
|
||||
const updatePromises = [];
|
||||
|
||||
// Recalculate all rack positions at once
|
||||
for (const [id, gridPos] of rackGridPositions) {
|
||||
const rack = this.racks.get(id);
|
||||
if (!rack) continue;
|
||||
|
||||
const newX = gridPos.gridX * this.gridSize;
|
||||
const newY = gridPos.gridY * this.gridVertical;
|
||||
|
||||
// Update visual position
|
||||
rack.shape.position({ x: newX, y: newY });
|
||||
|
||||
// Update data
|
||||
rack.data.x = newX;
|
||||
rack.data.y = newY;
|
||||
|
||||
// Queue database update (don't await yet)
|
||||
updatePromises.push(this.api.updateRackPosition(id, newX, newY));
|
||||
}
|
||||
|
||||
// Redraw once for all changes
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Wait for all database updates to complete
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Save spacing to localStorage
|
||||
this.saveSpacing();
|
||||
|
||||
// Update status
|
||||
const horizontalSpacing = this.gridSize - this.rackWidth;
|
||||
const verticalSpacing = this.gridVertical - this.rackHeight;
|
||||
}
|
||||
}
|
||||
792
archive/old_public/js/table-manager.js
Normal file
792
archive/old_public/js/table-manager.js
Normal file
@@ -0,0 +1,792 @@
|
||||
export class TableManager {
|
||||
constructor(api, rackManager, deviceManager, connectionManager) {
|
||||
this.api = api;
|
||||
this.rackManager = rackManager;
|
||||
this.deviceManager = deviceManager;
|
||||
this.connectionManager = connectionManager;
|
||||
|
||||
this.currentTable = null; // 'racks', 'devices', 'connections'
|
||||
this.gridApi = null;
|
||||
this.gridColumnApi = null;
|
||||
this.tableContainer = document.getElementById('tableContent');
|
||||
}
|
||||
|
||||
isTableVisible() {
|
||||
return this.currentTable !== null;
|
||||
}
|
||||
|
||||
getCurrentTableType() {
|
||||
return this.currentTable;
|
||||
}
|
||||
|
||||
// Show specific table view
|
||||
async showTable(tableType) {
|
||||
// tableType can be: 'racks-table', 'devices-table', 'connections-table'
|
||||
const tableMap = {
|
||||
'racks-table': 'racks',
|
||||
'devices-table': 'devices',
|
||||
'connections-table': 'connections'
|
||||
};
|
||||
|
||||
this.currentTable = tableMap[tableType];
|
||||
|
||||
// Clear existing grid
|
||||
if (this.gridApi) {
|
||||
this.gridApi.destroy();
|
||||
this.gridApi = null;
|
||||
}
|
||||
|
||||
// Render appropriate table
|
||||
switch (this.currentTable) {
|
||||
case 'racks':
|
||||
await this.showRacksTable();
|
||||
break;
|
||||
case 'devices':
|
||||
await this.showDevicesTable();
|
||||
break;
|
||||
case 'connections':
|
||||
await this.showConnectionsTable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hideTable() {
|
||||
if (this.gridApi) {
|
||||
this.gridApi.destroy();
|
||||
this.gridApi = null;
|
||||
}
|
||||
this.currentTable = null;
|
||||
this.tableContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// ===== RACKS TABLE =====
|
||||
async showRacksTable() {
|
||||
const racks = await this.api.getRacks();
|
||||
|
||||
// Sort alphabetically by name
|
||||
const sortedRacks = racks.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const columnDefs = [
|
||||
{
|
||||
headerName: 'Rack Name',
|
||||
field: 'name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true
|
||||
},
|
||||
{
|
||||
headerName: 'Position X',
|
||||
field: 'x',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${Math.round(params.value)}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Position Y',
|
||||
field: 'y',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${Math.round(params.value)}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Width',
|
||||
field: 'width',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${params.value}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Height',
|
||||
field: 'height',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${params.value}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Device Count',
|
||||
field: 'deviceCount',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueGetter: params => {
|
||||
// Count devices in this rack
|
||||
const devices = this.deviceManager.getAllDevices();
|
||||
return devices.filter(d => d.rack_id === params.data.id).length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
columnDefs: columnDefs,
|
||||
rowData: sortedRacks,
|
||||
rowSelection: 'multiple',
|
||||
animateRows: true,
|
||||
enableCellTextSelection: true,
|
||||
defaultColDef: {
|
||||
flex: 1,
|
||||
minWidth: 100,
|
||||
resizable: true
|
||||
},
|
||||
onCellValueChanged: (params) => this.onRackCellValueChanged(params),
|
||||
onSelectionChanged: () => this.updateToolbarButtons()
|
||||
};
|
||||
|
||||
this.gridApi = agGrid.createGrid(this.tableContainer, gridOptions);
|
||||
}
|
||||
|
||||
async onRackCellValueChanged(params) {
|
||||
const rackId = params.data.id;
|
||||
const field = params.colDef.field;
|
||||
const newValue = params.newValue;
|
||||
|
||||
try {
|
||||
if (field === 'name') {
|
||||
await this.api.updateRackName(rackId, newValue);
|
||||
|
||||
// Update canvas
|
||||
const rackShape = this.rackManager.getRackShape(rackId);
|
||||
if (rackShape) {
|
||||
const nameLabel = rackShape.findOne('.rack-name');
|
||||
if (nameLabel) {
|
||||
nameLabel.text(newValue);
|
||||
this.rackManager.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Update local data
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
if (rackData) {
|
||||
rackData.name = newValue;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update rack:', err);
|
||||
alert('Failed to update rack: ' + err.message);
|
||||
// Revert the change
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DEVICES TABLE =====
|
||||
async showDevicesTable() {
|
||||
const devices = await this.api.getDevices();
|
||||
const racks = await this.api.getRacks();
|
||||
const deviceTypes = await this.api.getDeviceTypes();
|
||||
|
||||
const columnDefs = [
|
||||
{
|
||||
headerName: 'Device Name',
|
||||
field: 'name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true
|
||||
},
|
||||
{
|
||||
headerName: 'Type',
|
||||
field: 'type_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: deviceTypes.map(t => t.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Rack',
|
||||
field: 'rack_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: racks.map(r => r.name)
|
||||
},
|
||||
valueGetter: params => {
|
||||
const rack = racks.find(r => r.id === params.data.rack_id);
|
||||
return rack ? rack.name : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Slot/Position',
|
||||
field: 'position',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `U${params.value}`,
|
||||
cellEditor: 'agNumberCellEditor',
|
||||
cellEditorParams: {
|
||||
min: 1,
|
||||
max: 42,
|
||||
precision: 0
|
||||
},
|
||||
valueSetter: params => {
|
||||
const newValue = parseInt(params.newValue);
|
||||
if (newValue >= 1 && newValue <= 42) {
|
||||
params.data.position = newValue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Form Factor',
|
||||
field: 'rack_units',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `${params.value || 1}U`,
|
||||
cellEditor: 'agNumberCellEditor',
|
||||
cellEditorParams: {
|
||||
min: 1,
|
||||
max: 42,
|
||||
precision: 0
|
||||
},
|
||||
valueSetter: params => {
|
||||
const newValue = parseInt(params.newValue);
|
||||
if (newValue >= 1 && newValue <= 42) {
|
||||
params.data.rack_units = newValue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Ports',
|
||||
field: 'ports_count',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter'
|
||||
},
|
||||
{
|
||||
headerName: 'Color',
|
||||
field: 'color',
|
||||
editable: false,
|
||||
sortable: false,
|
||||
cellRenderer: params => {
|
||||
return `<div style="width: 100%; height: 100%; background-color: ${params.value}; border-radius: 3px;"></div>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Connections',
|
||||
field: 'connectionCount',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueGetter: params => {
|
||||
// Count connections for this device
|
||||
const connections = Array.from(this.connectionManager.connections.values());
|
||||
return connections.filter(c =>
|
||||
c.data.source_device_id === params.data.id ||
|
||||
c.data.target_device_id === params.data.id
|
||||
).length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
columnDefs: columnDefs,
|
||||
rowData: devices,
|
||||
rowSelection: 'multiple',
|
||||
animateRows: true,
|
||||
enableCellTextSelection: true,
|
||||
defaultColDef: {
|
||||
flex: 1,
|
||||
minWidth: 100,
|
||||
resizable: true
|
||||
},
|
||||
onCellValueChanged: (params) => this.onDeviceCellValueChanged(params, racks, deviceTypes),
|
||||
onSelectionChanged: () => this.updateToolbarButtons()
|
||||
};
|
||||
|
||||
this.gridApi = agGrid.createGrid(this.tableContainer, gridOptions);
|
||||
}
|
||||
|
||||
async onDeviceCellValueChanged(params, racks, deviceTypes) {
|
||||
const deviceId = params.data.id;
|
||||
const field = params.colDef.field;
|
||||
const newValue = params.newValue;
|
||||
|
||||
try {
|
||||
if (field === 'name') {
|
||||
// Check if name is already taken
|
||||
if (this.deviceManager.isDeviceNameTaken(newValue, deviceId)) {
|
||||
alert(`Device name "${newValue}" is already in use. Please choose a different name.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.updateDeviceName(deviceId, newValue);
|
||||
|
||||
// Update canvas
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
if (deviceShape) {
|
||||
const nameLabel = deviceShape.findOne('.device-text');
|
||||
if (nameLabel) {
|
||||
nameLabel.text(newValue);
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Update local data
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceData) {
|
||||
deviceData.name = newValue;
|
||||
}
|
||||
} else if (field === 'rack_name') {
|
||||
// Find the rack by name
|
||||
const rack = racks.find(r => r.name === newValue);
|
||||
if (rack) {
|
||||
const newPosition = this.deviceManager.getNextDevicePosition(rack.id);
|
||||
await this.api.request(`/api/devices/${deviceId}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: rack.id, position: newPosition })
|
||||
});
|
||||
|
||||
// Update device on canvas
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceShape && deviceData) {
|
||||
const oldRackId = deviceData.rack_id;
|
||||
deviceData.rack_id = rack.id;
|
||||
deviceData.position = newPosition;
|
||||
|
||||
// Move to new rack's container
|
||||
const newRackShape = this.rackManager.getRackShape(rack.id);
|
||||
if (newRackShape) {
|
||||
const newDevicesContainer = newRackShape.findOne('.devices-container');
|
||||
deviceShape.moveTo(newDevicesContainer);
|
||||
|
||||
// Calculate visual position using helper method
|
||||
const rackUnits = deviceData.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(rack.id);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.deviceManager.calculateDeviceY(newPosition, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
|
||||
// Compact old rack
|
||||
if (oldRackId !== rack.id) {
|
||||
this.deviceManager.compactRackDevices(oldRackId);
|
||||
}
|
||||
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh table to show updated position
|
||||
this.refreshTable();
|
||||
}
|
||||
} else if (field === 'position') {
|
||||
const rackId = params.data.rack_id;
|
||||
const newSlot = parseInt(newValue);
|
||||
const rackUnits = params.data.rack_units || 1;
|
||||
|
||||
// Validate slot range (1-42)
|
||||
if (newSlot < 1 || newSlot > 42) {
|
||||
alert('Slot position must be between U1 and U42');
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that device with its rack_units fits in the rack
|
||||
if (newSlot + rackUnits - 1 > 42) {
|
||||
alert(`Device with ${rackUnits}U form factor cannot fit at position U${newSlot}. Maximum position is U${43 - rackUnits}.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for slot conflicts with other devices
|
||||
const conflict = this.deviceManager.checkSlotConflict(rackId, newSlot, rackUnits, deviceId);
|
||||
if (conflict) {
|
||||
alert(`Slot conflict detected: ${conflict}`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.request(`/api/devices/${deviceId}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: rackId, position: newSlot })
|
||||
});
|
||||
|
||||
// Update device position on canvas using helper method
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceShape && deviceData) {
|
||||
deviceData.position = newSlot;
|
||||
const rackUnits = deviceData.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.deviceManager.calculateDeviceY(newSlot, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
} else if (field === 'rack_units') {
|
||||
const rackId = params.data.rack_id;
|
||||
const position = params.data.position;
|
||||
const newRackUnits = parseInt(newValue);
|
||||
|
||||
// Validate that device with its new rack_units fits in the rack
|
||||
if (position + newRackUnits - 1 > 42) {
|
||||
alert(`Device with ${newRackUnits}U form factor cannot fit at position U${position}. Maximum form factor at this position is ${43 - position}U.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for slot conflicts with other devices
|
||||
const conflict = this.deviceManager.checkSlotConflict(rackId, position, newRackUnits, deviceId);
|
||||
if (conflict) {
|
||||
alert(`Slot conflict detected: ${conflict}`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.request(`/api/devices/${deviceId}/rack-units`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackUnits: newRackUnits })
|
||||
});
|
||||
|
||||
// Update device rendering on canvas
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceShape && deviceData) {
|
||||
deviceData.rack_units = newRackUnits;
|
||||
|
||||
// Update device height
|
||||
const newHeight = (this.deviceManager.deviceHeight * newRackUnits) + (this.deviceManager.deviceSpacing * (newRackUnits - 1));
|
||||
const rect = deviceShape.findOne('Rect');
|
||||
const text = deviceShape.findOne('.device-text');
|
||||
if (rect) {
|
||||
rect.height(newHeight);
|
||||
}
|
||||
if (text) {
|
||||
text.height(newHeight);
|
||||
}
|
||||
|
||||
// Reposition device since height changed using helper method
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.deviceManager.calculateDeviceY(position, newRackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
|
||||
// Notify canvas that data changed
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
} else if (field === 'type_name') {
|
||||
// Find device type by name
|
||||
const deviceType = deviceTypes.find(dt => dt.name === newValue);
|
||||
if (deviceType) {
|
||||
// Note: We would need an API endpoint to update device type
|
||||
// For now, just show a message
|
||||
alert('Changing device type requires updating the device_type_id in the database. This feature needs backend support.');
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update device:', err);
|
||||
alert('Failed to update device: ' + err.message);
|
||||
// Revert the change
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CONNECTIONS TABLE =====
|
||||
async showConnectionsTable() {
|
||||
const connections = await this.api.getConnections();
|
||||
const devices = await this.api.getDevices();
|
||||
|
||||
// Enrich connection data with device names
|
||||
const enrichedConnections = connections.map(conn => {
|
||||
const sourceDevice = devices.find(d => d.id === conn.source_device_id);
|
||||
const targetDevice = devices.find(d => d.id === conn.target_device_id);
|
||||
|
||||
return {
|
||||
...conn,
|
||||
source_device_name: sourceDevice ? sourceDevice.name : 'Unknown',
|
||||
target_device_name: targetDevice ? targetDevice.name : 'Unknown',
|
||||
source_device_type: sourceDevice ? sourceDevice.type_name : '',
|
||||
target_device_type: targetDevice ? targetDevice.type_name : ''
|
||||
};
|
||||
});
|
||||
|
||||
const columnDefs = [
|
||||
{
|
||||
headerName: 'Source Device',
|
||||
field: 'source_device_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: devices.map(d => d.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Source Port',
|
||||
field: 'source_port',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `Port ${params.value}`
|
||||
},
|
||||
{
|
||||
headerName: 'Dest Device',
|
||||
field: 'target_device_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: devices.map(d => d.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Dest Port',
|
||||
field: 'target_port',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `Port ${params.value}`
|
||||
},
|
||||
{
|
||||
headerName: 'Status',
|
||||
field: 'status',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueGetter: params => {
|
||||
// Validate connection
|
||||
const sourceDevice = devices.find(d => d.id === params.data.source_device_id);
|
||||
const targetDevice = devices.find(d => d.id === params.data.target_device_id);
|
||||
|
||||
if (!sourceDevice || !targetDevice) return 'Invalid';
|
||||
if (params.data.source_port >= sourceDevice.ports_count) return 'Invalid Port';
|
||||
if (params.data.target_port >= targetDevice.ports_count) return 'Invalid Port';
|
||||
|
||||
return 'Valid';
|
||||
},
|
||||
cellStyle: params => {
|
||||
if (params.value === 'Valid') {
|
||||
return { color: '#4CAF50', fontWeight: 'bold' };
|
||||
} else {
|
||||
return { color: '#d32f2f', fontWeight: 'bold' };
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
columnDefs: columnDefs,
|
||||
rowData: enrichedConnections,
|
||||
rowSelection: 'multiple',
|
||||
animateRows: true,
|
||||
enableCellTextSelection: true,
|
||||
defaultColDef: {
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
resizable: true
|
||||
},
|
||||
onCellValueChanged: (params) => this.onConnectionCellValueChanged(params, devices),
|
||||
onSelectionChanged: () => this.updateToolbarButtons()
|
||||
};
|
||||
|
||||
this.gridApi = agGrid.createGrid(this.tableContainer, gridOptions);
|
||||
}
|
||||
|
||||
async onConnectionCellValueChanged(params, devices) {
|
||||
const connectionId = params.data.id;
|
||||
const field = params.colDef.field;
|
||||
const newValue = params.newValue;
|
||||
|
||||
try {
|
||||
let sourceDeviceId = params.data.source_device_id;
|
||||
let sourcePort = params.data.source_port;
|
||||
let targetDeviceId = params.data.target_device_id;
|
||||
let targetPort = params.data.target_port;
|
||||
|
||||
// Update the field that was changed
|
||||
if (field === 'source_device_name') {
|
||||
const device = devices.find(d => d.name === newValue);
|
||||
if (!device) {
|
||||
alert(`Device "${newValue}" not found.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
sourceDeviceId = device.id;
|
||||
params.data.source_device_id = device.id;
|
||||
params.data.source_device_type = device.type_name;
|
||||
} else if (field === 'source_port') {
|
||||
sourcePort = parseInt(newValue);
|
||||
const sourceDevice = devices.find(d => d.id === sourceDeviceId);
|
||||
if (sourcePort < 0 || sourcePort >= sourceDevice.ports_count) {
|
||||
alert(`Invalid source port. Device "${sourceDevice.name}" has ports 0-${sourceDevice.ports_count - 1}.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if port is already in use by another connection
|
||||
const connections = await this.api.getConnections();
|
||||
const portInUse = connections.some(c =>
|
||||
c.id !== connectionId &&
|
||||
((c.source_device_id === sourceDeviceId && c.source_port === sourcePort) ||
|
||||
(c.target_device_id === sourceDeviceId && c.target_port === sourcePort))
|
||||
);
|
||||
if (portInUse) {
|
||||
alert(`Port ${sourcePort} is already in use on device "${sourceDevice.name}".`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
} else if (field === 'target_device_name') {
|
||||
const device = devices.find(d => d.name === newValue);
|
||||
if (!device) {
|
||||
alert(`Device "${newValue}" not found.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
targetDeviceId = device.id;
|
||||
params.data.target_device_id = device.id;
|
||||
params.data.target_device_type = device.type_name;
|
||||
} else if (field === 'target_port') {
|
||||
targetPort = parseInt(newValue);
|
||||
const targetDevice = devices.find(d => d.id === targetDeviceId);
|
||||
if (targetPort < 0 || targetPort >= targetDevice.ports_count) {
|
||||
alert(`Invalid target port. Device "${targetDevice.name}" has ports 0-${targetDevice.ports_count - 1}.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if port is already in use by another connection
|
||||
const connections = await this.api.getConnections();
|
||||
const portInUse = connections.some(c =>
|
||||
c.id !== connectionId &&
|
||||
((c.source_device_id === targetDeviceId && c.source_port === targetPort) ||
|
||||
(c.target_device_id === targetDeviceId && c.target_port === targetPort))
|
||||
);
|
||||
if (portInUse) {
|
||||
alert(`Port ${targetPort} is already in use on device "${targetDevice.name}".`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update connection in database
|
||||
await this.api.request(`/api/connections/${connectionId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
sourceDeviceId,
|
||||
sourcePort,
|
||||
targetDeviceId,
|
||||
targetPort
|
||||
})
|
||||
});
|
||||
|
||||
// Update canvas - delete and recreate the connection
|
||||
await this.connectionManager.deleteConnection(connectionId);
|
||||
const newConnection = await this.api.getConnections();
|
||||
const updatedConnection = newConnection.find(c => c.id === connectionId);
|
||||
if (updatedConnection) {
|
||||
this.connectionManager.createConnectionShape(updatedConnection);
|
||||
this.connectionManager.layer.batchDraw();
|
||||
}
|
||||
|
||||
// Refresh table to show updated data
|
||||
this.refreshTable();
|
||||
} catch (err) {
|
||||
console.error('Failed to update connection:', err);
|
||||
alert('Failed to update connection: ' + err.message);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== REFRESH & SYNC =====
|
||||
async refreshTable() {
|
||||
if (!this.currentTable) return;
|
||||
|
||||
const tableType = `${this.currentTable}-table`;
|
||||
await this.showTable(tableType);
|
||||
}
|
||||
|
||||
async syncFromCanvas() {
|
||||
// Called when canvas data changes - refresh the table
|
||||
if (this.isTableVisible()) {
|
||||
await this.refreshTable();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CRUD OPERATIONS =====
|
||||
async addRow() {
|
||||
try {
|
||||
if (this.currentTable === 'racks') {
|
||||
await this.rackManager.addRack();
|
||||
await this.refreshTable();
|
||||
} else if (this.currentTable === 'devices') {
|
||||
alert('To add a device, please use the canvas view (right-click on a rack).');
|
||||
} else if (this.currentTable === 'connections') {
|
||||
alert('To add a connection, please use the canvas view (right-click on a device).');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add row:', err);
|
||||
alert('Failed to add row: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSelectedRows() {
|
||||
const selectedRows = this.gridApi.getSelectedRows();
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
alert('Please select rows to delete.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Delete ${selectedRows.length} row(s)?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const row of selectedRows) {
|
||||
if (this.currentTable === 'racks') {
|
||||
const rackShape = this.rackManager.getRackShape(row.id);
|
||||
await this.rackManager.deleteRack(row.id, rackShape);
|
||||
} else if (this.currentTable === 'devices') {
|
||||
const deviceShape = this.deviceManager.getDeviceShape(row.id);
|
||||
await this.deviceManager.deleteDevice(row.id, deviceShape);
|
||||
} else if (this.currentTable === 'connections') {
|
||||
await this.connectionManager.deleteConnection(row.id);
|
||||
}
|
||||
}
|
||||
|
||||
await this.refreshTable();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete rows:', err);
|
||||
alert('Failed to delete rows: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
updateToolbarButtons() {
|
||||
const deleteBtn = document.getElementById('deleteTableRowBtn');
|
||||
if (deleteBtn && this.gridApi) {
|
||||
const selectedRows = this.gridApi.getSelectedRows();
|
||||
deleteBtn.disabled = selectedRows.length === 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
547
archive/old_server/db.js
Normal file
547
archive/old_server/db.js
Normal file
@@ -0,0 +1,547 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
const DB_PATH = path.join(__dirname, '../database/datacenter.db');
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
this.createTables()
|
||||
.then(() => this.seedDeviceTypes())
|
||||
.then(() => this.ensureDefaultProject())
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createTables() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.serialize(() => {
|
||||
// Projects table
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Racks table
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS racks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
x REAL NOT NULL,
|
||||
y REAL NOT NULL,
|
||||
width REAL DEFAULT 520,
|
||||
height REAL DEFAULT 1510,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE(project_id, name)
|
||||
)
|
||||
`);
|
||||
|
||||
// Device types table (library of available devices)
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS device_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
ports_count INTEGER NOT NULL DEFAULT 24,
|
||||
color TEXT DEFAULT '#4A90E2'
|
||||
)
|
||||
`);
|
||||
|
||||
// Devices table (instances placed in racks)
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_type_id INTEGER NOT NULL,
|
||||
rack_id INTEGER NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
logical_x REAL,
|
||||
logical_y REAL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (device_type_id) REFERENCES device_types(id),
|
||||
FOREIGN KEY (rack_id) REFERENCES racks(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Connections table
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS connections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_device_id INTEGER NOT NULL,
|
||||
source_port INTEGER NOT NULL,
|
||||
target_device_id INTEGER NOT NULL,
|
||||
target_port INTEGER NOT NULL,
|
||||
waypoints TEXT,
|
||||
waypoints_physical TEXT,
|
||||
waypoints_logical TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (source_device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
UNIQUE(source_device_id, source_port),
|
||||
UNIQUE(target_device_id, target_port)
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
// Add waypoints column if it doesn't exist (for existing databases)
|
||||
this.db.run(`
|
||||
ALTER TABLE connections ADD COLUMN waypoints TEXT
|
||||
`, (err) => {
|
||||
// Ignore error if column already exists
|
||||
|
||||
// Add view-specific waypoints columns
|
||||
this.db.run(`
|
||||
ALTER TABLE connections ADD COLUMN waypoints_physical TEXT
|
||||
`, (err) => {
|
||||
// Ignore error if column already exists
|
||||
this.db.run(`
|
||||
ALTER TABLE connections ADD COLUMN waypoints_logical TEXT
|
||||
`, (err) => {
|
||||
// Ignore error if column already exists
|
||||
|
||||
// Add logical view position columns to devices if they don't exist
|
||||
this.db.run(`
|
||||
ALTER TABLE devices ADD COLUMN logical_x REAL
|
||||
`, (err) => {
|
||||
// Ignore error if column already exists
|
||||
this.db.run(`
|
||||
ALTER TABLE devices ADD COLUMN logical_y REAL
|
||||
`, (err) => {
|
||||
// Ignore error if column already exists
|
||||
this.db.run(`
|
||||
ALTER TABLE devices ADD COLUMN rack_units INTEGER DEFAULT 1
|
||||
`, (err) => {
|
||||
// Ignore error if column already exists
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
seedDeviceTypes() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stmt = this.db.prepare('INSERT OR IGNORE INTO device_types (name, ports_count, color) VALUES (?, ?, ?)');
|
||||
|
||||
const deviceTypes = [
|
||||
['Switch 24-Port', 24, '#4A90E2'],
|
||||
['Switch 48-Port', 48, '#5CA6E8'],
|
||||
['Router', 8, '#E27D60'],
|
||||
['Firewall', 6, '#E8A87C'],
|
||||
['Server', 4, '#41B3A3'],
|
||||
['Storage', 8, '#38A169'],
|
||||
['Patch Panel 24', 24, '#9B59B6'],
|
||||
['Patch Panel 48', 48, '#A569BD']
|
||||
];
|
||||
|
||||
deviceTypes.forEach(([name, ports, color]) => {
|
||||
stmt.run(name, ports, color);
|
||||
});
|
||||
|
||||
stmt.finalize((err) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
console.log('Device types seeded');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ensureDefaultProject() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'INSERT OR IGNORE INTO projects (id, name, description) VALUES (1, ?, ?)',
|
||||
['Default Project', 'Default datacenter project'],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
console.log('Default project ensured');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Project operations
|
||||
getAllProjects() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all('SELECT * FROM projects ORDER BY updated_at DESC', (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getProject(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get('SELECT * FROM projects WHERE id = ?', [id], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createProject(name, description = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'INSERT INTO projects (name, description) VALUES (?, ?)',
|
||||
[name, description],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID, name, description });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateProject(id, name, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE projects SET name = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[name, description, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
deleteProject(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if this is the last project
|
||||
this.db.get('SELECT COUNT(*) as count FROM projects', (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.count <= 1) {
|
||||
reject(new Error('Cannot delete the last project'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the project (cascade will handle racks, devices, connections)
|
||||
this.db.run('DELETE FROM projects WHERE id = ?', [id], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Rack operations
|
||||
getAllRacks(projectId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all('SELECT * FROM racks WHERE project_id = ? ORDER BY name', [projectId], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createRack(projectId, name, x, y) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'INSERT INTO racks (project_id, name, x, y) VALUES (?, ?, ?, ?)',
|
||||
[projectId, name, x, y],
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
// Fetch the complete rack data with width and height defaults
|
||||
this.db.get(
|
||||
'SELECT * FROM racks WHERE project_id = ? AND name = ? ORDER BY id DESC LIMIT 1',
|
||||
[projectId, name],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getNextRackName(projectId, prefix = 'RACK') {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(
|
||||
`SELECT name FROM racks WHERE project_id = ? AND name LIKE ? ORDER BY name DESC`,
|
||||
[projectId, `${prefix}.%`],
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (rows.length === 0) {
|
||||
resolve(`${prefix}.01`);
|
||||
} else {
|
||||
const lastNum = parseInt(rows[0].name.split('.').pop());
|
||||
const nextNum = (lastNum + 1).toString().padStart(2, '0');
|
||||
resolve(`${prefix}.${nextNum}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateRackPosition(id, x, y) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE racks SET x = ?, y = ? WHERE id = ?',
|
||||
[x, y, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateRackName(id, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE racks SET name = ? WHERE id = ?',
|
||||
[name, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
deleteRack(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run('DELETE FROM racks WHERE id = ?', [id], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Device type operations
|
||||
getAllDeviceTypes() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all('SELECT * FROM device_types ORDER BY name', (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Device operations
|
||||
getAllDevices(projectId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(`
|
||||
SELECT d.*, dt.name as type_name, dt.ports_count, dt.color
|
||||
FROM devices d
|
||||
JOIN device_types dt ON d.device_type_id = dt.id
|
||||
JOIN racks r ON d.rack_id = r.id
|
||||
WHERE r.project_id = ?
|
||||
ORDER BY d.rack_id, d.position
|
||||
`, [projectId], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createDevice(deviceTypeId, rackId, position, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'INSERT INTO devices (device_type_id, rack_id, position, name) VALUES (?, ?, ?, ?)',
|
||||
[deviceTypeId, rackId, position, name],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
deleteDevice(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run('DELETE FROM devices WHERE id = ?', [id], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateDeviceRack(id, rackId, position) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE devices SET rack_id = ?, position = ? WHERE id = ?',
|
||||
[rackId, position, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateDeviceLogicalPosition(id, x, y) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE devices SET logical_x = ?, logical_y = ? WHERE id = ?',
|
||||
[x, y, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateDeviceName(id, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE devices SET name = ? WHERE id = ?',
|
||||
[name, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateDeviceRackUnits(id, rackUnits) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE devices SET rack_units = ? WHERE id = ?',
|
||||
[rackUnits, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateConnection(id, sourceDeviceId, sourcePort, targetDeviceId, targetPort) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'UPDATE connections SET source_device_id = ?, source_port = ?, target_device_id = ?, target_port = ? WHERE id = ?',
|
||||
[sourceDeviceId, sourcePort, targetDeviceId, targetPort, id],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Connection operations
|
||||
getAllConnections(projectId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(`
|
||||
SELECT c.* FROM connections c
|
||||
JOIN devices d ON c.source_device_id = d.id
|
||||
JOIN racks r ON d.rack_id = r.id
|
||||
WHERE r.project_id = ?
|
||||
`, [projectId], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createConnection(sourceDeviceId, sourcePort, targetDeviceId, targetPort) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(
|
||||
'INSERT INTO connections (source_device_id, source_port, target_device_id, target_port) VALUES (?, ?, ?, ?)',
|
||||
[sourceDeviceId, sourcePort, targetDeviceId, targetPort],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
deleteConnection(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run('DELETE FROM connections WHERE id = ?', [id], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateConnectionWaypoints(id, waypoints, view = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const waypointsJson = JSON.stringify(waypoints);
|
||||
|
||||
let query, params;
|
||||
if (view === 'physical') {
|
||||
query = 'UPDATE connections SET waypoints_physical = ? WHERE id = ?';
|
||||
params = [waypointsJson, id];
|
||||
} else if (view === 'logical') {
|
||||
query = 'UPDATE connections SET waypoints_logical = ? WHERE id = ?';
|
||||
params = [waypointsJson, id];
|
||||
} else {
|
||||
// Legacy support - update old waypoints column
|
||||
query = 'UPDATE connections SET waypoints = ? WHERE id = ?';
|
||||
params = [waypointsJson, id];
|
||||
}
|
||||
|
||||
this.db.run(query, params, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUsedPorts(deviceId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(`
|
||||
SELECT source_port as port FROM connections WHERE source_device_id = ?
|
||||
UNION
|
||||
SELECT target_port as port FROM connections WHERE target_device_id = ?
|
||||
`, [deviceId, deviceId], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows.map(r => r.port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.close((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Database();
|
||||
276
archive/old_server/server.js
Normal file
276
archive/old_server/server.js
Normal file
@@ -0,0 +1,276 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const db = require('./db');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// API Routes
|
||||
|
||||
// Projects
|
||||
app.get('/api/projects', async (req, res) => {
|
||||
try {
|
||||
const projects = await db.getAllProjects();
|
||||
res.json(projects);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/:id', async (req, res) => {
|
||||
try {
|
||||
const project = await db.getProject(req.params.id);
|
||||
if (!project) {
|
||||
res.status(404).json({ error: 'Project not found' });
|
||||
} else {
|
||||
res.json(project);
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/projects', async (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
const project = await db.createProject(name, description);
|
||||
res.status(201).json(project);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/projects/:id', async (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
await db.updateProject(req.params.id, name, description);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/projects/:id', async (req, res) => {
|
||||
try {
|
||||
await db.deleteProject(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Racks
|
||||
app.get('/api/racks', async (req, res) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const racks = await db.getAllRacks(projectId);
|
||||
res.json(racks);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/racks/next-name', async (req, res) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const prefix = req.query.prefix || 'RACK';
|
||||
const name = await db.getNextRackName(projectId, prefix);
|
||||
res.json({ name });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/racks', async (req, res) => {
|
||||
try {
|
||||
const { projectId, name, x, y } = req.body;
|
||||
const rack = await db.createRack(projectId || 1, name, x, y);
|
||||
res.status(201).json(rack);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/racks/:id/position', async (req, res) => {
|
||||
try {
|
||||
const { x, y } = req.body;
|
||||
await db.updateRackPosition(req.params.id, x, y);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/racks/:id/name', async (req, res) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
await db.updateRackName(req.params.id, name);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/racks/:id', async (req, res) => {
|
||||
try {
|
||||
await db.deleteRack(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Device Types
|
||||
app.get('/api/device-types', async (req, res) => {
|
||||
try {
|
||||
const types = await db.getAllDeviceTypes();
|
||||
res.json(types);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Devices
|
||||
app.get('/api/devices', async (req, res) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const devices = await db.getAllDevices(projectId);
|
||||
res.json(devices);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/devices', async (req, res) => {
|
||||
try {
|
||||
const { deviceTypeId, rackId, position, name } = req.body;
|
||||
const device = await db.createDevice(deviceTypeId, rackId, position, name);
|
||||
res.status(201).json(device);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/devices/:id', async (req, res) => {
|
||||
try {
|
||||
await db.deleteDevice(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/devices/:id/rack', async (req, res) => {
|
||||
try {
|
||||
const { rackId, position } = req.body;
|
||||
await db.updateDeviceRack(req.params.id, rackId, position);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/devices/:id/logical-position', async (req, res) => {
|
||||
try {
|
||||
const { x, y } = req.body;
|
||||
await db.updateDeviceLogicalPosition(req.params.id, x, y);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/devices/:id/name', async (req, res) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
await db.updateDeviceName(req.params.id, name);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/devices/:id/rack-units', async (req, res) => {
|
||||
try {
|
||||
const { rackUnits } = req.body;
|
||||
await db.updateDeviceRackUnits(req.params.id, rackUnits);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/devices/:id/used-ports', async (req, res) => {
|
||||
try {
|
||||
const ports = await db.getUsedPorts(req.params.id);
|
||||
res.json(ports);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Connections
|
||||
app.get('/api/connections', async (req, res) => {
|
||||
try {
|
||||
const projectId = req.query.projectId || 1;
|
||||
const connections = await db.getAllConnections(projectId);
|
||||
res.json(connections);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/connections', async (req, res) => {
|
||||
try {
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = req.body;
|
||||
const connection = await db.createConnection(sourceDeviceId, sourcePort, targetDeviceId, targetPort);
|
||||
res.status(201).json(connection);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/connections/:id/waypoints', async (req, res) => {
|
||||
try {
|
||||
const { waypoints, view } = req.body;
|
||||
await db.updateConnectionWaypoints(req.params.id, waypoints, view);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/connections/:id', async (req, res) => {
|
||||
try {
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = req.body;
|
||||
await db.updateConnection(req.params.id, sourceDeviceId, sourcePort, targetDeviceId, targetPort);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/connections/:id', async (req, res) => {
|
||||
try {
|
||||
await db.deleteConnection(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize database and start server
|
||||
db.init()
|
||||
.then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to initialize database:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user