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.
@@ -6,18 +6,24 @@
6
6
  <style>
7
7
  body {
8
8
  font-family: sans-serif;
9
- padding: 20px;
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
- align-items: center;
20
- gap: 8px;
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
- width: 768px;
45
- margin-left: 20px;
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: -10px;
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: 834px;
180
- margin: 0 auto;
181
- padding: 0 20px;
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: 0 20px;
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-top: 40px;
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="America/New_York">America/New_York</option>
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="capacity">Capacity</button>
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="capacity-tab">
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
- labelLeft.appendChild(applyCalculated);
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