hcs-cli 0.1.321__py3-none-any.whl → 0.1.323__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 +9 -5
- 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 +592 -51
- hcs_cli/support/scm/plan_editor.py +128 -3
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.323.dist-info}/METADATA +2 -2
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.323.dist-info}/RECORD +15 -13
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.323.dist-info}/WHEEL +0 -0
- {hcs_cli-0.1.321.dist-info → hcs_cli-0.1.323.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,38 +293,41 @@
|
|
|
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="history">History</button>
|
|
296
306
|
</div>
|
|
297
|
-
|
|
298
|
-
<div class="tab-content active" id="
|
|
307
|
+
|
|
308
|
+
<div class="tab-content active" id="usage-tab">
|
|
309
|
+
<div style="margin-bottom: 10px; display: flex; justify-content: flex-end; align-items: center; gap: 10px;">
|
|
310
|
+
<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>
|
|
311
|
+
<span id="selection-info" style="font-size: 12px; color: #666; font-weight: 500;">All timepoints</span>
|
|
312
|
+
<button class="copy-button" id="copy-data">Copy Data</button>
|
|
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>
|
|
318
|
+
</div>
|
|
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
|
-
|
|
310
|
-
<div class="tab-content" id="history-tab">
|
|
311
|
-
<!-- History content will go here -->
|
|
312
|
-
</div>
|
|
313
331
|
</div>
|
|
314
332
|
</div>
|
|
315
333
|
|
|
@@ -340,7 +358,7 @@
|
|
|
340
358
|
|
|
341
359
|
function updateBarColor(bar, idealCapacity, calculatedCapacity) {
|
|
342
360
|
const effectiveCalculatedCapacity = calculatedCapacity === undefined ? 0 : calculatedCapacity;
|
|
343
|
-
|
|
361
|
+
|
|
344
362
|
if (idealCapacity < effectiveCalculatedCapacity) {
|
|
345
363
|
bar.style.backgroundColor = '#ffa3a3'; // light red
|
|
346
364
|
} else if (idealCapacity > effectiveCalculatedCapacity * 1.1) {
|
|
@@ -363,7 +381,7 @@
|
|
|
363
381
|
const forecastCapacity = slotRecord.forecastCapacity || 0;
|
|
364
382
|
const calculatedCapacity = slotRecord.calculatedCapacity || 0;
|
|
365
383
|
const idealCapacity = slotRecord.idealCapacity || 0;
|
|
366
|
-
|
|
384
|
+
|
|
367
385
|
// Calculate delta between calculated and forecast
|
|
368
386
|
const delta = calculatedCapacity - forecastCapacity;
|
|
369
387
|
if (delta > 0) {
|
|
@@ -410,14 +428,16 @@
|
|
|
410
428
|
|
|
411
429
|
const label = document.createElement('div');
|
|
412
430
|
label.className = 'label';
|
|
413
|
-
|
|
431
|
+
|
|
414
432
|
const labelLeft = document.createElement('div');
|
|
415
433
|
labelLeft.className = 'label-left';
|
|
416
|
-
|
|
434
|
+
|
|
417
435
|
const dayName = document.createElement('span');
|
|
418
436
|
dayName.textContent = weekday.charAt(0).toUpperCase() + weekday.slice(1);
|
|
419
437
|
labelLeft.appendChild(dayName);
|
|
420
438
|
|
|
439
|
+
label.appendChild(labelLeft);
|
|
440
|
+
|
|
421
441
|
const applyCalculated = document.createElement('span');
|
|
422
442
|
applyCalculated.className = 'apply-calculated';
|
|
423
443
|
applyCalculated.textContent = 'apply calculated';
|
|
@@ -426,15 +446,15 @@
|
|
|
426
446
|
const slotKey = `${weekday}/${time}`;
|
|
427
447
|
const slotRecord = calendarData.calendar[slotKey] || {};
|
|
428
448
|
const calculatedCapacity = slotRecord.calculatedCapacity;
|
|
429
|
-
|
|
449
|
+
|
|
430
450
|
// Initialize slot if it doesn't exist
|
|
431
451
|
if (!calendarData.calendar[slotKey]) {
|
|
432
452
|
calendarData.calendar[slotKey] = {};
|
|
433
453
|
}
|
|
434
|
-
|
|
454
|
+
|
|
435
455
|
// Set idealCapacity to calculatedCapacity (0 if undefined)
|
|
436
456
|
calendarData.calendar[slotKey].idealCapacity = calculatedCapacity || 0;
|
|
437
|
-
|
|
457
|
+
|
|
438
458
|
// Update the bar height and color
|
|
439
459
|
const bar = document.querySelector(`[data-key="${slotKey}"] .bar`);
|
|
440
460
|
const barWrapper = document.querySelector(`[data-key="${slotKey}"]`);
|
|
@@ -457,9 +477,7 @@
|
|
|
457
477
|
});
|
|
458
478
|
updateWeekStats();
|
|
459
479
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
label.appendChild(labelLeft);
|
|
480
|
+
label.appendChild(applyCalculated);
|
|
463
481
|
|
|
464
482
|
// Add day stats
|
|
465
483
|
const dayStats = document.createElement('span');
|
|
@@ -475,7 +493,7 @@
|
|
|
475
493
|
const slotRecord = calendarData.calendar[slotKey] || {};
|
|
476
494
|
const forecastCapacity = slotRecord.forecastCapacity || 0;
|
|
477
495
|
const calculatedCapacity = slotRecord.calculatedCapacity || 0;
|
|
478
|
-
|
|
496
|
+
|
|
479
497
|
const delta = calculatedCapacity - forecastCapacity;
|
|
480
498
|
if (delta > 0) {
|
|
481
499
|
neededVmHours += delta;
|
|
@@ -519,7 +537,7 @@
|
|
|
519
537
|
const idealCapacity = slotRecord.idealCapacity || 0;
|
|
520
538
|
const forecastCapacity = slotRecord.forecastCapacity;
|
|
521
539
|
const calculatedCapacity = slotRecord.calculatedCapacity === undefined ? 0 : slotRecord.calculatedCapacity;
|
|
522
|
-
|
|
540
|
+
|
|
523
541
|
// Create gauge container
|
|
524
542
|
const barWrapper = document.createElement('div');
|
|
525
543
|
barWrapper.className = 'bar-wrapper';
|
|
@@ -549,7 +567,7 @@
|
|
|
549
567
|
const clickY = e.clientY - rect.top;
|
|
550
568
|
const ratio = 1 - (clickY / rect.height);
|
|
551
569
|
const newValue = Math.round(ratio * maxCapacity);
|
|
552
|
-
|
|
570
|
+
|
|
553
571
|
if (!calendarData.calendar[key]) {
|
|
554
572
|
calendarData.calendar[key] = {};
|
|
555
573
|
}
|
|
@@ -583,7 +601,7 @@
|
|
|
583
601
|
const clickY = e.clientY - rect.top;
|
|
584
602
|
const ratio = 1 - (clickY / rect.height);
|
|
585
603
|
const newValue = Math.round(ratio * maxCapacity);
|
|
586
|
-
|
|
604
|
+
|
|
587
605
|
if (!calendarData.calendar[key]) {
|
|
588
606
|
calendarData.calendar[key] = {};
|
|
589
607
|
}
|
|
@@ -687,10 +705,6 @@
|
|
|
687
705
|
tooltip.style.display = 'block';
|
|
688
706
|
};
|
|
689
707
|
|
|
690
|
-
barWrapper.onmousemove = function(e) {
|
|
691
|
-
updateTooltipContent(e, key);
|
|
692
|
-
};
|
|
693
|
-
|
|
694
708
|
barWrapper.onmouseleave = function() {
|
|
695
709
|
barWrapper.style.backgroundColor = '#f0f0f0';
|
|
696
710
|
tooltip.style.display = 'none';
|
|
@@ -759,35 +773,544 @@
|
|
|
759
773
|
formatJsonForDisplay(calendarData);
|
|
760
774
|
}
|
|
761
775
|
|
|
776
|
+
function displayChartData() {
|
|
777
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
778
|
+
const container = document.getElementById('chart-data-json');
|
|
779
|
+
if (usageChartData) {
|
|
780
|
+
container.textContent = JSON.stringify(usageChartData, null, 2);
|
|
781
|
+
} else {
|
|
782
|
+
container.textContent = 'No usage data available';
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function convertTimeToTimezone(timeStr, timezone) {
|
|
787
|
+
// timeStr is in format "YYYY-MM-DDTHH:MM"
|
|
788
|
+
// Convert to the specified timezone
|
|
789
|
+
try {
|
|
790
|
+
const date = new Date(timeStr + 'Z'); // Parse as UTC
|
|
791
|
+
let tzToUse = timezone;
|
|
792
|
+
|
|
793
|
+
// If Browser timezone is selected, get the browser's timezone
|
|
794
|
+
if (timezone === 'Browser') {
|
|
795
|
+
tzToUse = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
799
|
+
year: 'numeric',
|
|
800
|
+
month: '2-digit',
|
|
801
|
+
day: '2-digit',
|
|
802
|
+
hour: '2-digit',
|
|
803
|
+
minute: '2-digit',
|
|
804
|
+
timeZone: tzToUse,
|
|
805
|
+
hour12: false
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const parts = formatter.formatToParts(date);
|
|
809
|
+
const values = {};
|
|
810
|
+
parts.forEach(part => {
|
|
811
|
+
if (part.type !== 'literal') {
|
|
812
|
+
values[part.type] = part.value;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}`;
|
|
817
|
+
} catch (e) {
|
|
818
|
+
return timeStr;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
let globalUsageChart = null; // Store chart instance for drag selection
|
|
823
|
+
let isSelectingTimeRange = false; // Global flag for selection mode
|
|
824
|
+
let currentFilteredStartIdx = null; // Track current time range filter start
|
|
825
|
+
let currentFilteredEndIdx = null; // Track current time range filter end
|
|
826
|
+
|
|
827
|
+
function renderUsageChart(filteredStartIdx = null, filteredEndIdx = null, timezone = 'UTC') {
|
|
828
|
+
// Store the current filter indices
|
|
829
|
+
currentFilteredStartIdx = filteredStartIdx;
|
|
830
|
+
currentFilteredEndIdx = filteredEndIdx;
|
|
831
|
+
|
|
832
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
833
|
+
if (!usageChartData) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
let xAxis = usageChartData.x_axis.map(time => convertTimeToTimezone(time, timezone));
|
|
838
|
+
let chartData = {
|
|
839
|
+
consumed_capacity: usageChartData.consumed_capacity,
|
|
840
|
+
spare_capacity: usageChartData.spare_capacity,
|
|
841
|
+
no_spare_error: usageChartData.no_spare_error,
|
|
842
|
+
powered_on_vms: usageChartData.powered_on_vms,
|
|
843
|
+
powered_on_vms_predicated: usageChartData.powered_on_vms_predicated,
|
|
844
|
+
consumed_capacity_predicated: usageChartData.consumed_capacity_predicated,
|
|
845
|
+
spare_capacity_predicated: usageChartData.spare_capacity_predicated,
|
|
846
|
+
no_spare_error_predicated: usageChartData.no_spare_error_predicated,
|
|
847
|
+
optimized_capacity: usageChartData.optimized_capacity,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// Apply time range filter if specified
|
|
851
|
+
if (filteredStartIdx !== null && filteredEndIdx !== null) {
|
|
852
|
+
const startIdx = Math.min(filteredStartIdx, filteredEndIdx);
|
|
853
|
+
const endIdx = Math.max(filteredStartIdx, filteredEndIdx);
|
|
854
|
+
|
|
855
|
+
xAxis = xAxis.slice(startIdx, endIdx + 1);
|
|
856
|
+
chartData.consumed_capacity = chartData.consumed_capacity.slice(startIdx, endIdx + 1);
|
|
857
|
+
chartData.spare_capacity = chartData.spare_capacity.slice(startIdx, endIdx + 1);
|
|
858
|
+
chartData.no_spare_error = chartData.no_spare_error.slice(startIdx, endIdx + 1);
|
|
859
|
+
chartData.powered_on_vms = chartData.powered_on_vms.slice(startIdx, endIdx + 1);
|
|
860
|
+
chartData.powered_on_vms_predicated = chartData.powered_on_vms_predicated.slice(startIdx, endIdx + 1);
|
|
861
|
+
chartData.consumed_capacity_predicated = chartData.consumed_capacity_predicated.slice(startIdx, endIdx + 1);
|
|
862
|
+
chartData.spare_capacity_predicated = chartData.spare_capacity_predicated.slice(startIdx, endIdx + 1);
|
|
863
|
+
chartData.no_spare_error_predicated = chartData.no_spare_error_predicated.slice(startIdx, endIdx + 1);
|
|
864
|
+
chartData.optimized_capacity = chartData.optimized_capacity.slice(startIdx, endIdx + 1);
|
|
865
|
+
|
|
866
|
+
// Show reset button when zoomed and display timezone-aware times
|
|
867
|
+
document.getElementById('reset-chart-zoom').style.display = 'inline-block';
|
|
868
|
+
const startTime = xAxis[0];
|
|
869
|
+
const endTime = xAxis[xAxis.length - 1];
|
|
870
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
871
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
872
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
873
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
874
|
+
} else {
|
|
875
|
+
// Hide reset button when not zoomed (showing default all timepoints)
|
|
876
|
+
document.getElementById('reset-chart-zoom').style.display = 'none';
|
|
877
|
+
// Display full time range with start and end times
|
|
878
|
+
const startTime = xAxis[0];
|
|
879
|
+
const endTime = xAxis[xAxis.length - 1];
|
|
880
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
881
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
882
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
883
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const chartDom = document.getElementById('usage-chart');
|
|
887
|
+
globalUsageChart = echarts.init(chartDom);
|
|
888
|
+
const myChart = globalUsageChart;
|
|
889
|
+
const option = {
|
|
890
|
+
animation: false,
|
|
891
|
+
title: {
|
|
892
|
+
text: "Capacity Details: {{TEMPLATE_NAME}} ({{TEMPLATE_ID_PATH}})"
|
|
893
|
+
},
|
|
894
|
+
tooltip: {
|
|
895
|
+
trigger: 'axis',
|
|
896
|
+
axisPointer: {
|
|
897
|
+
type: 'cross',
|
|
898
|
+
label: {
|
|
899
|
+
backgroundColor: '#6a7985',
|
|
900
|
+
formatter: function(params) {
|
|
901
|
+
// Only round Y-axis values, keep X-axis unchanged
|
|
902
|
+
if (params.axisDimension === 'y') {
|
|
903
|
+
return String(Math.round(params.value));
|
|
904
|
+
}
|
|
905
|
+
return params.value;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
formatter: function(params) {
|
|
910
|
+
// Suppress tooltip when in time-range selection mode
|
|
911
|
+
if (isSelectingTimeRange) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
916
|
+
const dataIndex = params[0].dataIndex;
|
|
917
|
+
|
|
918
|
+
// Determine the split point between real and predicted data
|
|
919
|
+
let splitIndex = usageChartData.split_index;
|
|
920
|
+
if (splitIndex === undefined) {
|
|
921
|
+
// If not provided, assume all data is real data
|
|
922
|
+
splitIndex = usageChartData.x_axis.length;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Adjust splitIndex if we're viewing a filtered time range
|
|
926
|
+
if (currentFilteredStartIdx !== null && currentFilteredEndIdx !== null) {
|
|
927
|
+
const startIdx = Math.min(currentFilteredStartIdx, currentFilteredEndIdx);
|
|
928
|
+
const endIdx = Math.max(currentFilteredStartIdx, currentFilteredEndIdx);
|
|
929
|
+
|
|
930
|
+
// Adjust the split point relative to the filtered data
|
|
931
|
+
splitIndex = Math.max(0, splitIndex - startIdx);
|
|
932
|
+
|
|
933
|
+
// If the split point is beyond the filtered range, all data is predicated
|
|
934
|
+
if (splitIndex > (endIdx - startIdx + 1)) {
|
|
935
|
+
splitIndex = endIdx - startIdx + 1;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const isInPredicatedArea = dataIndex >= splitIndex;
|
|
940
|
+
|
|
941
|
+
// Filter series based on which area we're in
|
|
942
|
+
const filteredParams = params.filter(item => {
|
|
943
|
+
if (!isInPredicatedArea) {
|
|
944
|
+
// In real data range, exclude predicated series
|
|
945
|
+
return !item.seriesName.includes('Predicated');
|
|
946
|
+
} else {
|
|
947
|
+
// In predicated data range, show all series that have values
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Build tooltip content with different background for predicted area
|
|
953
|
+
const bgColor = isInPredicatedArea ? '#fff8e6' : '#ffffff';
|
|
954
|
+
const borderColor = isInPredicatedArea ? '#ffd700' : '#ccc';
|
|
955
|
+
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>';
|
|
956
|
+
|
|
957
|
+
let content = params[0].axisValueLabel + '<br/>';
|
|
958
|
+
filteredParams.forEach(function(item) {
|
|
959
|
+
if (item.value !== null && item.value !== undefined) {
|
|
960
|
+
content += item.marker + ' ' + item.seriesName + ': ' + Math.round(item.value) + '<br/>';
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
return '<div style="background:' + bgColor + ';margin:-8px;padding:8px;border-radius:4px;border:1px solid ' + borderColor + ';">' + label + content + '</div>';
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
legend: {
|
|
968
|
+
orient: 'horizontal',
|
|
969
|
+
bottom: '5%',
|
|
970
|
+
data: [
|
|
971
|
+
'Powered On VMs',
|
|
972
|
+
'Consumed Sessions',
|
|
973
|
+
'Spare Capacity',
|
|
974
|
+
'Consumed Capacity',
|
|
975
|
+
"No-spare Error",
|
|
976
|
+
'Powered On VMs - Predicated',
|
|
977
|
+
'Spare Capacity - Predicated',
|
|
978
|
+
'Consumed Capacity - Predicated',
|
|
979
|
+
"No-spare Error - Predicated",
|
|
980
|
+
'Optimized Capacity'
|
|
981
|
+
],
|
|
982
|
+
selected: {
|
|
983
|
+
'Powered On VMs': true,
|
|
984
|
+
'Consumed Capacity': true,
|
|
985
|
+
'Spare Capacity': true,
|
|
986
|
+
'No-spare Error': true,
|
|
987
|
+
'Optimized Capacity': true,
|
|
988
|
+
'Powered On VMs - Predicated': true,
|
|
989
|
+
'Spare Capacity - Predicated': false,
|
|
990
|
+
'Consumed Capacity - Predicated': false,
|
|
991
|
+
'No-spare Error - Predicated': true
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
toolbox: {
|
|
995
|
+
feature: {
|
|
996
|
+
saveAsImage: {}
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
grid: {
|
|
1000
|
+
left: '3%',
|
|
1001
|
+
right: '4%',
|
|
1002
|
+
bottom: '20%',
|
|
1003
|
+
containLabel: true
|
|
1004
|
+
},
|
|
1005
|
+
xAxis: [
|
|
1006
|
+
{
|
|
1007
|
+
type: 'category',
|
|
1008
|
+
boundaryGap: false,
|
|
1009
|
+
data: xAxis
|
|
1010
|
+
}
|
|
1011
|
+
],
|
|
1012
|
+
yAxis: [
|
|
1013
|
+
{
|
|
1014
|
+
type: 'value'
|
|
1015
|
+
}
|
|
1016
|
+
],
|
|
1017
|
+
series: [
|
|
1018
|
+
{
|
|
1019
|
+
name: 'Consumed Capacity',
|
|
1020
|
+
type: 'line',
|
|
1021
|
+
lineStyle: { width: 1 },
|
|
1022
|
+
symbolSize: 0,
|
|
1023
|
+
stack: 'Total',
|
|
1024
|
+
areaStyle: {},
|
|
1025
|
+
emphasis: {
|
|
1026
|
+
focus: 'none'
|
|
1027
|
+
},
|
|
1028
|
+
color: '#0a0',
|
|
1029
|
+
data: chartData.consumed_capacity
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: 'Powered On VMs',
|
|
1033
|
+
type: 'line',
|
|
1034
|
+
lineStyle: { width: 1 },
|
|
1035
|
+
symbolSize: 0,
|
|
1036
|
+
stack: false,
|
|
1037
|
+
emphasis: {
|
|
1038
|
+
focus: 'none'
|
|
1039
|
+
},
|
|
1040
|
+
color: '#0066ff',
|
|
1041
|
+
data: chartData.powered_on_vms
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
name: 'Spare Capacity',
|
|
1045
|
+
type: 'line',
|
|
1046
|
+
lineStyle: { width: 1 },
|
|
1047
|
+
symbolSize: 0,
|
|
1048
|
+
stack: 'Total',
|
|
1049
|
+
areaStyle: {},
|
|
1050
|
+
emphasis: {
|
|
1051
|
+
focus: 'none'
|
|
1052
|
+
},
|
|
1053
|
+
color: '#aa0',
|
|
1054
|
+
data: chartData.spare_capacity,
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
name: 'No-spare Error',
|
|
1058
|
+
type: 'bar',
|
|
1059
|
+
areaStyle: {},
|
|
1060
|
+
emphasis: {
|
|
1061
|
+
focus: 'none'
|
|
1062
|
+
},
|
|
1063
|
+
color: '#a00',
|
|
1064
|
+
data: chartData.no_spare_error,
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: 'Consumed 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: '#6d6',
|
|
1077
|
+
data: chartData.consumed_capacity_predicated,
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: 'Spare Capacity - Predicated',
|
|
1081
|
+
type: 'line',
|
|
1082
|
+
lineStyle: { width: 1 },
|
|
1083
|
+
symbolSize: 0,
|
|
1084
|
+
stack: 'Predicated',
|
|
1085
|
+
areaStyle: {},
|
|
1086
|
+
emphasis: {
|
|
1087
|
+
focus: 'none'
|
|
1088
|
+
},
|
|
1089
|
+
color: '#dd6',
|
|
1090
|
+
data: chartData.spare_capacity_predicated,
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: 'No-spare Error - Predicated',
|
|
1094
|
+
type: 'bar',
|
|
1095
|
+
areaStyle: {},
|
|
1096
|
+
emphasis: {
|
|
1097
|
+
focus: 'none'
|
|
1098
|
+
},
|
|
1099
|
+
color: '#d66',
|
|
1100
|
+
data: chartData.no_spare_error_predicated,
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
name: 'Powered On VMs - Predicated',
|
|
1104
|
+
type: 'line',
|
|
1105
|
+
lineStyle: { width: 1 },
|
|
1106
|
+
symbolSize: 0,
|
|
1107
|
+
stack: false,
|
|
1108
|
+
emphasis: {
|
|
1109
|
+
focus: 'none'
|
|
1110
|
+
},
|
|
1111
|
+
color: '#ff2222',
|
|
1112
|
+
data: chartData.powered_on_vms_predicated
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
name: 'Optimized Capacity',
|
|
1116
|
+
type: 'line',
|
|
1117
|
+
lineStyle: { width: 1 },
|
|
1118
|
+
symbolSize: 0,
|
|
1119
|
+
emphasis: {
|
|
1120
|
+
focus: 'none'
|
|
1121
|
+
},
|
|
1122
|
+
color: '#6dd',
|
|
1123
|
+
data: chartData.optimized_capacity,
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
option && myChart.setOption(option);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
762
1131
|
// Add tab switching functionality
|
|
763
1132
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
764
1133
|
button.addEventListener('click', () => {
|
|
765
1134
|
// Remove active class from all buttons and contents
|
|
766
1135
|
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
|
767
1136
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
768
|
-
|
|
1137
|
+
|
|
769
1138
|
// Add active class to clicked button and corresponding content
|
|
770
1139
|
button.classList.add('active');
|
|
771
1140
|
document.getElementById(`${button.dataset.tab}-tab`).classList.add('active');
|
|
772
|
-
|
|
1141
|
+
|
|
773
1142
|
// Update config display when switching to config tab
|
|
774
1143
|
if (button.dataset.tab === 'configuration') {
|
|
775
1144
|
updateConfigDisplay();
|
|
776
1145
|
}
|
|
1146
|
+
|
|
1147
|
+
// Render usage chart when switching to usage tab
|
|
1148
|
+
if (button.dataset.tab === 'usage') {
|
|
1149
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
1150
|
+
renderUsageChart(currentFilteredStartIdx, currentFilteredEndIdx, timezone);
|
|
1151
|
+
}
|
|
777
1152
|
});
|
|
778
1153
|
});
|
|
779
1154
|
|
|
780
|
-
// Initialize timezone selector
|
|
781
1155
|
document.addEventListener('DOMContentLoaded', () => {
|
|
1156
|
+
// Detect and display browser timezone
|
|
1157
|
+
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1158
|
+
const browserOption = document.getElementById('browser-timezone-option');
|
|
1159
|
+
if (browserOption) {
|
|
1160
|
+
browserOption.textContent = `Browser (${browserTimezone})`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const initialTimezone = calendarData.timezone || 'UTC';
|
|
1164
|
+
renderUsageChart(null, null, initialTimezone);
|
|
782
1165
|
renderChart();
|
|
783
1166
|
updateConfigDisplay();
|
|
784
|
-
|
|
1167
|
+
|
|
1168
|
+
// Set up drag-to-select functionality once
|
|
1169
|
+
const overlay = document.getElementById('chart-selection-overlay');
|
|
1170
|
+
const usageChartEl = document.getElementById('usage-chart');
|
|
1171
|
+
const chartContainer = usageChartEl ? usageChartEl.parentElement : null;
|
|
1172
|
+
let selectionStart = null;
|
|
1173
|
+
|
|
1174
|
+
if (overlay && usageChartEl && chartContainer) {
|
|
1175
|
+
// Use mousedown on the chart element to start selection
|
|
1176
|
+
usageChartEl.addEventListener('mousedown', (e) => {
|
|
1177
|
+
// Only handle left mouse button
|
|
1178
|
+
if (e.button !== 0) return;
|
|
1179
|
+
|
|
1180
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
1181
|
+
const x = e.clientX - rect.left;
|
|
1182
|
+
const y = e.clientY - rect.top;
|
|
1183
|
+
|
|
1184
|
+
// Only start selection if we're in the chart area
|
|
1185
|
+
if (x >= 0 && y >= 0) {
|
|
1186
|
+
// Enter selection mode - this will suppress tooltip
|
|
1187
|
+
isSelectingTimeRange = true;
|
|
1188
|
+
selectionStart = x;
|
|
1189
|
+
|
|
1190
|
+
// Hide any existing tooltip immediately
|
|
1191
|
+
if (globalUsageChart) {
|
|
1192
|
+
globalUsageChart.dispatchAction({
|
|
1193
|
+
type: 'hideTip'
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
overlay.style.left = selectionStart + 'px';
|
|
1198
|
+
overlay.style.width = '0px';
|
|
1199
|
+
overlay.style.display = 'block';
|
|
1200
|
+
|
|
1201
|
+
// Prevent text selection during drag
|
|
1202
|
+
e.preventDefault();
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
document.addEventListener('mousemove', (e) => {
|
|
1207
|
+
if (!isSelectingTimeRange) return;
|
|
1208
|
+
|
|
1209
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
1210
|
+
const currentX = e.clientX - rect.left;
|
|
1211
|
+
const width = Math.abs(currentX - selectionStart);
|
|
1212
|
+
const left = Math.min(selectionStart, currentX);
|
|
1213
|
+
|
|
1214
|
+
overlay.style.left = left + 'px';
|
|
1215
|
+
overlay.style.width = width + 'px';
|
|
1216
|
+
|
|
1217
|
+
// Keep hiding tooltip during drag
|
|
1218
|
+
if (globalUsageChart) {
|
|
1219
|
+
globalUsageChart.dispatchAction({
|
|
1220
|
+
type: 'hideTip'
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
document.addEventListener('mouseup', (e) => {
|
|
1226
|
+
if (!isSelectingTimeRange) return;
|
|
1227
|
+
|
|
1228
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
1229
|
+
const currentX = e.clientX - rect.left;
|
|
1230
|
+
|
|
1231
|
+
// Exit selection mode
|
|
1232
|
+
isSelectingTimeRange = false;
|
|
1233
|
+
overlay.style.display = 'none';
|
|
1234
|
+
|
|
1235
|
+
// If selection is too small, treat as a click (no zoom)
|
|
1236
|
+
if (Math.abs(currentX - selectionStart) < 10) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Get the chart container dimensions
|
|
1241
|
+
const chartWidth = chartContainer.offsetWidth;
|
|
1242
|
+
|
|
1243
|
+
// Estimate grid area (echarts default: left 3%, right 4%)
|
|
1244
|
+
const gridLeftPercent = 3;
|
|
1245
|
+
const gridRightPercent = 4;
|
|
1246
|
+
const gridLeft = (gridLeftPercent / 100) * chartWidth;
|
|
1247
|
+
const gridRight = (gridRightPercent / 100) * chartWidth;
|
|
1248
|
+
const gridWidth = chartWidth - gridLeft - gridRight;
|
|
1249
|
+
|
|
1250
|
+
// Convert pixel positions to data indices
|
|
1251
|
+
const normalizedStart = Math.max(0, (Math.min(selectionStart, currentX) - gridLeft) / gridWidth);
|
|
1252
|
+
const normalizedEnd = Math.max(0, (Math.max(selectionStart, currentX) - gridLeft) / gridWidth);
|
|
1253
|
+
|
|
1254
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
1255
|
+
const totalPoints = usageChartData.x_axis.length;
|
|
1256
|
+
let startIdx = Math.floor(normalizedStart * totalPoints);
|
|
1257
|
+
let endIdx = Math.floor(normalizedEnd * totalPoints);
|
|
1258
|
+
|
|
1259
|
+
startIdx = Math.max(0, Math.min(startIdx, totalPoints - 1));
|
|
1260
|
+
endIdx = Math.max(startIdx, Math.min(endIdx, totalPoints - 1));
|
|
1261
|
+
|
|
1262
|
+
// Re-render chart with selected range in current timezone
|
|
1263
|
+
const timezone = document.getElementById('timezone-select').value;
|
|
1264
|
+
renderUsageChart(startIdx, endIdx, timezone);
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// Cancel selection if mouse leaves the chart area while dragging
|
|
1268
|
+
chartContainer.addEventListener('mouseleave', () => {
|
|
1269
|
+
if (isSelectingTimeRange) {
|
|
1270
|
+
isSelectingTimeRange = false;
|
|
1271
|
+
overlay.style.display = 'none';
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
785
1276
|
const timezoneSelect = document.getElementById('timezone-select');
|
|
786
1277
|
timezoneSelect.value = calendarData.timezone;
|
|
787
|
-
|
|
1278
|
+
|
|
788
1279
|
timezoneSelect.addEventListener('change', (e) => {
|
|
789
1280
|
calendarData.timezone = e.target.value;
|
|
790
1281
|
updateConfigDisplay();
|
|
1282
|
+
|
|
1283
|
+
// Refresh chart with new timezone
|
|
1284
|
+
renderUsageChart(currentFilteredStartIdx, currentFilteredEndIdx, e.target.value);
|
|
1285
|
+
|
|
1286
|
+
// Refresh time range display in new timezone
|
|
1287
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
1288
|
+
const xAxis = usageChartData.x_axis;
|
|
1289
|
+
const timezone = e.target.value;
|
|
1290
|
+
|
|
1291
|
+
if (currentFilteredStartIdx !== null && currentFilteredEndIdx !== null) {
|
|
1292
|
+
// Update for selected range
|
|
1293
|
+
const startIdx = Math.min(currentFilteredStartIdx, currentFilteredEndIdx);
|
|
1294
|
+
const endIdx = Math.max(currentFilteredStartIdx, currentFilteredEndIdx);
|
|
1295
|
+
const startTime = xAxis[startIdx];
|
|
1296
|
+
const endTime = xAxis[endIdx];
|
|
1297
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
1298
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
1299
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
1300
|
+
} else {
|
|
1301
|
+
// Update for default full range
|
|
1302
|
+
const startTime = xAxis[0];
|
|
1303
|
+
const endTime = xAxis[xAxis.length - 1];
|
|
1304
|
+
const displayStart = convertTimeToTimezone(startTime, timezone);
|
|
1305
|
+
const displayEnd = convertTimeToTimezone(endTime, timezone);
|
|
1306
|
+
document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// Add reset zoom button functionality
|
|
1311
|
+
const resetButton = document.getElementById('reset-chart-zoom');
|
|
1312
|
+
resetButton.addEventListener('click', () => {
|
|
1313
|
+
renderUsageChart();
|
|
791
1314
|
});
|
|
792
1315
|
|
|
793
1316
|
// Add copy to clipboard functionality
|
|
@@ -804,6 +1327,24 @@
|
|
|
804
1327
|
console.error('Failed to copy text: ', err);
|
|
805
1328
|
});
|
|
806
1329
|
});
|
|
1330
|
+
|
|
1331
|
+
// Add copy to clipboard functionality for data tab
|
|
1332
|
+
const copyDataButton = document.getElementById('copy-data');
|
|
1333
|
+
if (copyDataButton) {
|
|
1334
|
+
copyDataButton.addEventListener('click', () => {
|
|
1335
|
+
const usageChartData = {{USAGE_CHART_DATA}};
|
|
1336
|
+
const jsonString = JSON.stringify(usageChartData, null, 2);
|
|
1337
|
+
navigator.clipboard.writeText(jsonString).then(() => {
|
|
1338
|
+
const originalText = copyDataButton.textContent;
|
|
1339
|
+
copyDataButton.textContent = 'Copied!';
|
|
1340
|
+
setTimeout(() => {
|
|
1341
|
+
copyDataButton.textContent = originalText;
|
|
1342
|
+
}, 2000);
|
|
1343
|
+
}).catch(err => {
|
|
1344
|
+
console.error('Failed to copy text: ', err);
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
807
1348
|
});
|
|
808
1349
|
</script>
|
|
809
1350
|
|