user.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. {% extends "base.html" %}
  2. {% set active_page = "users" %}
  3. {% block title %} User {{ user.username }} {% endblock %}
  4. {% block divs %}
  5. {% if user %}
  6. <div class="container-fluid">
  7. <div class="row">
  8. <div class="col-md-2 on-top-md">
  9. <div class="d-grid gap-2 col-8 mt-3">
  10. {% if can_edit_user_details %}
  11. <button class="btn p-2 border-0 btn-info" type="button" title="Edit user details" data-bs-toggle="modal"
  12. data-bs-target="#editUserModal">Edit</button>
  13. {% endif %}
  14. {% if current_user.has_role('admin') or current_user.has_role('account-admin') %}
  15. <button class="btn p-2 border-0 {% if user.active %} btn-warning {% else %} btn-success {% endif %}"
  16. type="button" title="Toggle activation status of this user."
  17. onclick="window.location.href='/users/toggle_active/{{ user.id }}'">
  18. {% if user.active %} Deactivate user {% else %} Activate user {% endif %}
  19. </button>
  20. <button class="btn p-2 border-0 btn-info" type="button"
  21. title="Reset the password and send instructions how to choose a new one."
  22. onclick="window.location.href='/users/reset_password_for/{{ user.id }}'">
  23. Reset password
  24. </button>
  25. {% endif %}
  26. </div>
  27. </div>
  28. <div class="col-md-8">
  29. <div class="user-data-table card">
  30. <h2>User overview</h2>
  31. <small>User: {{ user.username }}</small>
  32. <div class="table-responsive">
  33. <table class="table table-striped">
  34. <tbody>
  35. <tr>
  36. <td>
  37. Email address
  38. </td>
  39. <td>
  40. <a href="mailto:{{ user.email }}">{{ user.email }}</a>
  41. </td>
  42. </tr>
  43. <tr>
  44. <td>
  45. Account
  46. </td>
  47. <td>
  48. <a href="/accounts/{{ user.account.id }}">{{ user.account.name }}</a>
  49. </td>
  50. </tr>
  51. <tr>
  52. <td>
  53. Assets in account
  54. </td>
  55. <td>
  56. <a href="/assets/owned_by/{{ user.account.id }}">{{ asset_count }}</a>
  57. </td>
  58. </tr>
  59. <tr>
  60. <td>
  61. Time Zone
  62. </td>
  63. <td>
  64. {{ user.timezone }}
  65. </td>
  66. </tr>
  67. <tr>
  68. <td>
  69. Last login was
  70. </td>
  71. <td title="{{  user.last_login_at | localized_datetime }}">
  72. {{  user.last_login_at | naturalized_datetime }}
  73. </td>
  74. </tr>
  75. <tr>
  76. <td>
  77. Last seen
  78. </td>
  79. <td title="{{  user.last_seen_at | localized_datetime }}">
  80. {{  user.last_seen_at | naturalized_datetime }}
  81. </td>
  82. </tr>
  83. <tr>
  84. <td>
  85. Roles
  86. </td>
  87. <td>
  88. {% for role in user.flexmeasures_roles %}
  89. {{ role.name }}{{ "," if not loop.last }}
  90. {% endfor %}
  91. </td>
  92. </tr>
  93. <tr>
  94. <td>
  95. Active
  96. </td>
  97. <td>
  98. {{ user.active }}
  99. </td>
  100. </tr>
  101. </tbody>
  102. </table>
  103. </div>
  104. </div>
  105. </div>
  106. <div class="col-md-2">
  107. {% if can_view_user_auditlog %}
  108. <button class="btn p-3 btn-info border-0 mb-3 mt-3" type="button"
  109. onclick="window.location.href='/users/auditlog/{{ user.id }}'" title="View history of user actions.">User audit
  110. log</button>
  111. {% endif %}
  112. </div>
  113. </div>
  114. </div>
  115. <!-- Edit User Modal -->
  116. <div class="modal fade modal-xl" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel"
  117. aria-hidden="true">
  118. <div class="modal-dialog">
  119. <div class="modal-content">
  120. <div class="modal-header">
  121. <h5 class="modal-title pe-2">Edit {{ user.username }}'s details</h5>
  122. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  123. </div>
  124. <div class="modal-body">
  125. <form id="editUserForm">
  126. <div class="mb-3">
  127. <label for="username" class="form-label">Username</label>
  128. <input type="text" class="form-control" id="username" name="username" value="{{ user.username }}" required>
  129. </div>
  130. <div class="mb-3">
  131. <label for="email" class="form-label">Email</label>
  132. <input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required>
  133. </div>
  134. <div class="mb-3">
  135. <label for="timezone" class="form-label">Timezone</label>
  136. <input type="text" class="form-control" id="timezone" name="timezone" value="{{ user.timezone }}" required>
  137. </div>
  138. <div class="mb-3">
  139. <label for="tagsInput" class="form-label">Roles</label>
  140. <div class="input-group">
  141. <select class="form-select" id="tagsInput" aria-label="Default select example">
  142. <option selected>Select Role</option>
  143. {% for role in roles %}
  144. <option value="{{ role }}">{{ role }}</option>
  145. {% endfor %}
  146. </select>
  147. <button class="btn btn-outline-secondary" type="button" id="addRoleBtn">Add</button>
  148. </div>
  149. <div id="rolesContainer" class="mt-2 d-flex flex-wrap gap-2">
  150. </div>
  151. <input type="hidden" name="roles" id="rolesHiddenInput">
  152. </div>
  153. <div class="mb-3 form-check">
  154. <input type="checkbox" class="form-check-input" id="active" name="active" {% if user.active %} checked {%
  155. endif %}>
  156. <label class="form-check-label" for="active">Active</label>
  157. </div>
  158. <button type="submit" class="btn btn-primary">Save changes</button>
  159. </form>
  160. </div>
  161. </div>
  162. </div>
  163. </div>
  164. {% endif %}
  165. <script>
  166. const editUserModal = document.getElementById('editUserModal');
  167. const editUserForm = document.getElementById('editUserForm');
  168. const userRoles = JSON.parse('{{ user_roles | tojson }}');
  169. const allRoles = JSON.parse('{{ roles | tojson }}');
  170. let currentRoles = new Set(); // Use a Set to store unique roles easily
  171. const user = {
  172. id: '{{ user.id }}',
  173. username: '{{ user.username }}',
  174. email: '{{ user.email }}',
  175. timezone: '{{ user.timezone }}',
  176. active: '{{ user.active | tojson }}',
  177. flexmeasures_roles: userRoles.map(role => allRoles[role]),
  178. };
  179. // Handle form submission
  180. editUserForm.addEventListener('submit', async function (event) {
  181. event.preventDefault(); // Prevent the default form submission
  182. const data = Object.fromEntries(new FormData(editUserForm).entries());
  183. delete data.roles;
  184. data.flexmeasures_roles = Array.from(currentRoles).map(role => allRoles[role]);
  185. data.active = `${data.active === 'on'}`;
  186. // remove unmodified fields
  187. const modifiedFields = Object.keys(data).reduce((acc, key) => {
  188. if (data[key] !== user[key]) {
  189. if (Array.isArray(data[key])) {
  190. // check if the arrays are equal
  191. if (JSON.stringify(data[key]) !== JSON.stringify(user[key])) {
  192. acc[key] = data[key];
  193. }
  194. } else {
  195. acc[key] = data[key];
  196. }
  197. }
  198. return acc;
  199. }, {});
  200. // Check if there are any modified fields
  201. if (Object.keys(modifiedFields).length === 0) {
  202. showToast('No changes made to the user details.', 'info');
  203. return;
  204. }
  205. const response = await fetch('/api/v3_0/users/{{ user.id }}', {
  206. method: 'PATCH',
  207. headers: {
  208. 'Content-Type': 'application/json'
  209. },
  210. body: JSON.stringify(modifiedFields)
  211. });
  212. if (response.ok) {
  213. showToast('User details updated successfully!', 'success');
  214. // Delay for 1 second and reload the page
  215. setTimeout(() => {
  216. window.location.reload();
  217. }, 1000);
  218. } else {
  219. const errorData = await response.json();
  220. let errorMessage;
  221. if (typeof errorData.message === 'object') {
  222. errorMessage = Object.entries(errorData.message.json).map(([key, value]) => value).join(', ');
  223. } else {
  224. errorMessage = errorData.message || 'An unknown error occurred.';
  225. }
  226. showToast('Error: ' + errorMessage, 'error');
  227. }
  228. });
  229. document.addEventListener('DOMContentLoaded', function () {
  230. const tagsInput = document.getElementById('tagsInput');
  231. const addRoleBtn = document.getElementById('addRoleBtn');
  232. const rolesContainer = document.getElementById('rolesContainer');
  233. const rolesHiddenInput = document.getElementById('rolesHiddenInput');
  234. const initialRoles = userRoles || [];
  235. function renderTags() {
  236. rolesContainer.innerHTML = ''; // Clear existing tags
  237. rolesHiddenInput.value = Array.from(currentRoles).join(','); // Update hidden input
  238. currentRoles.forEach(role => {
  239. const tagSpan = document.createElement('span');
  240. tagSpan.className = 'badge bg-primary-custom text-white d-flex align-items-center me-1';
  241. tagSpan.style.padding = '0.5em 0.75em';
  242. tagSpan.style.borderRadius = '0.25rem';
  243. const tagName = document.createElement('span');
  244. tagName.textContent = role;
  245. tagSpan.appendChild(tagName);
  246. const removeBtn = document.createElement('button');
  247. removeBtn.type = 'button';
  248. removeBtn.className = 'btn-close btn-close-white ms-2';
  249. removeBtn.setAttribute('aria-label', `Remove ${role}`);
  250. removeBtn.onclick = () => {
  251. currentRoles.delete(role);
  252. renderTags(); // Re-render to update the display
  253. };
  254. tagSpan.appendChild(removeBtn);
  255. rolesContainer.appendChild(tagSpan);
  256. });
  257. }
  258. function addRole() {
  259. const tagValue = tagsInput.value.trim();
  260. if (tagValue && !currentRoles.has(tagValue)) { // Check if not empty and not already added
  261. const isValidTag = true ? allRoles[tagValue] : false
  262. if (!isValidTag) {
  263. showToast('Invalid role name. Please choose a valid role.', 'error');
  264. tagsInput.value = '';
  265. return;
  266. }
  267. currentRoles.add(tagValue);
  268. tagsInput.value = '';
  269. renderTags();
  270. }
  271. }
  272. // Add tag on button click
  273. addRoleBtn.addEventListener('click', addRole);
  274. // Add tag on Enter key press in the input field
  275. tagsInput.addEventListener('keypress', function (event) {
  276. if (event.key === 'Enter') {
  277. event.preventDefault(); // Prevent form submission
  278. addRole();
  279. }
  280. });
  281. // Initialize with existing roles
  282. initialRoles.forEach(role => currentRoles.add(role));
  283. renderTags();
  284. });
  285. </script>
  286. {% endblock %}