skypilot-nightly 1.0.0.dev20250203__py3-none-any.whl → 1.0.0.dev20250205__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. sky/__init__.py +4 -2
  2. sky/adaptors/vast.py +29 -0
  3. sky/authentication.py +18 -0
  4. sky/backends/backend_utils.py +4 -1
  5. sky/backends/cloud_vm_ray_backend.py +1 -0
  6. sky/check.py +2 -2
  7. sky/clouds/__init__.py +2 -0
  8. sky/clouds/service_catalog/constants.py +1 -1
  9. sky/clouds/service_catalog/data_fetchers/fetch_vast.py +147 -0
  10. sky/clouds/service_catalog/kubernetes_catalog.py +11 -6
  11. sky/clouds/service_catalog/vast_catalog.py +104 -0
  12. sky/clouds/vast.py +279 -0
  13. sky/jobs/dashboard/dashboard.py +156 -20
  14. sky/jobs/dashboard/templates/index.html +557 -78
  15. sky/jobs/scheduler.py +14 -5
  16. sky/provision/__init__.py +1 -0
  17. sky/provision/lambda_cloud/instance.py +17 -1
  18. sky/provision/vast/__init__.py +10 -0
  19. sky/provision/vast/config.py +11 -0
  20. sky/provision/vast/instance.py +247 -0
  21. sky/provision/vast/utils.py +161 -0
  22. sky/serve/serve_state.py +23 -21
  23. sky/setup_files/dependencies.py +1 -0
  24. sky/templates/vast-ray.yml.j2 +70 -0
  25. sky/utils/controller_utils.py +5 -0
  26. {skypilot_nightly-1.0.0.dev20250203.dist-info → skypilot_nightly-1.0.0.dev20250205.dist-info}/METADATA +4 -1
  27. {skypilot_nightly-1.0.0.dev20250203.dist-info → skypilot_nightly-1.0.0.dev20250205.dist-info}/RECORD +31 -22
  28. {skypilot_nightly-1.0.0.dev20250203.dist-info → skypilot_nightly-1.0.0.dev20250205.dist-info}/LICENSE +0 -0
  29. {skypilot_nightly-1.0.0.dev20250203.dist-info → skypilot_nightly-1.0.0.dev20250205.dist-info}/WHEEL +0 -0
  30. {skypilot_nightly-1.0.0.dev20250203.dist-info → skypilot_nightly-1.0.0.dev20250205.dist-info}/entry_points.txt +0 -0
  31. {skypilot_nightly-1.0.0.dev20250203.dist-info → skypilot_nightly-1.0.0.dev20250205.dist-info}/top_level.txt +0 -0
@@ -5,61 +5,373 @@
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
8
- <title>SkyPilot Dashboard</title>
8
+ <title>SkyPilot Managed Jobs</title>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
10
10
  integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
11
11
  <style>
12
- .table-no-stripes tbody tr:nth-of-type(even) {
13
- background-color: transparent;
12
+ :root {
13
+ --secondary-color: #6c757d;
14
+ --success-color: #198754;
15
+ --warning-color: #ffc107;
16
+ --info-color: #0dcaf0;
17
+ --light-color: #f8f9fa;
14
18
  }
15
19
 
16
- .table-hover-selected tbody tr:hover {
17
- background-color: #f5f5f5;
20
+ body {
21
+ margin-top: 0;
22
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
23
+ background-color: white;
18
24
  }
19
25
 
20
- .footer {
21
- font-size: 14px;
22
- color: #777;
23
- margin-top: 20px;
26
+ .container {
27
+ max-width: 100%;
28
+ width: 100%;
29
+ background-color: white;
30
+ border-radius: 0;
31
+ box-shadow: none;
32
+ padding: 2rem;
33
+ margin-bottom: 0;
24
34
  }
25
35
 
26
- body {
27
- margin-top: 20px;
36
+ header {
37
+ position: sticky;
38
+ top: 0;
39
+ background: white;
40
+ z-index: 1000;
41
+ padding: 1.5rem 2rem;
42
+ margin: -2rem -2rem 1.5rem -2rem; /* Negative margins to match container padding */
43
+ border-bottom: 1px solid #dee2e6;
28
44
  }
29
45
 
30
- .bg-light {
31
- color: #212529;
32
- /* for some reason not in bootstrap? */
46
+ /* Add shadow when header is sticky */
47
+ header.sticky {
48
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
33
49
  }
34
50
 
35
- .fixed-header-table thead {
36
- position: sticky;
37
- top: 0;
38
- background-color: #f8f9fa;
39
- /* Replace with your desired background color */
40
- color: #000000;
41
- /* Replace with your desired text color */
42
- z-index: 1;
51
+ h1 {
52
+ color: var(--primary-color);
53
+ font-weight: 600;
54
+ font-size: 2rem;
43
55
  }
44
- .clickable {
45
- cursor: pointer; /* This makes the cursor a pointer when hovering over the element */
56
+
57
+ .table {
58
+ width: 100%; /* Ensure table takes full container width */
59
+ border-radius: 8px;
60
+ overflow: hidden;
61
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.05);
62
+ table-layout: auto; /* Allow table to adjust column widths automatically */
63
+ }
64
+
65
+ .fixed-header-table thead th,
66
+ .fixed-header-table thead td { /* Added td selector */
67
+ background-color: var(--light-color);
68
+ padding: 1rem;
69
+ font-weight: 600;
70
+ border-bottom: 2px solid #dee2e6;
71
+ }
72
+
73
+ .table th:nth-child(2), /* ID column */
74
+ .table td:nth-child(2) {
75
+ min-width: 30px; /* Reduced from 100px */
76
+ max-width: 60px; /* Added max-width */
77
+ overflow: hidden; /* Handle overflow */
78
+ text-overflow: ellipsis; /* Show ellipsis for overflow */
79
+ white-space: nowrap; /* Keep text on one line */
80
+ }
81
+
82
+ .table th:nth-child(4), /* Name column */
83
+ .table td:nth-child(4) {
84
+ min-width: 150px;
85
+ }
86
+
87
+ .table th:nth-child(10), /* Status column */
88
+ .table td:nth-child(10) {
89
+ min-width: 120px;
90
+ }
91
+
92
+ .table th,
93
+ .table td {
94
+ padding: 0.8rem 1rem;
95
+ white-space: nowrap; /* Prevent text wrapping in cells */
96
+ }
97
+
98
+ /* Allow Details column to wrap */
99
+ .table th:nth-child(12), /* Details column */
100
+
101
+ .table td:nth-child(12) {
102
+ max-width: 250px; /* Limit width */
103
+ overflow: hidden; /* Hide overflow */
104
+ text-overflow: ellipsis; /* Show ellipsis for overflow */
105
+ position: relative; /* For tooltip positioning */
106
+ cursor: pointer !important; /* Force show pointer cursor */
107
+ padding-right: 24px; /* Make room for the arrow */
108
+ }
109
+
110
+ .table td:nth-child(12)::after {
111
+ content: '▼';
112
+ position: absolute;
113
+ right: 8px;
114
+ top: 12px;
115
+ font-size: 0.6em;
116
+ opacity: 0.5;
117
+ display: none; /* Hide by default */
118
+ }
119
+
120
+ .table td:nth-child(12).expandable::after {
121
+ display: block; /* Only show for expandable content */
122
+ }
123
+
124
+ .table td:nth-child(12).expanded::after {
125
+ transform: rotate(180deg);
126
+ }
127
+
128
+ .table td:nth-child(12).expanded {
129
+ max-width: none;
130
+ white-space: normal;
131
+ word-wrap: break-word;
132
+ }
133
+
134
+ .badge {
135
+ padding: 0.5em 0.8em;
136
+ font-weight: 500;
137
+ border-radius: 6px;
138
+ }
139
+
140
+ .btn-outline-secondary {
141
+ transition: all 0.2s;
142
+ }
143
+
144
+ .btn-outline-secondary:hover {
145
+ background-color: var(--secondary-color);
146
+ color: white;
147
+ transform: translateY(-1px);
46
148
  }
47
149
 
48
150
  .filter-controls {
49
- display: flex;
50
- gap: 10px;
51
- align-items: center; /* This ensures vertical alignment */
52
- margin-top: 1rem;
151
+ background-color: var(--light-color);
152
+ padding: 1rem;
153
+ border-radius: 8px;
154
+ margin: 1rem 0;
155
+ }
156
+
157
+ .form-select {
158
+ border-radius: 6px;
159
+ border: 1px solid #dee2e6;
160
+ padding: 0.5rem 2rem 0.5rem 1rem;
161
+ transition: all 0.2s;
162
+ }
163
+
164
+ .form-select:hover {
165
+ border-color: var(--primary-color);
166
+ }
167
+
168
+ .form-check-input {
169
+ cursor: pointer;
170
+ }
171
+
172
+ .clickable {
173
+ transition: color 0.2s;
174
+ }
175
+
176
+ .clickable:hover {
177
+ color: var(--primary-color);
178
+ }
179
+
180
+ /* Status badge animations */
181
+ .badge {
182
+ animation: fadeIn 0.3s ease-in;
183
+ }
184
+
185
+ @keyframes fadeIn {
186
+ from { opacity: 0; transform: translateY(-2px); }
187
+ to { opacity: 1; transform: translateY(0); }
188
+ }
189
+
190
+ /* Loading indicator for auto-refresh */
191
+ .refresh-indicator {
192
+ display: inline-block;
193
+ margin-left: 8px;
194
+ font-size: 12px;
195
+ color: var(--secondary-color);
196
+ }
197
+
198
+ .refresh-spinner {
199
+ display: none;
200
+ width: 12px;
201
+ height: 12px;
202
+ border: 2px solid #f3f3f3;
203
+ border-top: 2px solid var(--primary-color);
204
+ border-radius: 50%;
205
+ animation: spin 1s linear infinite;
206
+ }
207
+
208
+ @keyframes spin {
209
+ 0% { transform: rotate(0deg); }
210
+ 100% { transform: rotate(360deg); }
211
+ }
212
+
213
+ .status-container {
214
+ cursor: help;
215
+ position: relative;
216
+ display: inline-block;
217
+ }
218
+
219
+ .status-container:hover::after {
220
+ content: attr(data-tooltip);
221
+ position: absolute;
222
+ left: 0;
223
+ top: 100%;
224
+ transform: translateY(8px);
225
+ z-index: 1001;
226
+ background-color: rgba(33, 37, 41, 0.9);
227
+ color: white;
228
+ padding: 0.75rem 1rem;
229
+ border-radius: 6px;
230
+ font-size: 0.875rem;
231
+ white-space: pre;
232
+ min-width: 500px;
233
+ max-width: 800px;
234
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
235
+ pointer-events: none;
236
+ opacity: 0;
237
+ animation: tooltipFadeIn 0.2s ease-in-out forwards;
238
+ }
239
+
240
+ /* Update the arrow position */
241
+ .status-container:hover::before {
242
+ content: '';
243
+ position: absolute;
244
+ left: 20px;
245
+ top: 100%; /* Changed from bottom: 100% */
246
+ transform: none;
247
+ border: 8px solid transparent;
248
+ border-bottom-color: rgba(33, 37, 41, 0.9); /* Changed from border-top-color */
249
+ z-index: 1001;
250
+ pointer-events: none;
251
+ opacity: 0;
252
+ animation: tooltipFadeIn 0.2s ease-in-out forwards;
253
+ }
254
+
255
+ @keyframes tooltipFadeIn {
256
+ to {
257
+ opacity: 1;
258
+ transform: translateY(0); /* Simplified animation */
259
+ }
260
+ }
261
+
262
+ /* Ensure the table doesn't cut off tooltips */
263
+ .table {
264
+ overflow: visible !important;
265
+ }
266
+
267
+ .fixed-header-table {
268
+ overflow: visible !important;
269
+ }
270
+
271
+ /* Add horizontal scroll for very small screens */
272
+ @media (max-width: 1200px) {
273
+ .table-responsive {
274
+ overflow-x: auto;
275
+ }
276
+ }
277
+
278
+ /* Update the timestamp hover styles */
279
+ #last-updated {
280
+ position: relative;
281
+ text-decoration: none;
282
+ border-bottom: 1px solid transparent;
283
+ transition: border-bottom-color 0.2s;
284
+ cursor: help;
285
+ }
286
+
287
+ #last-updated:hover {
288
+ border-bottom-color: var(--secondary-color);
289
+ }
290
+
291
+ #last-updated:hover::after {
292
+ content: attr(data-tooltip);
293
+ position: absolute;
294
+ top: 100%; /* Changed from bottom: 100% to top: 100% */
295
+ left: 50%;
296
+ transform: translateX(-50%);
297
+ padding: 0.5rem 1rem;
298
+ background-color: rgba(33, 37, 41, 0.9);
299
+ color: white;
300
+ border-radius: 6px;
301
+ font-size: 0.875rem;
302
+ white-space: nowrap;
303
+ z-index: 1000;
304
+ margin-top: 8px; /* Changed from margin-bottom to margin-top */
305
+ }
306
+
307
+ #last-updated:hover::before {
308
+ content: '';
309
+ position: absolute;
310
+ top: 100%; /* Changed from bottom: 100% to top: 100% */
311
+ left: 50%;
312
+ transform: translateX(-50%);
313
+ border: 8px solid transparent;
314
+ border-bottom-color: rgba(33, 37, 41, 0.9); /* Changed from border-top-color to border-bottom-color */
315
+ margin-top: -8px; /* Changed from margin-bottom to margin-top */
316
+ z-index: 1000;
317
+ }
318
+
319
+ .clickable-badge {
320
+ cursor: pointer;
321
+ transition: transform 0.2s, opacity 0.2s;
322
+ opacity: 0.4;
323
+ }
324
+
325
+ .clickable-badge:hover {
326
+ transform: scale(1.05);
327
+ opacity: 1;
328
+ }
329
+
330
+ .clickable-badge.selected-filter {
331
+ opacity: 1;
332
+ box-shadow: 0 0 0 2px #fff, 0 0 0 4px currentColor;
333
+ }
334
+
335
+ /* Ensure tooltips appear above the sticky header */
336
+ .status-container:hover::after,
337
+ .status-container:hover::before,
338
+ #last-updated:hover::after,
339
+ #last-updated:hover::before {
340
+ z-index: 1001;
341
+ }
342
+
343
+ /* Add tooltip styles for refresh label */
344
+ .refresh-label {
53
345
  position: relative;
54
- z-index: 2;
346
+ cursor: help;
55
347
  }
56
348
 
57
- /* Customize the select focus/hover states */
58
- .form-select:focus {
59
- border-color: #dee2e6;
60
- box-shadow: 0 0 0 0.1rem rgba(0,0,0,0.1);
349
+ .refresh-label:hover::after {
350
+ content: attr(data-tooltip);
351
+ position: absolute;
352
+ left: 50%;
353
+ top: 100%;
354
+ transform: translateX(-50%) translateY(8px);
355
+ z-index: 1001;
356
+ background-color: rgba(33, 37, 41, 0.9);
357
+ color: white;
358
+ padding: 0.5rem 1rem;
359
+ border-radius: 6px;
360
+ font-size: 0.875rem;
361
+ white-space: nowrap;
362
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
61
363
  }
62
364
 
365
+ .refresh-label:hover::before {
366
+ content: '';
367
+ position: absolute;
368
+ left: 50%;
369
+ top: 100%;
370
+ transform: translateX(-50%);
371
+ border: 8px solid transparent;
372
+ border-bottom-color: rgba(33, 37, 41, 0.9);
373
+ z-index: 1001;
374
+ }
63
375
  </style>
64
376
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
65
377
  <script
@@ -70,29 +382,84 @@
70
382
  <body>
71
383
  <div class="container">
72
384
  <header>
73
- <h1>SkyPilot managed jobs</h1>
74
- <p class="text-muted mt-4" id="last-updated"></p>
75
- <div class="form-check form-switch">
76
- <input class="form-check-input" type="checkbox" id="refresh-toggle" checked>
77
- <label class="form-check-label" for="refresh-toggle">Auto-refresh (every 30s)</label>
78
- </div>
79
- <div class="filter-controls">
80
- <span class="fw-medium fs-6">Filter by status:</span>
81
- <select class="form-select" id="status-filter" style="width: auto;">
82
- <option value="">All statuses</option>
83
- {% for status in status_values %}
84
- <option value="{{ status }}">{{ status }}</option>
85
- {% endfor %}
86
- </select>
385
+ <div class="d-flex justify-content-between align-items-center">
386
+ <h1>SkyPilot Managed Jobs</h1>
387
+ <div class="d-flex align-items-center">
388
+ <div class="form-check form-switch me-3">
389
+ <input class="form-check-input" type="checkbox" id="refresh-toggle" checked>
390
+ <label class="form-check-label refresh-label" for="refresh-toggle" data-tooltip="Refreshes every 30 seconds">
391
+ Auto-refresh
392
+ <span class="refresh-indicator">
393
+ <span class="refresh-spinner" id="refresh-spinner"></span>
394
+ </span>
395
+ </label>
396
+ </div>
397
+ <p class="text-muted mb-0" id="last-updated" data-tooltip="{{ utcTimestamp }}"></p>
398
+ </div>
87
399
  </div>
88
400
  </header>
89
401
 
402
+ <!-- Hidden status filter -->
403
+ <select id="status-filter" style="display: none;">
404
+ <option value="">All</option>
405
+ {% for status in status_values %}
406
+ <option value="{{ status }}">{{ status }}</option>
407
+ {% endfor %}
408
+ </select>
409
+
410
+ {% if rows %}
411
+ <p>Filter by status:
412
+ <span class="badge bg-secondary clickable-badge selected-filter me-2" data-status="ALL">All</span>
413
+ {% set status_dict = {} %}
414
+ {% for row in rows %}
415
+ {% set status = row[9].split()[0] %}
416
+ {% if status not in status_dict %}
417
+ {% set _ = status_dict.update({status: 1}) %}
418
+ {% else %}
419
+ {% set _ = status_dict.update({status: status_dict[status] + 1}) %}
420
+ {% endif %}
421
+ {% endfor %}
422
+ {% for status, count in status_dict|dictsort %}
423
+ <span class="me-2">
424
+ <span class="me-1">| {{ count }}</span>
425
+ {% if status.startswith('RUNNING') %}
426
+ <span class="badge bg-primary clickable-badge" data-status="{{ status }}">{{ status }}</span>
427
+ {% elif status.startswith('PENDING') or status.startswith('SUBMITTED') %}
428
+ <span class="badge bg-light clickable-badge" data-status="{{ status }}">{{ status }}</span>
429
+ {% elif status.startswith('RECOVERING') or status.startswith('CANCELLING') or status.startswith('STARTING') %}
430
+ <span class="badge bg-info clickable-badge" data-status="{{ status }}">{{ status }}</span>
431
+ {% elif status.startswith('SUCCEEDED') %}
432
+ <span class="badge bg-success clickable-badge" data-status="{{ status }}">{{ status }}</span>
433
+ {% elif status.startswith('CANCELLED') %}
434
+ <span class="badge bg-secondary clickable-badge" data-status="{{ status }}">{{ status }}</span>
435
+ {% elif status.startswith('FAILED') %}
436
+ <span class="badge bg-danger clickable-badge" data-status="{{ status }}">{{ status }}</span>
437
+ {% else %}
438
+ <span class="clickable-badge" data-status="{{ status }}">{{ status }}</span>
439
+ {% endif %}
440
+ </span>
441
+ {% endfor %}
442
+ </p>
443
+ {% else %}
444
+ <p>No jobs found.</p>
445
+ {% endif %}
446
+
90
447
  <table class="table table-hover table-hover-selected fixed-header-table" id="jobs-table">
91
448
  <thead>
92
449
  <tr>
93
- {% for column in columns %}
94
- <th>{{ column }}</th>
95
- {% endfor %}
450
+ <td></td>
451
+ <th>ID</th>
452
+ <th>Task</th>
453
+ <th>Name</th>
454
+ <th>Total Duration</th>
455
+ <th>Job Duration</th>
456
+ <th>Status</th>
457
+ <th>Resources</th>
458
+ <th>Cluster</th>
459
+ <th>Region</th>
460
+ <th>Recoveries</th>
461
+ <th>Details</th>
462
+ <th>Actions</th>
96
463
  </tr>
97
464
  </thead>
98
465
  <tbody>
@@ -102,33 +469,41 @@
102
469
  <td>{{ row[1]|string|replace(' \u21B3', '') }}</td>
103
470
  <td>{{ row[2] }}</td>
104
471
  <td>{{ row[3] }}</td>
105
- <td>{{ row[4] }}</td>
106
- <td>{{ row[5] }}</td>
107
472
  <td>{{ row[6] }}</td>
108
473
  <td>{{ row[7] }}</td>
109
- <td>{{ row[8] }}</td>
110
474
  <td>
111
- <!-- https://getbootstrap.com/docs/4.0/components/badge/ -->
112
- {% if row[9].startswith('RUNNING') %}
113
- <span class="badge bg-primary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
114
- {% elif row[9].startswith('PENDING') or row[9].startswith('SUBMITTED') %}
115
- <span class="badge bg-light">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
116
- {% elif row[9].startswith('RECOVERING') or row[9].startswith('CANCELLING') or row[9].startswith('STARTING') %}
117
- <span class="badge bg-info">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
118
- {% elif row[9].startswith('SUCCEEDED') %}
119
- <span class="badge bg-success">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
120
- {% elif row[9].startswith('CANCELLED') %}
121
- <span class="badge bg-secondary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
122
- {% elif row[9].startswith('FAILED') %}
123
- <span class="badge bg-warning">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
124
- {% else %}
125
- {{ row[9] }}
475
+ <!-- Status column with tooltip -->
476
+ <div class="status-container" style="position: relative;" data-tooltip="{{ row[14] }}">
477
+ {% if row[9].startswith('RUNNING') %}
478
+ <span class="badge bg-primary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
479
+ {% elif row[9].startswith('PENDING') or row[9].startswith('SUBMITTED') %}
480
+ <span class="badge bg-warning">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
481
+ {% elif row[9].startswith('RECOVERING') or row[9].startswith('CANCELLING') or row[9].startswith('STARTING') %}
482
+ <span class="badge bg-info">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
483
+ {% elif row[9].startswith('SUCCEEDED') %}
484
+ <span class="badge bg-success">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
485
+ {% elif row[9].startswith('CANCELLED') %}
486
+ <span class="badge bg-secondary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
487
+ {% elif row[9].startswith('FAILED') %}
488
+ <span class="badge bg-danger">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
489
+ {% else %}
490
+ {{ row[9] }}
491
+ {% endif %}
492
+ </div>
493
+ </td>
494
+ <td>{{ row[4] }}</td> {# Resources #}
495
+ <td>{{ row[11] }}</td> {# Cluster #}
496
+ <td>{{ row[12] }}</td> {# Region #}
497
+ <td>{{ row[8] }}</td> {# Recoveries #}
498
+ <td data-full-text="{{ row[13] }}">{{ row[13] }}</td> {# Details #}
499
+ <td>
500
+ {% if row[1]|string|replace(' \u21B3', '') and row[1]|string|replace(' \u21B3', '') != '-' %}
501
+ <a href="{{ url_for('download_log', job_id=row[1]|string|replace(' \u21B3', '')) }}"
502
+ class="btn btn-sm btn-outline-secondary">
503
+ controller log
504
+ </a>
126
505
  {% endif %}
127
506
  </td>
128
- <td>{{ row[10] }}</td>
129
- <td>{{ row[11] }}</td>
130
- <td>{{ row[12] }}</td>
131
- <td>{{ row[13] }}</td>
132
507
  </tr>
133
508
  {% endfor %}
134
509
  </tbody>
@@ -197,7 +572,9 @@
197
572
  document.addEventListener("DOMContentLoaded", function () {
198
573
  var timestamp = "{{ last_updated_timestamp }}"; // Get the UTC timestamp from the template
199
574
  var localTimestamp = moment.utc(timestamp).tz(moment.tz.guess()).format('YYYY-MM-DD HH:mm:ss z');
575
+ var utcTimestamp = moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss UTC');
200
576
  document.getElementById("last-updated").textContent = "Last updated: " + localTimestamp;
577
+ document.getElementById("last-updated").setAttribute('data-tooltip', utcTimestamp);
201
578
  });
202
579
  </script>
203
580
  <script>
@@ -210,6 +587,7 @@
210
587
  } else {
211
588
  refreshToggle.checked = false;
212
589
  }
590
+
213
591
  // Add event listener to the toggle switch
214
592
  function handleAutoRefresh() {
215
593
  localStorage.setItem("refreshState", refreshToggle.checked);
@@ -217,6 +595,9 @@
217
595
  if (refreshToggle.checked) {
218
596
  // Auto-refresh is enabled
219
597
  refreshInterval = setInterval(function () {
598
+ // Store current filter before reload
599
+ var currentFilter = document.getElementById("status-filter").value;
600
+ localStorage.setItem("statusFilter", currentFilter);
220
601
  location.reload();
221
602
  }, 30000); // 30 seconds in milliseconds
222
603
  } else {
@@ -224,6 +605,17 @@
224
605
  clearInterval(refreshInterval);
225
606
  }
226
607
  }
608
+
609
+ // Restore filter state after page load
610
+ document.addEventListener("DOMContentLoaded", function() {
611
+ var savedFilter = localStorage.getItem("statusFilter");
612
+ if (savedFilter) {
613
+ var statusFilter = document.getElementById("status-filter");
614
+ statusFilter.value = savedFilter;
615
+ filterStatus(savedFilter);
616
+ }
617
+ });
618
+
227
619
  refreshToggle.addEventListener("change", handleAutoRefresh);
228
620
  handleAutoRefresh();
229
621
  </script>
@@ -231,13 +623,15 @@
231
623
  function filterStatus(status) {
232
624
  var rows = document.querySelectorAll("#jobs-table tbody tr");
233
625
  rows.forEach(function(row) {
234
- var statusCell = row.querySelector("td:nth-child(10)"); // Status is in the 10th column
235
- var statusText = statusCell.textContent.trim().split(' ')[0]; // Get first word of status
626
+ var statusCell = row.querySelector("td:nth-child(7)"); // Status is now in the 7th column
627
+ if (statusCell) {
628
+ var statusText = statusCell.textContent.trim().split(' ')[0]; // Get first word of status
236
629
 
237
- if (status === '' || statusText === status) {
238
- row.style.display = "";
239
- } else {
240
- row.style.display = "none";
630
+ if (status === '' || statusText === status) {
631
+ row.style.display = "";
632
+ } else {
633
+ row.style.display = "none";
634
+ }
241
635
  }
242
636
  });
243
637
  }
@@ -249,7 +643,92 @@
249
643
  });
250
644
  });
251
645
  </script>
646
+ <script>
647
+ // Show loading spinner during refresh
648
+ window.addEventListener('beforeunload', function() {
649
+ document.getElementById('refresh-spinner').style.display = 'inline-block';
650
+ });
651
+ </script>
652
+ <script>
653
+ // Update column indices for job table
654
+ const JOB_TABLE_COLUMNS = [
655
+ '', 'ID', 'Task', 'Name', 'Total Duration',
656
+ 'Job Duration', 'Status', 'Resources', 'Cluster', 'Region', 'Recoveries', 'Details',
657
+ 'Actions'
658
+ ];
659
+ </script>
660
+ <script>
661
+ // Replace the existing click handler for status badges with this updated version
662
+ document.addEventListener("DOMContentLoaded", function() {
663
+ const statusFilter = document.getElementById('status-filter');
664
+ const badges = document.querySelectorAll('.clickable-badge');
665
+
666
+ // Set initial state
667
+ const savedFilter = localStorage.getItem("statusFilter") || '';
668
+ updateSelectedBadge(savedFilter);
669
+
670
+ badges.forEach(function(badge) {
671
+ badge.addEventListener('click', function() {
672
+ const status = this.dataset.status;
673
+ const currentFilter = statusFilter.value;
674
+
675
+ // If clicking the already selected filter, clear it (show all)
676
+ const newStatus = (status === currentFilter || (status === 'ALL' && currentFilter === '')) ? '' :
677
+ (status === 'ALL' ? '' : status);
678
+
679
+ // Update filter and UI
680
+ statusFilter.value = newStatus;
681
+ filterStatus(newStatus);
682
+ localStorage.setItem("statusFilter", newStatus);
683
+ updateSelectedBadge(newStatus);
684
+ });
685
+ });
686
+
687
+ function updateSelectedBadge(selectedStatus) {
688
+ badges.forEach(badge => {
689
+ badge.classList.remove('selected-filter');
690
+ if ((selectedStatus === '' && badge.dataset.status === 'ALL') ||
691
+ badge.dataset.status === selectedStatus) {
692
+ badge.classList.add('selected-filter');
693
+ }
694
+ });
695
+ }
696
+ });
697
+ </script>
698
+ <script>
699
+ // Add scroll event listener to handle sticky header shadow
700
+ document.addEventListener("DOMContentLoaded", function() {
701
+ const header = document.querySelector('header');
702
+ const container = document.querySelector('.container');
252
703
 
704
+ window.addEventListener('scroll', function() {
705
+ if (container.getBoundingClientRect().top < 0) {
706
+ header.classList.add('sticky');
707
+ } else {
708
+ header.classList.remove('sticky');
709
+ }
710
+ });
711
+ });
712
+ </script>
713
+ <script>
714
+ // Add click handler for Details cells
715
+ document.addEventListener('DOMContentLoaded', function() {
716
+ const detailsCells = document.querySelectorAll('#jobs-table td:nth-child(12)');
717
+
718
+ detailsCells.forEach(cell => {
719
+ // Check if content is truncated
720
+ if (cell.scrollWidth > cell.clientWidth) {
721
+ cell.classList.add('expandable');
722
+ }
723
+
724
+ cell.addEventListener('click', function() {
725
+ if (this.scrollWidth > this.clientWidth || this.classList.contains('expanded')) {
726
+ this.classList.toggle('expanded');
727
+ }
728
+ });
729
+ });
730
+ });
731
+ </script>
253
732
  </body>
254
733
 
255
734
  </html>