asset_graph.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. {% extends "base.html" %}
  2. {% set active_page = "assets" %}
  3. {% block title %} {{asset.name}} {% endblock %}
  4. {% block divs %}
  5. {% block breadcrumbs %} {{ super() }} {% endblock %}
  6. <div class="container-fluid">
  7. <div class="row mx-1">
  8. <div class="alert alert-info d-none" id="tzwarn"></div>
  9. <div class="alert alert-info d-none" id="dstwarn"></div>
  10. </div>
  11. <div class="row">
  12. <div class="col-md-2 on-top-md">
  13. <div class="header-action-button">
  14. <div>
  15. <button class="btn btn-primary" type="button" data-bs-toggle="modal"
  16. data-bs-target="#sensorsToShowModal">
  17. Edit Graphs
  18. </button>
  19. </div>
  20. </div>
  21. <div class="sidepanel-container ">
  22. <div class="left-sidepanel-label">Select dates</div>
  23. <div class="sidepanel left-sidepanel">
  24. <div id="datepicker"></div>
  25. </div>
  26. </div>
  27. </div>
  28. <div class="col-md-8">
  29. <div id="spinner">
  30. <i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i>
  31. <span class="sr-only">Loading...</span>
  32. </div>
  33. <div id="sensorchart" class="card" style="width: 100%;"></div>
  34. <div class="row">
  35. <div class="copy-url" title="Click to copy the URL to the current time range to clipboard.">
  36. <script>
  37. function toIsoString(date) {
  38. var tzo = -date.getTimezoneOffset(),
  39. dif = tzo >= 0 ? '+' : '-',
  40. pad = function (num) {
  41. return (num < 10 ? '0' : '') + num;
  42. };
  43. return date.getFullYear() +
  44. '-' + pad(date.getMonth() + 1) +
  45. '-' + pad(date.getDate()) +
  46. 'T' + pad(date.getHours()) +
  47. ':' + pad(date.getMinutes()) +
  48. ':' + pad(date.getSeconds()) +
  49. dif + pad(Math.floor(Math.abs(tzo) / 60)) +
  50. ':' + pad(Math.abs(tzo) % 60);
  51. }
  52. $(window).ready(() => {
  53. picker.on('selected', (startDate, endDate) => {
  54. startDate = encodeURIComponent(toIsoString(startDate.toJSDate()));
  55. endDate = encodeURIComponent(toIsoString(endDate.toJSDate()));
  56. var base_url = window.location.href.split("?")[0];
  57. var new_url = `${base_url}?start_time=${startDate}&end_time=${endDate}`;
  58. // change current url without reloading the page
  59. window.history.pushState({}, null, new_url);
  60. });
  61. });
  62. function copyUrl(event) {
  63. event.preventDefault();
  64. if (!window.getSelection) {
  65. alert('Please copy the URL from the location bar.');
  66. return;
  67. }
  68. const dummy = document.createElement('p');
  69. var startDate = encodeURIComponent(toIsoString(picker.getStartDate().toJSDate()));
  70. // add 1 day to end date as datepicker does not include the end date day
  71. var endDate = picker.getEndDate();
  72. endDate.setDate(endDate.getDate() + 1);
  73. endDate = encodeURIComponent(toIsoString(endDate.toJSDate()));
  74. var base_url = window.location.href.split("?")[0];
  75. dummy.textContent = `${base_url}?start_time=${startDate}&end_time=${endDate}`
  76. document.body.appendChild(dummy);
  77. const range = document.createRange();
  78. range.setStartBefore(dummy);
  79. range.setEndAfter(dummy);
  80. const selection = window.getSelection();
  81. // First clear, in case the user already selected some other text
  82. selection.removeAllRanges();
  83. selection.addRange(range);
  84. document.execCommand('copy');
  85. document.body.removeChild(dummy);
  86. $("#message").show().delay(1000).fadeOut();
  87. }
  88. </script>
  89. <a href="#" onclick="copyUrl(event)" style="display: block; text-align: center;">
  90. <i class="fa fa-link"></i>
  91. </a>
  92. <div id="message" style="display: none; text-align: center;">The URL to the time range currently
  93. shown has been copied to your clipboard.</div>
  94. </div>
  95. </div>
  96. </div>
  97. <div class="col-md-2">
  98. <div class="replay-container">
  99. <div id="replay" title="Press 'p' to play/pause/resume or 's' to stop." class="stopped"></div>
  100. <div id="replay-time"></div>
  101. </div>
  102. </div>
  103. </div>
  104. <!-- Modal -->
  105. <div class="modal fade modal-xl" id="sensorsToShowModal" tabindex="-1" aria-labelledby="sensorsToShowModalLabel"
  106. aria-hidden="true">
  107. <div class="modal-dialog">
  108. <div class="modal-content">
  109. <div class="modal-header">
  110. <h5 class="modal-title pe-2">Edit Dashboard Graphs</h5>
  111. <div class="dropdown" data-bs-auto-close="outside">
  112. <span class="fa fa-info dropdown-toggle" role="button" data-bs-toggle="dropdown"
  113. aria-expanded="false" rel="tooltip" aria-hidden="true" tabindex="0"
  114. data-bs-placement="right" data-bs-toggle="tooltip"></span>
  115. <div class="dropdown-menu p-3" style="width: 400px;">
  116. <div>
  117. <strong>Dashboard Graphs Help</strong>
  118. <p class="mb-2">
  119. Here you can edit what data is shown in the Dashboard Graphs. Each graph can show
  120. one or more sensors
  121. (<em>it makes sense to show sensors in a graph that share the same unit</em>).
  122. </p>
  123. <p class="mb-2">
  124. You can also set the title of each graph and re-order them. Select a graph
  125. <span class="text-primary">(by clicking on its card)</span> to add sensors to it.
  126. </p>
  127. <p class="mb-0">
  128. Sensors can be searched on the right. This will list sensors on the asset or its
  129. child asset, as well as public assets.
  130. </p>
  131. </div>
  132. </div>
  133. </div>
  134. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  135. </div>
  136. <div class="modal-body">
  137. <div class="row">
  138. <div class="col">
  139. <button class="btn btn-primary mb-3" onclick="addNewGraph()">
  140. Add Graph
  141. </button>
  142. <div id="graphList" class="row"></div>
  143. </div>
  144. <div class="col">
  145. <div class="row mb-3">
  146. <div class="col-8">
  147. <input type="text" id="searchInput" class="form-control"
  148. placeholder="Search sensors..." oninput="filterSensors()" />
  149. </div>
  150. <div class="col-4">
  151. <select id="unitsSelect" class="form-select" onchange="filterSensors()">
  152. <option selected>Units</option>
  153. {% for unit in available_units %}
  154. <option>{{ unit }}</option>
  155. {% endfor %}
  156. </select>
  157. </div>
  158. </div>
  159. <div class="container">
  160. <div id="spinnerElement" class="d-flex justify-content-center align-items-center mb-2"
  161. style="height: 50vh; display: none !important;">
  162. <div class="spinner-border text-primary" role="status"
  163. style="width: 4rem; height: 4rem;">
  164. <span class="visually-hidden">Loading...</span>
  165. </div>
  166. </div>
  167. <div id="apiSensorsList" class="row" style="display: none;"></div>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. </div>
  176. <script src="https://cdnjs.cloudflare.com/ajax/libs/jstimezonedetect/1.0.7/jstz.js"></script>
  177. <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/litepicker.js"></script>
  178. <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/ranges.js"></script>
  179. <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/keyboardnav.js"></script>
  180. {% block leftsidepanel %} {{ super() }} {% endblock %}
  181. {% block sensorChartSetup %} {{ super() }} {% endblock %}
  182. <!-- Initialise the map -->
  183. <script src="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet-src.min.js"></script>
  184. <script src="{{ url_for('flexmeasures_ui.static', filename='js/map-init.js') }}"></script>
  185. <script>
  186. const senSearchResEle = document.getElementById("sensorsSearchResults")
  187. const formModal = document.getElementById('sensorsToShowModal');
  188. const apiSensorsListElement = document.getElementById("apiSensorsList");
  189. const spinnerElement = document.getElementById('spinnerElement');
  190. let sensorsToShowRawJSON = "{{ asset.sensors_to_show | safe }}";
  191. sensorsToShowRawJSON = sensorsToShowRawJSON.replace(/'/g, '"');
  192. let sensorsToShow = JSON.parse(sensorsToShowRawJSON);
  193. let cachedFilteredSensors = []; // keeps track of the filtered sensors
  194. let editingIndex; // keeps track of which graph we are currently editing
  195. let savedGraphIndex; // keeps track of the graph that is currently selected
  196. let selectedGraphTitle; // keeps track of the graph title that is currently selected
  197. const apiBasePath = window.location.origin;
  198. const sensorsApiUrl = `${apiBasePath}/api/v3_0/sensors?page=1&per_page=100&asset_id={{ asset.id }}&include_public_assets=true`;
  199. // Fetch Account Details
  200. async function getAccount(accountId) {
  201. const cacheKey = `account_${accountId}`;
  202. const cachedData = localStorage.getItem(cacheKey);
  203. if (cachedData) {
  204. return JSON.parse(cachedData);
  205. }
  206. const apiUrl = apiBasePath + "/api/v3_0/accounts/" + accountId;
  207. const response = await fetch(apiUrl);
  208. const account = await response.json();
  209. localStorage.setItem(cacheKey, JSON.stringify(account));
  210. return account;
  211. }
  212. // Fetch Asset Details
  213. async function getAsset(assetId) {
  214. const cacheKey = `asset_${assetId}`;
  215. const cachedData = localStorage.getItem(cacheKey);
  216. if (cachedData) {
  217. return JSON.parse(cachedData);
  218. }
  219. const apiUrl = apiBasePath + "/api/v3_0/assets/" + assetId;
  220. const response = await fetch(apiUrl);
  221. const asset = await response.json();
  222. localStorage.setItem(cacheKey, JSON.stringify(asset));
  223. return asset;
  224. }
  225. // Fetch Sensor Details
  226. async function getSensor(id) {
  227. const cacheKey = `sensor_${id}`;
  228. const cachedData = localStorage.getItem(cacheKey);
  229. if (cachedData) {
  230. return JSON.parse(cachedData);
  231. }
  232. const apiUrl = apiBasePath + "/api/v3_0/sensors/" + id;
  233. const response = await fetch(apiUrl);
  234. const sensor = await response.json();
  235. localStorage.setItem(cacheKey, JSON.stringify(sensor));
  236. return sensor;
  237. }
  238. // highlight selected graph
  239. async function selectGraph(graphIndex) {
  240. if (graphIndex !== undefined) {
  241. savedGraphIndex = graphIndex;
  242. selectedGraphTitle = sensorsToShow[graphIndex]?.title;
  243. // check if graphIndex still exists(This is because this function is called when the removed button is clicked as well)
  244. if (sensorsToShow[graphIndex]) {
  245. renderApiSensors(cachedFilteredSensors, graphIndex);
  246. } else {
  247. renderApiSensors(cachedFilteredSensors);
  248. }
  249. } else {
  250. savedGraphIndex = undefined;
  251. selectedGraphTitle = undefined;
  252. renderGraphCards();
  253. }
  254. }
  255. // Function to render the graph cards
  256. async function renderGraphCards() {
  257. const graphList = document.getElementById("graphList");
  258. graphList.innerHTML = "";
  259. const newSensorsToShow = [];
  260. if (sensorsToShow.length === 0) {
  261. return;
  262. }
  263. for (const [index, item] of sensorsToShow.entries()) {
  264. const col = document.createElement("div");
  265. col.classList.add("col-12", "mb-1");
  266. const sensorsUnits = [];
  267. // the initializing of the newItem is to handle the case where the item is an array of ID's instead of an object
  268. const newItem = {
  269. title: item.title ?? "No Title",
  270. sensors: item.sensors ?? (Array.isArray(item) ? item : [item]),
  271. }
  272. newSensorsToShow.push(newItem);
  273. const sensorsContent = await Promise.all(
  274. newItem.sensors.map(async (sensor, sensorIndex) => {
  275. const sensorData = await getSensor(sensor);
  276. const Asset = await getAsset(sensorData.generic_asset_id);
  277. const Account = await getAccount(Asset.account_id);
  278. sensorsUnits.push(sensorData.unit);
  279. return `
  280. <div class="p-1 mb-3 border-bottom border-secondary">
  281. <div class="d-flex justify-content-between">
  282. <div>
  283. <b>ID:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
  284. <b>Unit:</b> ${sensorData.unit},
  285. <b>Name:</b> ${sensorData.name},
  286. <div style="padding-top: 1px;"></div>
  287. <b>Asset:</b> ${Asset.name},
  288. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  289. </div>
  290. <i class="fa fa-times"
  291. onclick="removeSensorFromGraph(${index}, ${sensorIndex})"
  292. data-bs-toggle="tooltip"
  293. data-bs-placement="top"
  294. title="Remove Sensor"
  295. style="cursor: pointer;"
  296. ></i>
  297. </div>
  298. </div>`;
  299. }));
  300. const uniqueUnits = [...new Set(sensorsUnits)];
  301. col.innerHTML = `
  302. <div class="card m-0 p-1">
  303. <div class="card-body card-highlight ${newItem.title === selectedGraphTitle ? " border-on-click" : ""}" id="graph_${index}" onclick="selectGraph(${index})">
  304. <div class="d-flex align-items-center">
  305. <div>
  306. ${editingIndex === index
  307. ? `<input type="text" class="form-control mb-2 me-2" id="editTitle_${index}" value="${newItem.title}" onkeydown="handleEnterKeyEventForTitleEditing(event, ${index})" />`
  308. : `<h5 class="card-title me-2">${newItem.title}</h5>`
  309. }
  310. </div>
  311. <div>
  312. ${editingIndex === index
  313. ? `<button class="btn btn-success btn-sm ms-2" id="saveTitleBtn" onclick="saveGraphTitle(${index})">Save</button>`
  314. : `<button class="btn btn-warning btn-sm ms-2" id="editTitleBtn" onclick="editGraphTitle(${index})">Edit</button>`
  315. }
  316. </div>
  317. </div>
  318. <h5 class="card-title pt-2"><b> Sensors: </b></h5>
  319. <div>
  320. ${sensorsContent.length > 0 ? sensorsContent.join("") : `<div class="alert alert-warning" role="alert"> No sensors added to this graph. Add sensors by selecting them from the right</div>`}
  321. ${uniqueUnits.length > 1 ? `<div class="alert alert-warning" role="alert">Note that you are showing sensors with different units</div>` : ""}
  322. </div>
  323. <button class="btn btn-danger btn-sm me-2" onclick="(function(e) { e.stopPropagation(); removeGraph(${index}); })(event)">Remove</button>
  324. <button class="btn btn-secondary btn-sm me-2" onclick="(function(e) { e.stopPropagation(); moveGraphUp(${index}); })(event)" ${index === 0 ? "disabled" : ""}>Move Up</button>
  325. <button class="btn btn-secondary btn-sm" onclick="(function(e) { e.stopPropagation(); moveGraphDown(${index}); })(event)" ${index === sensorsToShow.length - 1 ? "disabled" : ""}>Move Down</button>
  326. </div>
  327. </div>`;
  328. graphList.appendChild(col);
  329. }
  330. sensorsToShow = newSensorsToShow;
  331. }
  332. async function filterSensors() {
  333. const searchValue = document.getElementById("searchInput").value.toLowerCase();
  334. const filterValue = document.getElementById("unitsSelect").value;
  335. const highlightedCard = document.querySelector('.border-on-click');
  336. spinnerElement.style.display = 'flex';
  337. apiSensorsListElement.style.display = 'none';
  338. // Due to the nature of async functions, the highlightedCard might not be available
  339. // when the filterSensors function is called. So, we need to check if it exists
  340. if (highlightedCard) {
  341. const cardId = highlightedCard.id;
  342. const index = cardId.split("_")[1];
  343. savedGraphIndex = index;
  344. selectedGraphTitle = sensorsToShow[index].title;
  345. } else {
  346. savedGraphIndex = undefined;
  347. selectedGraphTitle = undefined;
  348. }
  349. // check if apiSensorsList has been rendered
  350. if (apiSensorsListElement.innerHTML === "") {
  351. document.getElementById('apiSensorsList').style.display = 'block';
  352. }
  353. const params = new URLSearchParams();
  354. if (searchValue) {
  355. params.append('filter', searchValue);
  356. }
  357. if (filterValue !== "Units") {
  358. params.append('unit', filterValue);
  359. }
  360. const apiUrl = `${sensorsApiUrl}&${params.toString()} `;
  361. try {
  362. const response = await fetch(apiUrl);
  363. if (!response.ok) {
  364. throw new Error('Failed to fetch sensors');
  365. }
  366. const responseData = await response.json();
  367. const filteredSensors = responseData.data;
  368. // Render the fetched sensors
  369. if (savedGraphIndex !== null && savedGraphIndex !== undefined) {
  370. renderApiSensors(filteredSensors, savedGraphIndex);
  371. } else {
  372. renderApiSensors(filteredSensors);
  373. }
  374. cachedFilteredSensors = filteredSensors;
  375. spinnerElement.classList.add('hidden-important');
  376. apiSensorsListElement.style.display = 'block';
  377. } catch (error) {
  378. showToast("Failed to filter sensors", "error");
  379. console.error(error);
  380. }
  381. }
  382. async function searchSensors() {
  383. const searchValue = document.getElementById("flexContextSensorSearch").value.toLowerCase();
  384. spinnerElement.style.display = 'flex';
  385. senSearchResEle.style.display = 'none';
  386. const params = new URLSearchParams();
  387. if (searchValue) {
  388. params.append('filter', searchValue);
  389. }
  390. const apiUrl = `${sensorsApiUrl}&${params.toString()} `;
  391. try {
  392. const response = await fetch(apiUrl);
  393. if (!response.ok) {
  394. throw new Error('Failed to fetch sensors');
  395. }
  396. const responseData = await response.json();
  397. const filteredSensors = responseData.data;
  398. // Render the fetched sensors
  399. renderSensorSearchResults(filteredSensors);
  400. spinnerElement.classList.add('hidden-important');
  401. senSearchResEle.style.display = 'block';
  402. } catch (error) {
  403. showToast("Failed to search sensors", "error");
  404. console.error(error);
  405. }
  406. }
  407. // ============== Graph Cards Management ============== //
  408. async function removeGraph(index) {
  409. sensorsToShow.splice(index, 1);
  410. savedGraphIndex = undefined;
  411. selectedGraphTitle = undefined;
  412. editingIndex = undefined;
  413. renderGraphCards();
  414. renderApiSensors(cachedFilteredSensors);
  415. }
  416. async function swapItems(index1, index2) {
  417. if (index1 >= 0 && index2 >= 0 && index1 < sensorsToShow.length && index2 < sensorsToShow.length) {
  418. [sensorsToShow[index1], sensorsToShow[index2]] = [sensorsToShow[index2], sensorsToShow[index1]];
  419. renderGraphCards()
  420. renderApiSensors(cachedFilteredSensors, index2);
  421. }
  422. }
  423. async function moveGraphUp(index) {
  424. if (index > 0) {
  425. await swapItems(index, index - 1);
  426. }
  427. }
  428. async function moveGraphDown(index) {
  429. if (index < sensorsToShow.length - 1) {
  430. await swapItems(index, index + 1);
  431. }
  432. }
  433. function editGraphTitle(index) {
  434. editingIndex = index;
  435. }
  436. async function saveGraphTitle(index) {
  437. const newTitle = document.getElementById(`editTitle_${index}`).value;
  438. sensorsToShow[index].title = newTitle;
  439. editingIndex = null;
  440. selectedGraphTitle = newTitle;
  441. renderGraphCards();
  442. }
  443. // ============== Graph Cards Management ============== //
  444. // Render the available API sensors
  445. function renderApiSensors(sensors, graphIndex) {
  446. // graphIndex is undefined when the sensors are being added to the graph
  447. // graphIndex is defined when the sensors are being added to the graph cards. In other words
  448. // when more sensors are being added to single graph
  449. apiSensorsList.innerHTML = ""; // Clear the previous sensors
  450. if (sensors.length === 0) {
  451. apiSensorsList.innerHTML = "<h3>No sensors found</h3>";
  452. return;
  453. }
  454. sensors.forEach(async (sensor) => {
  455. const Asset = await getAsset(sensor.generic_asset_id);
  456. const Account = await getAccount(Asset.account_id);
  457. const col = document.createElement("div");
  458. col.classList.add("col-12", "mb-1");
  459. col.innerHTML = `
  460. <div class="card m-0">
  461. <div class="card-body p-0 sensor-card">
  462. <h5 class="card-title">${sensor.name}</h5>
  463. <p class="card-text">
  464. <b>ID:</b> <a href="${apiBasePath}/sensors/${sensor.id}">${sensor.id}</a>,
  465. <b>Unit:</b> ${sensor.unit},
  466. <b>Asset:</b> ${Asset.name},
  467. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  468. </p>
  469. ${graphIndex !== undefined && savedGraphIndex !== undefined && selectedGraphTitle !== undefined
  470. ? `<button class="btn btn-primary btn-sm" onclick="addSensorToExistingGraph(${graphIndex}, ${sensor.id})">Add to '${selectedGraphTitle}' Graph</button>`
  471. : `<button class="btn btn-primary btn-sm" onclick="addSensorAsGraph(${sensor.id})">Add new Graph</button>`
  472. }
  473. </div>
  474. </div>
  475. `;
  476. apiSensorsList.appendChild(col);
  477. });
  478. }
  479. function renderSensorSearchResults(sensors) {
  480. senSearchResEle.innerHTML = "";
  481. if (sensors.length === 0) {
  482. senSearchResEle.innerHTML = "<h4>No sensors found</h4>";
  483. return;
  484. }
  485. sensors.forEach(async (sensor) => {
  486. const Asset = await getAsset(sensor.generic_asset_id);
  487. const Account = await getAccount(Asset.account_id);
  488. const col = document.createElement("div");
  489. col.classList.add("col-12", "mb-1");
  490. col.innerHTML = `
  491. <div class="card m-0">
  492. <div class="card-body p-0 sensor-card">
  493. <h5 class="card-title">${sensor.name}</h5>
  494. <p class="card-text">
  495. <b>ID:</b> <a href="${apiBasePath}/sensors/${sensor.id}">${sensor.id}</a>,
  496. <b>Unit:</b> ${sensor.unit},
  497. <b>Asset:</b> ${Asset.name},
  498. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  499. </p>
  500. <button class="btn btn-primary btn-sm" onclick="udpateFlexContextFieldValue('sensor', ${sensor.id})">Add Sensor</button>
  501. </div>
  502. </div>
  503. `;
  504. senSearchResEle.appendChild(col);
  505. });
  506. }
  507. async function updateAssetSensorsToShow(dataType) {
  508. const apiURL = apiBasePath + "/api/v3_0/assets/{{ asset.id }}";
  509. let requestBody;
  510. if (dataType === "sensorToShow") {
  511. requestBody = JSON.stringify({ sensors_to_show: JSON.stringify(sensorsToShow) });
  512. // Only show the spinner if relevant to this specific data type
  513. document.getElementById('spinner').style.display = 'block';
  514. } else if (dataType === "flexContext") {
  515. // remove null fields
  516. for (const [key, value] of Object.entries(assetFlexContext)) {
  517. if (value === null) {
  518. delete assetFlexContext[key];
  519. }
  520. }
  521. requestBody = JSON.stringify({ flex_context: JSON.stringify(assetFlexContext) });
  522. }
  523. const response = await fetch(apiURL, {
  524. method: "PATCH",
  525. headers: {
  526. "Content-Type": "application/json",
  527. },
  528. body: requestBody,
  529. });
  530. if (!response.ok) {
  531. const errorData = await response.json();
  532. const errorMessage = errorData?.message || errorData?.error || response.statusText;
  533. showToast(`Failed to update the asset: ${errorMessage}`, "error");
  534. } else {
  535. document.dispatchEvent(new Event('sensorsToShowUpdated'));
  536. showToast("Changes saved successfully", "success");
  537. }
  538. }
  539. // Add a sensor as a new graph card
  540. function addSensorAsGraph(id) {
  541. const newAsset = {
  542. title: "No Title",
  543. sensors: [id],
  544. };
  545. sensorsToShow.push(newAsset);
  546. renderGraphCards();
  547. }
  548. // Add blank graph to the graph cards
  549. function addNewGraph() {
  550. const newAsset = {
  551. title: "No Title " + (sensorsToShow.length + 1),
  552. sensors: [],
  553. };
  554. selectedGraphTitle = newAsset.title;
  555. sensorsToShow.push(newAsset);
  556. selectGraph(sensorsToShow.length - 1)
  557. renderGraphCards();
  558. }
  559. // Add Sensor to an existing graph card
  560. function addSensorToExistingGraph(graphIndex, sensorId) {
  561. sensorsToShow[graphIndex].sensors.push(sensorId);
  562. renderGraphCards();
  563. }
  564. // Remove sensor from the graph sensor list
  565. function removeSensorFromGraph(graphIndex, sensorIndex) {
  566. sensorsToShow[graphIndex].sensors.splice(sensorIndex, 1);
  567. renderGraphCards();
  568. renderApiSensors(cachedFilteredSensors);
  569. }
  570. function handleEnterKeyEventForTitleEditing(event, graphIndex) {
  571. if (event.key === "Enter") {
  572. saveGraphTitle(graphIndex);
  573. renderApiSensors(cachedFilteredSensors, graphIndex);
  574. }
  575. }
  576. // ============== Page Events ============== //
  577. formModal.addEventListener('hidden.bs.modal', function () {
  578. updateAssetSensorsToShow("sensorToShow");
  579. });
  580. formModal.addEventListener('shown.bs.modal', function () {
  581. // Initial renders
  582. renderGraphCards(); // Initial render of graph cards
  583. filterSensors(); // Initial render of sensors
  584. });
  585. document.addEventListener("click", function (event) {
  586. /**
  587. The logic in this block is majorly to remove the border on click of the card and add it to the selected card
  588. but as this event is added to the document, it will be triggered on any click event on the page
  589. so the if statements are used to check if the click event is on the card or not
  590. */
  591. const card = event.target.closest(".card-highlight");
  592. const sensorCard = event.target.closest(".sensor-card");
  593. const searchInput = event.target.id === "searchInput";
  594. const cardBody = event.target.closest(".card-body");
  595. const editTitleBtn = event.target.id === "editTitleBtn";
  596. const saveTitleBtn = event.target.id === "saveTitleBtn";
  597. const unSetBtn = event.target.id === "unSetFlexField";
  598. if (card) {
  599. if (card.classList.contains("border-on-click")) {
  600. if (editTitleBtn || saveTitleBtn) {
  601. renderGraphCards();
  602. }
  603. // Pass
  604. } else {
  605. renderGraphCards();
  606. }
  607. } else if (
  608. cardBody !== null && cardBody !== undefined ||
  609. sensorCard !== null && sensorCard !== undefined ||
  610. searchInput !== null && searchInput !== undefined
  611. ) {
  612. // Pass
  613. } else {
  614. document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
  615. renderApiSensors([], undefined);
  616. }
  617. });
  618. // ============== Page Events ============== //
  619. </script>
  620. {% block paginate_tables_script %} {{ super() }} {% endblock %}
  621. {% endblock %}