skypilot-nightly 1.0.0.dev20250611__py3-none-any.whl → 1.0.0.dev20250612__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 (33) hide show
  1. sky/__init__.py +2 -2
  2. sky/check.py +41 -2
  3. sky/clouds/kubernetes.py +1 -1
  4. sky/dashboard/out/404.html +1 -1
  5. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  6. sky/dashboard/out/clusters/[cluster].html +1 -1
  7. sky/dashboard/out/clusters.html +1 -1
  8. sky/dashboard/out/config.html +1 -1
  9. sky/dashboard/out/index.html +1 -1
  10. sky/dashboard/out/infra/[context].html +1 -1
  11. sky/dashboard/out/infra.html +1 -1
  12. sky/dashboard/out/jobs/[job].html +1 -1
  13. sky/dashboard/out/jobs.html +1 -1
  14. sky/dashboard/out/users.html +1 -1
  15. sky/dashboard/out/workspace/new.html +1 -1
  16. sky/dashboard/out/workspaces/[name].html +1 -1
  17. sky/dashboard/out/workspaces.html +1 -1
  18. sky/global_user_state.py +38 -0
  19. sky/jobs/server/core.py +1 -68
  20. sky/templates/jobs-controller.yaml.j2 +0 -23
  21. sky/utils/controller_utils.py +10 -0
  22. {skypilot_nightly-1.0.0.dev20250611.dist-info → skypilot_nightly-1.0.0.dev20250612.dist-info}/METADATA +1 -1
  23. {skypilot_nightly-1.0.0.dev20250611.dist-info → skypilot_nightly-1.0.0.dev20250612.dist-info}/RECORD +29 -33
  24. sky/jobs/dashboard/dashboard.py +0 -223
  25. sky/jobs/dashboard/static/favicon.ico +0 -0
  26. sky/jobs/dashboard/templates/index.html +0 -831
  27. sky/jobs/server/dashboard_utils.py +0 -69
  28. /sky/dashboard/out/_next/static/{zJqasksBQ3HcqMpA2wTUZ → G3DXdMFu2Jzd-Dody9iq1}/_buildManifest.js +0 -0
  29. /sky/dashboard/out/_next/static/{zJqasksBQ3HcqMpA2wTUZ → G3DXdMFu2Jzd-Dody9iq1}/_ssgManifest.js +0 -0
  30. {skypilot_nightly-1.0.0.dev20250611.dist-info → skypilot_nightly-1.0.0.dev20250612.dist-info}/WHEEL +0 -0
  31. {skypilot_nightly-1.0.0.dev20250611.dist-info → skypilot_nightly-1.0.0.dev20250612.dist-info}/entry_points.txt +0 -0
  32. {skypilot_nightly-1.0.0.dev20250611.dist-info → skypilot_nightly-1.0.0.dev20250612.dist-info}/licenses/LICENSE +0 -0
  33. {skypilot_nightly-1.0.0.dev20250611.dist-info → skypilot_nightly-1.0.0.dev20250612.dist-info}/top_level.txt +0 -0
@@ -1,831 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
8
- <title>SkyPilot Managed Jobs</title>
9
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
10
- integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
11
- <style>
12
- :root {
13
- --secondary-color: #6c757d;
14
- --success-color: #198754;
15
- --warning-color: #ffc107;
16
- --info-color: #0dcaf0;
17
- --light-color: #f8f9fa;
18
- }
19
-
20
- body {
21
- margin-top: 0;
22
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
23
- background-color: white;
24
- }
25
-
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;
34
- }
35
-
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;
44
- }
45
-
46
- /* Add shadow when header is sticky */
47
- header.sticky {
48
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
49
- }
50
-
51
- h1 {
52
- color: var(--primary-color);
53
- font-weight: 600;
54
- font-size: 2rem;
55
- }
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);
148
- }
149
-
150
- .filter-controls {
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 {
345
- position: relative;
346
- cursor: help;
347
- }
348
-
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);
363
- }
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
- }
375
- </style>
376
- <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
377
- <script
378
- src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
379
-
380
- </head>
381
-
382
- <body>
383
- <div class="container">
384
- <header>
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>
399
- </div>
400
- </header>
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
-
447
- <table class="table table-hover table-hover-selected fixed-header-table" id="jobs-table">
448
- <thead>
449
- <tr>
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>
463
- </tr>
464
- </thead>
465
- <tbody>
466
- {% for row in rows %}
467
- <tr class="{% if row[2] == '' %}folder{% endif %}{% if row[1] == ' \u21B3' %}folded{% endif %}">
468
- <td>{% if row[2] == '' %}<span class="clickable">▶</span>{% endif %}</td>
469
- <td>{{ row[1]|string|replace(' \u21B3', '') }}</td>
470
- <td>{{ row[2] }}</td>
471
- <td>{{ row[3] }}</td>
472
- <td>{{ row[6] }}</td>
473
- <td>{{ row[7] }}</td>
474
- <td>
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
- <button class="btn btn-sm btn-outline-secondary log-btn"
502
- data-job-id="{{ row[1]|string|replace(' \u21B3', '') }}">
503
- controller log
504
- </button>
505
- {% endif %}
506
- </td>
507
- </tr>
508
- {% endfor %}
509
- </tbody>
510
- </table>
511
- </div>
512
-
513
- <!-- Hidden container for log content -->
514
- <div style="display: none;">
515
- {% for row in rows %}
516
- {% if row[1]|string|replace(' \u21B3', '') and row[1]|string|replace(' \u21B3', '') != '-' %}
517
- <pre id="log-content-{{ row[1]|string|replace(' \u21B3', '') }}">{{ row[-1]|e }}</pre>
518
- {% endif %}
519
- {% endfor %}
520
- </div>
521
-
522
- <!-- Log Modal -->
523
- <div class="modal fade" id="logModal" tabindex="-1" aria-labelledby="logModalLabel" aria-hidden="true">
524
- <div class="modal-dialog modal-dialog-centered modal-lg">
525
- <div class="modal-content">
526
- <div class="modal-header">
527
- <h5 class="modal-title" id="logModalLabel">Controller Log</h5>
528
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
529
- </div>
530
- <div class="modal-body">
531
- <div id="logContent" style="white-space: pre-wrap; font-family: monospace; max-height: 70vh; overflow-y: auto; font-size: 0.85rem;"></div>
532
- <div id="logError" class="alert alert-danger d-none">
533
- Error loading log content. Please try again.
534
- </div>
535
- </div>
536
- <div class="modal-footer">
537
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
538
- <a id="downloadLogBtn" href="#" class="btn btn-primary" download>Download</a>
539
- </div>
540
- </div>
541
- </div>
542
- </div>
543
-
544
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
545
- integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
546
- crossorigin="anonymous"></script>
547
- <script>
548
- // Folder toggle for pipelines, this will fold/unfold the rows for
549
- // a pipeline and its tasks.
550
- document.querySelectorAll('.folder').forEach(function (folder) {
551
- folder.addEventListener('click', function () {
552
- let plusSign = folder.querySelector('.clickable');
553
- let folderIndex = Array.from(document.querySelectorAll('.folder')).indexOf(folder);
554
-
555
- if (!plusSign) {
556
- plusSign = document.createElement('span');
557
- plusSign.classList.add('clickable');
558
- folder.children[0].prepend(plusSign); // Ensure the clickable span is the first child of the folder cell
559
- }
560
-
561
- let displayState = 'none';
562
- if (plusSign.textContent === '▶') {
563
- plusSign.textContent = '▼'; // Change to downward-pointing triangle
564
- displayState = '';
565
- localStorage.setItem('folderState' + folderIndex, 'expanded');
566
- } else {
567
- plusSign.textContent = '▶'; // Change back to right-pointing triangle
568
- localStorage.setItem('folderState' + folderIndex, 'collapsed');
569
- }
570
-
571
- let next = folder.nextElementSibling;
572
- while (next && next.classList.contains('folded')) {
573
- next.style.display = displayState;
574
- next = next.nextElementSibling;
575
- }
576
- });
577
- });
578
-
579
- document.addEventListener("DOMContentLoaded", function () {
580
- // Setting initial fold state for each folder
581
- document.querySelectorAll('.folder').forEach(function (folder, index) {
582
- let storedState = localStorage.getItem('folderState' + index);
583
- let plusSign = folder.querySelector('.clickable');
584
- let next = folder.nextElementSibling;
585
- if (storedState === 'expanded') {
586
- plusSign.textContent = '▼';
587
- while (next && next.classList.contains('folded')) {
588
- next.style.display = ''; // Show
589
- next = next.nextElementSibling;
590
- }
591
- } else {
592
- plusSign.textContent = '▶';
593
- while (next && next.classList.contains('folded')) {
594
- next.style.display = 'none'; // Hide initially
595
- next = next.nextElementSibling;
596
- }
597
- }
598
- });
599
- document.querySelectorAll('.folded').forEach(function (folded) {
600
- if (!folded.previousElementSibling.querySelector('.clickable').textContent.includes('▼')) {
601
- folded.style.display = 'none'; // Make sure these rows are initially hidden unless state is expanded
602
- }
603
- });
604
- });
605
- </script>
606
- <script>
607
- document.addEventListener("DOMContentLoaded", function () {
608
- var timestamp = "{{ last_updated_timestamp }}"; // Get the UTC timestamp from the template
609
- var localTimestamp = moment.utc(timestamp).tz(moment.tz.guess()).format('YYYY-MM-DD HH:mm:ss z');
610
- var utcTimestamp = moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss UTC');
611
- document.getElementById("last-updated").textContent = "Last updated: " + localTimestamp;
612
- document.getElementById("last-updated").setAttribute('data-tooltip', utcTimestamp);
613
- });
614
- </script>
615
- <script>
616
- // Function to handle toggle of auto-refresh
617
- // Retrieve the previous state from localStorage
618
- var storedRefreshState = localStorage.getItem("refreshState");
619
- var refreshToggle = document.getElementById("refresh-toggle");
620
- if (storedRefreshState === 'true' || storedRefreshState === null) {
621
- refreshToggle.checked = true;
622
- } else {
623
- refreshToggle.checked = false;
624
- }
625
-
626
- // Add event listener to the toggle switch
627
- function handleAutoRefresh() {
628
- localStorage.setItem("refreshState", refreshToggle.checked);
629
- // Check the state of the toggle switch
630
- if (refreshToggle.checked) {
631
- // Auto-refresh is enabled
632
- refreshInterval = setInterval(function () {
633
- // Store current filter before reload
634
- var currentFilter = document.getElementById("status-filter").value;
635
- localStorage.setItem("statusFilter", currentFilter);
636
- location.reload();
637
- }, 30000); // 30 seconds in milliseconds
638
- } else {
639
- // Auto-refresh is disabled
640
- clearInterval(refreshInterval);
641
- }
642
- }
643
-
644
- // Restore filter state after page load
645
- document.addEventListener("DOMContentLoaded", function() {
646
- var savedFilter = localStorage.getItem("statusFilter");
647
- if (savedFilter) {
648
- var statusFilter = document.getElementById("status-filter");
649
- statusFilter.value = savedFilter;
650
- filterStatus(savedFilter);
651
- }
652
- });
653
-
654
- refreshToggle.addEventListener("change", handleAutoRefresh);
655
- handleAutoRefresh();
656
- </script>
657
- <script>
658
- function filterStatus(status) {
659
- var rows = document.querySelectorAll("#jobs-table tbody tr");
660
- rows.forEach(function(row) {
661
- var statusCell = row.querySelector("td:nth-child(7)"); // Status is now in the 7th column
662
- if (statusCell) {
663
- var statusText = statusCell.textContent.trim().split(' ')[0]; // Get first word of status
664
-
665
- if (status === '' || statusText === status) {
666
- row.style.display = "";
667
- } else {
668
- row.style.display = "none";
669
- }
670
- }
671
- });
672
- }
673
-
674
- // Add event listener for the status filter
675
- document.addEventListener("DOMContentLoaded", function() {
676
- document.getElementById("status-filter").addEventListener("change", function() {
677
- filterStatus(this.value);
678
- });
679
- });
680
- </script>
681
- <script>
682
- // Show loading spinner during refresh
683
- window.addEventListener('beforeunload', function() {
684
- document.getElementById('refresh-spinner').style.display = 'inline-block';
685
- });
686
- </script>
687
- <script>
688
- // Update column indices for job table
689
- const JOB_TABLE_COLUMNS = [
690
- '', 'ID', 'Task', 'Name', 'Total Duration',
691
- 'Job Duration', 'Status', 'Resources', 'Cluster', 'Region', 'Recoveries', 'Details',
692
- 'Actions'
693
- ];
694
- </script>
695
- <script>
696
- // Replace the existing click handler for status badges with this updated version
697
- document.addEventListener("DOMContentLoaded", function() {
698
- const statusFilter = document.getElementById('status-filter');
699
- const badges = document.querySelectorAll('.clickable-badge');
700
-
701
- // Set initial state
702
- const savedFilter = localStorage.getItem("statusFilter") || '';
703
- updateSelectedBadge(savedFilter);
704
-
705
- badges.forEach(function(badge) {
706
- badge.addEventListener('click', function() {
707
- const status = this.dataset.status;
708
- const currentFilter = statusFilter.value;
709
-
710
- // If clicking the already selected filter, clear it (show all)
711
- const newStatus = (status === currentFilter || (status === 'ALL' && currentFilter === '')) ? '' :
712
- (status === 'ALL' ? '' : status);
713
-
714
- // Update filter and UI
715
- statusFilter.value = newStatus;
716
- filterStatus(newStatus);
717
- localStorage.setItem("statusFilter", newStatus);
718
- updateSelectedBadge(newStatus);
719
- });
720
- });
721
-
722
- function updateSelectedBadge(selectedStatus) {
723
- badges.forEach(badge => {
724
- badge.classList.remove('selected-filter');
725
- if ((selectedStatus === '' && badge.dataset.status === 'ALL') ||
726
- badge.dataset.status === selectedStatus) {
727
- badge.classList.add('selected-filter');
728
- }
729
- });
730
- }
731
- });
732
- </script>
733
- <script>
734
- // Add scroll event listener to handle sticky header shadow
735
- document.addEventListener("DOMContentLoaded", function() {
736
- const header = document.querySelector('header');
737
- const container = document.querySelector('.container');
738
-
739
- window.addEventListener('scroll', function() {
740
- if (container.getBoundingClientRect().top < 0) {
741
- header.classList.add('sticky');
742
- } else {
743
- header.classList.remove('sticky');
744
- }
745
- });
746
- });
747
- </script>
748
- <script>
749
- // Add click handler for Details cells
750
- document.addEventListener('DOMContentLoaded', function() {
751
- const detailsCells = document.querySelectorAll('#jobs-table td:nth-child(12)');
752
-
753
- detailsCells.forEach(cell => {
754
- // Check if content is truncated
755
- if (cell.scrollWidth > cell.clientWidth) {
756
- cell.classList.add('expandable');
757
- }
758
-
759
- cell.addEventListener('click', function() {
760
- if (this.scrollWidth > this.clientWidth || this.classList.contains('expanded')) {
761
- this.classList.toggle('expanded');
762
- }
763
- });
764
- });
765
- });
766
- </script>
767
- <script>
768
- // Function to show log modal and display pre-loaded log content
769
- function showLogModal(jobId, logContent) {
770
- try {
771
- // Initialize modal
772
- const logModal = new bootstrap.Modal(document.getElementById('logModal'));
773
- const logContentEl = document.getElementById('logContent');
774
- const logError = document.getElementById('logError');
775
- const downloadBtn = document.getElementById('downloadLogBtn');
776
-
777
- // Create a Blob for download functionality
778
- const blob = new Blob([logContent], { type: 'text/plain' });
779
- const url = URL.createObjectURL(blob);
780
-
781
- // Set download button href
782
- downloadBtn.href = url;
783
- downloadBtn.setAttribute('download', `job_${jobId}.log`);
784
-
785
- // Clear previous content and show new content directly
786
- logContentEl.textContent = logContent || 'No log content available';
787
- logError.classList.add('d-none');
788
-
789
- // Set modal title
790
- document.getElementById('logModalLabel').textContent = `Controller Log - Job ${jobId}`;
791
-
792
- // Show modal
793
- logModal.show();
794
-
795
- // Cleanup the URL object when the modal is hidden
796
- document.getElementById('logModal').addEventListener('hidden.bs.modal', function() {
797
- URL.revokeObjectURL(url);
798
- }, { once: true });
799
- } catch (error) {
800
- console.error('Error showing log modal:', error);
801
- document.getElementById('logError').classList.remove('d-none');
802
- document.getElementById('logError').textContent = `Error showing log: ${error.message}`;
803
- }
804
- }
805
-
806
- // Add event listeners for log buttons
807
- document.addEventListener('DOMContentLoaded', function() {
808
- document.addEventListener('click', function(event) {
809
- if (event.target.closest('.log-btn')) {
810
- try {
811
- const button = event.target.closest('.log-btn');
812
- const jobId = button.dataset.jobId;
813
- const logContentEl = document.getElementById(`log-content-${jobId}`);
814
-
815
- if (!logContentEl) {
816
- throw new Error(`Log content element not found for job ${jobId}`);
817
- }
818
-
819
- const logContent = logContentEl.textContent;
820
- showLogModal(jobId, logContent);
821
- } catch (error) {
822
- console.error('Error getting log content:', error);
823
- showLogModal(jobId, `Error loading log: ${error.message}`);
824
- }
825
- }
826
- });
827
- });
828
- </script>
829
- </body>
830
-
831
- </html>