diff --git a/SAS/DataManagement/DataManagementCommon/path.py b/SAS/DataManagement/DataManagementCommon/path.py index 390c0ac5d24fb52fcd81ca5468b61d85700431fe..4cbf7afe6210359c5e3b2b10bc0c0225b09f9d3d 100644 --- a/SAS/DataManagement/DataManagementCommon/path.py +++ b/SAS/DataManagement/DataManagementCommon/path.py @@ -181,7 +181,7 @@ class PathResolver: dir_names = [l.split(' ')[-1].strip() for l in dir_lines] result = {'found': True, 'path': path, 'sub_directories': dir_names} - logger.info('getSubDirectories(%s) result: %s', path, result) + logger.debug('getSubDirectories(%s) result: %s', path, result) return result def pathExists(self, path): diff --git a/SAS/DataManagement/StorageQueryService/cache.py b/SAS/DataManagement/StorageQueryService/cache.py index 666ceb44deca022ac9862ac9aa76a72111a94179..2de9f88dffeff47f583f45f5a7c27acbff685ad3 100644 --- a/SAS/DataManagement/StorageQueryService/cache.py +++ b/SAS/DataManagement/StorageQueryService/cache.py @@ -89,16 +89,18 @@ class CacheManager: def _readCacheFromDisk(self): # maybe this cache on disk is slow, if so, revert to proper db solution try: - with open('.du_cache.py', 'r') as file: - with self._cacheLock: - self._cache = eval(file.read()) - if not isinstance(self._cache, dict) or 'paths' not in self._cache or 'otdb_ids' not in self._cache: - self._cache = {} + if os.path.exists('.du_cache.py'): + with open('.du_cache.py', 'r') as file: + with self._cacheLock: + self._cache = eval(file.read().strip()) + if not isinstance(self._cache, dict): + self._cache = {} except Exception as e: logger.error("Error while reading in du cache: %s", e) with self._cacheLock: self._cache = {} + def _writeCacheToDisk(self): try: with open('.du_cache.py', 'w') as file: @@ -138,7 +140,45 @@ class CacheManager: if path in self._cache: self._cache[path]['cache_timestamp'] = self._cache[path]['cache_timestamp'] - MAX_CACHE_ENTRY_AGE + def _scanProjectsTree(self): + try: + def addSubDirectoriesToCache(directory): + depth = len(directory.replace(self.disk_usage.path_resolver.projects_path, '').strip('/').split('/')) + if depth > 3: + return + + with self._cacheLock: + if directory not in self._cache: + logger.info('tree scan: adding \'%s\' with empty disk_usage to cache which will be du\'ed later', directory) + empty_du_result = {'found': True, 'disk_usage': None, 'path': directory, 'name': directory.split('/')[-1]} + self._updateCache(empty_du_result) + + if directory in self._cache: + # make cache entry for directory 'old', so it will be du'ed in _updateOldEntriesInCache immediately + self._cache[directory]['cache_timestamp'] -= MAX_CACHE_ENTRY_AGE + + if not self._updateCacheThreadRunning: + return + + if depth < 3: + logger.info('tree scan: scanning \'%s\'', directory) + sd_result = self.disk_usage.path_resolver.getSubDirectories(directory) + + if sd_result['found']: + subdir_paths = [os.path.join(directory,sd) for sd in sd_result['sub_directories']] + + for subdir_path in subdir_paths: + # recurse + addSubDirectoriesToCache(subdir_path) + + addSubDirectoriesToCache(self.disk_usage.path_resolver.projects_path) + logger.info('tree scan complete') + + except Exception as e: + logger.error(str(e)) + def _updateOldEntriesInCache(self): + logger.info('starting updating old cache entries') while self._updateCacheThreadRunning: try: now = datetime.datetime.utcnow() @@ -183,6 +223,10 @@ class CacheManager: except Exception as e: logger.error(str(e)) + def _scanProjectsTreeAndUpdateOldEntriesInCache(self): + self._scanProjectsTree() + self._updateOldEntriesInCache() + def _updateProjectsDiskUsageInRADB(self): try: projects_du_result = self.getDiskUsageForPath(self.disk_usage.path_resolver.projects_path) @@ -192,16 +236,17 @@ class CacheManager: storage_resources = radbrpc.getResources(resource_types='storage', include_availability=True) cep4_storage_resource = next(x for x in storage_resources if 'cep4' in x['name']) - total_capacity = cep4_storage_resource['total_capacity'] - used_capacity = projects_du_result['disk_usage'] - available_capacity = total_capacity - used_capacity + total_capacity = cep4_storage_resource.get('total_capacity') + used_capacity = projects_du_result.get('disk_usage') + if total_capacity != None and used_capacity != None: + available_capacity = total_capacity - used_capacity - logger.info('updating availability capacity for %s (id=%s) to %s in the RADB', - cep4_storage_resource['name'], - cep4_storage_resource['id'], - humanreadablesize(available_capacity)) + logger.info('updating availability capacity for %s (id=%s) to %s in the RADB', + cep4_storage_resource['name'], + cep4_storage_resource['id'], + humanreadablesize(available_capacity)) - radbrpc.updateResourceAvailability(cep4_storage_resource['id'], available_capacity=available_capacity) + radbrpc.updateResourceAvailability(cep4_storage_resource['id'], available_capacity=available_capacity) except Exception as e: logger.error(e) @@ -209,7 +254,7 @@ class CacheManager: self.disk_usage.open() self.event_bus.open() - self._updateCacheThread = Thread(target=self._updateOldEntriesInCache) + self._updateCacheThread = Thread(target=self._scanProjectsTreeAndUpdateOldEntriesInCache) self._updateCacheThread.daemon = True self._updateCacheThreadRunning = True self._updateCacheThread.start() diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/cleanupcontroller.js b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/cleanupcontroller.js index be1aed8cbb7c37baa9ff780f7599274d0eab7f27..45321de54451097a8ed5b6cae9d0e3178b09e605 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/cleanupcontroller.js +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/cleanupcontroller.js @@ -18,14 +18,28 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h return defer.promise; }; - self.deleteTaskDataWithConfirmation = function(task) { - dataService.getTaskDiskUsageByOTDBId(task.otdb_id).then(function(du_result) { - if(du_result.found) { - openDeleteConfirmationDialog(task, du_result); - } else { - alert(du_result.message); - } - }); + self.deleteSelectedTasksDataWithConfirmation = function() { + var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; }); + self.deleteTasksDataWithConfirmation(tasks); + } + + self.deleteTasksDataWithConfirmation = function(tasks) { + du_results = []; + for(var task of tasks) { + dataService.getTaskDiskUsage(task).then(function(du_result) { + if(du_result.found) { + du_results.push(du_result); + + if(du_results.length == tasks.length) { + openDeleteConfirmationDialog(du_results); + } + + console.log(du_results); + } else { + alert(du_result.message); + } + }); + } }; function deleteTaskData(task, delete_is, delete_cs, delete_uv, delete_im, delete_img, delete_pulp, delete_scratch) { @@ -37,17 +51,16 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h }); }; - function openDeleteConfirmationDialog(task, du_result) { - var path = du_result.task_directory.path; - + function openDeleteConfirmationDialog(du_results) { var modalInstance = $uibModal.open({ animation: false, template: '<div class="modal-header">\ <h3 class="modal-title">Are you sure?</h3>\ </div>\ <div class="modal-body">\ - <p>This will delete all selected data in ' + path + '<br>\ - Are you sure?</p>\ + <p>This will delete all selected data in: \ + <ul><li ng-repeat="path in paths">{{path}}</li></ul>\ + <br>Are you sure?</p>\ <label ng-if="has_is" style="margin-left:24px">IS: {{amount_is}}<input style="margin-left:8px" type="checkbox" ng-model="$parent.delete_is"></label>\ <label ng-if="has_cs" style="margin-left:24px">CS: {{amount_cs}}<input style="margin-left:8px" type="checkbox" ng-model="$parent.delete_cs"></label>\ <label ng-if="has_uv" style="margin-left:24px">UV: {{amount_uv}}<input style="margin-left:8px" type="checkbox" ng-model="$parent.delete_uv"></label>\ @@ -61,19 +74,53 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h <button class="btn btn-warning" type="button" autofocus ng-click="cancel()">Cancel</button>\ </div>', controller: function ($scope, $uibModalInstance) { - $scope.has_is = du_result.sub_directories.hasOwnProperty(path + '/is'); - $scope.has_cs = du_result.sub_directories.hasOwnProperty(path + '/cs'); - $scope.has_uv = du_result.sub_directories.hasOwnProperty(path + '/uv'); - $scope.has_im = du_result.sub_directories.hasOwnProperty(path + '/im'); - $scope.has_img = du_result.sub_directories.hasOwnProperty(path + '/img'); - $scope.has_pulp = du_result.sub_directories.hasOwnProperty(path + '/pulp'); - $scope.has_scratch = du_result.task_directory.hasOwnProperty('scratch_paths');; - $scope.amount_is = $scope.has_is ? du_result.sub_directories[path + '/is'].disk_usage_readable : ''; - $scope.amount_cs = $scope.has_cs ? du_result.sub_directories[path + '/cs'].disk_usage_readable : ''; - $scope.amount_uv = $scope.has_uv ? du_result.sub_directories[path + '/uv'].disk_usage_readable : ''; - $scope.amount_im = $scope.has_im ? du_result.sub_directories[path + '/im'].disk_usage_readable : ''; - $scope.amount_img = $scope.has_img ? du_result.sub_directories[path + '/img'].disk_usage_readable : ''; - $scope.amount_pulp = $scope.has_pulp ? du_result.sub_directories[path + '/pulp'].disk_usage_readable : ''; + $scope.paths = du_results.map(function(r) { return r.task_directory.path; }); + $scope.has_is = false; + $scope.has_cs = false; + $scope.has_uv = false; + $scope.has_im = false; + $scope.has_img = false; + $scope.has_pulp = false; + $scope.has_scratch = false; + $scope.amount_is = 0; + $scope.amount_cs = 0; + $scope.amount_uv = 0; + $scope.amount_im = 0; + $scope.amount_img = 0; + $scope.amount_pulp = 0; + + for(var du_result of du_results) { + var path = du_result.task_directory.path; + var has_is = du_result.sub_directories.hasOwnProperty(path + '/is'); + var has_cs = du_result.sub_directories.hasOwnProperty(path + '/cs'); + var has_uv = du_result.sub_directories.hasOwnProperty(path + '/uv'); + var has_im = du_result.sub_directories.hasOwnProperty(path + '/im'); + var has_img = du_result.sub_directories.hasOwnProperty(path + '/img'); + var has_pulp = du_result.sub_directories.hasOwnProperty(path + '/pulp'); + var has_scratch = du_result.sub_directories.hasOwnProperty('scratch_paths'); + + $scope.has_is |= has_is; + $scope.has_cs |= has_cs; + $scope.has_uv |= has_uv; + $scope.has_im |= has_im; + $scope.has_img |= has_img; + $scope.has_pulp |= has_pulp; + $scope.has_scratch |= has_scratch; + + $scope.amount_is += has_is ? du_result.sub_directories[path + '/is'].disk_usage : 0; + $scope.amount_cs += has_cs ? du_result.sub_directories[path + '/cs'].disk_usage : 0; + $scope.amount_uv += has_uv ? du_result.sub_directories[path + '/uv'].disk_usage : 0; + $scope.amount_im += has_im ? du_result.sub_directories[path + '/im'].disk_usage : 0; + $scope.amount_img += has_img ? du_result.sub_directories[path + '/img'].disk_usage : 0; + $scope.amount_pulp += has_pulp ? du_result.sub_directories[path + '/pulp'].disk_usage : 0; + } + + $scope.amount_is = dataService.humanreadablesize($scope.amount_is); + $scope.amount_cs = dataService.humanreadablesize($scope.amount_cs); + $scope.amount_uv = dataService.humanreadablesize($scope.amount_uv); + $scope.amount_im = dataService.humanreadablesize($scope.amount_im); + $scope.amount_img = dataService.humanreadablesize($scope.amount_img); + $scope.amount_pulp = dataService.humanreadablesize($scope.amount_pulp); $scope.delete_is = true; $scope.delete_cs = true; @@ -85,7 +132,10 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h $scope.ok = function () { $uibModalInstance.close(); - deleteTaskData(task, $scope.delete_is, $scope.delete_cs, $scope.delete_uv, $scope.delete_im, $scope.delete_img, $scope.delete_pulp, $scope.delete_scratch); + for(var du_result of du_results) { + var task = du_result.task; + deleteTaskData(task, $scope.delete_is, $scope.delete_cs, $scope.delete_uv, $scope.delete_im, $scope.delete_img, $scope.delete_pulp, $scope.delete_scratch); + } }; $scope.cancel = function () { @@ -95,6 +145,9 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h }); }; + self.showAllProjectsDiskUsage = function() { + self.showTaskDiskUsage(undefined); + } self.showTaskDiskUsage = function(task) { var modalInstance = $uibModal.open({ @@ -103,6 +156,8 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h <h3 class="modal-title">Disk usage</h3>\ </div>\ <div class="modal-body" style="text-align:right">\ + <highchart id="chart_total_disk_usage" config="totalDiskUsageChartConfig" style="width: 960px; height: 120px; margin-bottom: 20px;" ></highchart>\ + <hr>\ <highchart id="chart_disk_usage" config="diskUsageChartConfig" style="width: 960px; height: 720px;" ></highchart>\ <p>\ <span style="margin-right:50px">Last updated at: {{leastRecentCacheTimestamp | date }}</span>\ @@ -154,6 +209,9 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h type: 'pie', animation: { duration: 200 + }, + legend: { + enabled: false } }, legend: { @@ -191,6 +249,70 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h loading: false } + $scope.totalDiskUsageChartSeries = []; + + var cep4storage_resource = dataService.resources.find(function(r) { return r.name == 'cep4storage'; }); + if(cep4storage_resource) { + $scope.totalDiskUsageChartSeries = [{name:'Free', data:[100.0*cep4storage_resource.available_capacity/cep4storage_resource.total_capacity], color:'#a3f75c'}, + {name:'Used', data:[100.0*cep4storage_resource.used_capacity/cep4storage_resource.total_capacity], color:'#f45b5b'}]; + } + + $scope.totalDiskUsageChartConfig = { + options: { + chart: { + type: 'bar', + animation: { + duration: 200 + }, + legend: { + enabled: false + } + }, + navigation: { + buttonOptions: { + enabled: false + } + + }, + plotOptions: { + bar: { + allowPointSelect: false, + cursor: 'pointer', + dataLabels: { + enabled: false + }, + showInLegend: false, + }, + series: { + stacking: 'normal', + pointWidth: 32 + }, + }, + yAxis: { + visible: true, + title: {text:'Percentage'}, + min: 0, + max: 100, + endOnTick: false + }, + xAxis: { + visible: false + }, + tooltip: { + headerFormat: '{series.name}<br/>', + pointFormat: '{point.name}: <b>{point.percentage:.1f}%</b>' + }, + }, + series: $scope.totalDiskUsageChartSeries, + title: { + text: 'CEP4 total disk usage' + }, + credits: { + enabled: false + }, + loading: false + } + var loadTaskDiskUsage = function(otdb_id) { dataService.getTaskDiskUsageByOTDBId(otdb_id).then(function(result) { if(result.found) { @@ -206,7 +328,7 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h sub_directory_names.sort(function(a, b) { return ((a.name < b.name) ? -1 : ((a.name > b.name) ? 1 : 0)); }); for(var sub_dir of sub_directory_names) { var sub_dir_result = result.sub_directories[sub_dir]; - $scope.diskUsageChartSeries[0].data.push({name:sub_dir_result.name + ' ' + sub_dir_result.disk_usage_readable,y:sub_dir_result.disk_usage}); + $scope.diskUsageChartSeries[0].data.push({name:sub_dir_result.name + ' ' + sub_dir_result.disk_usage_readable,y:sub_dir_result.disk_usage || 0}); if(sub_dir_result.cache_timestamp < $scope.leastRecentCacheTimestamp) { $scope.leastRecentCacheTimestamp = sub_dir_result.cache_timestamp; @@ -215,6 +337,7 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h $scope.leastRecentCacheTimestamp = dataService.convertDatestringToLocalUTCDate($scope.leastRecentCacheTimestamp); }else { $scope.ok(); + $scope.$evalAsync(function() { alert("Could not find disk usage for task " + otdb_id); }); } }); }; @@ -233,7 +356,7 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h for(var sub_dir of sub_directory_names) { var sub_dir_result = result.sub_directories[sub_dir]; $scope.diskUsageChartSeries[0].data.push({name:sub_dir_result.name + ' ' + sub_dir_result.disk_usage_readable, - y:sub_dir_result.disk_usage, + y:sub_dir_result.disk_usage || 0, otdb_id: parseInt(sub_dir_result.name.slice(1)) }); if(sub_dir_result.cache_timestamp < $scope.leastRecentCacheTimestamp) { @@ -243,6 +366,7 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h $scope.leastRecentCacheTimestamp = dataService.convertDatestringToLocalUTCDate($scope.leastRecentCacheTimestamp); }else { $scope.ok(); + $scope.$evalAsync(function() { alert("Could not find disk usage for project " + project_name); }); } }); }; @@ -261,7 +385,7 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h for(var sub_dir of sub_directory_names) { var sub_dir_result = result.sub_directories[sub_dir]; $scope.diskUsageChartSeries[0].data.push({name:sub_dir_result.name + ' ' + sub_dir_result.disk_usage_readable, - y:sub_dir_result.disk_usage, + y:sub_dir_result.disk_usage || 0, project_name: sub_dir_result.name }); if(sub_dir_result.cache_timestamp < $scope.leastRecentCacheTimestamp) { @@ -271,11 +395,16 @@ cleanupControllerMod.controller('CleanupController', ['$scope', '$uibModal', '$h $scope.leastRecentCacheTimestamp = dataService.convertDatestringToLocalUTCDate($scope.leastRecentCacheTimestamp); }else { $scope.ok(); + $scope.$evalAsync(function() { alert("Could not find disk usage for all projects"); }); } }); }; - loadTaskDiskUsage(task.otdb_id); + if(task) { + loadTaskDiskUsage(task.otdb_id); + } else { + loadAllProjectsDiskUsage(); + } } }); }; diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/datacontroller.js b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/datacontroller.js index a2e911f744f381c81d57f04943db12fac368dff8..e9af7da1c89ff7b4625d8f7b020abab012b3b894 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/datacontroller.js +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/datacontroller.js @@ -33,7 +33,7 @@ angular.module('raeApp').factory("dataService", ['$http', '$q', function($http, self.selected_resource_id; self.selected_resourceGroup_id; - self.selected_task_id; + self.selected_task_ids = []; self.selected_project_id; self.selected_resourceClaim_id; @@ -48,6 +48,48 @@ angular.module('raeApp').factory("dataService", ['$http', '$q', function($http, self.viewTimeSpan = {from: new Date(), to: new Date() }; self.autoFollowNow = true; + + self.humanreadablesize = function(num) { + var units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']; + for(unit of units) { + if(Math.abs(num) < 1000.0) { + return num.toPrecision(4).toString() + unit; + } + num /= 1000.0; + } + return num.toPrecision(5).toString() + 'Y'; + } + + self.isTaskIdSelected = function(task_id) { + return self.selected_task_ids.indexOf(task_id) != -1; + } + + self.toggleTaskSelection = function(task_id) { + if(self.isTaskIdSelected(task_id)) { + self.removeSelectedTaskId(task_id); + } else { + self.addSelectedTaskId(task_id); + } + } + + self.addSelectedTaskId = function(task_id) { + if(self.selected_task_ids.indexOf(task_id) == -1) { + self.selected_task_ids.push(task_id); + } + } + + self.removeSelectedTaskId = function(task_id) { + var idx = self.selected_task_ids.indexOf(task_id); + if(idx != -1) { + self.selected_task_ids.splice(idx, 1); + } + } + + self.setSelectedTaskId = function(task_id) { + self.selected_task_ids.splice(0, self.selected_task_ids.length); + self.selected_task_ids.push(task_id); + } + self.floorDate = function(date, hourMod=1, minMod=1) { var min = date.getMinutes(); min = date.getMinutes()/minMod; @@ -246,25 +288,27 @@ angular.module('raeApp').factory("dataService", ['$http', '$q', function($http, if(initialTaskLoad && self.tasks.length > 0) { setTimeout(function() { + self.selected_task_ids.splice(0,self.selected_task_ids.length); + //try to select current task var currentTasks = self.tasks.filter(function(t) { return t.starttime <= self.lofarTime && t.endtime >= self.lofarTime; }); if(currentTasks.length > 0) { - self.selected_task_id = currentTasks[0].id; + self.selected_task_ids.push(currentTasks[0].id); } else { //try to select next task var nextTasks = self.tasks.filter(function(t) { return t.starttime >= self.lofarTime; }).sort(); if(nextTasks.length > 0) { - self.selected_task_id = nextTasks[0].id; + self.selected_task_ids.push(nextTasks[0].id); } else { //try to select most recent task var prevTasks = self.tasks.filter(function(t) { return t.endtime <= self.lofarTime; }).sort(); if(prevTasks.length > 0) { - self.selected_task_id = prevTasks[prevTasks.length-1].id; + self.selected_task_ids.push(prevTasks[prevTasks.length-1].id); } else { - self.selected_task_id = self.tasks[0].id; + self.selected_task_ids.push(self.tasks[0].id); } } } @@ -296,7 +340,18 @@ angular.module('raeApp').factory("dataService", ['$http', '$q', function($http, self.getTaskDiskUsageByOTDBId = function(otdb_id) { var defer = $q.defer(); $http.get('/rest/tasks/otdb/' + otdb_id + '/diskusage').success(function(result) { - console.log(result); + defer.resolve(result); + }).error(function(result) { + defer.resolve({found:false}); + }); + + return defer.promise; + }; + + self.getTaskDiskUsage = function(task) { + var defer = $q.defer(); + $http.get('/rest/tasks/otdb/' + task.otdb_id + '/diskusage').success(function(result) { + result.task = task; defer.resolve(result); }).error(function(result) { defer.resolve({found:false}); @@ -730,24 +785,27 @@ dataControllerMod.controller('DataController', $scope.selectCurrentTask = function() { var currentTasks = dataService.tasks.filter(function(t) { return t.starttime <= dataService.viewTimeSpan.to && t.endime >= dataService.viewTimeSpan.from; }); if(currentTasks.lenght > 0) { - dataService.selected_task_id = currentTasks[0].id; + dataService.setSelectedTaskId(currentTasks[0].id); } }; $scope.jumpToSelectedTask = function() { - if(dataService.selected_task_id == undefined) + if(dataService.selected_task_ids == undefined) return; - var task = dataService.taskDict[dataService.selected_task_id]; + var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; }); - if(task == undefined) + if(tasks.lenght == 0) return; - var taskDurationInmsec = task.endtime.getTime() - task.starttime.getTime(); - var taskDurationInMinutes = taskDurationInmsec/60000; - var viewSpanInMinutes = taskDurationInMinutes; + var minStarttime = new Date(Math.min.apply(null, tasks.map(function(t) { return t.starttime; }))); + var maxEndtime = new Date(Math.max.apply(null, tasks.map(function(t) { return t.endtime; }))); - var fittingSpans = $scope.zoomTimespans.filter(function(w) { return w.value >= taskDurationInMinutes; }); + var selectedTasksDurationInmsec = maxEndtime.getTime() - minStarttime.getTime(); + var selectedTasksDurationInMinutes = selectedTasksDurationInmsec/60000; + var viewSpanInMinutes = selectedTasksDurationInMinutes; + + var fittingSpans = $scope.zoomTimespans.filter(function(w) { return w.value >= selectedTasksDurationInMinutes; }); if(fittingSpans.length > 0) { $scope.zoomTimespan = fittingSpans[0]; //select one span larger if possible @@ -756,7 +814,7 @@ dataControllerMod.controller('DataController', viewSpanInMinutes = $scope.zoomTimespan.value; } - var focusTime = new Date(task.starttime.getTime() + 0.5*taskDurationInmsec); + var focusTime = new Date(minStarttime.getTime() + 0.5*selectedTasksDurationInmsec); dataService.viewTimeSpan = { from: dataService.floorDate(new Date(focusTime.getTime() - 0.4*viewSpanInMinutes*60*1000), 1, 5), @@ -788,11 +846,14 @@ dataControllerMod.controller('DataController', if(dataService.autoFollowNow) { focusTime = dataService.floorDate(dataService.lofarTime, 1, 5); - } else if(dataService.selected_task_id != undefined) { - var task = dataService.taskDict[dataService.selected_task_id]; + } else { + var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; }); + + if(tasks.lenght > 0) { + var minStarttime = new Date(Math.min.apply(null, tasks.map(function(t) { return t.starttime; }))); + var maxEndtime = new Date(Math.max.apply(null, tasks.map(function(t) { return t.endtime; }))); - if(task) { - focusTime = dataService.floorDate(task.starttime, 1, 5); + focusTime = dataService.floorDate(new Date(0.5*(minStarttime.getTime() + maxEndtime.getTime())), 1, 5); } } diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttprojectcontroller.js b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttprojectcontroller.js index 87f6ced5d7b99e3425b14c264dcaaff12083682b..09fd23f3e00d779e9a9082b73effb464b49ee871 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttprojectcontroller.js +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttprojectcontroller.js @@ -59,53 +59,19 @@ ganttProjectControllerMod.controller('GanttProjectController', ['$scope', 'dataS } else if (directiveName === 'ganttTask') { directiveElement.bind('click', function(event) { if(directiveScope.task.model.raTask) { - $scope.dataService.selected_task_id = directiveScope.task.model.raTask.id; + if(event.ctrlKey) { + $scope.dataService.toggleTaskSelection(directiveScope.task.model.raTask.id); + } else { + $scope.dataService.setSelectedTaskId(directiveScope.task.model.raTask.id); + } } }); directiveElement.bind('dblclick', function(event) { if(directiveScope.task.model.raTask) { - $scope.dataService.selected_task_id = directiveScope.task.model.raTask.id; + $scope.dataService.setSelectedTaskId(directiveScope.task.model.raTask.id); $scope.jumpToSelectedTask(); } }); -// directiveElement.bind('contextmenu', function(event) { -// if(directiveScope.task.model.raTask) { -// $scope.dataService.selected_task_id = directiveScope.task.model.raTask.id; -// } -// -// //search for already existing contextmenu element -// if(directiveElement.find('#gantt-project-context-menu').length) { -// //found, remove it, so we can create a fresh one -// directiveElement.find('#gantt-project-context-menu')[0].remove(); -// } -// -// //create contextmenu element -// //with list of menu items, -// //each with it's own action -// var contextmenuElement = angular.element('<div id="gantt-project-context-menu"></div>'); -// ulElement = angular.element('<ul style="z-index:10000; position:fixed; top:initial; left:initial; display:block;" role="menu" class="dropdown-menu"></ul>'); -// contextmenuElement.append(ulElement); -// liElement = angular.element('<li><a href="#">Copy Task</a></li>'); -// ulElement.append(liElement); -// liElement.on('click', function() { -// $scope.dataService.copyTask(directiveScope.task.model.raTask); -// closeContextMenu(); -// }); -// -// var closeContextMenu = function() { -// contextmenuElement.remove(); -// angular.element(document).unbind('click', closeContextMenu); -// }; -// -// //click anywhere to remove the contextmenu -// angular.element(document).bind('click', closeContextMenu); -// -// //add contextmenu to clicked element -// directiveElement.append(contextmenuElement); -// -// //prevent bubbling event upwards -// return false; -// }); } }); @@ -115,7 +81,6 @@ ganttProjectControllerMod.controller('GanttProjectController', ['$scope', 'dataS } if (directiveName === 'ganttTask') { directiveElement.unbind('dblclick'); -// directiveElement.unbind('contextmenu'); } }); } @@ -238,7 +203,7 @@ ganttProjectControllerMod.controller('GanttProjectController', ['$scope', 'dataS movable: $.inArray(task.status_id, editableTaskStatusIds) > -1 }; - if(task.id == dataService.selected_task_id) { + if(dataService.isTaskIdSelected(task.id)) { rowTask.classes += ' task-selected-task'; } @@ -265,7 +230,7 @@ ganttProjectControllerMod.controller('GanttProjectController', ['$scope', 'dataS }; $scope.$watch('dataService.initialLoadComplete', function() { $scope.$evalAsync(updateGanttData); }); - $scope.$watch('dataService.selected_task_id', function() { $scope.$evalAsync(updateGanttData); }); + $scope.$watch('dataService.selected_task_ids', function() { $scope.$evalAsync(updateGanttData); }, true); $scope.$watch('dataService.viewTimeSpan', function() { $scope.$evalAsync(updateGanttData); }, true); $scope.$watch('dataService.filteredTaskChangeCntr', function() { $scope.$evalAsync(updateGanttData); }); $scope.$watch('dataService.lofarTime', function() { diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttresourcecontroller.js b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttresourcecontroller.js index 9ec685de16ea6e07b287373f288b284197c50370..4ce9888a4e68bb635d9a5b06dea50d9acccc1efb 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttresourcecontroller.js +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/ganttresourcecontroller.js @@ -58,17 +58,17 @@ ganttResourceControllerMod.controller('GanttResourceController', ['$scope', 'dat } else if (directiveName === 'ganttTask') { directiveElement.bind('click', function(event) { if(directiveScope.task.model.raTask) { - $scope.dataService.selected_task_id = directiveScope.task.model.raTask.id; + if(event.ctrlKey) { + $scope.dataService.toggleTaskSelection(directiveScope.task.model.raTask.id); + } else { + $scope.dataService.setSelectedTaskId(directiveScope.task.model.raTask.id); + } } if(directiveScope.task.model.claim) { $scope.dataService.selected_resourceClaim_id = directiveScope.task.model.claim.id; } }); directiveElement.bind('contextmenu', function(event) { - if(directiveScope.task.model.raTask) { - $scope.dataService.selected_task_id = directiveScope.task.model.raTask.id; - } - //search for already existing contextmenu element if(directiveElement.find('#gantt-resource-context-menu').length) { //found, remove it, so we can create a fresh one @@ -335,10 +335,9 @@ ganttResourceControllerMod.controller('GanttResourceController', ['$scope', 'dat movable: $.inArray(task.status_id, editableTaskStatusIds) > -1 }; - if(claim.id == dataService.selected_resourceClaim_id) { claimTask.classes += ' claim-selected-claim'; - } else if(task.id == dataService.selected_task_id) { + } else if(dataService.isTaskIdSelected(task.id)) { claimTask.classes += ' claim-selected-task'; } @@ -427,7 +426,7 @@ ganttResourceControllerMod.controller('GanttResourceController', ['$scope', 'dat movable: $.inArray(task.status_id, editableTaskStatusIds) > -1 }; - if(task.id == dataService.selected_task_id) { + if(dataService.isTaskIdSelected(task.id)) { claimTask.classes += ' claim-selected-task'; } @@ -469,7 +468,7 @@ ganttResourceControllerMod.controller('GanttResourceController', ['$scope', 'dat }; $scope.$watch('dataService.initialLoadComplete', function() { $scope.$evalAsync(updateGanttData); }); - $scope.$watch('dataService.selected_task_id', function() { $scope.$evalAsync(updateGanttData); }); + $scope.$watch('dataService.selected_task_ids', function() { $scope.$evalAsync(updateGanttData); }, true); $scope.$watch('dataService.viewTimeSpan', function() { $scope.$evalAsync(updateGanttData); }, true); $scope.$watch('dataService.claimChangeCntr', function() { $scope.$evalAsync(updateGanttData); }); $scope.$watch('dataService.filteredTaskChangeCntr', function() { $scope.$evalAsync(updateGanttData); }); diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/gridcontroller.js b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/gridcontroller.js index 6f13c5ac69bbe04355d96c1fecf6a187bfec0cd8..e64b3574dbb522c36d889ccc511a9e2e69c97134 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/gridcontroller.js +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/controllers/gridcontroller.js @@ -113,8 +113,9 @@ $scope.columns = [ enableRowSelection: true, enableRowHeaderSelection: true, enableFullRowSelection: false, + modifierKeysToMultiSelect: true, + multiSelect:true, enableSelectionBatchEvent:false, - multiSelect:false, gridMenuShowHideColumns: false, columnDefs: $scope.columns, data: [], @@ -135,10 +136,13 @@ $scope.columns = [ var row = rows[i]; if(row.visible) { - var task = taskDict[row.entity.id]; + var task_id = row.entity.id; + var task = taskDict[task_id]; if(task) { $scope.dataService.filteredTasks.push(task); } + + row.setSelected($scope.dataService.selected_task_ids.indexOf(task_id) != -1); } } @@ -154,8 +158,12 @@ $scope.columns = [ }); gridApi.selection.on.rowSelectionChanged($scope,function(row){ - if(row.entity.id && row.isSelected) { - $scope.dataService.selected_task_id = row.entity.id; + if(row.entity.id) { + if(row.isSelected) { + $scope.dataService.addSelectedTaskId(row.entity.id); + } else if(!row.isSelected) { + $scope.dataService.removeSelectedTaskId(row.entity.id); + } } }); } @@ -221,19 +229,30 @@ $scope.columns = [ $scope.$evalAsync(fillGroupsColumFilterSelectOptions); }; - function jumpToSelectedTaskRow() { - var taskIdx = $scope.gridOptions.data.findIndex(function(row) {return row.id == dataService.selected_task_id}); + function jumpToSelectedTaskRows() { + var rowIndices = dataService.selected_task_ids.map(function(t_id) { return $scope.gridOptions.data.findIndex(function(row) {return row.id == t_id; } ); }); + rowIndices = rowIndices.filter(function(idx) {return idx > -1;}).sort(); + + for(var rowIndex of rowIndices) { + $scope.gridApi.core.scrollTo($scope.gridOptions.data[rowIndex], null); + } + }; + + function onSelectedTaskIdsChanged() { + var selected_task_ids = $scope.dataService.selected_task_ids; + var rows = $scope.gridApi.grid.rows; - if(taskIdx > -1) { - $scope.gridApi.selection.selectRow($scope.gridOptions.data[taskIdx]); - $scope.gridApi.core.scrollTo($scope.gridOptions.data[taskIdx], null); + for(var row of rows) { + row.setSelected(selected_task_ids.indexOf(row.entity.id) != -1); } + + $scope.$evalAsync(jumpToSelectedTaskRows); }; $scope.$watch('dataService.taskChangeCntr', function() { $scope.$evalAsync(populateList); }); $scope.$watch('dataService.viewTimeSpan', function() { $scope.$evalAsync(populateList); - $scope.$evalAsync(jumpToSelectedTaskRow); + $scope.$evalAsync(jumpToSelectedTaskRows); }, true); $scope.$watch('dataService.initialLoadComplete', function() { @@ -281,7 +300,7 @@ $scope.columns = [ fillColumFilterSelectOptions(groupSelectOptions, $scope.columns[7]); }; - $scope.$watch('dataService.selected_task_id', jumpToSelectedTaskRow);} + $scope.$watch('dataService.selected_task_ids', onSelectedTaskIdsChanged, true);} ]); gridControllerMod.directive('contextMenu', ['$document', '$window', function($document, $window) { @@ -301,7 +320,12 @@ gridControllerMod.directive('contextMenu', ['$document', '$window', function($do var taskId = rowEntity.id; var task = dataService.taskDict[taskId]; - dataService.selected_task_id = taskId; + if(!task) + return true; + + if(!dataService.isTaskIdSelected(taskId)) { + dataService.setSelectedTaskId(taskId); + } var docElement = angular.element($document); @@ -322,35 +346,36 @@ gridControllerMod.directive('contextMenu', ['$document', '$window', function($do var ulElement = angular.element('<ul class="dropdown-menu" role="menu" style="left:' + event.clientX + 'px; top:' + event.clientY + 'px; z-index: 100000; display:block;"></ul>'); contextmenuElement.append(ulElement); -// var liElement = angular.element('<li><a href="#">Copy Task</a></li>'); -// ulElement.append(liElement); -// liElement.on('click', function() { -// closeContextMenu(); -// dataService.copyTask(task); -// }); - if(task.type == 'observation' && dataService.config.inspection_plots_base_url) { var liElement = angular.element('<li><a href="#">Inspection Plots</a></li>'); ulElement.append(liElement); liElement.on('click', function() { closeContextMenu(); - var url = dataService.config.inspection_plots_base_url + '/' + task.otdb_id; - $window.open(url, '_blank'); + var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; }); + for(var t of tasks) { + if(t) { + var url = dataService.config.inspection_plots_base_url + '/' + t.otdb_id; + $window.open(url, '_blank'); + } + } }); } - var liElement = angular.element('<li><a href="#">Show disk usage</a></li>'); + var liContent = dataService.selected_task_ids.length == 1 ? '<li><a href="#">Show disk usage</a></li>' : '<li><a href="#" style="color:#aaaaaa">Show disk usage</a></li>' + var liElement = angular.element(liContent); ulElement.append(liElement); - liElement.on('click', function() { - closeContextMenu(); - cleanupCtrl.showTaskDiskUsage(task); - }); + if(dataService.selected_task_ids.length == 1) { + liElement.on('click', function() { + closeContextMenu(); + cleanupCtrl.showTaskDiskUsage(task); + }); + } var liElement = angular.element('<li><a href="#">Delete data</a></li>'); ulElement.append(liElement); liElement.on('click', function() { closeContextMenu(); - cleanupCtrl.deleteTaskDataWithConfirmation(task); + cleanupCtrl.deleteSelectedTasksDataWithConfirmation(); }); var closeContextMenu = function(cme) { diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/gantt-plugins/angular-gantt-contextmenu-plugin.js b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/gantt-plugins/angular-gantt-contextmenu-plugin.js index cf1c495d70bde42aa0ef7e4015d7dc7022e0877e..33c7cb4d4343478167ad8846381754173584b884 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/gantt-plugins/angular-gantt-contextmenu-plugin.js +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/static/app/gantt-plugins/angular-gantt-contextmenu-plugin.js @@ -35,7 +35,9 @@ if(!task) return; - dataService.selected_task_id = task.id; + if(!dataService.isTaskIdSelected(task.id)) { + dataService.setSelectedTaskId(task.id); + } //search for already existing contextmenu element while($document.find('#gantt-context-menu').length) { @@ -59,7 +61,7 @@ // liElement.on('click', function() { // closeContextMenu(); // //TODO: remove link to dataService in this generic plugin -// dataService.copyTask(dScope.task.model.raTask); +// dataService.copyTask(task); // }); if(task.type == 'observation' && dataService.config.inspection_plots_base_url) { @@ -67,23 +69,31 @@ ulElement.append(liElement); liElement.on('click', function() { closeContextMenu(); - var url = dataService.config.inspection_plots_base_url + '/' + task.otdb_id; - $window.open(url, '_blank'); + var tasks = dataService.selected_task_ids.map(function(t_id) { return dataService.taskDict[t_id]; }); + for(var t of tasks) { + if(t) { + var url = dataService.config.inspection_plots_base_url + '/' + t.otdb_id; + $window.open(url, '_blank'); + } + } }); } - var liElement = angular.element('<li><a href="#">Show disk usage</a></li>'); + var liContent = dataService.selected_task_ids.length == 1 ? '<li><a href="#">Show disk usage</a></li>' : '<li><a href="#" style="color:#aaaaaa">Show disk usage</a></li>' + var liElement = angular.element(liContent); ulElement.append(liElement); - liElement.on('click', function() { - closeContextMenu(); - cleanupCtrl.showTaskDiskUsage(dScope.task.model.raTask); - }); + if(dataService.selected_task_ids.length == 1) { + liElement.on('click', function() { + closeContextMenu(); + cleanupCtrl.showTaskDiskUsage(task); + }); + } var liElement = angular.element('<li><a href="#">Delete data</a></li>'); ulElement.append(liElement); liElement.on('click', function() { closeContextMenu(); - cleanupCtrl.deleteTaskDataWithConfirmation(dScope.task.model.raTask); + cleanupCtrl.deleteSelectedTasksDataWithConfirmation(); }); var closeContextMenu = function() { diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/templates/index.html b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/templates/index.html index 4261f285027f4e47c2ea8fae426a7cbcc28605fd..c4267fd13be79ab693004f393c1998ec21c64ee5 100644 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/templates/index.html +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/templates/index.html @@ -75,12 +75,12 @@ <uib-timepicker ng-model="$parent.dataService.viewTimeSpan.to" ng-change="$parent.onViewTimeSpanToChanged()" hour-step="1" minute-step="5" show-meridian="false" show-spinners="false"></uib-timepicker> </p> </div> - <div class="col-md-2"> + <div class="col-md-1"> <label>Scroll:</label> <p class="input-group"> <label title="Automatically scroll 'From' and 'To' to watch live events" style="padding-right: 4px; vertical-align: top;">Live <input type="checkbox" ng-model="$parent.dataService.autoFollowNow"></label> <button title="Scroll back in time" type="button" class="btn btn-default" ng-click="scrollBack()"><i class="glyphicon glyphicon-step-backward"></i></button> - <button title="Scroll forward in time"type="button" class="btn btn-default" ng-click="scrollForward()"><i class="glyphicon glyphicon-step-forward"></i></button> + <button title="Scroll forward in time" type="button" class="btn btn-default" ng-click="scrollForward()"><i class="glyphicon glyphicon-step-forward"></i></button> </p> </div> <div class="col-md-2"> @@ -89,6 +89,12 @@ <select class="form-control" ng-model="$parent.zoomTimespan" ng-options="option.name for option in $parent.zoomTimespans track by option.value" ng-change="$parent.onZoomTimespanChanged()"></select> </p> </div> + <div class="col-md-1"> + <label></label> + <p class="input-group"> + <button title="Show disk usage by project" type="button" class="btn btn-default" ng-click="cleanupCtrl.showAllProjectsDiskUsage()"><i class="glyphicon glyphicon-floppy-disk"></i></button> + </p> + </div> </div> <div class="top-stretch" ui-layout options="{flow: 'column'}"> diff --git a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/webservice.py b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/webservice.py index 24bf2d58bb527a522116163f9ce17370be7beda8..d2492c6dd73a971856f354deca6bd2d6446896a6 100755 --- a/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/webservice.py +++ b/SAS/ResourceAssignment/ResourceAssignmentEditor/lib/webservice.py @@ -277,13 +277,13 @@ def cleanupTaskData(task_id): if 'Content-Type' in request.headers and (request.headers['Content-Type'].startswith('application/json') or request.headers['Content-Type'].startswith('text/plain')): delete_params = json.loads(request.data) - print 'delete_params:', delete_params - task = rarpc.getTask(task_id) if not task: abort(404, 'No such task (id=%s)' % task_id) + logger.info("cleanup task data id=%s otdb_id=%s delete_params=%s", task_id, task['otdb_id'], delete_params) + result = curpc.removeTaskData(task['otdb_id'], delete_is=delete_params.get('delete_is', True), delete_cs=delete_params.get('delete_cs', True),