From 860021783b96769971e0629b341a76cc480687d5 Mon Sep 17 00:00:00 2001 From: Upstream Data Date: Tue, 26 Nov 2024 08:54:34 -0700 Subject: [PATCH 1/3] Update datatable column handling description for devices Simplify and prettify the column handling slightly. --- goosebit/ui/bff/common/columns.py | 24 +++++++++++++++ goosebit/ui/bff/devices/routes.py | 50 ++++++++++++++----------------- 2 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 goosebit/ui/bff/common/columns.py diff --git a/goosebit/ui/bff/common/columns.py b/goosebit/ui/bff/common/columns.py new file mode 100644 index 00000000..827064f8 --- /dev/null +++ b/goosebit/ui/bff/common/columns.py @@ -0,0 +1,24 @@ +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") 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) From 6aa1677149d2603f555bebad3a56fa667acca05f Mon Sep 17 00:00:00 2001 From: Upstream Data Date: Tue, 26 Nov 2024 09:16:42 -0700 Subject: [PATCH 2/3] Use new datatable column handling description for rollouts --- goosebit/ui/bff/common/columns.py | 12 +++++ goosebit/ui/bff/common/responses.py | 1 + goosebit/ui/bff/rollouts/routes.py | 27 +++++++++++ goosebit/ui/static/js/rollouts.js | 55 +++++++++++------------ goosebit/ui/templates/rollouts.html.jinja | 15 ------- 5 files changed, 66 insertions(+), 44 deletions(-) diff --git a/goosebit/ui/bff/common/columns.py b/goosebit/ui/bff/common/columns.py index 827064f8..46235170 100644 --- a/goosebit/ui/bff/common/columns.py +++ b/goosebit/ui/bff/common/columns.py @@ -22,3 +22,15 @@ class DeviceColumns: 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") 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/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/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/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 @@
- - - - - - - - - - - - - - -
IdCreatedNameFeedSoftware FileSoftware VersionPausedSuccess CountFailure Count
From 29350a7e5bc4e7d94ef7d87a7f75a03c0f14afab Mon Sep 17 00:00:00 2001 From: Upstream Data Date: Tue, 26 Nov 2024 09:32:59 -0700 Subject: [PATCH 3/3] Use new datatable column handling description for software --- goosebit/ui/bff/common/columns.py | 8 + goosebit/ui/bff/software/routes.py | 23 ++ goosebit/ui/static/js/software.js | 242 +++++++++++----------- goosebit/ui/templates/software.html.jinja | 11 - 4 files changed, 153 insertions(+), 131 deletions(-) diff --git a/goosebit/ui/bff/common/columns.py b/goosebit/ui/bff/common/columns.py index 46235170..64f30651 100644 --- a/goosebit/ui/bff/common/columns.py +++ b/goosebit/ui/bff/common/columns.py @@ -34,3 +34,11 @@ class RolloutColumns: 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/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/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/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 @@
- - - - - - - - - - -
IDNameVersionCompatibilitySize