index.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. function init() {
  2. refresh_builds();
  3. // initialise tooltips by selector
  4. $('body').tooltip({
  5. selector: '[data-bs-toggle="tooltip"]'
  6. });
  7. }
  8. function refresh_builds() {
  9. var xhr = new XMLHttpRequest();
  10. xhr.open('GET', "/api/v1/builds");
  11. // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload
  12. xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
  13. xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
  14. xhr.setRequestHeader("Pragma", "no-cache");
  15. xhr.onload = function () {
  16. if (xhr.status === 200) {
  17. updateBuildsTable(JSON.parse(xhr.response));
  18. }
  19. setTimeout(refresh_builds, 5000);
  20. }
  21. xhr.send();
  22. }
  23. function showFeatures(row_num) {
  24. document.getElementById("featureModalBody").innerHTML = document.getElementById(`${row_num}_features_all`).innerHTML;
  25. var feature_modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('featureModal'));
  26. feature_modal.show();
  27. return;
  28. }
  29. function timeAgo(timestampStr) {
  30. const timestamp = parseFloat(timestampStr);
  31. const now = Date.now() / 1000;
  32. const diff = now - timestamp;
  33. if (diff < 0) return "In the future";
  34. const hours = Math.floor(diff / 3600);
  35. const minutes = Math.floor((diff % 3600) / 60);
  36. return `${hours}h ${minutes}m`;
  37. }
  38. function updateBuildsTable(builds) {
  39. let output_container = document.getElementById('build_table_container');
  40. if (builds.length == 0) {
  41. output_container.innerHTML = `<div class="alert alert-success" role="alert" id="welcome_alert">
  42. <h4 class="alert-heading">Welcome!</h4>
  43. <p>No builds were queued to run on the server recently. To queue one, please click <a href="./add_build" class="alert-link">add a build</a>.</p>
  44. </div>`;
  45. return;
  46. }
  47. // hide any tooltips which are currently open
  48. // this is needed as they might get stuck
  49. // if the element to which they belong goes out of the dom tree
  50. $('.tooltip-button').tooltip("hide");
  51. let table_body_html = '';
  52. let row_num = 0;
  53. builds.forEach((build_info) => {
  54. let status_color = 'primary';
  55. if (build_info['progress']['state'] == 'SUCCESS') {
  56. status_color = 'success';
  57. } else if (build_info['progress']['state'] == 'PENDING') {
  58. status_color = 'warning';
  59. } else if (build_info['progress']['state'] == 'FAILURE' || build_info['progress']['state'] == 'ERROR' || build_info['progress']['state'] == 'TIMED_OUT') {
  60. status_color = 'danger';
  61. }
  62. const features_string = build_info['selected_features'].join(', ')
  63. const build_age = timeAgo(build_info['time_created'])
  64. const isNonTerminal = (build_info['progress']['state'] == 'PENDING' || build_info['progress']['state'] == 'RUNNING');
  65. const downloadDisabled = isNonTerminal ? 'disabled' : '';
  66. const download_button_color = isNonTerminal ? 'secondary' : 'primary';
  67. table_body_html += `<tr>
  68. <td class="align-middle"><span class="badge text-bg-${status_color}">${build_info['progress']['state']}</span></td>
  69. <td class="align-middle">${build_age}</td>
  70. <td class="align-middle"><a href="https://github.com/ArduPilot/ardupilot/commit/${build_info['git_hash']}">${build_info['git_hash'].substring(0,8)}</a></td>
  71. <td class="align-middle">${build_info['board']['name']}</td>
  72. <td class="align-middle">${build_info['vehicle']['name']}</td>
  73. <td class="align-middle" id="${row_num}_features">
  74. ${features_string.substring(0, 100)}...
  75. <span id="${row_num}_features_all" style="display:none;">${features_string}</span>
  76. <a href="javascript: showFeatures(${row_num});">show more</a>
  77. </td>
  78. <td class="align-middle">
  79. <div class="progress border" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
  80. <div class="progress-bar bg-${status_color}" style="width: ${build_info['progress']['percent']}%">${build_info['progress']['percent']}%</div>
  81. </div>
  82. </td>
  83. <td class="align-middle">
  84. <button class="btn btn-md btn-outline-primary m-1 tooltip-button" data-bs-toggle="tooltip" data-bs-animation="false" data-bs-title="View log" onclick="launchLogModal('${build_info['build_id']}');">
  85. <i class="bi bi-file-text"></i>
  86. </button>
  87. <button class="btn btn-md btn-outline-${download_button_color} m-1 tooltip-button" data-bs-toggle="tooltip" data-bs-animation="false" data-bs-title="Download build artifacts" id="${build_info['build_id']}-download-btn" onclick="window.location.href='/api/v1/builds/${build_info['build_id']}/artifact';" ${downloadDisabled}>
  88. <i class="bi bi-download"></i>
  89. </button>
  90. </td>
  91. </tr>`;
  92. row_num += 1;
  93. });
  94. let table_html = `<table class="table table-hover table-light shadow">
  95. <thead class="table-dark">
  96. <th scope="col" style="width: 5%">Status</th>
  97. <th scope="col" style="width: 5%">Age</th>
  98. <th scope="col" style="width: 5%">Git Hash</th>
  99. <th scope="col" style="width: 5%">Board</th>
  100. <th scope="col" style="width: 5%">Vehicle</th>
  101. <th scope="col">Features</th>
  102. <th scope="col" style="width: 15%">Progress</th>
  103. <th scope="col" style="width: 15%">Actions</th>
  104. </thead>
  105. <tbody>${table_body_html}</tbody>
  106. </table>`;
  107. output_container.innerHTML = table_html;
  108. }
  109. const LogFetch = (() => {
  110. var stopFetch = true;
  111. var build_id = null;
  112. var scheduled_fetches = 0;
  113. function startLogFetch(new_build_id) {
  114. build_id = new_build_id;
  115. stopFetch = false;
  116. if (scheduled_fetches <= 0) {
  117. scheduled_fetches = 1;
  118. fetchLogFile();
  119. }
  120. }
  121. function stopLogFetch() {
  122. stopFetch = true;
  123. }
  124. function getBuildId() {
  125. return build_id;
  126. }
  127. function fetchLogFile() {
  128. if (stopFetch || !build_id) {
  129. scheduled_fetches -= 1;
  130. return;
  131. }
  132. var xhr = new XMLHttpRequest();
  133. xhr.open('GET', `/api/v1/builds/${build_id}/logs`);
  134. // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload
  135. xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
  136. xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
  137. xhr.setRequestHeader("Pragma", "no-cache");
  138. xhr.onload = () => {
  139. if (xhr.status == 200) {
  140. let logTextArea = document.getElementById('logTextArea');
  141. let autoScrollSwitch = document.getElementById('autoScrollSwitch');
  142. logTextArea.textContent = xhr.responseText;
  143. if (autoScrollSwitch.checked) {
  144. logTextArea.scrollTop = logTextArea.scrollHeight;
  145. }
  146. if (xhr.responseText.includes('BUILD_FINISHED')) {
  147. stopFetch = true;
  148. }
  149. }
  150. if (!stopFetch) {
  151. setTimeout(fetchLogFile, 3000);
  152. } else {
  153. scheduled_fetches -= 1;
  154. }
  155. }
  156. xhr.send();
  157. }
  158. return {startLogFetch, stopLogFetch, getBuildId};
  159. })();
  160. function launchLogModal(build_id) {
  161. document.getElementById('logTextArea').textContent = `Fetching build log...\nBuild ID: ${build_id}`;
  162. LogFetch.startLogFetch(build_id);
  163. let logModalElement = document.getElementById('logModal');
  164. logModalElement.addEventListener('hide.bs.modal', () => {
  165. LogFetch.stopLogFetch();
  166. });
  167. let logModal = bootstrap.Modal.getOrCreateInstance(logModalElement);
  168. logModal.show();
  169. }
  170. // Trigger auto-download if state changes from "RUNNING" to "SUCCESS"
  171. let previousState = null;
  172. let autoDownloadIntervalId = null;
  173. async function tryAutoDownload(buildId) {
  174. if (!autoDownloadIntervalId) {
  175. return;
  176. }
  177. try {
  178. const apiUrl = `/api/v1/builds/${buildId}`
  179. const response = await fetch(apiUrl);
  180. const data = await response.json();
  181. const currentState = data.progress?.state;
  182. if (previousState === "RUNNING" && currentState === "SUCCESS") {
  183. console.log("Build completed successfully. Starting download...");
  184. window.location.href = `/api/v1/builds/${buildId}/artifact`;
  185. }
  186. // Stop running if the build is in a terminal state
  187. if (["FAILURE", "SUCCESS", "ERROR", "TIMED_OUT"].includes(currentState)) {
  188. clearInterval(autoDownloadIntervalId);
  189. return;
  190. }
  191. previousState = currentState;
  192. } catch (err) {
  193. console.error("Failed to fetch build status:", err);
  194. }
  195. };