Skip to content

Commit 329674f

Browse files
[Feature] New NEBULA UI (#40)
* Improve UI and code efficiency * improve style and particles * update UI * fix some issues with the UI * improve ui style * fix some issues in the ui style * improve UI elements * fix some isues in monitor * improve the inter-connection of the nodes
1 parent 9f57d46 commit 329674f

27 files changed

+5664
-4278
lines changed

nebula/frontend/app.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,37 @@ async def connect(self, websocket: WebSocket):
164164
pass
165165

166166
def disconnect(self, websocket: WebSocket):
167-
self.active_connections.remove(websocket)
167+
if websocket in self.active_connections:
168+
self.active_connections.remove(websocket)
168169

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

173174
async def send_personal_message(self, message: str, websocket: WebSocket):
174-
await websocket.send_text(message)
175+
try:
176+
await websocket.send_text(message)
177+
except RuntimeError:
178+
# Connection was closed, remove it from active connections
179+
self.disconnect(websocket)
175180

176181
async def broadcast(self, message: str):
177182
self.add_message(message)
183+
disconnected_websockets = []
184+
178185
for connection in self.active_connections:
179-
await connection.send_text(message)
186+
try:
187+
await connection.send_text(message)
188+
except RuntimeError:
189+
# Mark connection for removal
190+
disconnected_websockets.append(connection)
191+
except Exception as e:
192+
logging.error(f"Error broadcasting message: {e}")
193+
disconnected_websockets.append(connection)
194+
195+
# Remove disconnected websockets
196+
for websocket in disconnected_websockets:
197+
self.disconnect(websocket)
180198

181199
def get_historic(self):
182200
return self.historic_messages
@@ -195,12 +213,20 @@ async def websocket_endpoint(websocket: WebSocket, client_id: int):
195213
"type": "control",
196214
"message": f"Client #{client_id} says: {data}",
197215
}
198-
await manager.broadcast(json.dumps(message))
199-
# await manager.send_personal_message(f"You wrote: {data}", websocket)
216+
try:
217+
await manager.broadcast(json.dumps(message))
218+
except Exception as e:
219+
logging.error(f"Error broadcasting message: {e}")
200220
except WebSocketDisconnect:
201221
manager.disconnect(websocket)
202-
message = {"type": "control", "message": f"Client #{client_id} left the chat"}
203-
await manager.broadcast(json.dumps(message))
222+
try:
223+
message = {"type": "control", "message": f"Client #{client_id} left the chat"}
224+
await manager.broadcast(json.dumps(message))
225+
except Exception as e:
226+
logging.error(f"Error broadcasting disconnect message: {e}")
227+
except Exception as e:
228+
logging.error(f"WebSocket error: {e}")
229+
manager.disconnect(websocket)
204230

205231

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

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
@keyframes pulse {
2+
0% { transform: scale(1); }
3+
50% { transform: scale(1.05); }
4+
100% { transform: scale(1); }
5+
}
6+
7+
@keyframes float {
8+
0% { transform: translateY(0px); }
9+
50% { transform: translateY(-10px); }
10+
100% { transform: translateY(0px); }
11+
}
12+
13+
.loading-pulse {
14+
animation: pulse 2s infinite ease-in-out;
15+
}
16+
17+
.loading-float {
18+
animation: float 3s infinite ease-in-out;
19+
}
20+
21+
.scenario-running-indicator {
22+
position: fixed;
23+
top: 6rem;
24+
right: 2rem;
25+
background: rgba(255, 193, 7, 0.1);
26+
border: 2px solid #ffc107;
27+
border-radius: 1rem;
28+
padding: 1rem 1.5rem;
29+
display: flex;
30+
align-items: center;
31+
gap: 1rem;
32+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
33+
z-index: 1000;
34+
backdrop-filter: blur(8px);
35+
}
36+
37+
.scenario-running-indicator .spinner {
38+
width: 1.5rem;
39+
height: 1.5rem;
40+
border: 3px solid rgba(255, 193, 7, 0.3);
41+
border-top-color: #ffc107;
42+
border-radius: 50%;
43+
animation: spin 1s linear infinite;
44+
}
45+
46+
@keyframes spin {
47+
to { transform: rotate(360deg); }
48+
}
49+
50+
.progress-bar {
51+
width: var(--progress-width);
52+
}
53+
54+
.bg-success-subtle {
55+
background-color: rgba(25, 135, 84, 0.1);
56+
}
57+
58+
.bg-warning-subtle {
59+
background-color: rgba(255, 193, 7, 0.1);
60+
}
61+
62+
.bg-danger-subtle {
63+
background-color: rgba(220, 53, 69, 0.1);
64+
}
65+
66+
.bg-primary-subtle {
67+
background-color: rgba(13, 110, 253, 0.1);
68+
}
69+
70+
/* Smooth transitions */
71+
.btn, .badge, .card {
72+
transition: all 0.2s ease-in-out;
73+
}
74+
75+
.btn:hover {
76+
transform: translateY(-1px);
77+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
78+
}
79+
80+
#table-scenarios .btn i {
81+
display: flex;
82+
align-items: center;
83+
justify-content: center;
84+
width: 100%;
85+
height: 100%;
86+
}
87+
88+
/* Table styles */
89+
.table > :not(caption) > * > * {
90+
padding: 1rem;
91+
}
92+
93+
.table tbody tr {
94+
transition: background-color 0.2s ease;
95+
}
96+
97+
.table tbody tr:hover {
98+
background-color: rgba(0, 0, 0, 0.02);
99+
}
100+
101+
/* Card styles */
102+
.card {
103+
border-radius: 0.5rem;
104+
transition: transform 0.2s ease, box-shadow 0.2s ease;
105+
}
106+
107+
.card:hover {
108+
transform: translateY(-2px);
109+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1) !important;
110+
}
111+
112+
.card-header {
113+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
114+
}
115+
116+
/* Progress bar */
117+
.progress {
118+
height: 0.75rem;
119+
border-radius: 1rem;
120+
background-color: rgba(0, 0, 0, 0.05);
121+
}
122+
123+
.progress-bar {
124+
border-radius: 1rem;
125+
transition: width 0.6s ease;
126+
}
127+
128+
/* Tooltips */
129+
[title] {
130+
position: relative;
131+
}
132+
133+
[title]:hover::after {
134+
content: attr(title);
135+
position: absolute;
136+
bottom: 100%;
137+
left: 50%;
138+
transform: translateX(-50%);
139+
background-color: rgba(0, 0, 0, 0.8);
140+
color: white;
141+
padding: 0.5rem 0.75rem;
142+
border-radius: 0.375rem;
143+
font-size: 0.875rem;
144+
white-space: nowrap;
145+
z-index: 1000;
146+
margin-bottom: 0.5rem;
147+
opacity: 0;
148+
pointer-events: none;
149+
transition: opacity 0.2s ease, transform 0.2s ease;
150+
}
151+
152+
[title]:hover::after {
153+
opacity: 1;
154+
transform: translateX(-50%) translateY(-0.25rem);
155+
}
156+
157+
/* Icon styles */
158+
.fa {
159+
font-size: 1rem;
160+
}
161+
162+
.fa-3x {
163+
font-size: 3rem;
164+
}
165+
166+
/* Button styles */
167+
.btn-sm {
168+
padding: 0.4rem 0.6rem;
169+
}
170+
171+
.btn-outline-primary:hover {
172+
background-color: var(--bs-primary);
173+
color: white;
174+
}
175+
176+
/* Badge styles */
177+
.badge {
178+
font-weight: 500;
179+
}
180+
181+
/* Textarea styles */
182+
textarea.form-control {
183+
border-radius: 0.375rem;
184+
border: 1px solid rgba(0, 0, 0, 0.1);
185+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
186+
}
187+
188+
textarea.form-control:focus {
189+
border-color: var(--bs-primary);
190+
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
191+
}
192+
193+
/* Empty state styles */
194+
.empty-state-container {
195+
max-width: 500px;
196+
margin: 0 auto;
197+
}
198+
199+
.empty-state-container i {
200+
display: inline-block;
201+
margin-bottom: 1.5rem;
202+
color: var(--bs-primary);
203+
opacity: 0.9;
204+
}
205+
206+
.empty-state-container h3 {
207+
font-size: 1.75rem;
208+
margin-bottom: 1rem;
209+
}
210+
211+
.empty-state-container p {
212+
font-size: 1.1rem;
213+
line-height: 1.6;
214+
color: #6c757d;
215+
}
216+
217+
/* Button styles */
218+
.btn-lg {
219+
padding: 0.75rem 1.5rem;
220+
font-size: 1.1rem;
221+
}
222+
223+
.btn-lg i {
224+
font-size: 1.2rem;
225+
}
226+
227+
/* Card hover effect */
228+
.card {
229+
transition: transform 0.3s ease, box-shadow 0.3s ease;
230+
}
231+
232+
.card:hover {
233+
transform: translateY(-5px);
234+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1) !important;
235+
}
236+
237+
/* Progress bar animation */
238+
.progress-bar {
239+
position: relative;
240+
overflow: hidden;
241+
}
242+
243+
.progress-bar::after {
244+
content: '';
245+
position: absolute;
246+
top: 0;
247+
left: 0;
248+
right: 0;
249+
bottom: 0;
250+
background: linear-gradient(
251+
90deg,
252+
rgba(255, 255, 255, 0) 0%,
253+
rgba(255, 255, 255, 0.2) 50%,
254+
rgba(255, 255, 255, 0) 100%
255+
);
256+
animation: progress-shine 2s infinite;
257+
}
258+
259+
@keyframes progress-shine {
260+
0% { transform: translateX(-100%); }
261+
100% { transform: translateX(100%); }
262+
}

0 commit comments

Comments
 (0)