diff --git a/frontend/app/scripts/controllers/case/CaseTasksCtrl.js b/frontend/app/scripts/controllers/case/CaseTasksCtrl.js index 2a295d2d5e..7618d83dfc 100755 --- a/frontend/app/scripts/controllers/case/CaseTasksCtrl.js +++ b/frontend/app/scripts/controllers/case/CaseTasksCtrl.js @@ -18,6 +18,11 @@ $scope.taskResponders = null; $scope.collapseOptions = {}; + $scope.selection = []; + $scope.menu = { + selectAll: false + }; + this.$onInit = function() { $scope.filtering = new FilteringSrv('task', 'task.list', { version: 'v1', @@ -48,7 +53,7 @@ }; $scope.load = function() { - $scope.tasks = new PaginatedQuerySrv({ + $scope.list = new PaginatedQuerySrv({ name: 'case-tasks', root: $scope.caseId, objectType: 'case_task', @@ -71,11 +76,69 @@ extraData: ['shareCount', 'actionRequired'], //extraData: ['isOwner', 'shareCount'], onUpdate: function() { - $scope.buildTaskGroups($scope.tasks.values); + $scope.buildTaskGroups($scope.list.values); + $scope.resetSelection(); } }); }; + $scope.resetSelection = function() { + if ($scope.menu.selectAll) { + $scope.selectAll(); + } else { + $scope.selection = []; + $scope.menu.selectAll = false; + $scope.updateMenu(); + } + }; + + $scope.updateMenu = function() { + // Handle flag/unflag menu items + var temp = _.uniq(_.pluck($scope.selection, 'flag')); + $scope.menu.unflag = temp.length === 1 && temp[0] === true; + $scope.menu.flag = temp.length === 1 && temp[0] === false; + + // Handle close menu item + // temp = _.uniq(_.pluck($scope.selection, 'status')); + // $scope.menu.close = temp.length === 1 && temp[0] === 'Open'; + // $scope.menu.reopen = temp.length === 1 && temp[0] === 'Resolved'; + + // $scope.menu.delete = $scope.selection.length > 0; + }; + + $scope.select = function(task) { + if (task.selected) { + $scope.selection.push(task); + } else { + $scope.selection = _.reject($scope.selection, function(item) { + return item._id === task._id; + }); + } + $scope.updateMenu(); + }; + + $scope.selectAll = function() { + var selected = $scope.menu.selectAll; + + _.each($scope.list.values, function(item) { + // if(SecuritySrv.checkPermissions(['manageCase'], item.extraData.permissions)) { + item.selected = selected; + // } + }); + + if (selected) { + $scope.selection = _.filter($scope.list.values, function(item) { + return !!item.selected; + }); + } else { + $scope.selection = []; + } + + $scope.updateMenu(); + }; + + // ######################@@@ + $scope.toggleStats = function () { $scope.filtering.toggleStats(); }; @@ -215,6 +278,18 @@ }); }; + $scope.bulkFlag = function(flag) { + var ids = _.pluck($scope.selection, '_id'); + + return CaseTaskSrv.bulkUpdate(ids, {flag: flag}) + .then(function(/*responses*/) { + NotificationSrv.log('Selected tasks have been updated successfully', 'success'); + }) + .catch(function(err) { + NotificationSrv.error('Bulk flag tasks', err.data, err.status); + }); + } + // open task tab with its details $scope.startTask = function(task) { var taskId = task._id; diff --git a/frontend/app/scripts/services/api/CaseTaskSrv.js b/frontend/app/scripts/services/api/CaseTaskSrv.js index 6b3e96e8a4..1c8d6c3469 100644 --- a/frontend/app/scripts/services/api/CaseTaskSrv.js +++ b/frontend/app/scripts/services/api/CaseTaskSrv.js @@ -57,6 +57,10 @@ }); }; + this.bulkUpdate = function(ids, update) { + return $http.patch('./api/v1/task/_bulk', _.extend({ids: ids}, update)); + }; + this.removeShare = function(id, share) { return $http.delete('./api/task/'+id+'/shares', { data: { diff --git a/frontend/app/styles/case.css b/frontend/app/styles/case.css index 3a71aae0e0..9ba0437f5f 100644 --- a/frontend/app/styles/case.css +++ b/frontend/app/styles/case.css @@ -80,7 +80,7 @@ table.case-list .case-tags .label, font-size: 12px !important; font-weight: normal; } - +table.data-list .btn-icon, table.case-list .btn-icon { padding: 6px; padding-top: 0; diff --git a/frontend/app/views/partials/case/case.tasks.html b/frontend/app/views/partials/case/case.tasks.html index 0811411f68..55553882aa 100755 --- a/frontend/app/views/partials/case/case.tasks.html +++ b/frontend/app/views/partials/case/case.tasks.html @@ -7,7 +7,7 @@

- Tasks List ({{tasks.values.length || 0}} of {{tasks.total}}) + Tasks List ({{list.values.length || 0}} of {{list.total}})

@@ -17,7 +17,7 @@

-
+
No task found for this case.
@@ -52,17 +52,20 @@

-
+
- + + - + @@ -70,8 +73,12 @@

- + + -
+ + GroupGroup Task DateActions
+ + - - + + @@ -168,33 +175,36 @@

+
- +
-
+
- +
- {{group.group || 'Not Specified'}} ({{group.tasks.length}} task(s)) + {{group.group || 'Not Specified'}} ({{group.list.length}} task(s))
- +
+ - + @@ -204,6 +214,9 @@

+ @@ -304,7 +311,7 @@

- + diff --git a/frontend/app/views/partials/case/tasks/toolbar.html b/frontend/app/views/partials/case/tasks/toolbar.html index afddfc4cd2..ad5cd717c0 100644 --- a/frontend/app/views/partials/case/tasks/toolbar.html +++ b/frontend/app/views/partials/case/tasks/toolbar.html @@ -2,6 +2,31 @@
+
+ +
diff --git a/thehive/app/org/thp/thehive/controllers/v1/Router.scala b/thehive/app/org/thp/thehive/controllers/v1/Router.scala index 5f609a94be..9cdc55f323 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/Router.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/Router.scala @@ -111,6 +111,7 @@ class Router @Inject() ( case GET(p"/task") => taskCtrl.list case POST(p"/task") => taskCtrl.create case GET(p"/task/$taskId") => taskCtrl.get(taskId) + case PATCH(p"/task/_bulk") => taskCtrl.bulkUpdate case PATCH(p"/task/$taskId") => taskCtrl.update(taskId) case GET(p"/task/$taskId/actionRequired") => taskCtrl.isActionRequired(taskId) case PUT(p"/task/$taskId/actionRequired/$orgaId") => taskCtrl.actionRequired(taskId, orgaId, required = true) diff --git a/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala index 57f80452c5..35a8aae662 100644 --- a/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala +++ b/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala @@ -1,5 +1,6 @@ package org.thp.thehive.controllers.v1 +import org.thp.scalligraph._ import org.thp.scalligraph.EntityIdOrName import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser} import org.thp.scalligraph.models.Database @@ -126,6 +127,25 @@ class TaskCtrl @Inject() ( .map(_ => Results.NoContent) } + def bulkUpdate: Action[AnyContent] = + entrypoint("bulk update") + .extract("input", FieldsParser.update("task", publicProperties)) + .extract("ids", FieldsParser.seq[String].on("ids")) + .authTransaction(db) { implicit request => implicit graph => + val properties: Seq[PropertyUpdater] = request.body("input") + val ids: Seq[String] = request.body("ids") + ids + .toTry { id => + taskSrv + .update( + _.get(EntityIdOrName(id)) + .can(Permissions.manageTask), + properties + ) + } + .map(_ => Results.NoContent) + } + def isActionRequired(taskId: String): Action[AnyContent] = entrypoint("is action required") .authTransaction(db) { implicit request => implicit graph =>

+ + GroupGroup Task Date
+ + - - + + @@ -270,28 +283,22 @@

- - - Delete - - - - Reopen - - - - Close - - - - Start - - - - - - + + + + + + + + + + + + + + +