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.
- goosebit/__init__.py +56 -6
- goosebit/api/telemetry/metrics.py +1 -5
- goosebit/api/v1/devices/device/responses.py +1 -0
- goosebit/api/v1/devices/device/routes.py +8 -8
- goosebit/api/v1/devices/requests.py +20 -0
- goosebit/api/v1/devices/routes.py +83 -8
- goosebit/api/v1/download/routes.py +14 -3
- goosebit/api/v1/rollouts/routes.py +5 -4
- goosebit/api/v1/routes.py +2 -1
- goosebit/api/v1/settings/routes.py +14 -0
- goosebit/api/v1/settings/users/__init__.py +1 -0
- goosebit/api/v1/settings/users/requests.py +16 -0
- goosebit/api/v1/settings/users/responses.py +7 -0
- goosebit/api/v1/settings/users/routes.py +56 -0
- goosebit/api/v1/software/routes.py +18 -14
- goosebit/auth/__init__.py +54 -14
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
- goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
- goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
- goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
- goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
- goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
- goosebit/db/models.py +22 -7
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +9 -6
- goosebit/schema/plugins.py +67 -0
- goosebit/schema/updates.py +15 -0
- goosebit/schema/users.py +9 -0
- goosebit/settings/__init__.py +0 -3
- goosebit/settings/schema.py +62 -14
- goosebit/storage/__init__.py +62 -0
- goosebit/storage/base.py +14 -0
- goosebit/storage/filesystem.py +111 -0
- goosebit/storage/s3.py +104 -0
- goosebit/ui/bff/common/columns.py +50 -0
- goosebit/ui/bff/common/requests.py +3 -15
- goosebit/ui/bff/common/responses.py +17 -0
- goosebit/ui/bff/devices/device/__init__.py +1 -0
- goosebit/ui/bff/devices/device/routes.py +17 -0
- goosebit/ui/bff/devices/requests.py +1 -0
- goosebit/ui/bff/devices/responses.py +6 -2
- goosebit/ui/bff/devices/routes.py +71 -17
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/responses.py +6 -2
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +6 -3
- goosebit/ui/bff/settings/__init__.py +1 -0
- goosebit/ui/bff/settings/routes.py +20 -0
- goosebit/ui/bff/settings/users/__init__.py +1 -0
- goosebit/ui/bff/settings/users/responses.py +33 -0
- goosebit/ui/bff/settings/users/routes.py +80 -0
- goosebit/ui/bff/software/responses.py +19 -9
- goosebit/ui/bff/software/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +70 -26
- goosebit/ui/static/js/devices.js +72 -80
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +39 -35
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -127
- goosebit/ui/static/js/util.js +45 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/devices.html.jinja +0 -20
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +26 -7
- goosebit/ui/templates/rollouts.html.jinja +4 -22
- goosebit/ui/templates/settings.html.jinja +88 -0
- goosebit/ui/templates/setup.html.jinja +71 -0
- goosebit/ui/templates/software.html.jinja +0 -11
- goosebit/updater/controller/v1/routes.py +120 -72
- goosebit/updater/routes.py +86 -7
- goosebit/updates/__init__.py +24 -31
- goosebit/updates/swdesc.py +15 -8
- goosebit/users/__init__.py +63 -0
- goosebit/util/__init__.py +0 -0
- goosebit/util/path.py +42 -0
- goosebit/util/version.py +92 -0
- goosebit-0.2.6.dist-info/METADATA +280 -0
- goosebit-0.2.6.dist-info/RECORD +133 -0
- {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/WHEEL +1 -1
- goosebit-0.2.6.dist-info/entry_points.txt +3 -0
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/ui/static/js/index.js +0 -155
- goosebit/ui/templates/index.html.jinja +0 -25
- goosebit/updater/manager.py +0 -357
- goosebit-0.2.4.dist-info/METADATA +0 -181
- goosebit-0.2.4.dist-info/RECORD +0 -98
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {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
|
-
|
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");
|