goosebit 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. goosebit/__init__.py +41 -7
  2. goosebit/api/telemetry/metrics.py +1 -5
  3. goosebit/api/v1/devices/device/responses.py +1 -0
  4. goosebit/api/v1/devices/device/routes.py +8 -8
  5. goosebit/api/v1/devices/requests.py +20 -0
  6. goosebit/api/v1/devices/routes.py +68 -8
  7. goosebit/api/v1/download/routes.py +14 -3
  8. goosebit/api/v1/rollouts/routes.py +5 -4
  9. goosebit/api/v1/routes.py +2 -1
  10. goosebit/api/v1/settings/routes.py +14 -0
  11. goosebit/api/v1/settings/users/__init__.py +1 -0
  12. goosebit/api/v1/settings/users/requests.py +16 -0
  13. goosebit/api/v1/settings/users/responses.py +7 -0
  14. goosebit/api/v1/settings/users/routes.py +56 -0
  15. goosebit/api/v1/software/routes.py +18 -14
  16. goosebit/auth/__init__.py +49 -13
  17. goosebit/auth/permissions.py +80 -0
  18. goosebit/db/config.py +57 -1
  19. goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
  20. goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
  21. goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
  22. goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
  23. goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
  24. goosebit/db/models.py +19 -8
  25. goosebit/db/pg_ssl_context.py +51 -0
  26. goosebit/device_manager.py +262 -0
  27. goosebit/plugins/__init__.py +32 -0
  28. goosebit/schema/devices.py +8 -5
  29. goosebit/schema/plugins.py +67 -0
  30. goosebit/schema/updates.py +15 -0
  31. goosebit/schema/users.py +9 -0
  32. goosebit/settings/__init__.py +0 -3
  33. goosebit/settings/schema.py +60 -14
  34. goosebit/storage/__init__.py +62 -0
  35. goosebit/storage/base.py +14 -0
  36. goosebit/storage/filesystem.py +111 -0
  37. goosebit/storage/s3.py +104 -0
  38. goosebit/ui/bff/common/columns.py +50 -0
  39. goosebit/ui/bff/common/responses.py +1 -0
  40. goosebit/ui/bff/devices/device/__init__.py +1 -0
  41. goosebit/ui/bff/devices/device/routes.py +17 -0
  42. goosebit/ui/bff/devices/requests.py +1 -0
  43. goosebit/ui/bff/devices/routes.py +49 -46
  44. goosebit/ui/bff/download/routes.py +14 -3
  45. goosebit/ui/bff/rollouts/routes.py +32 -4
  46. goosebit/ui/bff/routes.py +2 -1
  47. goosebit/ui/bff/settings/__init__.py +1 -0
  48. goosebit/ui/bff/settings/routes.py +20 -0
  49. goosebit/ui/bff/settings/users/__init__.py +1 -0
  50. goosebit/ui/bff/settings/users/responses.py +33 -0
  51. goosebit/ui/bff/settings/users/routes.py +80 -0
  52. goosebit/ui/bff/software/routes.py +40 -12
  53. goosebit/ui/nav.py +12 -2
  54. goosebit/ui/routes.py +66 -13
  55. goosebit/ui/static/js/devices.js +32 -24
  56. goosebit/ui/static/js/login.js +21 -5
  57. goosebit/ui/static/js/logs.js +7 -22
  58. goosebit/ui/static/js/rollouts.js +31 -30
  59. goosebit/ui/static/js/settings.js +322 -0
  60. goosebit/ui/static/js/setup.js +28 -0
  61. goosebit/ui/static/js/software.js +127 -121
  62. goosebit/ui/static/js/util.js +25 -4
  63. goosebit/ui/templates/__init__.py +10 -1
  64. goosebit/ui/templates/login.html.jinja +5 -0
  65. goosebit/ui/templates/nav.html.jinja +13 -5
  66. goosebit/ui/templates/rollouts.html.jinja +4 -22
  67. goosebit/ui/templates/settings.html.jinja +88 -0
  68. goosebit/ui/templates/setup.html.jinja +71 -0
  69. goosebit/ui/templates/software.html.jinja +0 -11
  70. goosebit/updater/controller/v1/routes.py +119 -77
  71. goosebit/updater/routes.py +83 -8
  72. goosebit/updates/__init__.py +24 -31
  73. goosebit/updates/swdesc.py +15 -8
  74. goosebit/users/__init__.py +63 -0
  75. goosebit/util/__init__.py +0 -0
  76. goosebit/util/path.py +42 -0
  77. goosebit/util/version.py +92 -0
  78. goosebit-0.2.7.dist-info/METADATA +280 -0
  79. goosebit-0.2.7.dist-info/RECORD +133 -0
  80. {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/WHEEL +1 -1
  81. goosebit/realtime/logs.py +0 -42
  82. goosebit/realtime/routes.py +0 -13
  83. goosebit/updater/manager.py +0 -325
  84. goosebit-0.2.5.dist-info/METADATA +0 -189
  85. goosebit-0.2.5.dist-info/RECORD +0 -99
  86. /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
  87. {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/LICENSE +0 -0
  88. {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,322 @@
1
+ let dataTable;
2
+
3
+ const renderFunctions = {
4
+ enabled: (data, type) => {
5
+ if (type === "display") {
6
+ const color = data ? "success" : "muted";
7
+ return `
8
+ <div class="text-${color}">
9
+
10
+ </div>
11
+ `;
12
+ }
13
+ return data;
14
+ },
15
+ permissions: (data, type) => {
16
+ return data.join(",");
17
+ },
18
+ };
19
+
20
+ document.addEventListener("DOMContentLoaded", async () => {
21
+ const columnConfig = await get_request("/ui/bff/settings/users/columns");
22
+ for (const col in columnConfig.columns) {
23
+ const colDesc = columnConfig.columns[col];
24
+ const colName = colDesc.data;
25
+ if (renderFunctions[colName]) {
26
+ columnConfig.columns[col].render = renderFunctions[colName];
27
+ }
28
+ }
29
+
30
+ dataTable = new DataTable("#users-table", {
31
+ responsive: true,
32
+ paging: true,
33
+ processing: false,
34
+ serverSide: true,
35
+ order: { name: "username", dir: "asc" },
36
+ scrollCollapse: true,
37
+ scroller: true,
38
+ scrollY: "65vh",
39
+ stateSave: true,
40
+ select: true,
41
+ rowId: "username",
42
+ ajax: {
43
+ url: "/ui/bff/settings/users",
44
+ data: (data) => {
45
+ // biome-ignore lint/performance/noDelete: really has to be deleted
46
+ delete data.columns;
47
+ },
48
+ contentType: "application/json",
49
+ },
50
+ initComplete: () => {
51
+ updateBtnState();
52
+ },
53
+ layout: {
54
+ top1Start: {
55
+ buttons: [],
56
+ },
57
+ bottom1Start: {
58
+ buttons: [
59
+ {
60
+ text: '<i class="bi bi-plus" ></i>',
61
+ action: async () => {
62
+ const permissionsSelection = document.getElementById("create-user-permissions");
63
+ permissionsSelection.innerHTML = await createPermissions();
64
+ new bootstrap.Modal("#create-user-modal").show();
65
+ },
66
+ className: "buttons-create-user",
67
+ titleAttr: "Add User",
68
+ },
69
+ {
70
+ text: '<i class="bi bi-play-fill" ></i>',
71
+ action: (e, dt) => {
72
+ const selectedUsers = dt
73
+ .rows({ selected: true })
74
+ .data()
75
+ .toArray()
76
+ .map((d) => d.username);
77
+ enableUsers(selectedUsers, true);
78
+ },
79
+ className: "buttons-enable-users",
80
+ titleAttr: "Enable Users",
81
+ },
82
+ {
83
+ text: '<i class="bi bi-pause-fill" ></i>',
84
+ action: (e, dt) => {
85
+ const selectedUsers = dt
86
+ .rows({ selected: true })
87
+ .data()
88
+ .toArray()
89
+ .map((d) => d.username);
90
+ enableUsers(selectedUsers, false);
91
+ },
92
+ className: "buttons-disable-users",
93
+ titleAttr: "Disable Users",
94
+ },
95
+ {
96
+ text: '<i class="bi bi-trash" ></i>',
97
+ action: async (e, dt) => {
98
+ const selectedUsers = dt
99
+ .rows({ selected: true })
100
+ .data()
101
+ .toArray()
102
+ .map((d) => d.username);
103
+ deleteUsers(selectedUsers);
104
+ },
105
+ className: "buttons-delete-users",
106
+ titleAttr: "Delete Users",
107
+ },
108
+ ],
109
+ },
110
+ },
111
+ columnDefs: [
112
+ {
113
+ targets: "_all",
114
+ searchable: false,
115
+ orderable: false,
116
+ render: (data) => data || "-",
117
+ },
118
+ ],
119
+ columns: columnConfig.columns,
120
+ });
121
+
122
+ dataTable
123
+ .on("select", () => {
124
+ updateBtnState();
125
+ })
126
+ .on("deselect", () => {
127
+ updateBtnState();
128
+ });
129
+
130
+ setInterval(() => {
131
+ updateUsersList();
132
+ }, TABLE_UPDATE_TIME);
133
+ const form = document.getElementById("create-user-form");
134
+ form.addEventListener("submit", (event) => {
135
+ const permissionsContainer = document.getElementById("create-user-permissions");
136
+ const permissions = [
137
+ ...permissionsContainer.querySelectorAll('input[type="checkbox"]:checked:not(:disabled)'),
138
+ ].map((checkbox) => checkbox.value);
139
+ const permissionsValidatorCheckbox = document.getElementById("create-user-permissions-validator");
140
+ permissionsValidatorCheckbox.checked = permissions.length > 0;
141
+
142
+ if (form.checkValidity() === false) {
143
+ if (permissions.length === 0) {
144
+ permissionsContainer.classList.add("is-invalid");
145
+ }
146
+ event.preventDefault();
147
+ event.stopPropagation();
148
+ form.classList.add("was-validated");
149
+ } else {
150
+ event.preventDefault();
151
+ createUser();
152
+ form.classList.remove("was-validated");
153
+ permissionsContainer.classList.remove("is-invalid");
154
+ form.reset();
155
+ const modal = bootstrap.Modal.getInstance(document.getElementById("create-user-modal"));
156
+ modal.hide();
157
+ }
158
+ });
159
+ });
160
+
161
+ function updateBtnState() {
162
+ if (dataTable.rows({ selected: true }).any()) {
163
+ document.querySelector("button.buttons-delete-users").classList.remove("disabled");
164
+ document.querySelector("button.buttons-disable-users").classList.remove("disabled");
165
+ document.querySelector("button.buttons-enable-users").classList.remove("disabled");
166
+ } else {
167
+ document.querySelector("button.buttons-delete-users").classList.add("disabled");
168
+ document.querySelector("button.buttons-disable-users").classList.add("disabled");
169
+ document.querySelector("button.buttons-enable-users").classList.add("disabled");
170
+ }
171
+ if (dataTable.rows({ selected: true }).count() === 1) {
172
+ } else {
173
+ }
174
+ }
175
+
176
+ function updateUsersList() {
177
+ const scrollPosition = $("#users-table").parent().scrollTop(); // Get current scroll position
178
+
179
+ const selectedRows = dataTable
180
+ .rows({ selected: true })
181
+ .data()
182
+ .toArray()
183
+ .map((d) => d.username);
184
+
185
+ dataTable.ajax.reload(() => {
186
+ dataTable.rows().every(function () {
187
+ const rowData = this.data();
188
+ if (selectedRows.includes(rowData.username)) {
189
+ this.select();
190
+ }
191
+ });
192
+ $("#users-table").parent().scrollTop(scrollPosition); // Restore scroll position after reload
193
+ }, false);
194
+ }
195
+
196
+ async function createPermissions() {
197
+ const permissions = await get_request("/ui/bff/settings/permissions");
198
+
199
+ innerAccordion = document.createElement("div");
200
+ innerAccordion.classList = "accordion-body p-0";
201
+
202
+ for (innerPermission in permissions.sub_permissions) {
203
+ dropdown = createPermissionDropdown(permissions.sub_permissions[innerPermission]);
204
+ innerAccordion.innerHTML += dropdown;
205
+ }
206
+
207
+ return `<div class="input-group d-flex">
208
+ <div class="input-group-text p-2 px-3">
209
+ <input class="form-check-input mt-0 ignore-validation" type="checkbox" value="${permissions.value}" id="${permissions.value}-checkbox" onchange="permissionCheckOnUpdate(this)">
210
+ </div>
211
+ <div class="d-flex flex-fill accordion rounded-start-0">
212
+ <div class="accordion-item w-100 rounded-start-0">
213
+ <div class="accordion-header w-100">
214
+ <button class="accordion-button collapsed py-2 rounded-start-0"
215
+ type="button"
216
+ data-bs-toggle="collapse"
217
+ data-bs-target="#${permissions.value}">
218
+ ${permissions.description}
219
+ </button>
220
+ </div>
221
+ <div id="${permissions.value}" class="accordion-collapse collapse">
222
+ ${innerAccordion.outerHTML}
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>`;
227
+ }
228
+
229
+ function createPermissionDropdown(permission) {
230
+ if (!permission.sub_permissions) {
231
+ return `<div class="input-group d-flex border-top">
232
+ <div class="input-group-text p-2 px-3 rounded-0 border-0">
233
+ <input class="form-check-input mt-0 ignore-validation" type="checkbox" value="${permission.value}" data-permission-parent="${permission.parent}">
234
+ </div>
235
+ <div class="d-flex flex-fill my-auto py-2 p-3 border-start">
236
+ ${permission.description}
237
+ </div>
238
+ </div>`;
239
+ }
240
+
241
+ subAccordion = document.createElement("div");
242
+ subAccordion.classList = "accordion-body p-0";
243
+
244
+ for (innerPermission in permission.sub_permissions) {
245
+ dropdown = createPermissionDropdown(permission.sub_permissions[innerPermission]);
246
+ subAccordion.innerHTML += dropdown;
247
+ }
248
+ permissionId = permission.value.replaceAll(".", "-");
249
+
250
+ return `<div class="input-group d-flex border-top">
251
+ <div class="input-group-text p-2 px-3 rounded-0 border-0 border-start">
252
+ <input class="form-check-input mt-0" type="checkbox" value="${permission.value}" id="${permissionId}-checkbox" data-permission-parent="${permission.parent}" onchange="permissionCheckOnUpdate(this)">
253
+ </div>
254
+ <div class="d-flex flex-fill accordion accordion-flush border-start">
255
+ <div class="accordion-item w-100 rounded-start-0">
256
+ <div class="accordion-header w-100">
257
+ <button class="accordion-button py-2 collapsed rounded-start-0"
258
+ type="button"
259
+ data-bs-toggle="collapse"
260
+ data-bs-target="#${permissionId}">
261
+ ${permission.description}
262
+ </button>
263
+ </div>
264
+ <div id="${permissionId}" class="accordion-collapse collapse">
265
+ ${subAccordion.outerHTML}
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>`;
270
+ }
271
+
272
+ function permissionCheckOnUpdate(checkbox) {
273
+ const childPermissions = document.querySelectorAll(`input[data-permission-parent="${checkbox.value}"`);
274
+ for (const permissionCheckbox of childPermissions) {
275
+ permissionCheckbox.checked = checkbox.checked;
276
+ permissionCheckbox.disabled = checkbox.checked;
277
+ permissionCheckbox.dispatchEvent(new Event("change"));
278
+ }
279
+ }
280
+
281
+ async function createUser() {
282
+ const username = document.getElementById("create-user-username").value;
283
+ const password = document.getElementById("create-user-password").value;
284
+
285
+ const permissionsContainer = document.getElementById("create-user-permissions");
286
+ const permissions = [...permissionsContainer.querySelectorAll('input[type="checkbox"]:checked:not(:disabled)')].map(
287
+ (checkbox) => checkbox.value,
288
+ );
289
+
290
+ try {
291
+ await post_request("/ui/bff/settings/users", {
292
+ username: username,
293
+ password: password,
294
+ permissions: permissions,
295
+ });
296
+ } catch (error) {
297
+ console.error("User creation failed:", error);
298
+ }
299
+
300
+ setTimeout(updateUsersList, 50);
301
+ }
302
+
303
+ async function deleteUsers(usernames) {
304
+ try {
305
+ await delete_request("/ui/bff/settings/users", { usernames });
306
+ } catch (error) {
307
+ console.error("Users deletion failed:", error);
308
+ }
309
+
310
+ updateBtnState();
311
+ setTimeout(updateUsersList, 50);
312
+ }
313
+
314
+ async function enableUsers(usernames, enabled) {
315
+ try {
316
+ await patch_request("/ui/bff/settings/users", { usernames, enabled });
317
+ } catch (error) {
318
+ console.error(`Users ${enabled ? "enabling" : "disabling"} failed:`, error);
319
+ }
320
+
321
+ setTimeout(updateUsersList, 50);
322
+ }
@@ -0,0 +1,28 @@
1
+ setupForm = document.getElementById("setup_form");
2
+
3
+ async function setup() {
4
+ const formData = new FormData(setupForm);
5
+
6
+ if (!(formData.password === formData.password_confirm)) {
7
+ console.error("Passwords dont match");
8
+ return;
9
+ }
10
+
11
+ try {
12
+ const response = await fetch("/setup", {
13
+ method: "POST",
14
+ body: formData,
15
+ });
16
+ tokenData = await response.json();
17
+ document.cookie = `session_id=${tokenData.access_token}; path=/`;
18
+ window.location.assign("/");
19
+ } catch (e) {
20
+ // handle form errors later
21
+ console.error(e);
22
+ }
23
+ }
24
+
25
+ setupForm.addEventListener("submit", (event) => {
26
+ event.preventDefault();
27
+ setup();
28
+ });
@@ -6,6 +6,128 @@ const uploadProgressBar = document.getElementById("upload-progress");
6
6
 
7
7
  let dataTable;
8
8
 
9
+ const renderFunctions = {
10
+ compatibility: (data, type) => {
11
+ const result = data.reduce((acc, { model, revision }) => {
12
+ if (!acc[model]) {
13
+ acc[model] = [];
14
+ }
15
+ acc[model].push(revision);
16
+ return acc;
17
+ }, {});
18
+
19
+ return Object.entries(result)
20
+ .map(([model, revision]) => `${model} - ${revision.join(", ")}`)
21
+ .join("\n");
22
+ },
23
+ size: (data, type) => {
24
+ if (type === "display" || type === "filter") {
25
+ return `${(data / 1024 / 1024).toFixed(2)}MB`;
26
+ }
27
+ return data;
28
+ },
29
+ };
30
+
31
+ document.addEventListener("DOMContentLoaded", async () => {
32
+ const columnConfig = await get_request("/ui/bff/software/columns");
33
+ for (const col in columnConfig.columns) {
34
+ const colDesc = columnConfig.columns[col];
35
+ const colName = colDesc.data;
36
+ if (renderFunctions[colName]) {
37
+ columnConfig.columns[col].render = renderFunctions[colName];
38
+ }
39
+ }
40
+
41
+ const buttons = [
42
+ {
43
+ text: '<i class="bi bi-cloud-download" ></i>',
44
+ action: (e, dt) => {
45
+ const selectedSoftware = dt
46
+ .rows({ selected: true })
47
+ .data()
48
+ .toArray()
49
+ .map((d) => d.id);
50
+ downloadSoftware(selectedSoftware[0]);
51
+ },
52
+ className: "buttons-download",
53
+ titleAttr: "Download Software",
54
+ },
55
+ {
56
+ text: '<i class="bi bi-trash" ></i>',
57
+ action: async (e, dt) => {
58
+ const selectedSoftware = dt
59
+ .rows({ selected: true })
60
+ .data()
61
+ .toArray()
62
+ .map((d) => d.id);
63
+ await deleteSoftware(selectedSoftware);
64
+ },
65
+ className: "buttons-delete",
66
+ titleAttr: "Delete Software",
67
+ },
68
+ ];
69
+
70
+ // add create button at the beginning if upload modal exists
71
+ if ($("#upload-modal").length > 0) {
72
+ buttons.unshift({
73
+ text: '<i class="bi bi-plus" ></i>',
74
+ action: () => {
75
+ new bootstrap.Modal("#upload-modal").show();
76
+ },
77
+ className: "buttons-create",
78
+ titleAttr: "Add Software",
79
+ });
80
+ }
81
+
82
+ dataTable = new DataTable("#software-table", {
83
+ responsive: true,
84
+ paging: true,
85
+ processing: false,
86
+ serverSide: true,
87
+ scrollCollapse: true,
88
+ scroller: true,
89
+ scrollY: "60vh",
90
+ stateSave: true,
91
+ ajax: {
92
+ url: "/ui/bff/software",
93
+ data: (data) => {
94
+ // biome-ignore lint/performance/noDelete: really has to be deleted
95
+ delete data.columns;
96
+ },
97
+ contentType: "application/json",
98
+ },
99
+ initComplete: () => {
100
+ updateBtnState();
101
+ },
102
+ columnDefs: [
103
+ {
104
+ targets: "_all",
105
+ searchable: false,
106
+ orderable: false,
107
+ render: (data) => data || "-",
108
+ },
109
+ ],
110
+ columns: columnConfig.columns,
111
+ select: true,
112
+ rowId: "id",
113
+ layout: {
114
+ bottom1Start: {
115
+ buttons,
116
+ },
117
+ },
118
+ });
119
+
120
+ dataTable
121
+ .on("select", () => {
122
+ updateBtnState();
123
+ })
124
+ .on("deselect", () => {
125
+ updateBtnState();
126
+ });
127
+
128
+ updateSoftwareList();
129
+ });
130
+
9
131
  uploadForm.addEventListener("submit", async (e) => {
10
132
  e.preventDefault();
11
133
  await sendFileChunks(uploadFileInput.files[0]);
@@ -109,7 +231,11 @@ async function sendFileUrl(url) {
109
231
  }
110
232
 
111
233
  function updateSoftwareList() {
112
- dataTable.ajax.reload(null, false);
234
+ const scrollPosition = $("#software-table").parent().scrollTop(); // Get current scroll position
235
+
236
+ dataTable.ajax.reload(() => {
237
+ $("#software-table").parent().scrollTop(scrollPosition); // Restore scroll position after reload
238
+ }, false);
113
239
  }
114
240
 
115
241
  function resetProgress() {
@@ -125,126 +251,6 @@ function resetProgress() {
125
251
  updateSoftwareList();
126
252
  }
127
253
 
128
- document.addEventListener("DOMContentLoaded", () => {
129
- const buttons = [
130
- {
131
- text: '<i class="bi bi-cloud-download" ></i>',
132
- action: (e, dt) => {
133
- const selectedSoftware = dt
134
- .rows({ selected: true })
135
- .data()
136
- .toArray()
137
- .map((d) => d.id);
138
- downloadSoftware(selectedSoftware[0]);
139
- },
140
- className: "buttons-download",
141
- titleAttr: "Download Software",
142
- },
143
- {
144
- text: '<i class="bi bi-trash" ></i>',
145
- action: async (e, dt) => {
146
- const selectedSoftware = dt
147
- .rows({ selected: true })
148
- .data()
149
- .toArray()
150
- .map((d) => d.id);
151
- await deleteSoftware(selectedSoftware);
152
- },
153
- className: "buttons-delete",
154
- titleAttr: "Delete Software",
155
- },
156
- ];
157
-
158
- // add create button at the beginning if upload modal exists
159
- if ($("#upload-modal").length > 0) {
160
- buttons.unshift({
161
- text: '<i class="bi bi-plus" ></i>',
162
- action: () => {
163
- new bootstrap.Modal("#upload-modal").show();
164
- },
165
- className: "buttons-create",
166
- titleAttr: "Add Software",
167
- });
168
- }
169
-
170
- dataTable = new DataTable("#software-table", {
171
- responsive: true,
172
- paging: true,
173
- processing: false,
174
- serverSide: true,
175
- scrollCollapse: true,
176
- scroller: true,
177
- scrollY: "60vh",
178
- stateSave: true,
179
- ajax: {
180
- url: "/ui/bff/software",
181
- data: (data) => {
182
- // biome-ignore lint/performance/noDelete: really has to be deleted
183
- delete data.columns;
184
- },
185
- contentType: "application/json",
186
- },
187
- initComplete: () => {
188
- updateBtnState();
189
- },
190
- columnDefs: [
191
- {
192
- targets: "_all",
193
- searchable: false,
194
- orderable: false,
195
- render: (data) => data || "-",
196
- },
197
- ],
198
- columns: [
199
- { data: "id", visible: false },
200
- { data: "name" },
201
- { data: "version", name: "version", searchable: true, orderable: true },
202
- {
203
- data: "compatibility",
204
- render: (data) => {
205
- const result = data.reduce((acc, { model, revision }) => {
206
- if (!acc[model]) {
207
- acc[model] = [];
208
- }
209
- acc[model].push(revision);
210
- return acc;
211
- }, {});
212
-
213
- return Object.entries(result)
214
- .map(([model, revision]) => `${model} - ${revision.join(", ")}`)
215
- .join("\n");
216
- },
217
- },
218
- {
219
- data: "size",
220
- render: (data, type) => {
221
- if (type === "display" || type === "filter") {
222
- return `${(data / 1024 / 1024).toFixed(2)}MB`;
223
- }
224
- return data;
225
- },
226
- },
227
- ],
228
- select: true,
229
- rowId: "id",
230
- layout: {
231
- bottom1Start: {
232
- buttons,
233
- },
234
- },
235
- });
236
-
237
- dataTable
238
- .on("select", () => {
239
- updateBtnState();
240
- })
241
- .on("deselect", () => {
242
- updateBtnState();
243
- });
244
-
245
- updateSoftwareList();
246
- });
247
-
248
254
  function updateBtnState() {
249
255
  if (dataTable.rows({ selected: true }).any()) {
250
256
  document.querySelector("button.buttons-delete").classList.remove("disabled");
@@ -25,7 +25,7 @@ async function updateSoftwareSelection(devices = null) {
25
25
  const url = new URL("/ui/bff/software?order[0][dir]=desc&order[0][name]=version", window.location.origin);
26
26
  if (devices != null) {
27
27
  for (const device of devices) {
28
- url.searchParams.append("uuids", device.uuid);
28
+ url.searchParams.append("ids", device.id);
29
29
  }
30
30
  }
31
31
  const response = await fetch(url.toString());
@@ -80,7 +80,7 @@ async function get_request(url) {
80
80
  });
81
81
  }
82
82
 
83
- throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
83
+ throw new Error(`GET ${url} failed for ${JSON.stringify(object)}`);
84
84
  }
85
85
  return result;
86
86
  }
@@ -105,6 +105,27 @@ async function post_request(url, object) {
105
105
  throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
106
106
  }
107
107
  }
108
+ async function put_request(url, object) {
109
+ const response = await fetch(url, {
110
+ method: "PUT",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify(object),
113
+ });
114
+
115
+ if (!response.ok) {
116
+ const result = await response.json();
117
+ if (result.detail) {
118
+ Swal.fire({
119
+ title: "Warning",
120
+ text: result.detail,
121
+ icon: "warning",
122
+ confirmButtonText: "Understood",
123
+ });
124
+ }
125
+
126
+ throw new Error(`PUT ${url} failed for ${JSON.stringify(object)}`);
127
+ }
128
+ }
108
129
  async function patch_request(url, object) {
109
130
  const response = await fetch(url, {
110
131
  method: "PATCH",
@@ -123,7 +144,7 @@ async function patch_request(url, object) {
123
144
  });
124
145
  }
125
146
 
126
- throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
147
+ throw new Error(`PATCH ${url} failed for ${JSON.stringify(object)}`);
127
148
  }
128
149
  }
129
150
  async function delete_request(url, object) {
@@ -144,6 +165,6 @@ async function delete_request(url, object) {
144
165
  });
145
166
  }
146
167
 
147
- throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
168
+ throw new Error(`DELETE ${url} failed for ${JSON.stringify(object)}`);
148
169
  }
149
170
  }
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
+ import jinja2
3
4
  from fastapi.requests import Request
4
5
  from fastapi.templating import Jinja2Templates
5
6
 
@@ -10,4 +11,12 @@ def attach_permissions_comparison(_: Request):
10
11
  return {"compare_permissions": check_permissions}
11
12
 
12
13
 
13
- templates = Jinja2Templates(str(Path(__file__).resolve().parent), context_processors=[attach_permissions_comparison])
14
+ env = jinja2.Environment(loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(str(Path(__file__).resolve().parent))]))
15
+ templates = Jinja2Templates(context_processors=[attach_permissions_comparison], env=env)
16
+
17
+
18
+ def add_template_handler(handler: Jinja2Templates):
19
+ templates.env.loader.loaders.append(handler.env.loader)
20
+
21
+
22
+ templates.add_template_handler = add_template_handler