Skip to content

Commit 4d83667

Browse files
committed
added Sortable headers for Pipeline Table.
fixed import order fixed import order fixed import order fixed import order
1 parent 99ae8e6 commit 4d83667

File tree

6 files changed

+202
-16
lines changed

6 files changed

+202
-16
lines changed

dispatcher/backend/src/common/schemas/fields.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,10 @@ def validate_multiple_of_100(value):
7575
offliner_field = String(required=False, validate=validate_offliner)
7676
email_field = fields.Email(required=False, validate=validate_not_empty)
7777
username_field = String(required=True, validate=validate_not_empty)
78+
79+
80+
def validate_sort_order(value):
81+
"""Validate that sort order is either 'asc' or 'desc'"""
82+
if value not in ["asc", "desc"]:
83+
raise ValidationError("Sort order must be either 'asc' or 'desc'")
84+
return True

dispatcher/backend/src/common/schemas/parameters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
validate_priority,
2323
validate_role,
2424
validate_schedule_name,
25+
validate_sort_order,
2526
validate_status,
2627
validate_warehouse_path,
2728
validate_worker_name,
@@ -56,6 +57,9 @@ class RequestedTaskSchema(Schema):
5657
priority = priority_field
5758
schedule_name = fields.List(schedule_name_field, required=False)
5859

60+
sort_by = fields.String(required=False)
61+
sort_order = fields.String(required=False, validate=validate_sort_order)
62+
5963
matching_cpu = fields.Integer(required=False, validate=validate_cpu)
6064
matching_memory = fields.Integer(required=False, validate=validate_memory)
6165
matching_disk = fields.Integer(required=False, validate=validate_disk)
@@ -123,6 +127,8 @@ class TasksSchema(Schema):
123127
limit = limit_field_20_200
124128
status = fields.List(String(validate=validate_status), required=False)
125129
schedule_name = schedule_name_field
130+
sort_by = fields.String(required=False)
131+
sort_order = fields.String(required=False, validate=validate_sort_order)
126132

127133

128134
# tasks POST

dispatcher/backend/src/routes/requested_tasks/requested_task.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from routes import auth_info_if_supplied, authenticate, require_perm, url_uuid
3131
from routes.base import BaseRoute
3232
from routes.errors import NotFound
33-
from routes.utils import remove_secrets_from_response
33+
from routes.utils import get_sort_field_and_apply_order, remove_secrets_from_response
3434
from utils.scheduling import find_requested_task_for, request_a_schedule
3535
from utils.token import AccessToken
3636

@@ -73,6 +73,9 @@ def list_of_requested_tasks(session: so.Session, token: AccessToken.Payload = No
7373
skip, limit = request_args["skip"], request_args["limit"]
7474
schedule_names = request_args["schedule_name"]
7575
priority = request_args.get("priority")
76+
# Get sorting parameters
77+
sort_by = request_args.get("sort_by", "priority")
78+
sort_order = request_args.get("sort_order", "desc")
7679

7780
# get requested tasks from database
7881
stmt = (
@@ -93,14 +96,21 @@ def list_of_requested_tasks(session: so.Session, token: AccessToken.Payload = No
9396
)
9497
.join(dbm.Worker, dbm.RequestedTask.worker, isouter=True)
9598
.join(dbm.Schedule, dbm.RequestedTask.schedule, isouter=True)
96-
.order_by(dbm.RequestedTask.priority.desc())
97-
.order_by(
99+
)
100+
join_models = {"Schedule": dbm.Schedule, "Worker": dbm.Worker}
101+
default_field = dbm.RequestedTask.priority
102+
stmt = get_sort_field_and_apply_order(
103+
model=dbm.RequestedTask,
104+
sort_by=sort_by,
105+
sort_order=sort_order,
106+
stmt=stmt,
107+
join_models=join_models,
108+
fallback_field=default_field,
109+
)
110+
if sort_by == "priority" or not sort_by:
111+
stmt = stmt.order_by(
98112
dbm.RequestedTask.timestamp["reserved"]["$date"].astext.cast(sa.BigInteger)
99113
)
100-
.order_by(
101-
dbm.RequestedTask.timestamp["requested"]["$date"].astext.cast(sa.BigInteger)
102-
)
103-
)
104114

105115
if schedule_names:
106116
stmt = stmt.filter(dbm.Schedule.name.in_(schedule_names))

dispatcher/backend/src/routes/tasks/task.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from routes import auth_info_if_supplied, authenticate, require_perm, url_uuid
2424
from routes.base import BaseRoute
2525
from routes.errors import BadRequest
26-
from routes.utils import remove_secrets_from_response
26+
from routes.utils import get_sort_field_and_apply_order, remove_secrets_from_response
2727
from utils.check import raise_if, raise_if_none
2828
from utils.token import AccessToken
2929

@@ -47,6 +47,9 @@ def get(self, session: so.Session):
4747
skip, limit = request_args["skip"], request_args["limit"]
4848
statuses = request_args.get("status")
4949
schedule_name = request_args.get("schedule_name")
50+
# Get sorting parameters
51+
sort_by = request_args.get("sort_by")
52+
sort_order = request_args.get("sort_order", "desc")
5053

5154
stmt = (
5255
sa.select(
@@ -65,7 +68,16 @@ def get(self, session: so.Session):
6568
)
6669
.join(dbm.Worker, dbm.Task.worker, isouter=True)
6770
.join(dbm.Schedule, dbm.Task.schedule, isouter=True)
68-
.order_by(dbm.Task.updated_at.desc())
71+
)
72+
join_models = {"Schedule": dbm.Schedule, "Worker": dbm.Worker}
73+
default_field = dbm.Task.updated_at
74+
stmt = get_sort_field_and_apply_order(
75+
model=dbm.Task,
76+
sort_by=sort_by,
77+
sort_order=sort_order,
78+
stmt=stmt,
79+
join_models=join_models,
80+
fallback_field=default_field,
6981
)
7082

7183
# get tasks from database

dispatcher/backend/src/routes/utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from typing import Any, Dict, List
33
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
44

5+
import sqlalchemy as sa
56
from common.constants import SECRET_REPLACEMENT
67
from common.schemas.models import ScheduleConfigSchema
8+
79
from utils.offliners import build_str_command
810

911
logger = logging.getLogger(__name__)
@@ -135,3 +137,59 @@ def remove_url_secrets(response: dict):
135137
)
136138
)
137139
response[key] = response[key].replace(url, secured_url)
140+
141+
142+
def get_sort_field_and_apply_order(
143+
model, sort_by, sort_order, stmt, join_models=None, fallback_field=None
144+
):
145+
"""
146+
Determines the sort field based on the model and sort_by parameter,
147+
then applies the sort order to the statement.
148+
"""
149+
join_models = join_models or {}
150+
151+
def apply_default_sort():
152+
if fallback_field:
153+
return stmt.order_by(
154+
fallback_field.desc() if sort_order == "desc" else fallback_field.asc()
155+
)
156+
return stmt
157+
158+
if not sort_by:
159+
return apply_default_sort()
160+
try:
161+
if "." in sort_by:
162+
field_parts = sort_by.split(".")
163+
parent, child = field_parts[0], field_parts[1]
164+
if parent == "timestamp":
165+
sort_field = model.timestamp[child]["$date"].astext.cast(sa.BigInteger)
166+
else:
167+
logger.warning(f"Unsupported field with dot notation: {sort_by}")
168+
return apply_default_sort()
169+
elif sort_by in ("worker", "worker_name"):
170+
if "Worker" in join_models:
171+
sort_field = join_models["Worker"].name
172+
else:
173+
logger.warning(
174+
f"Cannot sort by {sort_by}: Worker relationship not found"
175+
)
176+
return apply_default_sort()
177+
elif sort_by == "schedule_name":
178+
if "Schedule" in join_models:
179+
sort_field = join_models["Schedule"].name
180+
else:
181+
logger.warning(
182+
"Cannot sort by schedule_name: Schedule relationship not found"
183+
)
184+
return apply_default_sort()
185+
elif hasattr(model, sort_by):
186+
sort_field = getattr(model, sort_by)
187+
else:
188+
logger.warning(f"Field {sort_by} not recognized")
189+
return apply_default_sort()
190+
return stmt.order_by(
191+
sort_field.desc() if sort_order == "desc" else sort_field.asc()
192+
)
193+
except Exception as e:
194+
logger.error(f"Error applying sorting: {e}")
195+
return apply_default_sort()

dispatcher/frontend-ui/src/components/PipelineTable.vue

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,84 @@
1313
</caption>
1414
<thead v-if="selectedTable == 'todo'">
1515
<tr>
16-
<th>Schedule</th>
17-
<th>Requested</th>
18-
<th>By</th>
16+
<th @click="sortBy('schedule_name')" class="sortable">
17+
Schedule
18+
<span v-if="sortColumn === 'schedule_name'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
19+
</th>
20+
<th @click="sortBy('timestamp.requested')" class="sortable">
21+
Requested
22+
<span v-if="sortColumn === 'timestamp.requested'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
23+
</th>
24+
<th @click="sortBy('requested_by')" class="sortable">
25+
By
26+
<span v-if="sortColumn === 'requested_by'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
27+
</th>
1928
<th>Resources</th>
20-
<th>Worker</th>
29+
<th @click="sortBy('worker')" class="sortable">
30+
Worker
31+
<span v-if="sortColumn === 'worker'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
32+
</th>
2133
<th v-show="canUnRequestTasks">Remove</th>
2234
</tr>
2335
</thead>
2436
<thead v-if="selectedTable == 'doing'">
25-
<tr><th>Schedule</th><th>Started</th><th>Worker</th></tr>
37+
<tr>
38+
<th @click="sortBy('schedule_name')" class="sortable">
39+
Schedule
40+
<span v-if="sortColumn === 'schedule_name'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
41+
</th>
42+
<th @click="sortBy('timestamp.reserved')" class="sortable">
43+
Started
44+
<span v-if="sortColumn === 'timestamp.reserved'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
45+
</th>
46+
<th @click="sortBy('worker')" class="sortable">
47+
Worker
48+
<span v-if="sortColumn === 'worker'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
49+
</th>
50+
</tr>
2651
</thead>
2752
<thead v-if="selectedTable == 'done'">
28-
<tr><th>Schedule</th><th>Completed</th><th>Worker</th><th>Duration</th></tr>
53+
<tr>
54+
<th @click="sortBy('schedule_name')" class="sortable">
55+
Schedule
56+
<span v-if="sortColumn === 'schedule_name'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
57+
</th>
58+
<th @click="sortBy('updated_at')" class="sortable">
59+
Completed
60+
<span v-if="sortColumn === 'updated_at'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
61+
</th>
62+
<th @click="sortBy('worker')" class="sortable">
63+
Worker
64+
<span v-if="sortColumn === 'worker'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
65+
</th>
66+
<th>
67+
Duration
68+
</th>
69+
</tr>
2970
</thead>
3071
<thead v-if="selectedTable == 'failed'">
31-
<tr><th>Schedule</th><th>Stopped</th><th>Worker</th><th>Duration</th><th>Status</th><th>Last Run</th></tr>
72+
<tr>
73+
<th @click="sortBy('schedule_name')" class="sortable">
74+
Schedule
75+
<span v-if="sortColumn === 'schedule_name'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
76+
</th>
77+
<th @click="sortBy('updated_at')" class="sortable">
78+
Stopped
79+
<span v-if="sortColumn === 'updated_at'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
80+
</th>
81+
<th @click="sortBy('worker')" class="sortable">
82+
Worker
83+
<span v-if="sortColumn === 'worker'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
84+
</th>
85+
<th>
86+
Duration
87+
</th>
88+
<th @click="sortBy('status')" class="sortable">
89+
Status
90+
<span v-if="sortColumn === 'status'" class="sort-icon">{{ sortOrder === 'asc' ? '▲' : '▼' }}</span>
91+
</th>
92+
<th>Last Run</th>
93+
</tr>
3294
</thead>
3395
<tbody>
3496
<tr v-for="task in tasks" :key="task._id">
@@ -97,6 +159,8 @@
97159
loading: false,
98160
schedules_last_runs: {}, // last runs for all schedule_names of tasks
99161
last_runs_loaded: false, // used to trigger render() on last_run cell
162+
sortColumn: null,
163+
sortOrder: 'desc',
100164
};
101165
},
102166
computed: {
@@ -128,6 +192,12 @@
128192
let parent = this;
129193
parent.toggleLoader("fetching tasks…");
130194
parent.loading = true;
195+
if (this.sortColumn) {
196+
params.sort_by = this.sortColumn;
197+
params.sort_order = this.sortOrder;
198+
if (params.sort) delete params.sort;
199+
if (params.order) delete params.order;
200+
}
131201
this.queryAPI('get', url, {params})
132202
.then(function (response) {
133203
parent.resetData();
@@ -207,6 +277,15 @@
207277
parent.last_runs_loaded = true;
208278
parent.toggleLoader(false);
209279
},
280+
sortBy(column) {
281+
if (this.sortColumn === column) {
282+
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
283+
} else {
284+
this.sortColumn = column;
285+
this.sortOrder = 'desc';
286+
}
287+
this.loadData();
288+
},
210289
},
211290
mounted() {
212291
this.loadData();
@@ -222,3 +301,17 @@
222301
}
223302
};
224303
</script>
304+
<style>
305+
.sortable {
306+
cursor: pointer;
307+
position: relative;
308+
user-select: none;
309+
}
310+
.sortable:hover {
311+
background-color: rgba(0, 0, 0, 0.05);
312+
}
313+
.sort-icon {
314+
margin-left: 5px;
315+
font-size: 0.8em;
316+
}
317+
</style>

0 commit comments

Comments
 (0)