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.
- goosebit/__init__.py +41 -7
- 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 +68 -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 +49 -13
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- 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 +19 -8
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +8 -5
- 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 +60 -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/responses.py +1 -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/routes.py +49 -46
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +2 -1
- 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/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +66 -13
- goosebit/ui/static/js/devices.js +32 -24
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +31 -30
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -121
- goosebit/ui/static/js/util.js +25 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +13 -5
- 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 +119 -77
- goosebit/updater/routes.py +83 -8
- 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.7.dist-info/METADATA +280 -0
- goosebit-0.2.7.dist-info/RECORD +133 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/WHEEL +1 -1
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/updater/manager.py +0 -325
- goosebit-0.2.5.dist-info/METADATA +0 -189
- goosebit-0.2.5.dist-info/RECORD +0 -99
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/LICENSE +0 -0
- {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
|
-
|
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");
|
goosebit/ui/static/js/util.js
CHANGED
@@ -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("
|
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(`
|
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(`
|
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(`
|
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
|
-
|
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
|