index.js 10 KB

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