Skip to content

Commit 65a6e6c

Browse files
BenjaminCharmesml-evs
authored andcommitted
Added manager management to the admin dashboard
1 parent 20f3391 commit 65a6e6c

File tree

3 files changed

+255
-31
lines changed

3 files changed

+255
-31
lines changed

pydatalab/src/pydatalab/routes/v0_1/admin.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,22 @@ def get_users():
3535
"then": "user",
3636
"else": {"$arrayElemAt": ["$role.role", 0]},
3737
}
38-
}
38+
},
39+
"immutable_id": {"$ifNull": ["$immutable_id", {"$toString": "$_id"}]},
3940
}
4041
},
4142
]
4243
)
4344

44-
return jsonify({"status": "success", "data": list(users)})
45+
users_list = list(users)
46+
47+
for user in users_list:
48+
if "managers" not in user:
49+
user["managers"] = []
50+
elif not isinstance(user["managers"], list):
51+
user["managers"] = []
52+
53+
return jsonify({"status": "success", "data": users_list})
4554

4655

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

95104
return (jsonify({"status": "success"}), 200)
105+
106+
107+
@ADMIN.route("/users/<user_id>/managers", methods=["PATCH"])
108+
def update_user_managers(user_id):
109+
"""Update the managers for a specific user using ObjectIds"""
110+
request_json = request.get_json()
111+
112+
if request_json is None or "managers" not in request_json:
113+
return jsonify({"status": "error", "message": "Managers list not provided"}), 400
114+
115+
managers = request_json["managers"]
116+
117+
if not isinstance(managers, list):
118+
return jsonify({"status": "error", "message": "Managers must be a list"}), 400
119+
120+
existing_user = flask_mongo.db.users.find_one({"_id": ObjectId(user_id)})
121+
if not existing_user:
122+
return jsonify({"status": "error", "message": "User not found"}), 404
123+
124+
manager_object_ids = []
125+
for manager_id in managers:
126+
if manager_id:
127+
try:
128+
manager_oid = ObjectId(manager_id)
129+
if not flask_mongo.db.users.find_one({"_id": manager_oid}):
130+
return jsonify(
131+
{"status": "error", "message": f"Manager with ID {manager_id} not found"}
132+
), 404
133+
manager_object_ids.append(str(manager_oid))
134+
except (TypeError, ValueError):
135+
return jsonify(
136+
{"status": "error", "message": f"Invalid manager ID format: {manager_id}"}
137+
), 400
138+
139+
update_result = flask_mongo.db.users.update_one(
140+
{"_id": ObjectId(user_id)}, {"$set": {"managers": manager_object_ids}}
141+
)
142+
143+
if update_result.matched_count != 1:
144+
return jsonify({"status": "error", "message": "Unable to update user managers"}), 400
145+
146+
return jsonify({"status": "success"}), 200

webapp/src/components/UserTable.vue

Lines changed: 180 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<template>
2-
<table class="table table-hover table-sm" data-testid="user-table">
2+
<table class="table table-hover table-sm table-responsive-sm" data-testid="user-table">
33
<thead>
44
<tr>
55
<th scope="col">Name</th>
66
<th scope="col">Email</th>
77
<th scope="col">Role</th>
8+
<th scope="col">Managers</th>
89
<th scope="col">Actions</th>
910
</tr>
1011
</thead>
1112
<tbody>
12-
<tr v-for="user in users" :key="user._id">
13+
<tr v-for="user in users" :key="user.immutable_id">
1314
<td align="left">
1415
{{ user.display_name }}
1516
<span v-if="user.account_status === 'active'" class="badge badge-success text-uppercase">
@@ -30,35 +31,61 @@
3031
</td>
3132
<td align="left">{{ user.contact_email }}</td>
3233
<td align="left">
33-
<select
34+
<vSelect
3435
v-model="user.role"
35-
class="dropdown"
36-
@change="confirmUpdateUserRole(user._id.$oid, $event.target.value)"
36+
:options="roleOptions"
37+
:clearable="false"
38+
:searchable="false"
39+
class="form-control p-0 border-0"
40+
@update:model-value="(value) => confirmUpdateUserRole(user.immutable_id, value)"
41+
/>
42+
</td>
43+
<td align="left">
44+
<vSelect
45+
v-model="user.managers"
46+
:options="potentialManagersMap[user.immutable_id]"
47+
label="display_name"
48+
multiple
49+
placeholder="No managers"
50+
:clearable="false"
51+
class="w-100"
52+
@update:model-value="(value) => handleManagersChange(user.immutable_id, value)"
3753
>
38-
<option value="user">User</option>
39-
<option value="admin">Admin</option>
40-
<option value="manager">Manager</option>
41-
</select>
54+
<template #option="option">
55+
<div class="d-flex align-items-center">
56+
<UserBubble :creator="option" :size="20" />
57+
<span class="ml-2">{{ option.display_name }}</span>
58+
<span class="ml-auto badge badge-secondary small">{{ option.role }}</span>
59+
</div>
60+
</template>
61+
62+
<template #selected-option="option">
63+
<div class="d-flex align-items-center">
64+
<UserBubble :creator="option" :size="18" />
65+
<span class="ml-2 small">{{ option.display_name }}</span>
66+
</div>
67+
</template>
68+
</vSelect>
4269
</td>
4370
<td align="left">
4471
<button
4572
v-if="user.account_status === 'active'"
4673
class="btn btn-outline-danger btn-sm text-uppercase text-monospace"
47-
@click="confirmUpdateUserStatus(user._id.$oid, 'deactivated')"
74+
@click="confirmUpdateUserStatus(user.immutable_id, 'deactivated')"
4875
>
4976
Deactivate
5077
</button>
5178
<button
5279
v-else-if="user.account_status === 'unverified'"
5380
class="btn btn-outline-success btn-sm text-uppercase text-monospace"
54-
@click="confirmUpdateUserStatus(user._id.$oid, 'active')"
81+
@click="confirmUpdateUserStatus(user.immutable_id, 'active')"
5582
>
5683
Activate
5784
</button>
5885
<button
5986
v-else-if="user.account_status === 'deactivated'"
6087
class="btn btn-outline-success btn-sm text-uppercase text-monospace"
61-
@click="confirmUpdateUserStatus(user._id.$oid, 'active')"
88+
@click="confirmUpdateUserStatus(user.immutable_id, 'active')"
6289
>
6390
Activate
6491
</button>
@@ -70,71 +97,198 @@
7097

7198
<script>
7299
import { DialogService } from "@/services/DialogService";
73-
74-
import { getUsersList, saveRole, saveUser } from "@/server_fetch_utils.js";
100+
import vSelect from "vue-select";
101+
import UserBubble from "@/components/UserBubble.vue";
102+
import { getUsersList, saveRole, saveUser, saveUserManagers } from "@/server_fetch_utils.js";
75103
76104
export default {
105+
components: {
106+
vSelect,
107+
UserBubble,
108+
},
109+
77110
data() {
78111
return {
79112
users: null,
80113
original_users: null,
81114
tempRole: null,
115+
roleOptions: ["user", "admin", "manager"],
82116
};
83117
},
118+
computed: {
119+
potentialManagersMap() {
120+
if (!this.users) return {};
121+
const map = {};
122+
this.users.forEach((u) => {
123+
map[u.immutable_id] = this.users.filter(
124+
(user) =>
125+
user.immutable_id !== u.immutable_id &&
126+
(user.role === "admin" || user.role === "manager"),
127+
);
128+
});
129+
return map;
130+
},
131+
},
84132
created() {
85133
this.getUsers();
86134
},
87135
methods: {
88136
async getUsers() {
89137
let data = await getUsersList();
90138
if (data != null) {
139+
const byId = {};
140+
data.forEach((u) => {
141+
const id = u.immutable_id || u._id;
142+
byId[id] = u;
143+
});
144+
145+
data.forEach((user) => {
146+
if (!user.managers) {
147+
user.managers = [];
148+
}
149+
150+
user.managers = user.managers
151+
.map((m) => {
152+
const mid = typeof m === "string" ? m : m.$oid || m.immutable_id;
153+
return byId[mid];
154+
})
155+
.filter(Boolean);
156+
});
157+
91158
this.users = JSON.parse(JSON.stringify(data));
92159
this.original_users = JSON.parse(JSON.stringify(data));
93160
}
94161
},
162+
getPotentialManagers(userId) {
163+
if (!this.users) return [];
164+
165+
const potentials = this.users.filter((u) => {
166+
const isEligible =
167+
u.immutable_id !== userId && (u.role === "admin" || u.role === "manager");
168+
return isEligible;
169+
});
170+
171+
return potentials.sort((a, b) => (a.display_name || "").localeCompare(b.display_name || ""));
172+
},
173+
95174
async confirmUpdateUserRole(user_id, new_role) {
96-
const originalCurrentUser = this.original_users.find((user) => user._id.$oid === user_id);
175+
const originalCurrentUser = this.original_users.find((user) => user.immutable_id === user_id);
97176
98-
if (originalCurrentUser.role === "admin") {
177+
if (!originalCurrentUser) {
99178
DialogService.error({
100-
title: "Role Change Error",
101-
message: "You can't change an admin's role.",
179+
title: "Error",
180+
message: "Original user not found (id mismatch).",
102181
});
103-
this.users.find((user) => user._id.$oid === user_id).role = originalCurrentUser.role;
182+
const uiUser = this.users.find((u) => u.immutable_id === user_id);
183+
if (uiUser) uiUser.role = uiUser.role || "user";
104184
return;
105185
}
106186
187+
if (originalCurrentUser.role === "admin" && new_role !== "admin") {
188+
const confirmed = await DialogService.confirm({
189+
title: "Change Admin Role",
190+
message: `Are you sure you want to remove admin privileges from ${originalCurrentUser.display_name}?`,
191+
type: "warning",
192+
});
193+
if (!confirmed) {
194+
this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role;
195+
return;
196+
}
197+
}
198+
107199
const confirmed = await DialogService.confirm({
108200
title: "Change User Role",
109-
message: `Are you sure you want to change ${originalCurrentUser.display_name}'s role to ${new_role}?`,
201+
message: `Are you sure you want to change ${originalCurrentUser.display_name}'s role from "${originalCurrentUser.role}" to "${new_role}"?`,
110202
type: "warning",
111203
});
204+
112205
if (confirmed) {
113-
await this.updateUserRole(user_id, new_role);
206+
try {
207+
await this.updateUserRole(user_id, new_role);
208+
} catch (err) {
209+
this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role;
210+
}
114211
} else {
115-
this.users.find((user) => user._id.$oid === user_id).role = originalCurrentUser.role;
212+
this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role;
213+
}
214+
},
215+
216+
async handleManagersChange(userId, managers) {
217+
if (!managers) managers = [];
218+
219+
const managerIds = managers.map((m) => m.immutable_id);
220+
221+
const userIndex = this.users.findIndex((u) => u.immutable_id === userId);
222+
const originalIndex = this.original_users.findIndex((u) => u.immutable_id === userId);
223+
224+
const originalUser = this.original_users[originalIndex];
225+
const originalManagerIds = (originalUser?.managers || []).map((m) => m.immutable_id);
226+
227+
if (JSON.stringify(managerIds.sort()) === JSON.stringify(originalManagerIds.sort())) return;
228+
229+
const confirmed = await DialogService.confirm({
230+
title: "Update Managers",
231+
message: `Are you sure you want to update managers for ${this.users[userIndex].display_name}?`,
232+
type: "info",
233+
});
234+
235+
if (!confirmed) {
236+
this.users[userIndex].managers = originalUser.managers;
237+
return;
238+
}
239+
240+
try {
241+
await saveUserManagers(userId, managerIds);
242+
243+
const newManagers = this.potentialManagersMap[userId].filter((u) =>
244+
managerIds.includes(u.immutable_id),
245+
);
246+
247+
this.users[userIndex].managers = newManagers;
248+
this.original_users[originalIndex].managers = [...newManagers];
249+
} catch (err) {
250+
this.users[userIndex].managers = originalUser.managers;
251+
DialogService.error({
252+
title: "Error",
253+
message: "Failed to update managers.",
254+
});
116255
}
117256
},
118257
async confirmUpdateUserStatus(user_id, new_status) {
119-
const originalCurrentUser = this.original_users.find((user) => user._id.$oid === user_id);
258+
const originalCurrentUser = this.original_users.find((user) => user.immutable_id === user_id);
259+
260+
if (!originalCurrentUser) {
261+
DialogService.error({
262+
title: "Error",
263+
message: "Original user not found (id mismatch).",
264+
});
265+
return;
266+
}
120267
121268
const confirmed = await DialogService.confirm({
122269
title: "Change User Status",
123270
message: `Are you sure you want to change ${originalCurrentUser.display_name}'s status from "${originalCurrentUser.account_status}" to "${new_status}"?`,
124271
type: "warning",
125272
});
126273
if (confirmed) {
127-
this.users.find((user) => user._id.$oid == user_id).account_status = new_status;
128-
await this.updateUserStatus(user_id, new_status);
274+
this.users.find((user) => user.immutable_id == user_id).account_status = new_status;
275+
try {
276+
await this.updateUserStatus(user_id, new_status);
277+
} catch (err) {
278+
this.users.find((user) => user.immutable_id === user_id).account_status =
279+
originalCurrentUser.account_status;
280+
}
129281
} else {
130-
this.users.find((user) => user._id.$oid === user_id).account_status =
282+
this.users.find((user) => user.immutable_id === user_id).account_status =
131283
originalCurrentUser.account_status;
132284
}
133285
},
286+
134287
async updateUserRole(user_id, user_role) {
135288
await saveRole(user_id, { role: user_role });
136289
this.original_users = JSON.parse(JSON.stringify(this.users));
137290
},
291+
138292
async updateUserStatus(user_id, status) {
139293
await saveUser(user_id, { account_status: status });
140294
this.original_users = JSON.parse(JSON.stringify(this.users));

0 commit comments

Comments
 (0)