Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
55 changes: 53 additions & 2 deletions pydatalab/src/pydatalab/routes/v0_1/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,22 @@ def get_users():
"then": "user",
"else": {"$arrayElemAt": ["$role.role", 0]},
}
}
},
"immutable_id": {"$ifNull": ["$immutable_id", {"$toString": "$_id"}]},
}
},
]
)

return jsonify({"status": "success", "data": list(users)})
users_list = list(users)

for user in users_list:
if "managers" not in user:
user["managers"] = []
elif not isinstance(user["managers"], list):
user["managers"] = []

return jsonify({"status": "success", "data": users_list})


@ADMIN.route("/roles/<user_id>", methods=["PATCH"])
Expand Down Expand Up @@ -93,3 +102,45 @@ def save_role(user_id):
)

return (jsonify({"status": "success"}), 200)


@ADMIN.route("/users/<user_id>/managers", methods=["PATCH"])
def update_user_managers(user_id):
"""Update the managers for a specific user using ObjectIds"""
request_json = request.get_json()

if request_json is None or "managers" not in request_json:
return jsonify({"status": "error", "message": "Managers list not provided"}), 400

managers = request_json["managers"]

if not isinstance(managers, list):
return jsonify({"status": "error", "message": "Managers must be a list"}), 400

existing_user = flask_mongo.db.users.find_one({"_id": ObjectId(user_id)})
if not existing_user:
return jsonify({"status": "error", "message": "User not found"}), 404

manager_object_ids = []
for manager_id in managers:
if manager_id:
try:
manager_oid = ObjectId(manager_id)
if not flask_mongo.db.users.find_one({"_id": manager_oid}):
return jsonify(
{"status": "error", "message": f"Manager with ID {manager_id} not found"}
), 404
manager_object_ids.append(str(manager_oid))
except (TypeError, ValueError):
return jsonify(
{"status": "error", "message": f"Invalid manager ID format: {manager_id}"}
), 400

update_result = flask_mongo.db.users.update_one(
{"_id": ObjectId(user_id)}, {"$set": {"managers": manager_object_ids}}
)

if update_result.matched_count != 1:
return jsonify({"status": "error", "message": "Unable to update user managers"}), 400

return jsonify({"status": "success"}), 200
206 changes: 180 additions & 26 deletions webapp/src/components/UserTable.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<template>
<table class="table table-hover table-sm" data-testid="user-table">
<table class="table table-hover table-sm table-responsive-sm" data-testid="user-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Managers</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user._id">
<tr v-for="user in users" :key="user.immutable_id">
<td align="left">
{{ user.display_name }}
<span v-if="user.account_status === 'active'" class="badge badge-success text-uppercase">
Expand All @@ -30,35 +31,61 @@
</td>
<td align="left">{{ user.contact_email }}</td>
<td align="left">
<select
<vSelect
v-model="user.role"
class="dropdown"
@change="confirmUpdateUserRole(user._id.$oid, $event.target.value)"
:options="roleOptions"
:clearable="false"
:searchable="false"
class="form-control p-0 border-0"
@update:model-value="(value) => confirmUpdateUserRole(user.immutable_id, value)"
/>
</td>
<td align="left">
<vSelect
v-model="user.managers"
:options="potentialManagersMap[user.immutable_id]"
label="display_name"
multiple
placeholder="No managers"
:clearable="false"
class="w-100"
@update:model-value="(value) => handleManagersChange(user.immutable_id, value)"
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="manager">Manager</option>
</select>
<template #option="option">
<div class="d-flex align-items-center">
<UserBubble :creator="option" :size="20" />
<span class="ml-2">{{ option.display_name }}</span>
<span class="ml-auto badge badge-secondary small">{{ option.role }}</span>
</div>
</template>

<template #selected-option="option">
<div class="d-flex align-items-center">
<UserBubble :creator="option" :size="18" />
<span class="ml-2 small">{{ option.display_name }}</span>
</div>
</template>
</vSelect>
</td>
<td align="left">
<button
v-if="user.account_status === 'active'"
class="btn btn-outline-danger btn-sm text-uppercase text-monospace"
@click="confirmUpdateUserStatus(user._id.$oid, 'deactivated')"
@click="confirmUpdateUserStatus(user.immutable_id, 'deactivated')"
>
Deactivate
</button>
<button
v-else-if="user.account_status === 'unverified'"
class="btn btn-outline-success btn-sm text-uppercase text-monospace"
@click="confirmUpdateUserStatus(user._id.$oid, 'active')"
@click="confirmUpdateUserStatus(user.immutable_id, 'active')"
>
Activate
</button>
<button
v-else-if="user.account_status === 'deactivated'"
class="btn btn-outline-success btn-sm text-uppercase text-monospace"
@click="confirmUpdateUserStatus(user._id.$oid, 'active')"
@click="confirmUpdateUserStatus(user.immutable_id, 'active')"
>
Activate
</button>
Expand All @@ -70,71 +97,198 @@

<script>
import { DialogService } from "@/services/DialogService";

import { getUsersList, saveRole, saveUser } from "@/server_fetch_utils.js";
import vSelect from "vue-select";
import UserBubble from "@/components/UserBubble.vue";
import { getUsersList, saveRole, saveUser, saveUserManagers } from "@/server_fetch_utils.js";

export default {
components: {
vSelect,
UserBubble,
},

data() {
return {
users: null,
original_users: null,
tempRole: null,
roleOptions: ["user", "admin", "manager"],
};
},
computed: {
potentialManagersMap() {
if (!this.users) return {};
const map = {};
this.users.forEach((u) => {
map[u.immutable_id] = this.users.filter(
(user) =>
user.immutable_id !== u.immutable_id &&
(user.role === "admin" || user.role === "manager"),
);
});
return map;
},
},
created() {
this.getUsers();
},
methods: {
async getUsers() {
let data = await getUsersList();
if (data != null) {
const byId = {};
data.forEach((u) => {
const id = u.immutable_id || u._id;
byId[id] = u;
});

data.forEach((user) => {
if (!user.managers) {
user.managers = [];
}

user.managers = user.managers
.map((m) => {
const mid = typeof m === "string" ? m : m.$oid || m.immutable_id;
return byId[mid];
})
.filter(Boolean);
});

this.users = JSON.parse(JSON.stringify(data));
this.original_users = JSON.parse(JSON.stringify(data));
}
},
getPotentialManagers(userId) {
if (!this.users) return [];

const potentials = this.users.filter((u) => {
const isEligible =
u.immutable_id !== userId && (u.role === "admin" || u.role === "manager");
return isEligible;
});

return potentials.sort((a, b) => (a.display_name || "").localeCompare(b.display_name || ""));
},

async confirmUpdateUserRole(user_id, new_role) {
const originalCurrentUser = this.original_users.find((user) => user._id.$oid === user_id);
const originalCurrentUser = this.original_users.find((user) => user.immutable_id === user_id);

if (originalCurrentUser.role === "admin") {
if (!originalCurrentUser) {
DialogService.error({
title: "Role Change Error",
message: "You can't change an admin's role.",
title: "Error",
message: "Original user not found (id mismatch).",
});
this.users.find((user) => user._id.$oid === user_id).role = originalCurrentUser.role;
const uiUser = this.users.find((u) => u.immutable_id === user_id);
if (uiUser) uiUser.role = uiUser.role || "user";
return;
}

if (originalCurrentUser.role === "admin" && new_role !== "admin") {
const confirmed = await DialogService.confirm({
title: "Change Admin Role",
message: `Are you sure you want to remove admin privileges from ${originalCurrentUser.display_name}?`,
type: "warning",
});
if (!confirmed) {
this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role;
return;
}
}

const confirmed = await DialogService.confirm({
title: "Change User Role",
message: `Are you sure you want to change ${originalCurrentUser.display_name}'s role to ${new_role}?`,
message: `Are you sure you want to change ${originalCurrentUser.display_name}'s role from "${originalCurrentUser.role}" to "${new_role}"?`,
type: "warning",
});

if (confirmed) {
await this.updateUserRole(user_id, new_role);
try {
await this.updateUserRole(user_id, new_role);
} catch (err) {
this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role;
}
} else {
this.users.find((user) => user._id.$oid === user_id).role = originalCurrentUser.role;
this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role;
}
},

async handleManagersChange(userId, managers) {
if (!managers) managers = [];

const managerIds = managers.map((m) => m.immutable_id);

const userIndex = this.users.findIndex((u) => u.immutable_id === userId);
const originalIndex = this.original_users.findIndex((u) => u.immutable_id === userId);

const originalUser = this.original_users[originalIndex];
const originalManagerIds = (originalUser?.managers || []).map((m) => m.immutable_id);

if (JSON.stringify(managerIds.sort()) === JSON.stringify(originalManagerIds.sort())) return;

const confirmed = await DialogService.confirm({
title: "Update Managers",
message: `Are you sure you want to update managers for ${this.users[userIndex].display_name}?`,
type: "info",
});

if (!confirmed) {
this.users[userIndex].managers = originalUser.managers;
return;
}

try {
await saveUserManagers(userId, managerIds);

const newManagers = this.potentialManagersMap[userId].filter((u) =>
managerIds.includes(u.immutable_id),
);

this.users[userIndex].managers = newManagers;
this.original_users[originalIndex].managers = [...newManagers];
} catch (err) {
this.users[userIndex].managers = originalUser.managers;
DialogService.error({
title: "Error",
message: "Failed to update managers.",
});
}
},
async confirmUpdateUserStatus(user_id, new_status) {
const originalCurrentUser = this.original_users.find((user) => user._id.$oid === user_id);
const originalCurrentUser = this.original_users.find((user) => user.immutable_id === user_id);

if (!originalCurrentUser) {
DialogService.error({
title: "Error",
message: "Original user not found (id mismatch).",
});
return;
}

const confirmed = await DialogService.confirm({
title: "Change User Status",
message: `Are you sure you want to change ${originalCurrentUser.display_name}'s status from "${originalCurrentUser.account_status}" to "${new_status}"?`,
type: "warning",
});
if (confirmed) {
this.users.find((user) => user._id.$oid == user_id).account_status = new_status;
await this.updateUserStatus(user_id, new_status);
this.users.find((user) => user.immutable_id == user_id).account_status = new_status;
try {
await this.updateUserStatus(user_id, new_status);
} catch (err) {
this.users.find((user) => user.immutable_id === user_id).account_status =
originalCurrentUser.account_status;
}
} else {
this.users.find((user) => user._id.$oid === user_id).account_status =
this.users.find((user) => user.immutable_id === user_id).account_status =
originalCurrentUser.account_status;
}
},

async updateUserRole(user_id, user_role) {
await saveRole(user_id, { role: user_role });
this.original_users = JSON.parse(JSON.stringify(this.users));
},

async updateUserStatus(user_id, status) {
await saveUser(user_id, { account_status: status });
this.original_users = JSON.parse(JSON.stringify(this.users));
Expand Down
Loading