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

Show active users modal #2863

Merged
merged 32 commits into from
Aug 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4c0ccc9
Added usersWithOpenTasks to the admin api. Added a button to the proj…
MichaelBuessemeyer Jul 4, 2018
c65843c
added method to transfer many tasks at once to admin_rest_api, create…
MichaelBuessemeyer Jul 6, 2018
18129e5
adding a fetch function, that fetches all users with open tasks -> on…
MichaelBuessemeyer Jul 6, 2018
a94b18f
creating mock data, ongoing
MichaelBuessemeyer Jul 6, 2018
d285460
added mock data, the modal is now working and displayable but has no …
MichaelBuessemeyer Jul 8, 2018
93bce49
fixed flow errors
MichaelBuessemeyer Jul 8, 2018
f31013b
fixed flow errors and added some error handling if no project was sel…
MichaelBuessemeyer Jul 9, 2018
a3acacf
[WIP] transfer annotations to different user for api #2862
Jul 9, 2018
5628221
Merge branch 'master' of github.com:scalableminds/webknossos into add…
Jul 9, 2018
df84522
[WIP] route for transferring active tasks of a project to a user #2862
Jul 9, 2018
b88a2ea
routes for showing active tasks and transfering them #2862
Jul 9, 2018
0aa6baf
Merge branch 'master' of github.com:scalableminds/webknossos into sho…
MichaelBuessemeyer Jul 9, 2018
16c6f52
Merge branch 'show-active-users-modal' of github.com:scalableminds/we…
Jul 9, 2018
7c958f5
added changelog, moved error message to messages, fixed flow error
MichaelBuessemeyer Jul 9, 2018
9c21832
added the request for active users to the admin_rest_api and used it …
MichaelBuessemeyer Jul 11, 2018
7fc64d3
added a type for activeUsers, added an api request to transfer all ac…
MichaelBuessemeyer Jul 12, 2018
923aaba
reverted changelog -> entry for this issue still missing
MichaelBuessemeyer Jul 12, 2018
586f21d
added changelog entry
MichaelBuessemeyer Jul 12, 2018
f368318
Merge branch 'master' into show-active-users-modal
MichaelBuessemeyer Jul 12, 2018
dd9c02b
added message for successful transfer, added transfer functionality, …
MichaelBuessemeyer Jul 12, 2018
1bf3214
Merge branch 'show-active-users-modal' of github.com:scalableminds/we…
MichaelBuessemeyer Jul 12, 2018
6ea5366
corrected changelog entry
MichaelBuessemeyer Jul 12, 2018
69ca333
added error handling to the transfer request, added an error message …
MichaelBuessemeyer Jul 12, 2018
3d0c911
refeshed snapshots
MichaelBuessemeyer Jul 12, 2018
330e19c
merge master into show-active-users-modal
fm3 Jul 30, 2018
c00e535
refactored requested changes
MichaelBuessemeyer Aug 9, 2018
b3e83a7
resolved conflicts
MichaelBuessemeyer Aug 9, 2018
464ac2e
updated snapshots and resolved auto merging errors
MichaelBuessemeyer Aug 9, 2018
f3c31b8
fixed flow type problems
MichaelBuessemeyer Aug 10, 2018
970f3d7
fixed linter issues
MichaelBuessemeyer Aug 10, 2018
0a2aba0
added a spinner while fetching data
MichaelBuessemeyer Aug 10, 2018
6b5b4af
disallow transferring tasks to users who cannot access the dataset
fm3 Aug 13, 2018
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
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