diff --git a/sirepo/package_data/static/js/raydata.js b/sirepo/package_data/static/js/raydata.js index a018f24ea5..1783445e65 100644 --- a/sirepo/package_data/static/js/raydata.js +++ b/sirepo/package_data/static/js/raydata.js @@ -446,7 +446,7 @@ SIREPO.app.directive('scansTable', function() { PDF - + {{ column }} @@ -502,10 +502,11 @@ SIREPO.app.directive('scansTable', function() { let scanArgs = { pageCount: 0, pageNumber: 0, - sortColumn: $scope.analysisStatus == 'queued' ? 'queue order' : 'start', - sortOrder: $scope.analysisStatus == 'queued', + sortColumns: defaultSortColumns(), }; + const MAX_NUM_SORT_COLUMNS = 3; + const errorOptions = { modelName: $scope.modelName, onError: data => { @@ -540,6 +541,14 @@ SIREPO.app.directive('scansTable', function() { } } + function defaultSortColumns() { + if ($scope.analysisStatus == 'queued') { + return [['queue order', true]]; + } else { + return [['start', false]]; + } + } + function findScan(scanId) { return $scope.scans[ $scope.scans.findIndex(s => s.rduid === scanId) @@ -579,8 +588,7 @@ SIREPO.app.directive('scansTable', function() { } scanArgs.pageCount = scanInfo.pageCount || 0; scanArgs.pageNumber = scanInfo.pageNumber; - scanArgs.sortColumn = scanInfo.sortColumn; - scanArgs.sortOrder = scanInfo.sortOrder; + scanArgs.sortColumns = scanInfo.sortColumns; updatePageLocation(); } @@ -629,8 +637,7 @@ SIREPO.app.directive('scansTable', function() { pageNumber: scanArgs.pageNumber, searchText: m.searchText, searchTerms: buildSearchTerms(m.searchTerms), - sortColumn: scanArgs.sortColumn, - sortOrder: scanArgs.sortOrder, + sortColumns: scanArgs.sortColumns } }, errorOptions @@ -711,13 +718,29 @@ SIREPO.app.directive('scansTable', function() { } $scope.arrowClass = column => { - if (scanArgs.sortColumn !== column) { - return {}; + let r = {}; + scanArgs.sortColumns.forEach((sortField) => { + if (sortField[0] === column) { + r= { + glyphicon: true, + [`glyphicon-arrow-${sortField[1] ? 'up' : 'down'}`]: true, + }; + } + }); + return r; + }; + + $scope.arrowStyle = column => { + let r = ''; + for (const [i, sortField] of scanArgs.sortColumns.entries()) { + if (sortField[0] === column) { + // logic for alpha value assumes that sortColumns are in descending priority and MAX_NUM_SORT_COLUMNS is 3 + let a = i === 0 ? 1 : (-0.3 * i) + 0.8; + r = `color: rgba(0, 0, 0, ${a})`; + break; + } } - return { - glyphicon: true, - [`glyphicon-arrow-${scanArgs.sortOrder ? 'up' : 'down'}`]: true, - }; + return r; }; $scope.canNextPage = () => { @@ -843,15 +866,33 @@ SIREPO.app.directive('scansTable', function() { }; $scope.columnIsSortable = (column) => { - return column !== 'stop'; + return ! (column === 'stop' || $scope.scans.length > 0 && $scope.scans[0][column] !== null && typeof $scope.scans[0][column] === 'object'); }; + $scope.sortCol = column => { if (! $scope.columnIsSortable(column)) { return; } - scanArgs.sortColumn = column; - scanArgs.sortOrder = ! scanArgs.sortOrder; + let inList = false; + scanArgs.sortColumns.forEach((sortField, i) => { + if (sortField[0] === column) { + inList = true; + sortField[1] = ! sortField[1]; + scanArgs.sortColumns.splice(i, 1); + scanArgs.sortColumns.unshift(sortField); + } + }); + if (! inList) { + if ($scope.analysisStatus !== 'allStatuses') { + scanArgs.sortColumns = [[column, true]]; + } else { + scanArgs.sortColumns.unshift([column, true]); + if (scanArgs.sortColumns.length > MAX_NUM_SORT_COLUMNS) { + scanArgs.sortColumns.pop(); + } + } + } sendScanRequest(true, true); }; diff --git a/sirepo/raydata/scan_monitor.py b/sirepo/raydata/scan_monitor.py index d6c7a75295..4430fe247a 100644 --- a/sirepo/raydata/scan_monitor.py +++ b/sirepo/raydata/scan_monitor.py @@ -348,25 +348,6 @@ def _search_params(req_data): } return q - def _sort_params(req_data): - c = _default_columns(req_data.catalogName).get( - req_data.sortColumn, req_data.sortColumn - ) - s = [ - ( - c, - pymongo.ASCENDING if req_data.sortOrder else pymongo.DESCENDING, - ), - ] - if c != "time": - s.append( - ( - "time", - pymongo.DESCENDING, - ) - ) - return s - c = sirepo.raydata.databroker.catalog(req_data.catalogName) pc = math.ceil( len( @@ -530,6 +511,24 @@ async def _sr_post(self, *args, **kwargs): self.write(await self._incoming(PKDict(pkjson.load_any(self.request.body)))) +def _sort_params(req_data): + r = [] + has_time = False + for x in req_data.sortColumns: + n = _default_columns(req_data.catalogName).get(x[0], x[0]) + if n == "time": + has_time = True + r.append( + [ + n, + pymongo.ASCENDING if x[1] else pymongo.DESCENDING, + ] + ) + if not has_time: + r.append(["time", pymongo.DESCENDING]) + return r + + async def _init_analysis_processors(): global _ANALYSIS_PROCESSOR_TASKS @@ -730,9 +729,10 @@ def _get_start_field(metadata, column): def _scan_info_result(scans, page_count, req_data): def _compare_values(v1, v2): + sort_column = _sort_params(req_data)[0][0] # very careful compare - needs to account for missing values or mismatched types - v1 = v1.get(req_data.sortColumn) - v2 = v2.get(req_data.sortColumn) + v1 = v1.get(sort_column) + v2 = v2.get(sort_column) if v1 is None and v2 is None: return 0 if v1 is None: @@ -765,7 +765,7 @@ def _compare_values(v1, v2): s = sorted( s, key=functools.cmp_to_key(_compare_values), - reverse=not req_data.sortOrder, + reverse=not _sort_params(req_data)[0][1], ) return PKDict( data=PKDict( @@ -773,8 +773,7 @@ def _compare_values(v1, v2): cols=_display_columns(all_columns), pageCount=page_count, pageNumber=req_data.pageNumber, - sortColumn=req_data.sortColumn, - sortOrder=req_data.sortOrder, + sortColumns=req_data.sortColumns, ) )