index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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['version']['git_hash']}">${build_info['version']['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. <button class="btn btn-md btn-outline-primary m-1 tooltip-button" data-bs-toggle="tooltip" data-bs-animation="false" data-bs-title="Copy and re-build" onclick="window.location.href='/add_build?rebuild_from=${build_info['build_id']}';">
  91. <i class="bi bi-arrow-clockwise"></i>
  92. </button>
  93. </td>
  94. </tr>`;
  95. row_num += 1;
  96. });
  97. let table_html = `<table class="table table-hover table-light shadow">
  98. <thead class="table-dark">
  99. <th scope="col" style="width: 5%">Status</th>
  100. <th scope="col" style="width: 5%">Age</th>
  101. <th scope="col" style="width: 5%">Git Hash</th>
  102. <th scope="col" style="width: 5%">Board</th>
  103. <th scope="col" style="width: 5%">Vehicle</th>
  104. <th scope="col">Features</th>
  105. <th scope="col" style="width: 15%">Progress</th>
  106. <th scope="col" style="width: 18%">Actions</th>
  107. </thead>
  108. <tbody>${table_body_html}</tbody>
  109. </table>`;
  110. output_container.innerHTML = table_html;
  111. }
  112. const LogFetch = (() => {
  113. var stopFetch = true;
  114. var build_id = null;
  115. var scheduled_fetches = 0;
  116. function startLogFetch(new_build_id) {
  117. build_id = new_build_id;
  118. stopFetch = false;
  119. if (scheduled_fetches <= 0) {
  120. scheduled_fetches = 1;
  121. fetchLogFile();
  122. }
  123. }
  124. function stopLogFetch() {
  125. stopFetch = true;
  126. }
  127. function getBuildId() {
  128. return build_id;
  129. }
  130. function fetchLogFile() {
  131. if (stopFetch || !build_id) {
  132. scheduled_fetches -= 1;
  133. return;
  134. }
  135. var xhr = new XMLHttpRequest();
  136. xhr.open('GET', `/api/v1/builds/${build_id}/logs`);
  137. // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload
  138. xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
  139. xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
  140. xhr.setRequestHeader("Pragma", "no-cache");
  141. xhr.onload = () => {
  142. if (xhr.status == 200) {
  143. let logTextArea = document.getElementById('logTextArea');
  144. let autoScrollSwitch = document.getElementById('autoScrollSwitch');
  145. logTextArea.textContent = xhr.responseText;
  146. if (autoScrollSwitch.checked) {
  147. logTextArea.scrollTop = logTextArea.scrollHeight;
  148. }
  149. if (xhr.responseText.includes('BUILD_FINISHED')) {
  150. stopFetch = true;
  151. }
  152. }
  153. if (!stopFetch) {
  154. setTimeout(fetchLogFile, 3000);
  155. } else {
  156. scheduled_fetches -= 1;
  157. }
  158. }
  159. xhr.send();
  160. }
  161. return {startLogFetch, stopLogFetch, getBuildId};
  162. })();
  163. function launchLogModal(build_id) {
  164. document.getElementById('logTextArea').textContent = `Fetching build log...\nBuild ID: ${build_id}`;
  165. LogFetch.startLogFetch(build_id);
  166. let logModalElement = document.getElementById('logModal');
  167. logModalElement.addEventListener('hide.bs.modal', () => {
  168. LogFetch.stopLogFetch();
  169. });
  170. let logModal = bootstrap.Modal.getOrCreateInstance(logModalElement);
  171. logModal.show();
  172. }
  173. // Trigger auto-download if state changes from "RUNNING" to "SUCCESS"
  174. let previousState = null;
  175. let autoDownloadIntervalId = null;
  176. async function tryAutoDownload(buildId) {
  177. if (!autoDownloadIntervalId) {
  178. return;
  179. }
  180. try {
  181. const apiUrl = `/api/v1/builds/${buildId}`
  182. const response = await fetch(apiUrl);
  183. const data = await response.json();
  184. const currentState = data.progress?.state;
  185. if (previousState === "RUNNING" && currentState === "SUCCESS") {
  186. console.log("Build completed successfully. Starting download...");
  187. window.location.href = `/api/v1/builds/${buildId}/artifact`;
  188. }
  189. // Stop running if the build is in a terminal state
  190. if (["FAILURE", "SUCCESS", "ERROR", "TIMED_OUT"].includes(currentState)) {
  191. clearInterval(autoDownloadIntervalId);
  192. return;
  193. }
  194. previousState = currentState;
  195. } catch (err) {
  196. console.error("Failed to fetch build status:", err);
  197. }
  198. };