Skip to content

Commit b41c165

Browse files
committed
ui: add per-column search to software and rollouts page
1 parent 8f33b1b commit b41c165

6 files changed

Lines changed: 119 additions & 23 deletions

File tree

goosebit/ui/bff/rollouts/responses.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ async def convert(
2121
if dt_query.search.value:
2222
query = query.filter(search_filter(dt_query.search.value))
2323

24+
for column in dt_query.columns:
25+
query = query.filter(column.query)
26+
2427
filtered_records = await query.count()
2528

2629
if dt_query.order_query:

goosebit/ui/bff/rollouts/routes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["read"]()])],
2323
)
2424
async def rollouts_get(dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)]) -> BFFRolloutsResponse:
25+
return await rollouts_post(dt_query)
26+
27+
28+
@router.post(
29+
"",
30+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["read"]()])],
31+
)
32+
async def rollouts_post(dt_query: DataTableRequest) -> BFFRolloutsResponse:
2533
def search_filter(search_value: str) -> Q:
2634
return (
2735
Q(name__icontains=search_value)

goosebit/ui/bff/software/responses.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ class BFFSoftwareResponse(BaseModel):
1717

1818
@classmethod
1919
async def convert(
20-
cls, dt_query: DataTableRequest, query: QuerySet[Any], search_filter: Callable[[str], Any], alt_filter: Q
20+
cls,
21+
dt_query: DataTableRequest,
22+
query: QuerySet[Any],
23+
search_filter: Callable[[str], Any],
24+
alt_filter: Q | None = None,
2125
) -> "BFFSoftwareResponse":
2226
total_records = await query.count()
23-
query = query.filter(alt_filter)
27+
28+
if alt_filter is not None:
29+
query = query.filter(alt_filter)
30+
2431
if dt_query.search.value:
2532
query = query.filter(search_filter(dt_query.search.value))
2633

34+
for column in dt_query.columns:
35+
query = query.filter(column.query)
36+
2737
filtered_records = await query.count()
2838

2939
if len(dt_query.order) > 0 and dt_query.order[0].name == "version":

goosebit/ui/bff/software/routes.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,6 @@ async def software_get(
3232
dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)],
3333
ids: list[str] = Query(default=None),
3434
) -> BFFSoftwareResponse:
35-
return await software_post(dt_query, ids)
36-
37-
38-
@router.post(
39-
"",
40-
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["read"]()])],
41-
)
42-
async def software_post(dt_query: DataTableRequest, ids: list[str] | None = None) -> BFFSoftwareResponse:
4335
filters: list[Q] = []
4436

4537
if ids is not None:
@@ -48,13 +40,26 @@ async def software_post(dt_query: DataTableRequest, ids: list[str] | None = None
4840

4941
def search_filter(search_value: str) -> Q:
5042
base_filter = Q(Q(uri__icontains=search_value), Q(version__icontains=search_value), join_type="OR")
51-
return Q(base_filter, *filters, join_type="AND")
43+
return Q(base_filter, join_type="AND")
5244

5345
query = Software.all().prefetch_related("compatibility")
5446

5547
return await BFFSoftwareResponse.convert(dt_query, query, search_filter, Q(*filters))
5648

5749

50+
@router.post(
51+
"",
52+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["read"]()])],
53+
)
54+
async def software_post(dt_query: DataTableRequest) -> BFFSoftwareResponse:
55+
def search_filter(search_value: str) -> Q:
56+
return Q(uri__icontains=search_value) | Q(version__icontains=search_value)
57+
58+
query = Software.all().prefetch_related("compatibility")
59+
60+
return await BFFSoftwareResponse.convert(dt_query, query, search_filter)
61+
62+
5863
router.add_api_route(
5964
"",
6065
routes.software_delete,

goosebit/ui/static/js/rollouts.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ document.addEventListener("DOMContentLoaded", async () => {
2323
if (renderFunctions[colName]) {
2424
columnConfig.columns[col].render = renderFunctions[colName];
2525
}
26+
columnConfig.columns[col].footer = createColumnFooter().outerHTML;
2627
}
2728

2829
dataTable = new DataTable("#rollout-table", {
29-
responsive: true,
3030
paging: true,
3131
processing: true,
3232
serverSide: true,
@@ -42,14 +42,49 @@ document.addEventListener("DOMContentLoaded", async () => {
4242
rowId: "id",
4343
ajax: {
4444
url: "/ui/bff/rollouts",
45-
data: (data) => {
46-
// biome-ignore lint/performance/noDelete: really has to be deleted
47-
delete data.columns;
48-
},
45+
method: "POST",
46+
submitAs: "json",
4947
contentType: "application/json",
5048
},
51-
initComplete: () => {
49+
initComplete: function () {
5250
updateBtnState();
51+
const api = this.api();
52+
53+
columnConfig.columns.forEach((col, idx) => {
54+
if (col.visible === false) {
55+
return;
56+
}
57+
58+
if (col.searchable) {
59+
const inputGroup = document.createElement("div");
60+
inputGroup.className = "input-group input-group-sm";
61+
inputGroup.style.width = "100%";
62+
63+
const column = api.column(idx);
64+
65+
const input = document.createElement("input");
66+
input.type = "text";
67+
input.placeholder = col.title;
68+
input.value = column.search();
69+
input.className = "form-control form-control-sm";
70+
71+
const iconSpan = document.createElement("span");
72+
iconSpan.className = "input-group-text";
73+
iconSpan.innerHTML = '<i class="bi bi-search"></i>'; // Bootstrap icon
74+
75+
inputGroup.appendChild(iconSpan);
76+
inputGroup.appendChild(input);
77+
78+
input.addEventListener("input", function () {
79+
const column = api.column(idx);
80+
if (column.search() !== this.value) {
81+
column.search(this.value).draw();
82+
}
83+
});
84+
85+
api.column(idx).footer().replaceChildren(inputGroup);
86+
}
87+
});
5388
},
5489
columnDefs: [
5590
{

goosebit/ui/static/js/software.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ document.addEventListener("DOMContentLoaded", async () => {
3636
if (renderFunctions[colName]) {
3737
columnConfig.columns[col].render = renderFunctions[colName];
3838
}
39+
columnConfig.columns[col].footer = createColumnFooter().outerHTML;
3940
}
4041

4142
const buttons = [
@@ -80,7 +81,6 @@ document.addEventListener("DOMContentLoaded", async () => {
8081
}
8182

8283
dataTable = new DataTable("#software-table", {
83-
responsive: true,
8484
paging: true,
8585
processing: false,
8686
serverSide: true,
@@ -90,14 +90,49 @@ document.addEventListener("DOMContentLoaded", async () => {
9090
stateSave: true,
9191
ajax: {
9292
url: "/ui/bff/software",
93-
data: (data) => {
94-
// biome-ignore lint/performance/noDelete: really has to be deleted
95-
delete data.columns;
96-
},
93+
method: "POST",
94+
submitAs: "json",
9795
contentType: "application/json",
9896
},
99-
initComplete: () => {
97+
initComplete: function () {
10098
updateBtnState();
99+
const api = this.api();
100+
101+
columnConfig.columns.forEach((col, idx) => {
102+
if (col.visible === false) {
103+
return;
104+
}
105+
106+
if (col.searchable) {
107+
const inputGroup = document.createElement("div");
108+
inputGroup.className = "input-group input-group-sm";
109+
inputGroup.style.width = "100%";
110+
111+
const column = api.column(idx);
112+
113+
const input = document.createElement("input");
114+
input.type = "text";
115+
input.placeholder = col.title;
116+
input.value = column.search();
117+
input.className = "form-control form-control-sm";
118+
119+
const iconSpan = document.createElement("span");
120+
iconSpan.className = "input-group-text";
121+
iconSpan.innerHTML = '<i class="bi bi-search"></i>'; // Bootstrap icon
122+
123+
inputGroup.appendChild(iconSpan);
124+
inputGroup.appendChild(input);
125+
126+
input.addEventListener("input", function () {
127+
const column = api.column(idx);
128+
if (column.search() !== this.value) {
129+
column.search(this.value).draw();
130+
}
131+
});
132+
133+
api.column(idx).footer().replaceChildren(inputGroup);
134+
}
135+
});
101136
},
102137
columnDefs: [
103138
{

0 commit comments

Comments
 (0)