Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions nebula/frontend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,37 @@ async def connect(self, websocket: WebSocket):
pass

def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
if websocket in self.active_connections:
self.active_connections.remove(websocket)

def add_message(self, message):
current_timestamp = datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d %H:%M:%S")
self.historic_messages.update({current_timestamp: json.loads(message)})

async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
try:
await websocket.send_text(message)
except RuntimeError:
# Connection was closed, remove it from active connections
self.disconnect(websocket)

async def broadcast(self, message: str):
self.add_message(message)
disconnected_websockets = []

for connection in self.active_connections:
await connection.send_text(message)
try:
await connection.send_text(message)
except RuntimeError:
# Mark connection for removal
disconnected_websockets.append(connection)
except Exception as e:
logging.error(f"Error broadcasting message: {e}")
disconnected_websockets.append(connection)

# Remove disconnected websockets
for websocket in disconnected_websockets:
self.disconnect(websocket)

def get_historic(self):
return self.historic_messages
Expand All @@ -195,12 +213,20 @@ async def websocket_endpoint(websocket: WebSocket, client_id: int):
"type": "control",
"message": f"Client #{client_id} says: {data}",
}
await manager.broadcast(json.dumps(message))
# await manager.send_personal_message(f"You wrote: {data}", websocket)
try:
await manager.broadcast(json.dumps(message))
except Exception as e:
logging.error(f"Error broadcasting message: {e}")
except WebSocketDisconnect:
manager.disconnect(websocket)
message = {"type": "control", "message": f"Client #{client_id} left the chat"}
await manager.broadcast(json.dumps(message))
try:
message = {"type": "control", "message": f"Client #{client_id} left the chat"}
await manager.broadcast(json.dumps(message))
except Exception as e:
logging.error(f"Error broadcasting disconnect message: {e}")
except Exception as e:
logging.error(f"WebSocket error: {e}")
manager.disconnect(websocket)


templates = Jinja2Templates(directory=settings.templates_dir)
Expand Down Expand Up @@ -917,7 +943,7 @@ async def nebula_monitor_log_error(scenario_name: str, id: str):
async def nebula_monitor_image(scenario_name: str):
topology_image = FileUtils.check_path(settings.config_dir, os.path.join(scenario_name, "topology.png"))
if os.path.exists(topology_image):
return FileResponse(topology_image, media_type="image/png")
return FileResponse(topology_image, media_type="image/png", filename=f"{scenario_name}_topology.png")
else:
raise HTTPException(status_code=404, detail="Topology image not found")

Expand Down
262 changes: 262 additions & 0 deletions nebula/frontend/static/css/dashboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}

@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}

.loading-pulse {
animation: pulse 2s infinite ease-in-out;
}

.loading-float {
animation: float 3s infinite ease-in-out;
}

.scenario-running-indicator {
position: fixed;
top: 6rem;
right: 2rem;
background: rgba(255, 193, 7, 0.1);
border: 2px solid #ffc107;
border-radius: 1rem;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
backdrop-filter: blur(8px);
}

.scenario-running-indicator .spinner {
width: 1.5rem;
height: 1.5rem;
border: 3px solid rgba(255, 193, 7, 0.3);
border-top-color: #ffc107;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
to { transform: rotate(360deg); }
}

.progress-bar {
width: var(--progress-width);
}

.bg-success-subtle {
background-color: rgba(25, 135, 84, 0.1);
}

.bg-warning-subtle {
background-color: rgba(255, 193, 7, 0.1);
}

.bg-danger-subtle {
background-color: rgba(220, 53, 69, 0.1);
}

.bg-primary-subtle {
background-color: rgba(13, 110, 253, 0.1);
}

/* Smooth transitions */
.btn, .badge, .card {
transition: all 0.2s ease-in-out;
}

.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

#table-scenarios .btn i {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}

/* Table styles */
.table > :not(caption) > * > * {
padding: 1rem;
}

.table tbody tr {
transition: background-color 0.2s ease;
}

.table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}

/* Card styles */
.card {
border-radius: 0.5rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1) !important;
}

.card-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}

/* Progress bar */
.progress {
height: 0.75rem;
border-radius: 1rem;
background-color: rgba(0, 0, 0, 0.05);
}

.progress-bar {
border-radius: 1rem;
transition: width 0.6s ease;
}

/* Tooltips */
[title] {
position: relative;
}

[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.875rem;
white-space: nowrap;
z-index: 1000;
margin-bottom: 0.5rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}

[title]:hover::after {
opacity: 1;
transform: translateX(-50%) translateY(-0.25rem);
}

/* Icon styles */
.fa {
font-size: 1rem;
}

.fa-3x {
font-size: 3rem;
}

/* Button styles */
.btn-sm {
padding: 0.4rem 0.6rem;
}

.btn-outline-primary:hover {
background-color: var(--bs-primary);
color: white;
}

/* Badge styles */
.badge {
font-weight: 500;
}

/* Textarea styles */
textarea.form-control {
border-radius: 0.375rem;
border: 1px solid rgba(0, 0, 0, 0.1);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

textarea.form-control:focus {
border-color: var(--bs-primary);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}

/* Empty state styles */
.empty-state-container {
max-width: 500px;
margin: 0 auto;
}

.empty-state-container i {
display: inline-block;
margin-bottom: 1.5rem;
color: var(--bs-primary);
opacity: 0.9;
}

.empty-state-container h3 {
font-size: 1.75rem;
margin-bottom: 1rem;
}

.empty-state-container p {
font-size: 1.1rem;
line-height: 1.6;
color: #6c757d;
}

/* Button styles */
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
}

.btn-lg i {
font-size: 1.2rem;
}

/* Card hover effect */
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1) !important;
}

/* Progress bar animation */
.progress-bar {
position: relative;
overflow: hidden;
}

.progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: progress-shine 2s infinite;
}

@keyframes progress-shine {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
Loading
Loading