diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx
deleted file mode 100644
index 1f1ff9a92..000000000
--- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
- Edit {workspace.name}
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx
index e755e1857..a54e027b8 100644
--- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx
+++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx
@@ -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 }) {
{workspace.slug}
- | {workspace.userIds?.length} |
+
+
+ {workspace.userIds?.length}
+
+ |
{workspace.createdAt} |
-
|
-
-
-
>
);
}
diff --git a/frontend/src/pages/Admin/Workspaces/index.jsx b/frontend/src/pages/Admin/Workspaces/index.jsx
index 0085c32b2..63b9fb346 100644
--- a/frontend/src/pages/Admin/Workspaces/index.jsx
+++ b/frontend/src/pages/Admin/Workspaces/index.jsx
@@ -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([]);
diff --git a/frontend/src/pages/WorkspaceSettings/Members/AddMemberModal/index.jsx b/frontend/src/pages/WorkspaceSettings/Members/AddMemberModal/index.jsx
new file mode 100644
index 000000000..0799e5486
--- /dev/null
+++ b/frontend/src/pages/WorkspaceSettings/Members/AddMemberModal/index.jsx
@@ -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 (
+
+ );
+}
diff --git a/frontend/src/pages/WorkspaceSettings/Members/WorkspaceMemberRow/index.jsx b/frontend/src/pages/WorkspaceSettings/Members/WorkspaceMemberRow/index.jsx
new file mode 100644
index 000000000..4da5b7c3e
--- /dev/null
+++ b/frontend/src/pages/WorkspaceSettings/Members/WorkspaceMemberRow/index.jsx
@@ -0,0 +1,15 @@
+import { titleCase } from "text-case";
+
+export default function WorkspaceMemberRow({ user }) {
+ return (
+ <>
+
+
+ {user.username}
+ |
+ {titleCase(user.role)} |
+ {user.lastUpdatedAt} |
+
+ >
+ );
+}
diff --git a/frontend/src/pages/WorkspaceSettings/Members/index.jsx b/frontend/src/pages/WorkspaceSettings/Members/index.jsx
new file mode 100644
index 000000000..f315619a4
--- /dev/null
+++ b/frontend/src/pages/WorkspaceSettings/Members/index.jsx
@@ -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 (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Username
+ |
+
+ Role
+ |
+
+ Date Added
+ |
+
+ {" "}
+ |
+
+
+
+ {workspaceUsers.length > 0 ? (
+ workspaceUsers.map((user, index) => (
+
+ ))
+ ) : (
+
+
+ No workspace members
+ |
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/WorkspaceSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/index.jsx
index 952860ade..1ee44f7db 100644
--- a/frontend/src/pages/WorkspaceSettings/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/index.jsx
@@ -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={}
to={paths.workspace.settings.vectorDatabase(slug)}
/>
+ }
+ to={paths.workspace.settings.members(slug)}
+ />
diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js
index 5625fafb9..e496211fa 100644
--- a/frontend/src/utils/paths.js
+++ b/frontend/src/utils/paths.js
@@ -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}`;
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index f55cbb6e7..34bd66c3f 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -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])],
diff --git a/server/models/workspace.js b/server/models/workspace.js
index 7056468d1..f061ca206 100644
--- a/server/models/workspace.js
+++ b/server/models/workspace.js
@@ -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) });