Skip to content

Commit

Permalink
Show active users modal (#2863)
Browse files Browse the repository at this point in the history
* Added usersWithOpenTasks to the admin api. Added a button to the projects-view that displays all users with open tasks of the selected project in a modal

* added method to transfer many tasks at once to admin_rest_api, created a modified copy of the task_transfer_modal to control a bulk task transfer

* adding a fetch function, that fetches all users with open tasks -> onhold, waiting for backend to provide the api request

* creating mock data, ongoing

* added mock data, the modal is now working and displayable but has no real functionallity

* fixed flow errors

* fixed flow errors and added some error handling if no project was selected

* [WIP] transfer annotations to different user for api #2862

* [WIP] route for transferring active tasks of a project to a user #2862

* routes for showing active tasks and transfering them #2862

* added changelog, moved error message to messages, fixed flow error

* added the request for active users to the admin_rest_api and used it to display the table in the modal

* added a type for activeUsers, added an api request to transfer all active tasks of a project, transfer modal only renders if set to visible

* reverted changelog -> entry for this issue still missing

* added changelog entry

* added message for successful transfer, added transfer functionality, and change api call to post-type

* corrected changelog entry

* added error handling to the transfer request, added an error message to messages

* refeshed snapshots

* refactored requested changes

* updated snapshots and resolved auto merging errors

* fixed flow type problems

* fixed linter issues

* added a spinner while fetching data

* disallow transferring tasks to users who cannot access the dataset
  • Loading branch information
MichaelBuessemeyer authored and fm3 committed Aug 13, 2018
1 parent ea0856f commit d099fd7
Show file tree
Hide file tree
Showing 17 changed files with 554 additions and 81 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
- Added shortcuts for moving along the current tracing direction in orthogonal mode. Pressing 'e' (and 'r' for the reverse direction) will move along the "current direction", which is defined by the vector between the last two created nodes.
- Added a banner to the user list to notify admins of new inactive users that need to be activated. [#2994](https://github.com/scalableminds/webknossos/pull/2994)
- When a lot of changes need to be persisted to the server (e.g., after importing a large NML), the save button will show a percentage-based progress indicator.
- Added placeholders and functionality hints to (nearly) empty lists and tables in the admin views. [#2969](https://github.com/scalableminds/webknossos/pull/2969)
- Added the possibility for admins to see and transfer all active tasks of a project to a single user in the project tab[#2863](https://github.com/scalableminds/webknossos/pull/2863)
- Added the possibility to import multiple NML files into the active tracing. This can be done by dragging and dropping the files directly into the tracing view. [#2908](https://github.com/scalableminds/webknossos/pull/2908)
- Added placeholders and functionality hints to (nearly) empty lists and tables in the admin views. [#2969](https://github.com/scalableminds/webknossos/pull/2969)
- Added the possibility to copy volume tracings to own account
- During the import of multiple NML files, the user can select an option to automatically create a group per file so that the imported trees are organized in a hierarchy. [#2908](https://github.com/scalableminds/webknossos/pull/2908)
- Added functions to the front-end API to activate a tree and to change the color of a tree. [#2997](https://github.com/scalableminds/webknossos/pull/2997)
Expand Down
19 changes: 19 additions & 0 deletions app/assets/javascripts/admin/admin_rest_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
APIFeatureToggles,
APIOrganizationType,
ServerTracingType,
APIActiveUserType,
HybridServerTracingType,
ServerSkeletonTracingType,
ServerVolumeTracingType,
Expand Down Expand Up @@ -408,6 +409,24 @@ export function transferTask(annotationId: string, userId: string): Promise<APIA
});
}

export async function transferActiveTasksOfProject(
projectName: string,
userId: string,
): Promise<APIAnnotationType> {
return Request.sendJSONReceiveJSON(`/api/projects/${projectName}/transferActiveTasks`, {
data: {
userId,
},
method: "POST",
});
}

export async function getUsersWithActiveTasks(
projectName: string,
): Promise<Array<APIActiveUserType>> {
return Request.receiveJSON(`/api/projects/${projectName}/usersWithActiveTasks`);
}

// ### Annotations
export function reOpenAnnotation(
annotationId: string,
Expand Down
5 changes: 5 additions & 0 deletions app/assets/javascripts/admin/api_flow_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ export type APIUserLoggedTimeType = {
loggedTime: Array<APITimeIntervalType>,
};

export type APIActiveUserType = {
email: string,
activeTasks: number,
};

export type APIRestrictionsType = {|
+allowAccess: boolean,
+allowUpdate: boolean,
Expand Down
30 changes: 28 additions & 2 deletions app/assets/javascripts/admin/project/project_list_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
pauseProject,
resumeProject,
} from "admin/admin_rest_api";
import TransferAllTasksModal from "admin/project/transfer_all_tasks_modal";
import Persistence from "libs/persistence";
import { PropTypes } from "@scalableminds/prop-types";
import type { APIProjectType, APIUserType } from "admin/api_flow_types";
Expand All @@ -38,6 +39,8 @@ type State = {
isLoading: boolean,
projects: Array<APIProjectType>,
searchQuery: string,
isTransferTasksVisible: boolean,
selectedProject: ?APIProjectType,
};

const persistence: Persistence<State> = new Persistence(
Expand All @@ -50,6 +53,8 @@ class ProjectListView extends React.PureComponent<Props, State> {
isLoading: true,
projects: [],
searchQuery: "",
isTransferTasksVisible: false,
selectedProject: null,
};

componentWillMount() {
Expand Down Expand Up @@ -138,6 +143,18 @@ class ProjectListView extends React.PureComponent<Props, State> {
});
};

showActiveUsersModal = async (project: APIProjectType) => {
this.setState({
selectedProject: project,
isTransferTasksVisible: true,
});
};

onTaskTransferComplete = () => {
this.setState({ isTransferTasksVisible: false });
this.fetchData();
};

renderPlaceholder() {
return this.state.isLoading ? null : (
<React.Fragment>
Expand Down Expand Up @@ -170,7 +187,6 @@ class ProjectListView extends React.PureComponent<Props, State> {
</div>
<h3>Projects</h3>
<div className="clearfix" style={{ margin: "20px 0px" }} />

<Spin spinning={this.state.isLoading} size="large">
<Table
dataSource={Utils.filterWithSearchQueryOR(
Expand Down Expand Up @@ -283,7 +299,10 @@ class ProjectListView extends React.PureComponent<Props, State> {
<Icon type="download" />Download
</a>
<br />

<a onClick={_.partial(this.showActiveUsersModal, project)}>
<Icon type="team" />Show active users
</a>
<br />
{project.owner.email === this.props.activeUser.email ? (
<a onClick={_.partial(this.deleteProject, project)}>
<Icon type="delete" />Delete
Expand All @@ -294,6 +313,13 @@ class ProjectListView extends React.PureComponent<Props, State> {
/>
</Table>
</Spin>
{this.state.isTransferTasksVisible ? (
<TransferAllTasksModal
project={this.state.selectedProject}
onCancel={this.onTaskTransferComplete}
onComplete={this.onTaskTransferComplete}
/>
) : null}
</div>
</div>
);
Expand Down
162 changes: 162 additions & 0 deletions app/assets/javascripts/admin/project/transfer_all_tasks_modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// @flow

import _ from "lodash";
import * as React from "react";
import { Modal, Button, Table, Spin } from "antd";
import {
getUsers,
getUsersWithActiveTasks,
transferActiveTasksOfProject,
} from "admin/admin_rest_api";
import type { APIUserType, APIProjectType, APIActiveUserType } from "admin/api_flow_types";
import Toast from "libs/toast";
import messages from "messages";
import { handleGenericError } from "libs/error_handling";
import UserSelectionComponent from "admin/user/user_selection_component";

type Props = {
project: ?APIProjectType,
onCancel: () => void,
onComplete: () => void,
};

type State = {
users: Array<APIUserType>,
selectedUser: ?APIUserType,
usersWithActiveTasks: Array<APIActiveUserType>,
isLoading: boolean,
};

class TransferAllTasksModal extends React.PureComponent<Props, State> {
state = {
users: [],
selectedUser: null,
usersWithActiveTasks: [],
isLoading: false,
};

componentDidMount() {
this.fetchData();
}

async fetchData() {
try {
this.setState({ isLoading: true });
const users = await getUsers();
const activeUsers = users.filter(u => u.isActive);
const usersWithActiveTasks = this.props.project
? await getUsersWithActiveTasks(this.props.project.name)
: [];
const sortedUsers = _.sortBy(activeUsers, "lastName");
this.setState({
users: sortedUsers,
usersWithActiveTasks,
});
} catch (error) {
handleGenericError(error);
} finally {
this.setState({ isLoading: false });
}
}

transferAllActiveTasks = async () => {
if (!this.state.selectedUser || !this.props.project) {
return;
}
try {
const selectedUser = this.state.selectedUser;
await transferActiveTasksOfProject(this.props.project.name, selectedUser.id);
if (selectedUser) {
Toast.success(
`${messages["project.successful_active_tasks_transfer"]} ${selectedUser.lastName}, ${
selectedUser.firstName
}`,
);
}
this.props.onComplete();
} catch (e) {
Toast.error(messages["project.unsuccessful_active_tasks_transfer"]);
}
};

renderTableContent() {
const activeUsersWithKey = this.state.usersWithActiveTasks.map(activeUser => ({
email: activeUser.email,
activeTasks: activeUser.activeTasks,
key: activeUser.email,
}));
const columns = [
{
title: "User Email",
dataIndex: "email",
key: "email",
},
{
title: "Number of Active Tasks",
dataIndex: "activeTasks",
key: "activeTasks",
},
];
return (
<Table
columns={columns}
dataSource={activeUsersWithKey}
rowKey="email"
pagination={false}
size="small"
/>
);
}

handleSelectChange = (userId: string) => {
const selectedUser = this.state.users.find(user => user.id === userId);
this.setState({ selectedUser });
};

render() {
const project = this.props.project;
if (!project) {
return (
<Modal title="Error" visible onOk={this.props.onCancel} onCancel={this.props.onCancel}>
<p>{messages["project.none_selected"]}</p>
</Modal>
);
} else {
const title = `All users with open tasks of ${project.name}`;
return (
<Modal
title={title}
visible
onCancel={this.props.onCancel}
pagination="false"
footer={
<div>
<Button
type="primary"
disabled={!this.state.selectedUser}
onClick={this.transferAllActiveTasks}
>
Transfer all tasks
</Button>
<Button onClick={this.props.onCancel}>Close</Button>
</div>
}
>
<div>
{this.state.isLoading ? <Spin size="large" /> : this.renderTableContent()}
<br />
<br />
</div>
Select a user to transfer the tasks to:
<div className="control-group">
<div className="form-group">
<UserSelectionComponent handleSelection={this.handleSelectChange} />
</div>
</div>
</Modal>
);
}
}
}

export default TransferAllTasksModal;
81 changes: 81 additions & 0 deletions app/assets/javascripts/admin/user/user_selection_component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// @flow

import _ from "lodash";
import * as React from "react";
import { Spin, Select } from "antd";
import { getUsers } from "admin/admin_rest_api";
import type { APIUserType } from "admin/api_flow_types";
import { handleGenericError } from "libs/error_handling";

const { Option } = Select;

type Props = {
handleSelection: string => void,
};

type State = {
isLoading: boolean,
users: Array<APIUserType>,
currentUserIdValue: string,
};

class UserSelectionComponent extends React.PureComponent<Props, State> {
state = {
isLoading: false,
users: [],
currentUserIdValue: "",
};

componentDidMount() {
this.fetchData();
}

async fetchData() {
try {
this.setState({ isLoading: true });
const users = await getUsers();
const activeUsers = users.filter(u => u.isActive);
const sortedUsers = _.sortBy(activeUsers, "lastName");
this.setState({
users: sortedUsers,
});
} catch (error) {
handleGenericError(error);
} finally {
this.setState({ isLoading: false });
}
}

handleSelectChange = (userId: string) => {
this.setState({ currentUserIdValue: userId });
this.props.handleSelection(userId);
};

render() {
return this.state.isLoading ? (
<div className="text-center">
<Spin size="large" />
</div>
) : (
<Select
showSearch
placeholder="Select a New User"
value={this.state.currentUserIdValue}
onChange={this.handleSelectChange}
optionFilterProp="children"
style={{ width: "100%" }}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{this.state.users.map(user => (
<Option key={user.id} value={user.id}>
{`${user.lastName}, ${user.firstName} ${user.email}`}
</Option>
))}
</Select>
);
}
}

export default UserSelectionComponent;
Loading

0 comments on commit d099fd7

Please sign in to comment.