goosebit 0.2.4__py3-none-any.whl → 0.2.6__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 (96) hide show
  1. goosebit/__init__.py +56 -6
  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 +83 -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 +54 -14
  17. goosebit/auth/permissions.py +80 -0
  18. goosebit/db/config.py +57 -1
  19. goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
  20. goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
  21. goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
  22. goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
  23. goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
  24. goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
  25. goosebit/db/models.py +22 -7
  26. goosebit/db/pg_ssl_context.py +51 -0
  27. goosebit/device_manager.py +262 -0
  28. goosebit/plugins/__init__.py +32 -0
  29. goosebit/schema/devices.py +9 -6
  30. goosebit/schema/plugins.py +67 -0
  31. goosebit/schema/updates.py +15 -0
  32. goosebit/schema/users.py +9 -0
  33. goosebit/settings/__init__.py +0 -3
  34. goosebit/settings/schema.py +62 -14
  35. goosebit/storage/__init__.py +62 -0
  36. goosebit/storage/base.py +14 -0
  37. goosebit/storage/filesystem.py +111 -0
  38. goosebit/storage/s3.py +104 -0
  39. goosebit/ui/bff/common/columns.py +50 -0
  40. goosebit/ui/bff/common/requests.py +3 -15
  41. goosebit/ui/bff/common/responses.py +17 -0
  42. goosebit/ui/bff/devices/device/__init__.py +1 -0
  43. goosebit/ui/bff/devices/device/routes.py +17 -0
  44. goosebit/ui/bff/devices/requests.py +1 -0
  45. goosebit/ui/bff/devices/responses.py +6 -2
  46. goosebit/ui/bff/devices/routes.py +71 -17
  47. goosebit/ui/bff/download/routes.py +14 -3
  48. goosebit/ui/bff/rollouts/responses.py +6 -2
  49. goosebit/ui/bff/rollouts/routes.py +32 -4
  50. goosebit/ui/bff/routes.py +6 -3
  51. goosebit/ui/bff/settings/__init__.py +1 -0
  52. goosebit/ui/bff/settings/routes.py +20 -0
  53. goosebit/ui/bff/settings/users/__init__.py +1 -0
  54. goosebit/ui/bff/settings/users/responses.py +33 -0
  55. goosebit/ui/bff/settings/users/routes.py +80 -0
  56. goosebit/ui/bff/software/responses.py +19 -9
  57. goosebit/ui/bff/software/routes.py +40 -12
  58. goosebit/ui/nav.py +12 -2
  59. goosebit/ui/routes.py +70 -26
  60. goosebit/ui/static/js/devices.js +72 -80
  61. goosebit/ui/static/js/login.js +21 -5
  62. goosebit/ui/static/js/logs.js +7 -22
  63. goosebit/ui/static/js/rollouts.js +39 -35
  64. goosebit/ui/static/js/settings.js +322 -0
  65. goosebit/ui/static/js/setup.js +28 -0
  66. goosebit/ui/static/js/software.js +127 -127
  67. goosebit/ui/static/js/util.js +45 -4
  68. goosebit/ui/templates/__init__.py +10 -1
  69. goosebit/ui/templates/devices.html.jinja +0 -20
  70. goosebit/ui/templates/login.html.jinja +5 -0
  71. goosebit/ui/templates/nav.html.jinja +26 -7
  72. goosebit/ui/templates/rollouts.html.jinja +4 -22
  73. goosebit/ui/templates/settings.html.jinja +88 -0
  74. goosebit/ui/templates/setup.html.jinja +71 -0
  75. goosebit/ui/templates/software.html.jinja +0 -11
  76. goosebit/updater/controller/v1/routes.py +120 -72
  77. goosebit/updater/routes.py +86 -7
  78. goosebit/updates/__init__.py +24 -31
  79. goosebit/updates/swdesc.py +15 -8
  80. goosebit/users/__init__.py +63 -0
  81. goosebit/util/__init__.py +0 -0
  82. goosebit/util/path.py +42 -0
  83. goosebit/util/version.py +92 -0
  84. goosebit-0.2.6.dist-info/METADATA +280 -0
  85. goosebit-0.2.6.dist-info/RECORD +133 -0
  86. {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/WHEEL +1 -1
  87. goosebit-0.2.6.dist-info/entry_points.txt +3 -0
  88. goosebit/realtime/logs.py +0 -42
  89. goosebit/realtime/routes.py +0 -13
  90. goosebit/ui/static/js/index.js +0 -155
  91. goosebit/ui/templates/index.html.jinja +0 -25
  92. goosebit/updater/manager.py +0 -357
  93. goosebit-0.2.4.dist-info/METADATA +0 -181
  94. goosebit-0.2.4.dist-info/RECORD +0 -98
  95. /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
  96. {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/LICENSE +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,132 +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
- order: [2, "desc"],
176
- scrollCollapse: true,
177
- scroller: true,
178
- scrollY: "60vh",
179
- stateSave: true,
180
- stateLoadParams: (settings, data) => {
181
- // if save state is older than last breaking code change...
182
- if (data.time <= 1722415428000) {
183
- // ... delete it
184
- for (const key of Object.keys(data)) {
185
- delete data[key];
186
- }
187
- }
188
- },
189
- ajax: {
190
- url: "/ui/bff/software",
191
- contentType: "application/json",
192
- },
193
- initComplete: () => {
194
- updateBtnState();
195
- },
196
- columnDefs: [
197
- {
198
- targets: "_all",
199
- searchable: false,
200
- orderable: false,
201
- render: (data) => data || "-",
202
- },
203
- ],
204
- columns: [
205
- { data: "id", visible: false },
206
- { data: "name" },
207
- { data: "version", searchable: true, orderable: true },
208
- {
209
- data: "compatibility",
210
- render: (data) => {
211
- const result = data.reduce((acc, { model, revision }) => {
212
- if (!acc[model]) {
213
- acc[model] = [];
214
- }
215
- acc[model].push(revision);
216
- return acc;
217
- }, {});
218
-
219
- return Object.entries(result)
220
- .map(([model, revision]) => `${model} - ${revision.join(", ")}`)
221
- .join("\n");
222
- },
223
- },
224
- {
225
- data: "size",
226
- render: (data, type) => {
227
- if (type === "display" || type === "filter") {
228
- return `${(data / 1024 / 1024).toFixed(2)}MB`;
229
- }
230
- return data;
231
- },
232
- },
233
- ],
234
- select: true,
235
- rowId: "id",
236
- layout: {
237
- bottom1Start: {
238
- buttons,
239
- },
240
- },
241
- });
242
-
243
- dataTable
244
- .on("select", () => {
245
- updateBtnState();
246
- })
247
- .on("deselect", () => {
248
- updateBtnState();
249
- });
250
-
251
- updateSoftwareList();
252
- });
253
-
254
254
  function updateBtnState() {
255
255
  if (dataTable.rows({ selected: true }).any()) {
256
256
  document.querySelector("button.buttons-delete").classList.remove("disabled");