mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-10 17:00:11 +01:00
[FEAT] Implement new workspace members settings and admin users UI updates (#990)
* members workspace settings menu and admin users UI updates * change copy/fix admin and managers in workspace when not showing in UI * remove existing workspace user mgmt modal --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
e524afae9e
commit
41fe20f2e0
@ -104,6 +104,18 @@ const Admin = {
|
||||
return [];
|
||||
});
|
||||
},
|
||||
workspaceUsers: async (workspaceId) => {
|
||||
return await fetch(`${API_BASE}/admin/workspaces/${workspaceId}/users`, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => res?.users || [])
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
newWorkspace: async (name) => {
|
||||
return await fetch(`${API_BASE}/admin/workspaces/new`, {
|
||||
method: "POST",
|
||||
|
@ -2,7 +2,6 @@ import { useRef, useState } from "react";
|
||||
import { titleCase } from "text-case";
|
||||
import Admin from "@/models/admin";
|
||||
import EditUserModal from "./EditUserModal";
|
||||
import { DotsThreeOutline } from "@phosphor-icons/react";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
@ -69,22 +68,22 @@ export default function UserRow({ currUser, user }) {
|
||||
{canModify && (
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="font-medium text-white text-opacity-80 rounded-lg hover:text-white px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10"
|
||||
className="text-sm font-medium text-white/80 rounded-lg hover:text-white px-2 py-1 hover:bg-white hover:bg-opacity-10"
|
||||
>
|
||||
<DotsThreeOutline weight="fill" className="h-5 w-5" />
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
{currUser?.id !== user.id && canModify && (
|
||||
<>
|
||||
<button
|
||||
onClick={handleSuspend}
|
||||
className="font-medium text-orange-600 dark:text-orange-300 px-2 py-1 rounded-lg hover:bg-orange-50 hover:dark:bg-orange-800 hover:dark:bg-opacity-20"
|
||||
className="text-sm font-medium text-white/80 hover:text-orange-300 rounded-lg px-2 py-1 hover:bg-white hover:bg-opacity-10"
|
||||
>
|
||||
{suspended ? "Unsuspend" : "Suspend"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20"
|
||||
className="text-sm font-medium text-white/80 hover:text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
@ -77,7 +77,7 @@ function UsersContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="w-full text-sm text-left rounded-lg mt-6">
|
||||
<table className="w-full text-sm text-left rounded-lg">
|
||||
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
|
@ -1,149 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import { titleCase } from "text-case";
|
||||
|
||||
export const EditWorkspaceUsersModalId = (workspace) =>
|
||||
`edit-workspace-${workspace.id}-modal`;
|
||||
|
||||
export default function EditWorkspaceUsersModal({
|
||||
workspace,
|
||||
users,
|
||||
closeModal,
|
||||
}) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
setError(null);
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
userIds: [],
|
||||
};
|
||||
const form = new FormData(e.target);
|
||||
for (var [key, value] of form.entries()) {
|
||||
if (key.includes("user-") && value === "yes") {
|
||||
const [_, id] = key.split(`-`);
|
||||
data.userIds.push(+id);
|
||||
}
|
||||
}
|
||||
const { success, error } = await Admin.updateUsersInWorkspace(
|
||||
workspace.id,
|
||||
data.userIds
|
||||
);
|
||||
if (success) window.location.reload();
|
||||
setError(error);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Edit {workspace.name}
|
||||
</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
data-modal-hide="staticModal"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4 max-h-[350px] overflow-y-scroll">
|
||||
{users
|
||||
.filter((user) => user.role !== "admin")
|
||||
.map((user) => {
|
||||
return (
|
||||
<div
|
||||
key={`workspace-${workspace.id}-user-${user.id}`}
|
||||
data-workspace={workspace.id}
|
||||
className="flex items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
|
||||
onClick={() => {
|
||||
document
|
||||
.getElementById(
|
||||
`workspace-${workspace.id}-user-${user.id}`
|
||||
)
|
||||
?.click();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id={`workspace-${workspace.id}-user-${user.id}`}
|
||||
defaultChecked={workspace.userIds.includes(user.id)}
|
||||
type="checkbox"
|
||||
value="yes"
|
||||
name={`user-${user.id}`}
|
||||
className="w-4 h-4 text-blue-600 bg-zinc-900 border border-gray-500/50 rounded focus:ring-blue-500 focus:border-blue-500 pointer-events-none"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`user-${user.id}`}
|
||||
className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-white"
|
||||
>
|
||||
{titleCase(user.username)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-x-4">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
|
||||
onClick={() => {
|
||||
document
|
||||
.getElementById(`workspace-${workspace.id}-select-all`)
|
||||
?.click();
|
||||
Array.from(
|
||||
document.querySelectorAll(
|
||||
`[data-workspace='${workspace.id}']`
|
||||
)
|
||||
).forEach((el) => {
|
||||
if (!el.firstChild.checked) el.firstChild.click();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
|
||||
onClick={() => {
|
||||
document
|
||||
.getElementById(`workspace-${workspace.id}-select-all`)
|
||||
?.click();
|
||||
Array.from(
|
||||
document.querySelectorAll(
|
||||
`[data-workspace='${workspace.id}']`
|
||||
)
|
||||
).forEach((el) => {
|
||||
if (el.firstChild.checked) el.firstChild.click();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||
>
|
||||
Update workspace
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
import { useRef } from "react";
|
||||
import Admin from "@/models/admin";
|
||||
import paths from "@/utils/paths";
|
||||
import EditWorkspaceUsersModal from "./EditWorkspaceUsersModal";
|
||||
import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { LinkSimple, Trash } from "@phosphor-icons/react";
|
||||
|
||||
export default function WorkspaceRow({ workspace, users }) {
|
||||
const rowRef = useRef(null);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
@ -39,15 +35,16 @@ export default function WorkspaceRow({ workspace, users }) {
|
||||
<LinkSimple className="mr-2 w-5 h-5" /> {workspace.slug}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-6 py-4">{workspace.userIds?.length}</td>
|
||||
<td className="px-6 py-4">
|
||||
<a
|
||||
href={paths.workspace.settings.members(workspace.slug)}
|
||||
className="text-white flex items-center underline"
|
||||
>
|
||||
{workspace.userIds?.length}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-6 py-4">{workspace.createdAt}</td>
|
||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="font-medium rounded-lg hover:text-white hover:text-opacity-60 px-2 py-1 hover:bg-white hover:bg-opacity-10"
|
||||
>
|
||||
<DotsThreeOutline weight="fill" className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||
@ -56,13 +53,6 @@ export default function WorkspaceRow({ workspace, users }) {
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<EditWorkspaceUsersModal
|
||||
workspace={workspace}
|
||||
users={users}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { isMobile } from "react-device-detect";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { BookOpen } from "@phosphor-icons/react";
|
||||
import usePrefersDarkMode from "@/hooks/usePrefersDarkMode";
|
||||
import Admin from "@/models/admin";
|
||||
import WorkspaceRow from "./WorkspaceRow";
|
||||
import NewWorkspaceModal from "./NewWorkspaceModal";
|
||||
@ -50,7 +49,6 @@ export default function AdminWorkspaces() {
|
||||
}
|
||||
|
||||
function WorkspacesContainer() {
|
||||
const darkMode = usePrefersDarkMode();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [workspaces, setWorkspaces] = useState([]);
|
||||
|
@ -0,0 +1,162 @@
|
||||
import React, { useState } from "react";
|
||||
import { MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
export default function AddMemberModal({ closeModal, workspace, users }) {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedUsers, setSelectedUsers] = useState(workspace?.userIds || []);
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
e.preventDefault();
|
||||
const { success, error } = await Admin.updateUsersInWorkspace(
|
||||
workspace.id,
|
||||
selectedUsers
|
||||
);
|
||||
if (success) {
|
||||
showToast("Users updated successfully.", "success");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
showToast(error, "error");
|
||||
};
|
||||
|
||||
const handleUserSelect = (userId) => {
|
||||
setSelectedUsers((prevSelectedUsers) => {
|
||||
if (prevSelectedUsers.includes(userId)) {
|
||||
return prevSelectedUsers.filter((id) => id !== userId);
|
||||
} else {
|
||||
return [...prevSelectedUsers, userId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedUsers.length === filteredUsers.length) {
|
||||
setSelectedUsers([]);
|
||||
} else {
|
||||
setSelectedUsers(filteredUsers.map((user) => user.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnselect = () => {
|
||||
setSelectedUsers([]);
|
||||
};
|
||||
|
||||
const isUserSelected = (userId) => {
|
||||
return selectedUsers.includes(userId);
|
||||
};
|
||||
|
||||
const handleSearch = (event) => {
|
||||
setSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
const filteredUsers = users
|
||||
.filter((user) =>
|
||||
user.username.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
.filter((user) => user.role !== "admin")
|
||||
.filter((user) => user.role !== "manager");
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-[550px] max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)]">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<h3 className="text-base font-semibold text-white">Users</h3>
|
||||
<div className="relative">
|
||||
<input
|
||||
onChange={handleSearch}
|
||||
className="w-[400px] h-[34px] bg-[#030712] rounded-[100px] text-white placeholder:text-white/50 text-sm px-10 pl-10"
|
||||
placeholder="Search for a user"
|
||||
/>
|
||||
<MagnifyingGlass
|
||||
size={16}
|
||||
weight="bold"
|
||||
className="text-white text-lg absolute left-3 top-1/2 transform -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
data-modal-hide="staticModal"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="py-[17px] px-[20px]">
|
||||
<table className="gap-y-[8px] flex flex-col max-h-[385px] overflow-y-auto no-scroll">
|
||||
{filteredUsers.length > 0 ? (
|
||||
filteredUsers.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="flex items-center gap-x-2 cursor-pointer"
|
||||
onClick={() => handleUserSelect(user.id)}
|
||||
>
|
||||
<div
|
||||
className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center"
|
||||
role="checkbox"
|
||||
aria-checked={isUserSelected(user.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{isUserSelected(user.id) && (
|
||||
<div className="w-2 h-2 bg-white rounded-[2px]" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white text-sm font-medium">
|
||||
{user.username}
|
||||
</p>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<p className="text-white text-opacity-60 text-sm font-medium ">
|
||||
No users found
|
||||
</p>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-3 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSelectAll}
|
||||
className="flex items-center gap-x-2 ml-2"
|
||||
>
|
||||
<div
|
||||
className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
||||
role="checkbox"
|
||||
aria-checked={selectedUsers.length === filteredUsers.length}
|
||||
tabIndex={0}
|
||||
>
|
||||
{selectedUsers.length === filteredUsers.length && (
|
||||
<div className="w-2 h-2 bg-white rounded-[2px]" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white text-sm font-medium">Select All</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUnselect}
|
||||
className="flex items-center gap-x-2 ml-2"
|
||||
>
|
||||
<p className="text-white/60 text-sm font-medium hover:text-white">
|
||||
Unselect
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 text-xs px-2 py-1 font-semibold rounded-lg bg-[#46C8FF] hover:bg-[#2C2F36] border-2 border-transparent hover:border-[#46C8FF] hover:text-white h-[32px] w-[68px] -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { titleCase } from "text-case";
|
||||
|
||||
export default function WorkspaceMemberRow({ user }) {
|
||||
return (
|
||||
<>
|
||||
<tr className="bg-transparent text-white text-opacity-80 text-sm font-medium">
|
||||
<th scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||
{user.username}
|
||||
</th>
|
||||
<td className="px-6 py-4">{titleCase(user.role)}</td>
|
||||
<td className="px-6 py-4">{user.lastUpdatedAt}</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
97
frontend/src/pages/WorkspaceSettings/Members/index.jsx
Normal file
97
frontend/src/pages/WorkspaceSettings/Members/index.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import Admin from "@/models/admin";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import AddMemberModal from "./AddMemberModal";
|
||||
import WorkspaceMemberRow from "./WorkspaceMemberRow";
|
||||
|
||||
export default function Members({ workspace }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [workspaceUsers, setWorkspaceUsers] = useState([]);
|
||||
const [adminWorkspace, setAdminWorkspace] = useState(null);
|
||||
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const _users = await Admin.users();
|
||||
const workspaceUsers = await Admin.workspaceUsers(workspace.id);
|
||||
const adminWorkspaces = await Admin.workspaces();
|
||||
setAdminWorkspace(
|
||||
adminWorkspaces.find(
|
||||
(adminWorkspace) => adminWorkspace.id === workspace.id
|
||||
)
|
||||
);
|
||||
setWorkspaceUsers(workspaceUsers);
|
||||
setUsers(_users);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchData();
|
||||
}, [workspace]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
highlightColor="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between -mt-3">
|
||||
<table className="w-full max-w-[700px] text-sm text-left rounded-lg">
|
||||
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
Username
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Role
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Date Added
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{workspaceUsers.length > 0 ? (
|
||||
workspaceUsers.map((user, index) => (
|
||||
<WorkspaceMemberRow key={index} user={user} />
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td className="text-center py-4 text-white/80" colSpan="4">
|
||||
No workspace members
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="text-xs px-2 py-1 font-semibold rounded-lg bg-[#46C8FF] hover:bg-[#2C2F36] hover:text-white h-[34px] w-[100px] -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
Manage Users
|
||||
</button>
|
||||
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<AddMemberModal
|
||||
closeModal={closeModal}
|
||||
users={users}
|
||||
workspace={adminWorkspace}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -9,6 +9,7 @@ import {
|
||||
ArrowUUpLeft,
|
||||
ChatText,
|
||||
Database,
|
||||
User,
|
||||
Wrench,
|
||||
} from "@phosphor-icons/react";
|
||||
import paths from "@/utils/paths";
|
||||
@ -17,11 +18,13 @@ import { NavLink } from "react-router-dom";
|
||||
import GeneralAppearance from "./GeneralAppearance";
|
||||
import ChatSettings from "./ChatSettings";
|
||||
import VectorDatabase from "./VectorDatabase";
|
||||
import Members from "./Members";
|
||||
|
||||
const TABS = {
|
||||
"general-appearance": GeneralAppearance,
|
||||
"chat-settings": ChatSettings,
|
||||
"vector-database": VectorDatabase,
|
||||
members: Members,
|
||||
};
|
||||
|
||||
export default function WorkspaceSettings() {
|
||||
@ -91,6 +94,11 @@ function ShowWorkspaceChat() {
|
||||
icon={<Database className="h-6 w-6" />}
|
||||
to={paths.workspace.settings.vectorDatabase(slug)}
|
||||
/>
|
||||
<TabItem
|
||||
title="Members"
|
||||
icon={<User className="h-6 w-6" />}
|
||||
to={paths.workspace.settings.members(slug)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-16 py-6">
|
||||
<TabContent slug={slug} workspace={workspace} />
|
||||
|
@ -65,6 +65,9 @@ export default {
|
||||
vectorDatabase: (slug) => {
|
||||
return `/workspace/${slug}/settings/vector-database`;
|
||||
},
|
||||
members: (slug) => {
|
||||
return `/workspace/${slug}/settings/members`;
|
||||
},
|
||||
},
|
||||
thread: (wsSlug, threadSlug) => {
|
||||
return `/workspace/${wsSlug}/t/${threadSlug}`;
|
||||
|
@ -227,6 +227,21 @@ function adminEndpoints(app) {
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/admin/workspaces/:workspaceId/users",
|
||||
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { workspaceId } = request.params;
|
||||
const users = await Workspace.workspaceUsers(workspaceId);
|
||||
response.status(200).json({ users });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/admin/workspaces/new",
|
||||
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||
|
@ -4,6 +4,7 @@ const { Document } = require("./documents");
|
||||
const { WorkspaceUser } = require("./workspaceUsers");
|
||||
const { ROLES } = require("../utils/middleware/multiUserProtected");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { User } = require("./user");
|
||||
|
||||
const Workspace = {
|
||||
defaultPrompt:
|
||||
@ -191,6 +192,32 @@ const Workspace = {
|
||||
}
|
||||
},
|
||||
|
||||
workspaceUsers: async function (workspaceId) {
|
||||
try {
|
||||
const users = (
|
||||
await WorkspaceUser.where({ workspace_id: Number(workspaceId) })
|
||||
).map((rel) => rel);
|
||||
|
||||
const usersById = await User.where({
|
||||
id: { in: users.map((user) => user.user_id) },
|
||||
});
|
||||
|
||||
const userInfo = usersById.map((user) => {
|
||||
const workspaceUser = users.find((u) => u.user_id === user.id);
|
||||
return {
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
lastUpdatedAt: workspaceUser.lastUpdatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
updateUsers: async function (workspaceId, userIds = []) {
|
||||
try {
|
||||
await WorkspaceUser.delete({ workspace_id: Number(workspaceId) });
|
||||
|
Loading…
Reference in New Issue
Block a user