mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 20:50:09 +01:00
Merge branch 'master' of github.com:Mintplex-Labs/anything-llm into render
This commit is contained in:
commit
8305bbaf7f
@ -104,6 +104,18 @@ const Admin = {
|
|||||||
return [];
|
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) => {
|
newWorkspace: async (name) => {
|
||||||
return await fetch(`${API_BASE}/admin/workspaces/new`, {
|
return await fetch(`${API_BASE}/admin/workspaces/new`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -2,7 +2,6 @@ import { useRef, useState } from "react";
|
|||||||
import { titleCase } from "text-case";
|
import { titleCase } from "text-case";
|
||||||
import Admin from "@/models/admin";
|
import Admin from "@/models/admin";
|
||||||
import EditUserModal from "./EditUserModal";
|
import EditUserModal from "./EditUserModal";
|
||||||
import { DotsThreeOutline } from "@phosphor-icons/react";
|
|
||||||
import showToast from "@/utils/toast";
|
import showToast from "@/utils/toast";
|
||||||
import { useModal } from "@/hooks/useModal";
|
import { useModal } from "@/hooks/useModal";
|
||||||
import ModalWrapper from "@/components/ModalWrapper";
|
import ModalWrapper from "@/components/ModalWrapper";
|
||||||
@ -69,22 +68,22 @@ export default function UserRow({ currUser, user }) {
|
|||||||
{canModify && (
|
{canModify && (
|
||||||
<button
|
<button
|
||||||
onClick={openModal}
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
{currUser?.id !== user.id && canModify && (
|
{currUser?.id !== user.id && canModify && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={handleSuspend}
|
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"}
|
{suspended ? "Unsuspend" : "Suspend"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
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
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -77,7 +77,7 @@ function UsersContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
<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 { useRef } from "react";
|
||||||
import Admin from "@/models/admin";
|
import Admin from "@/models/admin";
|
||||||
import paths from "@/utils/paths";
|
import paths from "@/utils/paths";
|
||||||
import EditWorkspaceUsersModal from "./EditWorkspaceUsersModal";
|
import { LinkSimple, Trash } from "@phosphor-icons/react";
|
||||||
import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
import ModalWrapper from "@/components/ModalWrapper";
|
|
||||||
|
|
||||||
export default function WorkspaceRow({ workspace, users }) {
|
export default function WorkspaceRow({ workspace, users }) {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
const { isOpen, openModal, closeModal } = useModal();
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
@ -39,15 +35,16 @@ export default function WorkspaceRow({ workspace, users }) {
|
|||||||
<LinkSimple className="mr-2 w-5 h-5" /> {workspace.slug}
|
<LinkSimple className="mr-2 w-5 h-5" /> {workspace.slug}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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">{workspace.createdAt}</td>
|
||||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
<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
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
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>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
import { BookOpen } from "@phosphor-icons/react";
|
import { BookOpen } from "@phosphor-icons/react";
|
||||||
import usePrefersDarkMode from "@/hooks/usePrefersDarkMode";
|
|
||||||
import Admin from "@/models/admin";
|
import Admin from "@/models/admin";
|
||||||
import WorkspaceRow from "./WorkspaceRow";
|
import WorkspaceRow from "./WorkspaceRow";
|
||||||
import NewWorkspaceModal from "./NewWorkspaceModal";
|
import NewWorkspaceModal from "./NewWorkspaceModal";
|
||||||
@ -50,7 +49,6 @@ export default function AdminWorkspaces() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WorkspacesContainer() {
|
function WorkspacesContainer() {
|
||||||
const darkMode = usePrefersDarkMode();
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [workspaces, setWorkspaces] = 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,
|
ArrowUUpLeft,
|
||||||
ChatText,
|
ChatText,
|
||||||
Database,
|
Database,
|
||||||
|
User,
|
||||||
Wrench,
|
Wrench,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import paths from "@/utils/paths";
|
import paths from "@/utils/paths";
|
||||||
@ -17,11 +18,13 @@ import { NavLink } from "react-router-dom";
|
|||||||
import GeneralAppearance from "./GeneralAppearance";
|
import GeneralAppearance from "./GeneralAppearance";
|
||||||
import ChatSettings from "./ChatSettings";
|
import ChatSettings from "./ChatSettings";
|
||||||
import VectorDatabase from "./VectorDatabase";
|
import VectorDatabase from "./VectorDatabase";
|
||||||
|
import Members from "./Members";
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
"general-appearance": GeneralAppearance,
|
"general-appearance": GeneralAppearance,
|
||||||
"chat-settings": ChatSettings,
|
"chat-settings": ChatSettings,
|
||||||
"vector-database": VectorDatabase,
|
"vector-database": VectorDatabase,
|
||||||
|
members: Members,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WorkspaceSettings() {
|
export default function WorkspaceSettings() {
|
||||||
@ -91,6 +94,11 @@ function ShowWorkspaceChat() {
|
|||||||
icon={<Database className="h-6 w-6" />}
|
icon={<Database className="h-6 w-6" />}
|
||||||
to={paths.workspace.settings.vectorDatabase(slug)}
|
to={paths.workspace.settings.vectorDatabase(slug)}
|
||||||
/>
|
/>
|
||||||
|
<TabItem
|
||||||
|
title="Members"
|
||||||
|
icon={<User className="h-6 w-6" />}
|
||||||
|
to={paths.workspace.settings.members(slug)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-16 py-6">
|
<div className="px-16 py-6">
|
||||||
<TabContent slug={slug} workspace={workspace} />
|
<TabContent slug={slug} workspace={workspace} />
|
||||||
|
@ -65,6 +65,9 @@ export default {
|
|||||||
vectorDatabase: (slug) => {
|
vectorDatabase: (slug) => {
|
||||||
return `/workspace/${slug}/settings/vector-database`;
|
return `/workspace/${slug}/settings/vector-database`;
|
||||||
},
|
},
|
||||||
|
members: (slug) => {
|
||||||
|
return `/workspace/${slug}/settings/members`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
thread: (wsSlug, threadSlug) => {
|
thread: (wsSlug, threadSlug) => {
|
||||||
return `/workspace/${wsSlug}/t/${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(
|
app.post(
|
||||||
"/admin/workspaces/new",
|
"/admin/workspaces/new",
|
||||||
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||||
|
@ -4,6 +4,7 @@ const { Document } = require("./documents");
|
|||||||
const { WorkspaceUser } = require("./workspaceUsers");
|
const { WorkspaceUser } = require("./workspaceUsers");
|
||||||
const { ROLES } = require("../utils/middleware/multiUserProtected");
|
const { ROLES } = require("../utils/middleware/multiUserProtected");
|
||||||
const { v4: uuidv4 } = require("uuid");
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
const { User } = require("./user");
|
||||||
|
|
||||||
const Workspace = {
|
const Workspace = {
|
||||||
defaultPrompt:
|
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 = []) {
|
updateUsers: async function (workspaceId, userIds = []) {
|
||||||
try {
|
try {
|
||||||
await WorkspaceUser.delete({ workspace_id: Number(workspaceId) });
|
await WorkspaceUser.delete({ workspace_id: Number(workspaceId) });
|
||||||
|
Loading…
Reference in New Issue
Block a user