hcs-cli 0.1.321__py3-none-any.whl → 0.1.322__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.
- hcs_cli/__init__.py +1 -1
- hcs_cli/cmds/api.py +10 -28
- hcs_cli/cmds/clouddriver/{summary.py → service.py} +11 -4
- hcs_cli/cmds/scm/template.py +7 -3
- hcs_cli/cmds/task.py +47 -1
- hcs_cli/service/api.py +104 -0
- hcs_cli/service/clouddriver/__init__.py +2 -0
- hcs_cli/service/clouddriver/service.py +29 -0
- hcs_cli/service/task.py +6 -0
- hcs_cli/support/scm/plan-editor.html.template +570 -48
- hcs_cli/support/scm/plan_editor.py +73 -2
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.322.dist-info}/METADATA +2 -2
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.322.dist-info}/RECORD +15 -13
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.322.dist-info}/WHEEL +0 -0
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.322.dist-info}/entry_points.txt +0 -0
|
@@ -6,18 +6,24 @@
|
|
|
6
6
|
<style>
|
|
7
7
|
body {
|
|
8
8
|
font-family: sans-serif;
|
|
9
|
-
padding:
|
|
9
|
+
padding: 0;
|
|
10
|
+
margin: 0;
|
|
10
11
|
}
|
|
11
12
|
.day-row {
|
|
12
13
|
margin-bottom: 40px;
|
|
13
14
|
position: relative;
|
|
15
|
+
padding: 0 40px;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: flex-start;
|
|
18
|
+
gap: 20px;
|
|
14
19
|
}
|
|
15
20
|
.label {
|
|
16
21
|
font-weight: bold;
|
|
17
22
|
margin-bottom: 8px;
|
|
18
23
|
display: flex;
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
align-items: flex-start;
|
|
26
|
+
gap: 4px;
|
|
21
27
|
}
|
|
22
28
|
.label-left {
|
|
23
29
|
display: flex;
|
|
@@ -39,10 +45,10 @@
|
|
|
39
45
|
align-items: flex-end;
|
|
40
46
|
height: 200px;
|
|
41
47
|
border-left: 1px solid #ccc;
|
|
42
|
-
position: relative;
|
|
43
48
|
border-bottom: 1px solid #ccc;
|
|
44
|
-
|
|
45
|
-
margin-left:
|
|
49
|
+
flex: 1;
|
|
50
|
+
margin-left: 0;
|
|
51
|
+
min-width: 400px;
|
|
46
52
|
}
|
|
47
53
|
.bar-wrapper {
|
|
48
54
|
position: relative;
|
|
@@ -89,9 +95,11 @@
|
|
|
89
95
|
.y-axis-label {
|
|
90
96
|
position: absolute;
|
|
91
97
|
top: 30px;
|
|
92
|
-
left: -
|
|
98
|
+
left: -50px;
|
|
93
99
|
font-size: 10px;
|
|
94
100
|
font-weight: normal;
|
|
101
|
+
text-align: right;
|
|
102
|
+
width: 40px;
|
|
95
103
|
}
|
|
96
104
|
.x-tick {
|
|
97
105
|
position: absolute;
|
|
@@ -176,16 +184,19 @@
|
|
|
176
184
|
z-index: 10;
|
|
177
185
|
}
|
|
178
186
|
.container {
|
|
179
|
-
width:
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
width: 100%;
|
|
188
|
+
max-width: none;
|
|
189
|
+
margin: 0;
|
|
190
|
+
padding: 0;
|
|
182
191
|
}
|
|
183
192
|
.header {
|
|
184
193
|
display: flex;
|
|
185
194
|
justify-content: space-between;
|
|
186
195
|
align-items: center;
|
|
187
196
|
margin-bottom: 20px;
|
|
188
|
-
padding:
|
|
197
|
+
padding: 20px 40px;
|
|
198
|
+
background-color: #f9f9f9;
|
|
199
|
+
border-bottom: 1px solid #e0e0e0;
|
|
189
200
|
}
|
|
190
201
|
.timezone-selector {
|
|
191
202
|
display: flex;
|
|
@@ -204,6 +215,7 @@
|
|
|
204
215
|
/* Tab styles */
|
|
205
216
|
.tab-container {
|
|
206
217
|
margin-top: 20px;
|
|
218
|
+
padding: 0 40px;
|
|
207
219
|
}
|
|
208
220
|
.tab-buttons {
|
|
209
221
|
display: flex;
|
|
@@ -265,9 +277,12 @@
|
|
|
265
277
|
background-color: #e0e0e0;
|
|
266
278
|
}
|
|
267
279
|
#config-json {
|
|
268
|
-
padding
|
|
280
|
+
padding: 20px;
|
|
281
|
+
max-height: 600px;
|
|
282
|
+
overflow-y: auto;
|
|
269
283
|
}
|
|
270
284
|
</style>
|
|
285
|
+
<script src="https://echarts.apache.org/en/js/vendors/echarts/dist/echarts.min.js"></script>
|
|
271
286
|
</head>
|
|
272
287
|
<body>
|
|
273
288
|
|
|
@@ -278,37 +293,45 @@
|
|
|
278
293
|
<span>Timezone:</span>
|
|
279
294
|
<select id="timezone-select">
|
|
280
295
|
<option value="UTC">UTC</option>
|
|
281
|
-
<option value="
|
|
282
|
-
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
|
283
|
-
<option value="Europe/London">Europe/London</option>
|
|
284
|
-
<option value="Europe/Paris">Europe/Paris</option>
|
|
285
|
-
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
|
286
|
-
<option value="Australia/Sydney">Australia/Sydney</option>
|
|
296
|
+
<option id="browser-timezone-option" value="Browser">Browser</option>
|
|
287
297
|
</select>
|
|
288
298
|
</div>
|
|
289
299
|
</div>
|
|
290
|
-
|
|
300
|
+
|
|
291
301
|
<div class="tab-container">
|
|
292
302
|
<div class="tab-buttons">
|
|
293
|
-
<button class="tab-button active" data-tab="
|
|
303
|
+
<button class="tab-button active" data-tab="usage">Usage</button>
|
|
304
|
+
<button class="tab-button" data-tab="capacity">Capacity</button>
|
|
294
305
|
<button class="tab-button" data-tab="configuration">Configuration</button>
|
|
295
|
-
<button class="tab-button" data-tab="
|
|
306
|
+
<button class="tab-button" data-tab="data">Data</button>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div class="tab-content active" id="usage-tab">
|
|
310
|
+
<div style="margin-bottom: 10px; display: flex; justify-content: flex-end; align-items: center; gap: 10px;">
|
|
311
|
+
<button id="reset-chart-zoom" style="padding: 6px 12px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; display: none;">Reset Time Range</button>
|
|
312
|
+
<span id="selection-info" style="font-size: 12px; color: #666; font-weight: 500;">All timepoints</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div style="width: 100%; height: 400px; position: relative;">
|
|
315
|
+
<div id="usage-chart" style="width: 100%; height: 100%; cursor: crosshair;"></div>
|
|
316
|
+
<div id="chart-selection-overlay" style="position: absolute; top: 0; left: 0; width: 0; height: 100%; z-index: 10; display: none; background: rgba(100, 150, 255, 0.2); border-left: 2px solid #0066ff; border-right: 2px solid #0066ff; pointer-events: none;"></div>
|
|
317
|
+
</div>
|
|
296
318
|
</div>
|
|
297
|
-
|
|
298
|
-
<div class="tab-content
|
|
319
|
+
|
|
320
|
+
<div class="tab-content" id="capacity-tab">
|
|
299
321
|
<div class="week-stats"></div>
|
|
300
322
|
<div class="chart-container">
|
|
301
323
|
<div id="chart"></div>
|
|
302
324
|
</div>
|
|
303
325
|
</div>
|
|
304
|
-
|
|
326
|
+
|
|
305
327
|
<div class="tab-content" id="configuration-tab">
|
|
306
328
|
<button class="copy-button" id="copy-config">Copy to Clipboard</button>
|
|
307
329
|
<div class="code-container" id="config-json"></div>
|
|
308
330
|
</div>
|
|
309
331
|
|
|
310
|
-
<div class="tab-content" id="
|
|
311
|
-
|
|
332
|
+
<div class="tab-content" id="data-tab">
|
|
333
|
+
<button class="copy-button" id="copy-data">Copy to Clipboard</button>
|
|
334
|
+
<div class="code-container" id="chart-data-json"></div>
|
|
312
335
|
</div>
|
|
313
336
|
</div>
|
|
314
337
|
</div>
|
|
@@ -340,7 +363,7 @@
|
|
|
340
363
|
|
|
341
364
|
function updateBarColor(bar, idealCapacity, calculatedCapacity) {
|
|
342
365
|
const effectiveCalculatedCapacity = calculatedCapacity === undefined ? 0 : calculatedCapacity;
|
|
343
|
-
|
|
366
|
+
|
|
344
367
|
if (idealCapacity < effectiveCalculatedCapacity) {
|
|
345
368
|
bar.style.backgroundColor = '#ffa3a3'; // light red
|
|
346
369
|
} else if (idealCapacity > effectiveCalculatedCapacity * 1.1) {
|
|
@@ -363,7 +386,7 @@
|
|
|
363
386
|
const forecastCapacity = slotRecord.forecastCapacity || 0;
|
|
364
387
|
const calculatedCapacity = slotRecord.calculatedCapacity || 0;
|
|
365
388
|
const idealCapacity = slotRecord.idealCapacity || 0;
|
|
366
|
-
|
|
389
|
+
|
|
367
390
|
// Calculate delta between calculated and forecast
|
|
368
391
|
const delta = calculatedCapacity - forecastCapacity;
|
|
369
392
|
if (delta > 0) {
|
|
@@ -410,14 +433,16 @@
|
|
|
410
433
|
|
|
411
434
|
const label = document.createElement('div');
|
|
412
435
|
label.className = 'label';
|
|
413
|
-
|
|
436
|
+
|
|
414
437
|
const labelLeft = document.createElement('div');
|
|
415
438
|
labelLeft.className = 'label-left';
|
|
416
|
-
|
|
439
|
+
|
|
417
440
|
const dayName = document.createElement('span');
|
|
418
441
|
dayName.textContent = weekday.charAt(0).toUpperCase() + weekday.slice(1);
|
|
419
442
|
labelLeft.appendChild(dayName);
|
|
420
443
|
|
|
444
|
+
label.appendChild(labelLeft);
|
|
445
|
+
|
|
421
446
|
const applyCalculated = document.createElement('span');
|
|
422
447
|
applyCalculated.className = 'apply-calculated';
|
|
423
448
|
applyCalculated.textContent = 'apply calculated';
|
|
@@ -426,15 +451,15 @@
|
|
|
426
451
|
const slotKey = `${weekday}/${time}`;
|
|
427
452
|
const slotRecord = calendarData.calendar[slotKey] || {};
|
|
428
453
|
const calculatedCapacity = slotRecord.calculatedCapacity;
|
|
429
|
-
|
|
454
|
+
|
|
430
455
|
// Initialize slot if it doesn't exist
|
|
431
456
|
if (!calendarData.calendar[slotKey]) {
|
|
432
457
|
calendarData.calendar[slotKey] = {};
|
|
433
458
|
}
|
|
434
|
-
|
|
459
|
+
|
|
435
460
|
// Set idealCapacity to calculatedCapacity (0 if undefined)
|
|
436
461
|
calendarData.calendar[slotKey].idealCapacity = calculatedCapacity || 0;
|
|
437
|
-
|
|
462
|
+
|
|
438
463
|
// Update the bar height and color
|
|
439
464
|
const bar = document.querySelector(`[data-key="${slotKey}"] .bar`);
|
|
440
465
|
const barWrapper = document.querySelector(`[data-key="${slotKey}"]`);
|
|
@@ -457,9 +482,7 @@
|
|
|
457
482
|
});
|
|
458
483
|
updateWeekStats();
|
|
459
484
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
label.appendChild(labelLeft);
|
|
485
|
+
label.appendChild(applyCalculated);
|
|
463
486
|
|
|
464
487
|
// Add day stats
|
|
465
488
|
const dayStats = document.createElement('span');
|
|
@@ -475,7 +498,7 @@
|
|
|
475
498
|
const slotRecord = calendarData.calendar[slotKey] || {};
|
|
476
499
|
const forecastCapacity = slotRecord.forecastCapacity || 0;
|
|
477
500
|
const calculatedCapacity = slotRecord.calculatedCapacity || 0;
|
|
478
|
-
|
|
501
|
+
|
|
479
502
|
const delta = calculatedCapacity - forecastCapacity;
|
|
480
503
|
if (delta > 0) {
|
|
481
504
|
neededVmHours += delta;
|
|
@@ -519,7 +542,7 @@
|
|
|
519
542
|
const idealCapacity = slotRecord.idealCapacity || 0;
|
|
520
543
|
const forecastCapacity = slotRecord.forecastCapacity;
|
|
521
544
|
const calculatedCapacity = slotRecord.calculatedCapacity === undefined ? 0 : slotRecord.calculatedCapacity;
|
|
522
|
-
|
|
545
|
+
|
|
523
546
|
// Create gauge container
|
|
524
547
|
const barWrapper = document.createElement('div');
|
|
525
548
|
barWrapper.className = 'bar-wrapper';
|
|
@@ -549,7 +572,7 @@
|
|
|
549
572
|
const clickY = e.clientY - rect.top;
|
|
550
573
|
const ratio = 1 - (clickY / rect.height);
|
|
551
574
|
const newValue = Math.round(ratio * maxCapacity);
|
|
552
|
-
|
|
575
|
+
|
|
553
576
|
if (!calendarData.calendar[key]) {
|
|
554
577
|
calendarData.calendar[key] = {};
|
|
555
578
|
}
|
|
@@ -583,7 +606,7 @@
|
|
|
583
606
|
const clickY = e.clientY - rect.top;
|
|
584
607
|
const ratio = 1 - (clickY / rect.height);
|
|
585
608
|
const newValue = Math.round(ratio * maxCapacity);
|
|
586
|
-
|
|
609
|
+
|
|
587
610
|
if (!calendarData.calendar[key]) {
|
|
588
611
|
calendarData.calendar[key] = {};
|
|
589
612
|
}
|
|
@@ -687,10 +710,6 @@
|
|
|
687
710
|
tooltip.style.display = 'block';
|
|
688
711
|
};
|
|
689
712
|
|
|
690
|
-
barWrapper.onmousemove = function(e) {
|
|
691
|
-
updateTooltipContent(e, key);
|
|
692
|
-
};
|
|
693
|
-
|
|
694
713
|
barWrapper.onmouseleave = function() {
|
|
695
714
|
barWrapper.style.backgroundColor = '#f0f0f0';
|
|
696
715
|
tooltip.style.display = 'none';
|
|
@@ -759,35 +778,520 @@
|
|
|
759
778
|
formatJsonForDisplay(calendarData);
|
|
760
779
|
}
|
|
761
780
|
|
|
781
|
+
function displayChartData() {
|
|
782
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
783
|
+
const container = document.getElementById('chart-data-json');
|
|
784
|
+
if (usageChartData) {
|
|
785
|
+
container.textContent = JSON.stringify(usageChartData, null, 2);
|
|
786
|
+
} else {
|
|
787
|
+
container.textContent = 'No usage data available';
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function convertTimeToTimezone(timeStr, timezone) {
|
|
792
|
+
// timeStr is in format "YYYY-MM-DDTHH:MM"
|
|
793
|
+
// Convert to the specified timezone
|
|
794
|
+
try {
|
|
795
|
+
const date = new Date(timeStr + 'Z'); // Parse as UTC
|
|
796
|
+
let tzToUse = timezone;
|
|
797
|
+
|
|
798
|
+
// If Browser timezone is selected, get the browser's timezone
|
|
799
|
+
if (timezone === 'Browser') {
|
|
800
|
+
tzToUse = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
804
|
+
year: 'numeric',
|
|
805
|
+
month: '2-digit',
|
|
806
|
+
day: '2-digit',
|
|
807
|
+
hour: '2-digit',
|
|
808
|
+
minute: '2-digit',
|
|
809
|
+
timeZone: tzToUse,
|
|
810
|
+
hour12: false
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const parts = formatter.formatToParts(date);
|
|
814
|
+
const values = {};
|
|
815
|
+
parts.forEach(part => {
|
|
816
|
+
if (part.type !== 'literal') {
|
|
817
|
+
values[part.type] = part.value;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}`;
|
|
822
|
+
} catch (e) {
|
|
823
|
+
return timeStr;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let globalUsageChart = null; // Store chart instance for drag selection
|
|
828
|
+
let isSelectingTimeRange = false; // Global flag for selection mode
|
|
829
|
+
let currentFilteredStartIdx = null; // Track current time range filter start
|
|
830
|
+
let currentFilteredEndIdx = null; // Track current time range filter end
|
|
831
|
+
|
|
832
|
+
function renderUsageChart(filteredStartIdx = null, filteredEndIdx = null, timezone = 'UTC') {
|
|
833
|
+
// Store the current filter indices
|
|
834
|
+
currentFilteredStartIdx = filteredStartIdx;
|
|
835
|
+
currentFilteredEndIdx = filteredEndIdx;
|
|
836
|
+
|
|
837
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
838
|
+
if (!usageChartData) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
let xAxis = usageChartData.x_axis.map(time => convertTimeToTimezone(time, timezone));
|
|
843
|
+
let chartData = {
|
|
844
|
+
consumed_capacity: usageChartData.consumed_capacity,
|
|
845
|
+
spare_capacity: usageChartData.spare_capacity,
|
|
846
|
+
no_spare_error: usageChartData.no_spare_error,
|
|
847
|
+
powered_on_vms: usageChartData.powered_on_vms,
|
|
848
|
+
consumed_capacity_predicated: usageChartData.consumed_capacity_predicated,
|
|
849
|
+
spare_capacity_predicated: usageChartData.spare_capacity_predicated,
|
|
850
|
+
no_spare_error_predicated: usageChartData.no_spare_error_predicated,
|
|
851
|
+
optimized_capacity: usageChartData.optimized_capacity,
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// Apply time range filter if specified
|
|
855
|
+
if (filteredStartIdx !== null && filteredEndIdx !== null) {
|
|
856
|
+
const startIdx = Math.min(filteredStartIdx, filteredEndIdx);
|
|
857
|
+
const endIdx = Math.max(filteredStartIdx, filteredEndIdx);
|
|
858
|
+
|
|
859
|
+
xAxis = xAxis.slice(startIdx, endIdx + 1);
|
|
860
|
+
chartData.consumed_capacity = chartData.consumed_capacity.slice(startIdx, endIdx + 1);
|
|
861
|
+
chartData.spare_capacity = chartData.spare_capacity.slice(startIdx, endIdx + 1);
|
|
862
|
+
chartData.no_spare_error = chartData.no_spare_error.slice(startIdx, endIdx + 1);
|
|
863
|
+
chartData.powered_on_vms = chartData.powered_on_vms.slice(startIdx, endIdx + 1);
|
|
864
|
+
chartData.consumed_capacity_predicated = chartData.consumed_capacity_predicated.slice(startIdx, endIdx + 1);
|
|
865
|
+
chartData.spare_capacity_predicated = chartData.spare_capacity_predicated.slice(startIdx, endIdx + 1);
|
|
866
|
+
chartData.no_spare_error_predicated = chartData.no_spare_error_predicated.slice(startIdx, endIdx + 1);
|
|
867
|
+
chartData.optimized_capacity = chartData.optimized_capacity.slice(startIdx, endIdx + 1);
|
|
868
|
+
|
|
869
|
+
// Show reset button when zoomed and display timezone-aware times
|
|
870
|
+
document.getElementById('reset-chart-zoom').style.display = 'inline-block';
|
|
871
|
+
const startTime = xAxis[0];
|
|
872
|
+
const endTime = xAxis[xAxis.length - 1];
|
|
873
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
874
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
875
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
876
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
877
|
+
} else {
|
|
878
|
+
// Hide reset button when not zoomed (showing default all timepoints)
|
|
879
|
+
document.getElementById('reset-chart-zoom').style.display = 'none';
|
|
880
|
+
// Display full time range with start and end times
|
|
881
|
+
const startTime = xAxis[0];
|
|
882
|
+
const endTime = xAxis[xAxis.length - 1];
|
|
883
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
884
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
885
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
886
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const chartDom = document.getElementById('usage-chart');
|
|
890
|
+
globalUsageChart = echarts.init(chartDom);
|
|
891
|
+
const myChart = globalUsageChart;
|
|
892
|
+
const option = {
|
|
893
|
+
animation: false,
|
|
894
|
+
title: {
|
|
895
|
+
text: "Template Usage"
|
|
896
|
+
},
|
|
897
|
+
tooltip: {
|
|
898
|
+
trigger: 'axis',
|
|
899
|
+
axisPointer: {
|
|
900
|
+
type: 'cross',
|
|
901
|
+
label: {
|
|
902
|
+
backgroundColor: '#6a7985',
|
|
903
|
+
formatter: function(params) {
|
|
904
|
+
// Only round Y-axis values, keep X-axis unchanged
|
|
905
|
+
if (params.axisDimension === 'y') {
|
|
906
|
+
return String(Math.round(params.value));
|
|
907
|
+
}
|
|
908
|
+
return params.value;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
formatter: function(params) {
|
|
913
|
+
// Suppress tooltip when in time-range selection mode
|
|
914
|
+
if (isSelectingTimeRange) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
919
|
+
const dataIndex = params[0].dataIndex;
|
|
920
|
+
|
|
921
|
+
// Determine the split point between real and predicted data
|
|
922
|
+
let splitIndex = usageChartData.split_index;
|
|
923
|
+
if (splitIndex === undefined) {
|
|
924
|
+
// If not provided, assume all data is real data
|
|
925
|
+
splitIndex = usageChartData.x_axis.length;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const isInPredicatedArea = dataIndex >= splitIndex;
|
|
929
|
+
|
|
930
|
+
// Filter series based on which area we're in
|
|
931
|
+
const filteredParams = params.filter(item => {
|
|
932
|
+
if (!isInPredicatedArea) {
|
|
933
|
+
// In real data range, exclude predicated series
|
|
934
|
+
return !item.seriesName.includes('Predicated');
|
|
935
|
+
} else {
|
|
936
|
+
// In predicated data range, show all series that have values
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Build tooltip content with different background for predicted area
|
|
942
|
+
const bgColor = isInPredicatedArea ? '#fff8e6' : '#ffffff';
|
|
943
|
+
const borderColor = isInPredicatedArea ? '#ffd700' : '#ccc';
|
|
944
|
+
const label = isInPredicatedArea ? '<div style="font-size:10px;color:#b8860b;margin-bottom:4px;">📊 Predicted</div>' : '<div style="font-size:10px;color:#666;margin-bottom:4px;">📈 Historical Data</div>';
|
|
945
|
+
|
|
946
|
+
let content = params[0].axisValueLabel + '<br/>';
|
|
947
|
+
filteredParams.forEach(function(item) {
|
|
948
|
+
if (item.value !== null && item.value !== undefined) {
|
|
949
|
+
content += item.marker + ' ' + item.seriesName + ': ' + Math.round(item.value) + '<br/>';
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
return '<div style="background:' + bgColor + ';margin:-8px;padding:8px;border-radius:4px;border:1px solid ' + borderColor + ';">' + label + content + '</div>';
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
legend: {
|
|
957
|
+
orient: 'horizontal',
|
|
958
|
+
bottom: '5%',
|
|
959
|
+
data: [
|
|
960
|
+
'Powered On VMs',
|
|
961
|
+
'Consumed Sessions',
|
|
962
|
+
'Spare Capacity',
|
|
963
|
+
'Consumed Capacity',
|
|
964
|
+
"No-spare Error",
|
|
965
|
+
'Spare Capacity - Predicated',
|
|
966
|
+
'Consumed Capacity - Predicated',
|
|
967
|
+
"No-spare Error - Predicated",
|
|
968
|
+
'Optimized Capacity'
|
|
969
|
+
],
|
|
970
|
+
selected: {
|
|
971
|
+
'Powered On VMs': true,
|
|
972
|
+
'Consumed Capacity': true,
|
|
973
|
+
'Spare Capacity': true,
|
|
974
|
+
'No-spare Error': true,
|
|
975
|
+
'Optimized Capacity': true,
|
|
976
|
+
'Spare Capacity - Predicated': false,
|
|
977
|
+
'Consumed Capacity - Predicated': false,
|
|
978
|
+
'No-spare Error - Predicated': true
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
toolbox: {
|
|
982
|
+
feature: {
|
|
983
|
+
saveAsImage: {}
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
grid: {
|
|
987
|
+
left: '3%',
|
|
988
|
+
right: '4%',
|
|
989
|
+
bottom: '20%',
|
|
990
|
+
containLabel: true
|
|
991
|
+
},
|
|
992
|
+
xAxis: [
|
|
993
|
+
{
|
|
994
|
+
type: 'category',
|
|
995
|
+
boundaryGap: false,
|
|
996
|
+
data: xAxis
|
|
997
|
+
}
|
|
998
|
+
],
|
|
999
|
+
yAxis: [
|
|
1000
|
+
{
|
|
1001
|
+
type: 'value'
|
|
1002
|
+
}
|
|
1003
|
+
],
|
|
1004
|
+
series: [
|
|
1005
|
+
{
|
|
1006
|
+
name: 'Consumed Capacity',
|
|
1007
|
+
type: 'line',
|
|
1008
|
+
lineStyle: { width: 1 },
|
|
1009
|
+
symbolSize: 0,
|
|
1010
|
+
stack: 'Total',
|
|
1011
|
+
areaStyle: {},
|
|
1012
|
+
emphasis: {
|
|
1013
|
+
focus: 'none'
|
|
1014
|
+
},
|
|
1015
|
+
color: '#0a0',
|
|
1016
|
+
data: chartData.consumed_capacity
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: 'Powered On VMs',
|
|
1020
|
+
type: 'line',
|
|
1021
|
+
lineStyle: { width: 1 },
|
|
1022
|
+
symbolSize: 0,
|
|
1023
|
+
stack: false,
|
|
1024
|
+
emphasis: {
|
|
1025
|
+
focus: 'none'
|
|
1026
|
+
},
|
|
1027
|
+
color: '#0066ff',
|
|
1028
|
+
data: chartData.powered_on_vms
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
name: 'Spare Capacity',
|
|
1032
|
+
type: 'line',
|
|
1033
|
+
lineStyle: { width: 1 },
|
|
1034
|
+
symbolSize: 0,
|
|
1035
|
+
stack: 'Total',
|
|
1036
|
+
areaStyle: {},
|
|
1037
|
+
emphasis: {
|
|
1038
|
+
focus: 'none'
|
|
1039
|
+
},
|
|
1040
|
+
color: '#aa0',
|
|
1041
|
+
data: chartData.spare_capacity,
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
name: 'No-spare Error',
|
|
1045
|
+
type: 'bar',
|
|
1046
|
+
areaStyle: {},
|
|
1047
|
+
emphasis: {
|
|
1048
|
+
focus: 'none'
|
|
1049
|
+
},
|
|
1050
|
+
color: '#a00',
|
|
1051
|
+
data: chartData.no_spare_error,
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: 'Consumed Capacity - Predicated',
|
|
1055
|
+
type: 'line',
|
|
1056
|
+
lineStyle: { width: 1 },
|
|
1057
|
+
symbolSize: 0,
|
|
1058
|
+
stack: 'Predicated',
|
|
1059
|
+
areaStyle: {},
|
|
1060
|
+
emphasis: {
|
|
1061
|
+
focus: 'none'
|
|
1062
|
+
},
|
|
1063
|
+
color: '#6d6',
|
|
1064
|
+
data: chartData.consumed_capacity_predicated,
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: 'Spare Capacity - Predicated',
|
|
1068
|
+
type: 'line',
|
|
1069
|
+
lineStyle: { width: 1 },
|
|
1070
|
+
symbolSize: 0,
|
|
1071
|
+
stack: 'Predicated',
|
|
1072
|
+
areaStyle: {},
|
|
1073
|
+
emphasis: {
|
|
1074
|
+
focus: 'none'
|
|
1075
|
+
},
|
|
1076
|
+
color: '#dd6',
|
|
1077
|
+
data: chartData.spare_capacity_predicated,
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: 'No-spare Error - Predicated',
|
|
1081
|
+
type: 'bar',
|
|
1082
|
+
areaStyle: {},
|
|
1083
|
+
emphasis: {
|
|
1084
|
+
focus: 'none'
|
|
1085
|
+
},
|
|
1086
|
+
color: '#d66',
|
|
1087
|
+
data: chartData.no_spare_error_predicated,
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
name: 'Optimized Capacity',
|
|
1091
|
+
type: 'line',
|
|
1092
|
+
lineStyle: { width: 1 },
|
|
1093
|
+
symbolSize: 0,
|
|
1094
|
+
emphasis: {
|
|
1095
|
+
focus: 'none'
|
|
1096
|
+
},
|
|
1097
|
+
color: '#6dd',
|
|
1098
|
+
data: chartData.optimized_capacity,
|
|
1099
|
+
}
|
|
1100
|
+
]
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
option && myChart.setOption(option);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
762
1106
|
// Add tab switching functionality
|
|
763
1107
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
764
1108
|
button.addEventListener('click', () => {
|
|
765
1109
|
// Remove active class from all buttons and contents
|
|
766
1110
|
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
|
767
1111
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
768
|
-
|
|
1112
|
+
|
|
769
1113
|
// Add active class to clicked button and corresponding content
|
|
770
1114
|
button.classList.add('active');
|
|
771
1115
|
document.getElementById(`${button.dataset.tab}-tab`).classList.add('active');
|
|
772
|
-
|
|
1116
|
+
|
|
773
1117
|
// Update config display when switching to config tab
|
|
774
1118
|
if (button.dataset.tab === 'configuration') {
|
|
775
1119
|
updateConfigDisplay();
|
|
776
1120
|
}
|
|
1121
|
+
|
|
1122
|
+
// Render usage chart when switching to usage tab
|
|
1123
|
+
if (button.dataset.tab === 'usage') {
|
|
1124
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
1125
|
+
renderUsageChart(currentFilteredStartIdx, currentFilteredEndIdx, timezone);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Display chart data when switching to data tab
|
|
1129
|
+
if (button.dataset.tab === 'data') {
|
|
1130
|
+
displayChartData();
|
|
1131
|
+
}
|
|
777
1132
|
});
|
|
778
1133
|
});
|
|
779
1134
|
|
|
780
1135
|
// Initialize timezone selector
|
|
781
1136
|
document.addEventListener('DOMContentLoaded', () => {
|
|
1137
|
+
// Detect and display browser timezone
|
|
1138
|
+
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1139
|
+
const browserOption = document.getElementById('browser-timezone-option');
|
|
1140
|
+
if (browserOption) {
|
|
1141
|
+
browserOption.textContent = `Browser (${browserTimezone})`;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const initialTimezone = calendarData.timezone || 'UTC';
|
|
1145
|
+
renderUsageChart(null, null, initialTimezone);
|
|
782
1146
|
renderChart();
|
|
783
1147
|
updateConfigDisplay();
|
|
784
|
-
|
|
1148
|
+
|
|
1149
|
+
// Set up drag-to-select functionality once
|
|
1150
|
+
const overlay = document.getElementById('chart-selection-overlay');
|
|
1151
|
+
const usageChartEl = document.getElementById('usage-chart');
|
|
1152
|
+
const chartContainer = usageChartEl ? usageChartEl.parentElement : null;
|
|
1153
|
+
let selectionStart = null;
|
|
1154
|
+
|
|
1155
|
+
if (overlay && usageChartEl && chartContainer) {
|
|
1156
|
+
// Use mousedown on the chart element to start selection
|
|
1157
|
+
usageChartEl.addEventListener('mousedown', (e) => {
|
|
1158
|
+
// Only handle left mouse button
|
|
1159
|
+
if (e.button !== 0) return;
|
|
1160
|
+
|
|
1161
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
1162
|
+
const x = e.clientX - rect.left;
|
|
1163
|
+
const y = e.clientY - rect.top;
|
|
1164
|
+
|
|
1165
|
+
// Only start selection if we're in the chart area
|
|
1166
|
+
if (x >= 0 && y >= 0) {
|
|
1167
|
+
// Enter selection mode - this will suppress tooltip
|
|
1168
|
+
isSelectingTimeRange = true;
|
|
1169
|
+
selectionStart = x;
|
|
1170
|
+
|
|
1171
|
+
// Hide any existing tooltip immediately
|
|
1172
|
+
if (globalUsageChart) {
|
|
1173
|
+
globalUsageChart.dispatchAction({
|
|
1174
|
+
type: 'hideTip'
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
overlay.style.left = selectionStart + 'px';
|
|
1179
|
+
overlay.style.width = '0px';
|
|
1180
|
+
overlay.style.display = 'block';
|
|
1181
|
+
|
|
1182
|
+
// Prevent text selection during drag
|
|
1183
|
+
e.preventDefault();
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
document.addEventListener('mousemove', (e) => {
|
|
1188
|
+
if (!isSelectingTimeRange) return;
|
|
1189
|
+
|
|
1190
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
1191
|
+
const currentX = e.clientX - rect.left;
|
|
1192
|
+
const width = Math.abs(currentX - selectionStart);
|
|
1193
|
+
const left = Math.min(selectionStart, currentX);
|
|
1194
|
+
|
|
1195
|
+
overlay.style.left = left + 'px';
|
|
1196
|
+
overlay.style.width = width + 'px';
|
|
1197
|
+
|
|
1198
|
+
// Keep hiding tooltip during drag
|
|
1199
|
+
if (globalUsageChart) {
|
|
1200
|
+
globalUsageChart.dispatchAction({
|
|
1201
|
+
type: 'hideTip'
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
document.addEventListener('mouseup', (e) => {
|
|
1207
|
+
if (!isSelectingTimeRange) return;
|
|
1208
|
+
|
|
1209
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
1210
|
+
const currentX = e.clientX - rect.left;
|
|
1211
|
+
|
|
1212
|
+
// Exit selection mode
|
|
1213
|
+
isSelectingTimeRange = false;
|
|
1214
|
+
overlay.style.display = 'none';
|
|
1215
|
+
|
|
1216
|
+
// If selection is too small, treat as a click (no zoom)
|
|
1217
|
+
if (Math.abs(currentX - selectionStart) < 10) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Get the chart container dimensions
|
|
1222
|
+
const chartWidth = chartContainer.offsetWidth;
|
|
1223
|
+
|
|
1224
|
+
// Estimate grid area (echarts default: left 3%, right 4%)
|
|
1225
|
+
const gridLeftPercent = 3;
|
|
1226
|
+
const gridRightPercent = 4;
|
|
1227
|
+
const gridLeft = (gridLeftPercent / 100) * chartWidth;
|
|
1228
|
+
const gridRight = (gridRightPercent / 100) * chartWidth;
|
|
1229
|
+
const gridWidth = chartWidth - gridLeft - gridRight;
|
|
1230
|
+
|
|
1231
|
+
// Convert pixel positions to data indices
|
|
1232
|
+
const normalizedStart = Math.max(0, (Math.min(selectionStart, currentX) - gridLeft) / gridWidth);
|
|
1233
|
+
const normalizedEnd = Math.max(0, (Math.max(selectionStart, currentX) - gridLeft) / gridWidth);
|
|
1234
|
+
|
|
1235
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
1236
|
+
const totalPoints = usageChartData.x_axis.length;
|
|
1237
|
+
let startIdx = Math.floor(normalizedStart * totalPoints);
|
|
1238
|
+
let endIdx = Math.floor(normalizedEnd * totalPoints);
|
|
1239
|
+
|
|
1240
|
+
startIdx = Math.max(0, Math.min(startIdx, totalPoints - 1));
|
|
1241
|
+
endIdx = Math.max(startIdx, Math.min(endIdx, totalPoints - 1));
|
|
1242
|
+
|
|
1243
|
+
// Re-render chart with selected range in current timezone
|
|
1244
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
1245
|
+
renderUsageChart(startIdx, endIdx, timezone);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// Cancel selection if mouse leaves the chart area while dragging
|
|
1249
|
+
chartContainer.addEventListener('mouseleave', () => {
|
|
1250
|
+
if (isSelectingTimeRange) {
|
|
1251
|
+
isSelectingTimeRange = false;
|
|
1252
|
+
overlay.style.display = 'none';
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
785
1257
|
const timezoneSelect = document.getElementById('timezone-select');
|
|
786
1258
|
timezoneSelect.value = calendarData.timezone;
|
|
787
|
-
|
|
1259
|
+
|
|
788
1260
|
timezoneSelect.addEventListener('change', (e) => {
|
|
789
1261
|
calendarData.timezone = e.target.value;
|
|
790
1262
|
updateConfigDisplay();
|
|
1263
|
+
|
|
1264
|
+
// Refresh chart with new timezone
|
|
1265
|
+
renderUsageChart(currentFilteredStartIdx, currentFilteredEndIdx, e.target.value);
|
|
1266
|
+
|
|
1267
|
+
// Refresh time range display in new timezone
|
|
1268
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
1269
|
+
const xAxis = usageChartData.x_axis;
|
|
1270
|
+
const timezone = e.target.value;
|
|
1271
|
+
|
|
1272
|
+
if (currentFilteredStartIdx !== null && currentFilteredEndIdx !== null) {
|
|
1273
|
+
// Update for selected range
|
|
1274
|
+
const startIdx = Math.min(currentFilteredStartIdx, currentFilteredEndIdx);
|
|
1275
|
+
const endIdx = Math.max(currentFilteredStartIdx, currentFilteredEndIdx);
|
|
1276
|
+
const startTime = xAxis[startIdx];
|
|
1277
|
+
const endTime = xAxis[endIdx];
|
|
1278
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
1279
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
1280
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
1281
|
+
} else {
|
|
1282
|
+
// Update for default full range
|
|
1283
|
+
const startTime = xAxis[0];
|
|
1284
|
+
const endTime = xAxis[xAxis.length - 1];
|
|
1285
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
1286
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
1287
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// Add reset zoom button functionality
|
|
1292
|
+
const resetButton = document.getElementById('reset-chart-zoom');
|
|
1293
|
+
resetButton.addEventListener('click', () => {
|
|
1294
|
+
renderUsageChart();
|
|
791
1295
|
});
|
|
792
1296
|
|
|
793
1297
|
// Add copy to clipboard functionality
|
|
@@ -804,6 +1308,24 @@
|
|
|
804
1308
|
console.error('Failed to copy text: ', err);
|
|
805
1309
|
});
|
|
806
1310
|
});
|
|
1311
|
+
|
|
1312
|
+
// Add copy to clipboard functionality for data tab
|
|
1313
|
+
const copyDataButton = document.getElementById('copy-data');
|
|
1314
|
+
if (copyDataButton) {
|
|
1315
|
+
copyDataButton.addEventListener('click', () => {
|
|
1316
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
1317
|
+
const jsonString = JSON.stringify(usageChartData, null, 2);
|
|
1318
|
+
navigator.clipboard.writeText(jsonString).then(() => {
|
|
1319
|
+
const originalText = copyDataButton.textContent;
|
|
1320
|
+
copyDataButton.textContent = 'Copied!';
|
|
1321
|
+
setTimeout(() => {
|
|
1322
|
+
copyDataButton.textContent = originalText;
|
|
1323
|
+
}, 2000);
|
|
1324
|
+
}).catch(err => {
|
|
1325
|
+
console.error('Failed to copy text: ', err);
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
807
1329
|
});
|
|
808
1330
|
</script>
|
|
809
1331
|
|