Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Add task bulk actions #1823

Merged
merged 2 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions frontend/app/scripts/controllers/case/CaseTasksCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -48,7 +53,7 @@
};

$scope.load = function() {
$scope.tasks = new PaginatedQuerySrv({
$scope.list = new PaginatedQuerySrv({
name: 'case-tasks',
root: $scope.caseId,
objectType: 'case_task',
Expand All @@ -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();
};
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/scripts/services/api/CaseTaskSrv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/styles/case.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
85 changes: 46 additions & 39 deletions frontend/app/views/partials/case/case.tasks.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="col-md-12 clearfix">
<div class="pull-left">
<h4>
Tasks List ({{tasks.values.length || 0}} of {{tasks.total}})
Tasks List ({{list.values.length || 0}} of {{list.total}})
</h4>
</div>

Expand All @@ -17,7 +17,7 @@ <h4>
</div>
</div>

<div class="row mv-s" ng-show="tasks.total === 0">
<div class="row mv-s" ng-show="list.total === 0">
<div class="col-md-12">
<div class="empty-message">No task found for this case.</div>
</div>
Expand Down Expand Up @@ -52,26 +52,33 @@ <h4>
</div>
</div>

<div class="row" ng-if="!state.showGrouped && tasks.total !== 0">
<div class="row" ng-if="!state.showGrouped && list.total !== 0">
<div class="col-md-12">

<psearch control="tasks"></psearch>
<psearch control="list"></psearch>

<table class="table table-hover valigned tasks-table">
<thead>
<tr>
<th width="20px">
<input if-permission="manageTask" allowed="{{userPermissions}}" type="checkbox" ng-model="menu.selectAll" ng-change="selectAll()">
</th>
<th style="width: 20px"></th>
<th style="width: 40px"></th>
<th style="width: 250px">Group</th>
<th style="width: 150px">Group</th>
<th>Task</th>
<th style="width: 70px;"></th>
<th style="width: 150px">Date</th>
<th style="width: 150px">Assignee</th>
<th style="width: 250px" class="text-right" if-permission="manageTask" allowed="{{userPermissions}}">Actions</th>
</tr>
</thead>
<tbody ng-repeat="task in tasks.values">
<tbody ng-repeat="task in list.values">
<tr class="task-row" ng-class="{'warning': (task.status=== 'Waiting' || task.status=== 'InProgress') && task.flag == true}">
<td>
<input if-permission="manageTask" allowed="{{userPermissions}}"
type="checkbox" ng-model="task.selected" ng-change="select(task)">
</td>
<td>
<a href ng-click="collapseOptions[task._id] = !collapseOptions[task._id]" ng-show="task.description" uib-tooltip="Show/Hide description" tooltip-placement="right">
<i class="fa" ng-class="{
Expand All @@ -81,8 +88,8 @@ <h4>
</a>
</td>
<td class="task-status" align="center" ng-switch="task.status">
<i ng-switch-when="Completed" class="text-success glyphicon glyphicon-ok"></i>
<i ng-switch-when="InProgress" class="glyphicon" ng-class="{true:'text-yellow glyphicon-flag', false:'text-primary glyphicon-play'}[task.flag]"></i>
<i ng-switch-when="Completed" class="text-success glyphicon glyphicon-ok" uib-tooltip="Completed"></i>
<i ng-switch-when="InProgress" class="glyphicon" ng-class="{true:'text-yellow glyphicon-flag', false:'text-primary glyphicon-play'}[task.flag]" uib-tooltip="In Progress"></i>
<i ng-switch-when="Waiting" class="glyphicon" ng-class="{true:'text-yellow glyphicon-flag'}[task.flag]"></i>
</td>
<td ng-if="canEdit">
Expand Down Expand Up @@ -168,33 +175,36 @@ <h4>
</tr>

<tr ng-if="task.description && collapseOptions[task._id]">
<td colspan="7" class="wrap">
<td colspan="9" class="wrap">
<div marked="task.description" class="mt-xxs filter-panel markdown"></div>
</td>
</tr>
</tbody>
</table>

<psearch control="tasks"></psearch>
<psearch control="list"></psearch>
</div>
</div>

<div class="row" ng-if="state.showGrouped && tasks.total !== 0">
<div class="row" ng-if="state.showGrouped && list.total !== 0">
<div class="col-md-12">

<psearch control="tasks"></psearch>
<psearch control="list"></psearch>

<div class="panel panel-default" ng-repeat="group in groupedTasks">
<div class="panel-heading">
<strong>{{group.group || 'Not Specified'}}</strong> <small>({{group.tasks.length}} task(s))</small>
<strong>{{group.group || 'Not Specified'}}</strong> <small>({{group.list.length}} task(s))</small>
</div>
<div class="panel-body p-0">
<table class="table table-hover valigned tasks-table">
<table class="table table-hover valigned tasks-table data-list">
<thead>
<tr>
<th width="20px">
<input if-permission="manageTask" allowed="{{userPermissions}}" type="checkbox" ng-model="menu.selectAll" ng-change="selectAll()">
</th>
<th style="width: 20px"></th>
<th style="width: 40px"></th>
<th style="width: 250px">Group</th>
<th style="width: 150px">Group</th>
<th>Task</th>
<th style="width: 70px;"></th>
<th style="width: 150px">Date</th>
Expand All @@ -204,6 +214,9 @@ <h4>
</thead>
<tbody ng-repeat="task in group.tasks">
<tr class="task-row" ng-class="{'warning': (task.status=== 'Waiting' || task.status=== 'InProgress') && task.flag == true}">
<td>
<input if-permission="manageTask" allowed="{{userPermissions}}" type="checkbox" ng-model="task.selected" ng-change="select(task)">
</td>
<td>
<a href ng-click="collapseOptions[task._id] = !collapseOptions[task._id]" ng-show="task.description" uib-tooltip="Show/Hide description" tooltip-placement="right">
<i class="fa" ng-class="{
Expand All @@ -213,8 +226,8 @@ <h4>
</a>
</td>
<td class="task-status" align="center" ng-switch="task.status">
<i ng-switch-when="Completed" class="text-success glyphicon glyphicon-ok"></i>
<i ng-switch-when="InProgress" class="glyphicon" ng-class="{true:'text-yellow glyphicon-flag', false:'text-primary glyphicon-play'}[task.flag]"></i>
<i ng-switch-when="Completed" class="text-success glyphicon glyphicon-ok" uib-tooltip="Completed"></i>
<i ng-switch-when="InProgress" class="glyphicon" ng-class="{true:'text-yellow glyphicon-flag', false:'text-primary glyphicon-play'}[task.flag]" uib-tooltip="In Porgress"></i>
<i ng-switch-when="Waiting" class="glyphicon" ng-class="{true:'text-yellow glyphicon-flag'}[task.flag]"></i>
</td>
<td ng-if="canEdit">
Expand Down Expand Up @@ -270,28 +283,22 @@ <h4>
</td>
<!-- class="task-delete" -->
<td align="right" class="task-actions" if-permission="manageTask" allowed="{{userPermissions}}">
<span class="ml-xs clickable text-danger task-delete" ng-click="removeTask(task)">
<i class="fa fa-times"></i>
Delete
</span>
<span class="ml-xs clickable text-success action-button" ng-show="task.status == 'Completed'" ng-click="openTask(task)">
<i class="fa fa-check-circle"></i>
Reopen
</span>
<span class="ml-xs clickable text-muted action-button" ng-show="task.status == 'InProgress'" ng-click="closeTask(task)">
<i class="fa fa-check-circle-o"></i>
Close
</span>
<span class="ml-xs clickable text-primary action-button" ng-show="task.status == 'Waiting'" ng-click="startTask(task)">
<i class="fa fa-play"></i>
Start
</span>

<span class="ml-xs" uib-dropdown ng-if="appConfig.connectors.cortex.enabled" if-permission="manageAction" allowed="{{userPermissions}}">
<a href class="text-primary noline nowrap" ng-click="getTaskResponders(task, true)">
<i class="text-primary fa fa-cog"></i>
</a>
</span>
<a class="btn btn-icon btn-clear" ng-click="removeTask(task)" uib-tooltip="Delete">
<i class="fa fa-trash text-danger"></i>
</a>
<a class="btn btn-icon btn-clear" ng-show="task.status == 'Completed'" ng-click="openTask(task)" uib-tooltip="Reopen">
<i class="fa fa-check-circle text-success"></i>
</a>
<a class="btn btn-icon btn-clear" ng-show="task.status == 'InProgress'" ng-click="closeTask(task)" uib-tooltip="Close">
<i class="fa fa-check-circle-o text-muted"></i>
</a>
<a class="btn btn-icon btn-clear" ng-show="task.status == 'Waiting'" ng-click="startTask(task)" uib-toolip="Start">
<i class="fa fa-play text-primary"></i>
</a>
<a href class="btn btn-icon btn-clear" ng-click="getTaskResponders(task, true)" ng-if="appConfig.connectors.cortex.enabled" if-permission="manageAction" allowed="{{userPermissions}}">
<i class="text-primary fa fa-cog"></i>
</a>
</td>
</tr>
<tr ng-if="task.description && collapseOptions[task._id]">
Expand All @@ -304,7 +311,7 @@ <h4>
</div>
</div>

<psearch control="tasks"></psearch>
<psearch control="list"></psearch>

</div>
</div>
38 changes: 31 additions & 7 deletions frontend/app/views/partials/case/tasks/toolbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@
<div class="col-md-12">
<div class="btn-toolbar" role="toolbar">

<div class="btn-group" uib-dropdown if-permission="manageTask" allowed="{{userPermissions}}">
<button type="button" class="btn btn-sm btn-default" uib-dropdown-toggle ng-disabled="selection.length === 0">
<ng-pluralize count="selection.length" when="{'0': 'No tasks selected', 'one': '{} selected task', 'other': '{} selected tasks'}"></ng-pluralize>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu>
<li ng-if="menu.flag">
<a href ng-click="bulkFlag(true)"><i class="fa fa-flag"></i> Add flag</a>
</li>
<li ng-if="menu.unflag">
<a href ng-click="bulkFlag(false)"><i class="fa fa-flag-o"></i> Remove flag</a>
</li>
<!-- <li ng-if="$vm.menu.reopen">
<a href ng-click="$vm.bulkReopen()"><i class="fa fa-folder-open"></i> Reopen</a>
</li>
<li ng-if="$vm.menu.close">
<a href ng-click="$vm.bulkClose()"><i class="fa fa-folder"></i> Close</a>
</li>
<li ng-if="$vm.menu.delete" class="divider"></li>
<li>
<a href ng-click="$vm.bulkRemove()"><i class="fa fa-trash"></i> Delete</a>
</li> -->
</ul>
</div>

<div class="btn-group" if-permission="manageTask" allowed="{{userPermissions}}">
<button class="btn btn-sm btn-primary" ng-click="state.isNewTask = true">
<i class="glyphicon glyphicon-plus"></i>
Expand Down Expand Up @@ -36,13 +61,6 @@
</ul>
</div>

<div class="btn-group">
<button class="btn btn-sm btn-default" ng-click="toggleGroupedView()">
<i class="glyphicon glyphicon-th-list"></i>
Show {{state.showGrouped ? 'as a List' : 'Groups'}}
</button>
</div>

<div class="btn-group pull-right" role="group">
<page-sizer collection="tasks" sizes="[10, 15, 30, 100]"></page-sizer>
</div>
Expand All @@ -52,6 +70,12 @@
<i class="fa fa-search"></i> Filters
</button>
</div>
<div class="btn-group pull-right">
<button class="btn btn-sm btn-default" ng-click="toggleGroupedView()">
<i class="glyphicon glyphicon-th-list"></i>
Show {{state.showGrouped ? 'as a List' : 'Groups'}}
</button>
</div>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions thehive/app/org/thp/thehive/controllers/v1/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading