diff --git a/goosebit/ui/bff/common/columns.py b/goosebit/ui/bff/common/columns.py
new file mode 100644
index 00000000..64f30651
--- /dev/null
+++ b/goosebit/ui/bff/common/columns.py
@@ -0,0 +1,44 @@
+from .responses import DTColumnDescription
+
+
+class DeviceColumns:
+ uuid = DTColumnDescription(title="UUID", data="uuid", name="uuid", searchable=True, orderable=True)
+ name = DTColumnDescription(title="Name", data="name", name="name", searchable=True, orderable=True)
+ hw_model = DTColumnDescription(title="Model", data="hw_model")
+ hw_revision = DTColumnDescription(title="Revision", data="hw_revision")
+ feed = DTColumnDescription(title="Feed", data="feed", name="feed", searchable=True, orderable=True)
+ sw_version = DTColumnDescription(
+ title="Installed Software", data="sw_version", name="sw_version", searchable=True, orderable=True
+ )
+ sw_target_version = DTColumnDescription(title="Target Software", data="sw_target_version")
+ update_mode = DTColumnDescription(
+ title="Update Mode", data="update_mode", name="update_mode", searchable=True, orderable=True
+ )
+ last_state = DTColumnDescription(
+ title="State", data="last_state", name="last_state", searchable=True, orderable=True
+ )
+ force_update = DTColumnDescription(title="Force Update", data="force_update")
+ progress = DTColumnDescription(title="Progress", data="progress")
+ last_ip = DTColumnDescription(title="Last IP", data="last_ip")
+ polling = DTColumnDescription(title="Polling", data="polling")
+ last_seen = DTColumnDescription(title="Last Seen", data="last_seen")
+
+
+class RolloutColumns:
+ id = DTColumnDescription(title="ID", data="id", visible=False)
+ created_at = DTColumnDescription(title="Created", data="created_at", name="created_at", orderable=True)
+ name = DTColumnDescription(title="Name", data="name", name="name", searchable=True, orderable=True)
+ feed = DTColumnDescription(title="Feed", data="feed", name="feed", searchable=True, orderable=True)
+ sw_file = DTColumnDescription(title="Software File", data="sw_file", name="sw_file")
+ sw_version = DTColumnDescription(title="Software Version", data="sw_version", name="sw_version")
+ paused = DTColumnDescription(title="Paused", name="paused", data="paused")
+ success_count = DTColumnDescription(title="Success Count", data="success_count", name="success_count")
+ failure_count = DTColumnDescription(title="Failure Count", data="failure_count", name="failure_count")
+
+
+class SoftwareColumns:
+ id = DTColumnDescription(title="ID", data="id", visible=False)
+ name = DTColumnDescription(title="Name", data="name", name="name")
+ version = DTColumnDescription(title="Version", data="version", name="version", searchable=True, orderable=True)
+ compatibility = DTColumnDescription(title="Compatibility", name="compatibility", data="compatibility")
+ size = DTColumnDescription(title="Size", name="size", data="size")
diff --git a/goosebit/ui/bff/common/responses.py b/goosebit/ui/bff/common/responses.py
index 20dddb83..ded96c67 100644
--- a/goosebit/ui/bff/common/responses.py
+++ b/goosebit/ui/bff/common/responses.py
@@ -10,6 +10,7 @@ class DTColumnDescription(BaseModel):
searchable: bool | None = None
orderable: bool | None = None
+ visible: bool | None = None
class DTColumns(BaseModel):
diff --git a/goosebit/ui/bff/devices/routes.py b/goosebit/ui/bff/devices/routes.py
index b802487e..9156a616 100644
--- a/goosebit/ui/bff/devices/routes.py
+++ b/goosebit/ui/bff/devices/routes.py
@@ -14,11 +14,11 @@
from goosebit.device_manager import DeviceManager, get_device
from goosebit.schema.devices import DeviceSchema
from goosebit.schema.software import SoftwareSchema
-from goosebit.settings import config
from goosebit.ui.bff.common.requests import DataTableRequest
from goosebit.ui.bff.common.util import parse_datatables_query
-from ..common.responses import DTColumnDescription, DTColumns
+from ..common.columns import DeviceColumns
+from ..common.responses import DTColumns
from . import device
from .requests import DevicesPatchRequest
from .responses import BFFDeviceResponse
@@ -102,31 +102,27 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon
dependencies=[Security(validate_user_permissions, scopes=["device.read"])],
response_model_exclude_none=True,
)
-async def devices_get_columns() -> DTColumns:
- columns = []
- columns.append(DTColumnDescription(title="UUID", data="uuid", name="uuid", searchable=True, orderable=True))
- columns.append(DTColumnDescription(title="Name", data="name", name="name", searchable=True, orderable=True))
- columns.append(DTColumnDescription(title="Model", data="hw_model"))
- columns.append(DTColumnDescription(title="Revision", data="hw_revision"))
- columns.append(DTColumnDescription(title="Feed", data="feed", name="feed", searchable=True, orderable=True))
- columns.append(
- DTColumnDescription(
- title="Installed Software", data="sw_version", name="sw_version", searchable=True, orderable=True
+async def devices_get_columns(request: Request) -> DTColumns:
+ config = request.scope["config"]
+ columns = list(
+ filter(
+ None,
+ [
+ DeviceColumns.uuid,
+ DeviceColumns.name,
+ DeviceColumns.hw_model,
+ DeviceColumns.hw_revision,
+ DeviceColumns.feed,
+ DeviceColumns.sw_version,
+ DeviceColumns.sw_target_version,
+ DeviceColumns.update_mode,
+ DeviceColumns.last_state,
+ DeviceColumns.force_update,
+ DeviceColumns.progress,
+ DeviceColumns.last_ip if config.track_device_ip else None,
+ DeviceColumns.polling,
+ DeviceColumns.last_seen,
+ ],
)
)
- columns.append(DTColumnDescription(title="Target Software", data="sw_target_version"))
- columns.append(
- DTColumnDescription(
- title="Update Mode", data="update_mode", name="update_mode", searchable=True, orderable=True
- )
- )
- columns.append(
- DTColumnDescription(title="State", data="last_state", name="last_state", searchable=True, orderable=True)
- )
- columns.append(DTColumnDescription(title="Force Update", data="force_update"))
- columns.append(DTColumnDescription(title="Progress", data="progress"))
- if config.track_device_ip:
- columns.append(DTColumnDescription(title="Last IP", data="last_ip"))
- columns.append(DTColumnDescription(title="Polling", data="polling"))
- columns.append(DTColumnDescription(title="Last Seen", data="last_seen"))
return DTColumns(columns=columns)
diff --git a/goosebit/ui/bff/rollouts/routes.py b/goosebit/ui/bff/rollouts/routes.py
index aef101f3..11ca352a 100644
--- a/goosebit/ui/bff/rollouts/routes.py
+++ b/goosebit/ui/bff/rollouts/routes.py
@@ -9,6 +9,8 @@
from goosebit.ui.bff.common.requests import DataTableRequest
from goosebit.ui.bff.common.util import parse_datatables_query
+from ..common.columns import RolloutColumns
+from ..common.responses import DTColumns
from .responses import BFFRolloutsResponse
router = APIRouter(prefix="/rollouts")
@@ -52,3 +54,28 @@ def search_filter(search_value):
dependencies=[Security(validate_user_permissions, scopes=["rollout.delete"])],
name="bff_rollouts_delete",
)
+
+
+@router.get(
+ "/columns",
+ dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
+ response_model_exclude_none=True,
+)
+async def devices_get_columns() -> DTColumns:
+ columns = list(
+ filter(
+ None,
+ [
+ RolloutColumns.id,
+ RolloutColumns.created_at,
+ RolloutColumns.name,
+ RolloutColumns.feed,
+ RolloutColumns.sw_file,
+ RolloutColumns.sw_version,
+ RolloutColumns.paused,
+ RolloutColumns.success_count,
+ RolloutColumns.failure_count,
+ ],
+ )
+ )
+ return DTColumns(columns=columns)
diff --git a/goosebit/ui/bff/software/routes.py b/goosebit/ui/bff/software/routes.py
index 1edb6d7f..e38227f9 100644
--- a/goosebit/ui/bff/software/routes.py
+++ b/goosebit/ui/bff/software/routes.py
@@ -15,6 +15,8 @@
from goosebit.ui.bff.common.util import parse_datatables_query
from goosebit.updates import create_software_update
+from ..common.columns import SoftwareColumns
+from ..common.responses import DTColumns
from .responses import BFFSoftwareResponse
router = APIRouter(prefix="/software")
@@ -94,3 +96,24 @@ async def post_update(
await create_software_update(absolute.as_uri(), temp_file)
finally:
await temp_file.unlink(missing_ok=True)
+
+
+@router.get(
+ "/columns",
+ dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
+ response_model_exclude_none=True,
+)
+async def devices_get_columns() -> DTColumns:
+ columns = list(
+ filter(
+ None,
+ [
+ SoftwareColumns.id,
+ SoftwareColumns.name,
+ SoftwareColumns.version,
+ SoftwareColumns.compatibility,
+ SoftwareColumns.size,
+ ],
+ )
+ )
+ return DTColumns(columns=columns)
diff --git a/goosebit/ui/static/js/rollouts.js b/goosebit/ui/static/js/rollouts.js
index 6f095359..324dc4f2 100644
--- a/goosebit/ui/static/js/rollouts.js
+++ b/goosebit/ui/static/js/rollouts.js
@@ -1,6 +1,30 @@
let dataTable;
+const renderFunctions = {
+ paused: (data, type) => {
+ if (type === "display") {
+ const color = data ? "danger" : "muted";
+ return `
+
+ ●
+
+ `;
+ }
+ return data;
+ },
+ created_at: (data, type) => new Date(data).toLocaleString(),
+};
+
document.addEventListener("DOMContentLoaded", async () => {
+ const columnConfig = await get_request("/ui/bff/rollouts/columns");
+ for (const col in columnConfig.columns) {
+ const colDesc = columnConfig.columns[col];
+ const colName = colDesc.data;
+ if (renderFunctions[colName]) {
+ columnConfig.columns[col].render = renderFunctions[colName];
+ }
+ }
+
dataTable = new DataTable("#rollout-table", {
responsive: true,
paging: true,
@@ -32,37 +56,10 @@ document.addEventListener("DOMContentLoaded", async () => {
targets: "_all",
searchable: false,
orderable: false,
+ render: (data) => data || "-",
},
],
- columns: [
- { data: "id", visible: false },
- {
- data: "created_at",
- name: "created_at",
- orderable: true,
- render: (data) => new Date(data).toLocaleString(),
- },
- { data: "name", name: "name", searchable: true, orderable: true },
- { data: "feed", name: "feed", searchable: true, orderable: true },
- { data: "sw_file" },
- { data: "sw_version" },
- {
- data: "paused",
- render: (data, type) => {
- if (type === "display") {
- const color = data ? "danger" : "muted";
- return `
-
- ●
-
- `;
- }
- return data;
- },
- },
- { data: "success_count" },
- { data: "failure_count" },
- ],
+ columns: columnConfig.columns,
layout: {
top1Start: {
buttons: [],
diff --git a/goosebit/ui/static/js/software.js b/goosebit/ui/static/js/software.js
index 84dad5f6..09f29bd8 100644
--- a/goosebit/ui/static/js/software.js
+++ b/goosebit/ui/static/js/software.js
@@ -6,6 +6,128 @@ const uploadProgressBar = document.getElementById("upload-progress");
let dataTable;
+const renderFunctions = {
+ compatibility: (data, type) => {
+ const result = data.reduce((acc, { model, revision }) => {
+ if (!acc[model]) {
+ acc[model] = [];
+ }
+ acc[model].push(revision);
+ return acc;
+ }, {});
+
+ return Object.entries(result)
+ .map(([model, revision]) => `${model} - ${revision.join(", ")}`)
+ .join("\n");
+ },
+ size: (data, type) => {
+ if (type === "display" || type === "filter") {
+ return `${(data / 1024 / 1024).toFixed(2)}MB`;
+ }
+ return data;
+ },
+};
+
+document.addEventListener("DOMContentLoaded", async () => {
+ const columnConfig = await get_request("/ui/bff/software/columns");
+ for (const col in columnConfig.columns) {
+ const colDesc = columnConfig.columns[col];
+ const colName = colDesc.data;
+ if (renderFunctions[colName]) {
+ columnConfig.columns[col].render = renderFunctions[colName];
+ }
+ }
+
+ const buttons = [
+ {
+ text: '',
+ action: (e, dt) => {
+ const selectedSoftware = dt
+ .rows({ selected: true })
+ .data()
+ .toArray()
+ .map((d) => d.id);
+ downloadSoftware(selectedSoftware[0]);
+ },
+ className: "buttons-download",
+ titleAttr: "Download Software",
+ },
+ {
+ text: '',
+ action: async (e, dt) => {
+ const selectedSoftware = dt
+ .rows({ selected: true })
+ .data()
+ .toArray()
+ .map((d) => d.id);
+ await deleteSoftware(selectedSoftware);
+ },
+ className: "buttons-delete",
+ titleAttr: "Delete Software",
+ },
+ ];
+
+ // add create button at the beginning if upload modal exists
+ if ($("#upload-modal").length > 0) {
+ buttons.unshift({
+ text: '',
+ action: () => {
+ new bootstrap.Modal("#upload-modal").show();
+ },
+ className: "buttons-create",
+ titleAttr: "Add Software",
+ });
+ }
+
+ dataTable = new DataTable("#software-table", {
+ responsive: true,
+ paging: true,
+ processing: false,
+ serverSide: true,
+ scrollCollapse: true,
+ scroller: true,
+ scrollY: "60vh",
+ stateSave: true,
+ ajax: {
+ url: "/ui/bff/software",
+ data: (data) => {
+ // biome-ignore lint/performance/noDelete: really has to be deleted
+ delete data.columns;
+ },
+ contentType: "application/json",
+ },
+ initComplete: () => {
+ updateBtnState();
+ },
+ columnDefs: [
+ {
+ targets: "_all",
+ searchable: false,
+ orderable: false,
+ render: (data) => data || "-",
+ },
+ ],
+ columns: columnConfig.columns,
+ select: true,
+ rowId: "id",
+ layout: {
+ bottom1Start: {
+ buttons,
+ },
+ },
+ });
+
+ dataTable
+ .on("select", () => {
+ updateBtnState();
+ })
+ .on("deselect", () => {
+ updateBtnState();
+ });
+
+ updateSoftwareList();
+});
+
uploadForm.addEventListener("submit", async (e) => {
e.preventDefault();
await sendFileChunks(uploadFileInput.files[0]);
@@ -125,126 +247,6 @@ function resetProgress() {
updateSoftwareList();
}
-document.addEventListener("DOMContentLoaded", () => {
- const buttons = [
- {
- text: '',
- action: (e, dt) => {
- const selectedSoftware = dt
- .rows({ selected: true })
- .data()
- .toArray()
- .map((d) => d.id);
- downloadSoftware(selectedSoftware[0]);
- },
- className: "buttons-download",
- titleAttr: "Download Software",
- },
- {
- text: '',
- action: async (e, dt) => {
- const selectedSoftware = dt
- .rows({ selected: true })
- .data()
- .toArray()
- .map((d) => d.id);
- await deleteSoftware(selectedSoftware);
- },
- className: "buttons-delete",
- titleAttr: "Delete Software",
- },
- ];
-
- // add create button at the beginning if upload modal exists
- if ($("#upload-modal").length > 0) {
- buttons.unshift({
- text: '',
- action: () => {
- new bootstrap.Modal("#upload-modal").show();
- },
- className: "buttons-create",
- titleAttr: "Add Software",
- });
- }
-
- dataTable = new DataTable("#software-table", {
- responsive: true,
- paging: true,
- processing: false,
- serverSide: true,
- scrollCollapse: true,
- scroller: true,
- scrollY: "60vh",
- stateSave: true,
- ajax: {
- url: "/ui/bff/software",
- data: (data) => {
- // biome-ignore lint/performance/noDelete: really has to be deleted
- delete data.columns;
- },
- contentType: "application/json",
- },
- initComplete: () => {
- updateBtnState();
- },
- columnDefs: [
- {
- targets: "_all",
- searchable: false,
- orderable: false,
- render: (data) => data || "-",
- },
- ],
- columns: [
- { data: "id", visible: false },
- { data: "name" },
- { data: "version", name: "version", searchable: true, orderable: true },
- {
- data: "compatibility",
- render: (data) => {
- const result = data.reduce((acc, { model, revision }) => {
- if (!acc[model]) {
- acc[model] = [];
- }
- acc[model].push(revision);
- return acc;
- }, {});
-
- return Object.entries(result)
- .map(([model, revision]) => `${model} - ${revision.join(", ")}`)
- .join("\n");
- },
- },
- {
- data: "size",
- render: (data, type) => {
- if (type === "display" || type === "filter") {
- return `${(data / 1024 / 1024).toFixed(2)}MB`;
- }
- return data;
- },
- },
- ],
- select: true,
- rowId: "id",
- layout: {
- bottom1Start: {
- buttons,
- },
- },
- });
-
- dataTable
- .on("select", () => {
- updateBtnState();
- })
- .on("deselect", () => {
- updateBtnState();
- });
-
- updateSoftwareList();
-});
-
function updateBtnState() {
if (dataTable.rows({ selected: true }).any()) {
document.querySelector("button.buttons-delete").classList.remove("disabled");
diff --git a/goosebit/ui/templates/rollouts.html.jinja b/goosebit/ui/templates/rollouts.html.jinja
index 40fc990a..11304aea 100644
--- a/goosebit/ui/templates/rollouts.html.jinja
+++ b/goosebit/ui/templates/rollouts.html.jinja
@@ -4,21 +4,6 @@
-
-
- Id |
- Created |
- Name |
- Feed |
- Software File |
- Software Version |
- Paused |
- Success Count |
- Failure Count |
-
-
-
-
diff --git a/goosebit/ui/templates/software.html.jinja b/goosebit/ui/templates/software.html.jinja
index e664527f..513d8b01 100644
--- a/goosebit/ui/templates/software.html.jinja
+++ b/goosebit/ui/templates/software.html.jinja
@@ -4,17 +4,6 @@
-
-
- ID |
- Name |
- Version |
- Compatibility |
- Size |
-
-
-
-