diff --git a/backend/danswer/auth/invited_users.py b/backend/danswer/auth/invited_users.py
index fb30332af..ff3a8cce9 100644
--- a/backend/danswer/auth/invited_users.py
+++ b/backend/danswer/auth/invited_users.py
@@ -9,7 +9,6 @@ from danswer.utils.special_types import JSON_ro
def get_invited_users() -> list[str]:
try:
store = get_kv_store()
-
return cast(list, store.load(KV_USER_STORE_KEY))
except KvKeyNotFoundError:
return list()
diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py
index 8d3955e0f..0e37fc891 100644
--- a/backend/danswer/server/manage/models.py
+++ b/backend/danswer/server/manage/models.py
@@ -266,5 +266,7 @@ class FullModelVersionResponse(BaseModel):
class AllUsersResponse(BaseModel):
accepted: list[FullUserSnapshot]
invited: list[InvitedUserSnapshot]
+ slack_users: list[FullUserSnapshot]
accepted_pages: int
invited_pages: int
+ slack_users_pages: int
diff --git a/backend/danswer/server/manage/users.py b/backend/danswer/server/manage/users.py
index 33162b934..75fd9dfe3 100644
--- a/backend/danswer/server/manage/users.py
+++ b/backend/danswer/server/manage/users.py
@@ -119,6 +119,7 @@ def set_user_role(
def list_all_users(
q: str | None = None,
accepted_page: int | None = None,
+ slack_users_page: int | None = None,
invited_page: int | None = None,
user: User | None = Depends(current_curator_or_admin_user),
db_session: Session = Depends(get_session),
@@ -131,7 +132,12 @@ def list_all_users(
for user in list_users(db_session, email_filter_string=q)
if not is_api_key_email_address(user.email)
]
- accepted_emails = {user.email for user in users}
+
+ slack_users = [user for user in users if user.role == UserRole.SLACK_USER]
+ accepted_users = [user for user in users if user.role != UserRole.SLACK_USER]
+
+ accepted_emails = {user.email for user in accepted_users}
+ slack_users_emails = {user.email for user in slack_users}
invited_emails = get_invited_users()
if q:
invited_emails = [
@@ -139,10 +145,11 @@ def list_all_users(
]
accepted_count = len(accepted_emails)
+ slack_users_count = len(slack_users_emails)
invited_count = len(invited_emails)
# If any of q, accepted_page, or invited_page is None, return all users
- if accepted_page is None or invited_page is None:
+ if accepted_page is None or invited_page is None or slack_users_page is None:
return AllUsersResponse(
accepted=[
FullUserSnapshot(
@@ -153,11 +160,23 @@ def list_all_users(
UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED
),
)
- for user in users
+ for user in accepted_users
+ ],
+ slack_users=[
+ FullUserSnapshot(
+ id=user.id,
+ email=user.email,
+ role=user.role,
+ status=(
+ UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED
+ ),
+ )
+ for user in slack_users
],
invited=[InvitedUserSnapshot(email=email) for email in invited_emails],
accepted_pages=1,
invited_pages=1,
+ slack_users_pages=1,
)
# Otherwise, return paginated results
@@ -169,13 +188,27 @@ def list_all_users(
role=user.role,
status=UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED,
)
- for user in users
+ for user in accepted_users
][accepted_page * USERS_PAGE_SIZE : (accepted_page + 1) * USERS_PAGE_SIZE],
+ slack_users=[
+ FullUserSnapshot(
+ id=user.id,
+ email=user.email,
+ role=user.role,
+ status=UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED,
+ )
+ for user in slack_users
+ ][
+ slack_users_page
+ * USERS_PAGE_SIZE : (slack_users_page + 1)
+ * USERS_PAGE_SIZE
+ ],
invited=[InvitedUserSnapshot(email=email) for email in invited_emails][
invited_page * USERS_PAGE_SIZE : (invited_page + 1) * USERS_PAGE_SIZE
],
accepted_pages=accepted_count // USERS_PAGE_SIZE + 1,
invited_pages=invited_count // USERS_PAGE_SIZE + 1,
+ slack_users_pages=slack_users_count // USERS_PAGE_SIZE + 1,
)
diff --git a/backend/tests/integration/common_utils/managers/tenant.py b/backend/tests/integration/common_utils/managers/tenant.py
index fc411018d..c25a1b2ec 100644
--- a/backend/tests/integration/common_utils/managers/tenant.py
+++ b/backend/tests/integration/common_utils/managers/tenant.py
@@ -69,8 +69,10 @@ class TenantManager:
return AllUsersResponse(
accepted=[FullUserSnapshot(**user) for user in data["accepted"]],
invited=[InvitedUserSnapshot(**user) for user in data["invited"]],
+ slack_users=[FullUserSnapshot(**user) for user in data["slack_users"]],
accepted_pages=data["accepted_pages"],
invited_pages=data["invited_pages"],
+ slack_users_pages=data["slack_users_pages"],
)
@staticmethod
diff --git a/backend/tests/integration/common_utils/managers/user.py b/backend/tests/integration/common_utils/managers/user.py
index 43286c6a7..26cb29cdf 100644
--- a/backend/tests/integration/common_utils/managers/user.py
+++ b/backend/tests/integration/common_utils/managers/user.py
@@ -130,8 +130,10 @@ class UserManager:
all_users = AllUsersResponse(
accepted=[FullUserSnapshot(**user) for user in data["accepted"]],
invited=[InvitedUserSnapshot(**user) for user in data["invited"]],
+ slack_users=[FullUserSnapshot(**user) for user in data["slack_users"]],
accepted_pages=data["accepted_pages"],
invited_pages=data["invited_pages"],
+ slack_users_pages=data["slack_users_pages"],
)
for accepted_user in all_users.accepted:
if accepted_user.email == user.email and accepted_user.id == user.id:
diff --git a/web/src/app/admin/users/page.tsx b/web/src/app/admin/users/page.tsx
index 8dbc6c308..e4ffca942 100644
--- a/web/src/app/admin/users/page.tsx
+++ b/web/src/app/admin/users/page.tsx
@@ -1,13 +1,13 @@
"use client";
+import { useEffect, useState } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
import InvitedUserTable from "@/components/admin/users/InvitedUserTable";
import SignedUpUserTable from "@/components/admin/users/SignedUpUserTable";
import { SearchBar } from "@/components/search/SearchBar";
-import { useState } from "react";
import { FiPlusSquare } from "react-icons/fi";
import { Modal } from "@/components/Modal";
-
-import { Button } from "@/components/ui/button";
-import Text from "@/components/ui/text";
import { LoadingAnimation } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
@@ -15,42 +15,10 @@ import { UsersIcon } from "@/components/icons/icons";
import { errorHandlingFetcher } from "@/lib/fetcher";
import useSWR, { mutate } from "swr";
import { ErrorCallout } from "@/components/ErrorCallout";
-import { HidableSection } from "@/app/admin/assistants/HidableSection";
import BulkAdd from "@/components/admin/users/BulkAdd";
import { UsersResponse } from "@/lib/users/interfaces";
-
-const ValidDomainsDisplay = ({ validDomains }: { validDomains: string[] }) => {
- if (!validDomains.length) {
- return (
-
- No invited users. Anyone can sign up with a valid email address. To
- restrict access you can:
-
- (1) Invite users above. Once a user has been invited, only emails that
- have explicitly been invited will be able to sign-up.
-
-
- (2) Set the{" "}
- VALID_EMAIL_DOMAINS {" "}
- environment variable to a comma separated list of email domains. This
- will restrict access to users with email addresses from these domains.
-
-
- );
- }
-
- return (
-
- No invited users. Anyone with an email address with any of the following
- domains can sign up:
{validDomains.join(", ")} .
-
- To further restrict access you can invite users above. Once a user has
- been invited, only emails that have explicitly been invited will be able
- to sign-up.
-
-
- );
-};
+import SlackUserTable from "@/components/admin/users/SlackUserTable";
+import Text from "@/components/ui/text";
const UsersTables = ({
q,
@@ -61,23 +29,48 @@ const UsersTables = ({
}) => {
const [invitedPage, setInvitedPage] = useState(1);
const [acceptedPage, setAcceptedPage] = useState(1);
- const { data, isLoading, mutate, error } = useSWR(
- `/api/manage/users?q=${encodeURI(q)}&accepted_page=${
+ const [slackUsersPage, setSlackUsersPage] = useState(1);
+
+ const [usersData, setUsersData] = useState(
+ undefined
+ );
+ const [domainsData, setDomainsData] = useState(
+ undefined
+ );
+
+ const { data, error, mutate } = useSWR(
+ `/api/manage/users?q=${encodeURIComponent(q)}&accepted_page=${
acceptedPage - 1
- }&invited_page=${invitedPage - 1}`,
+ }&invited_page=${invitedPage - 1}&slack_users_page=${slackUsersPage - 1}`,
errorHandlingFetcher
);
- const {
- data: validDomains,
- isLoading: isLoadingDomains,
- error: domainsError,
- } = useSWR("/api/manage/admin/valid-domains", errorHandlingFetcher);
- if (isLoading || isLoadingDomains) {
+ const { data: validDomains, error: domainsError } = useSWR(
+ "/api/manage/admin/valid-domains",
+ errorHandlingFetcher
+ );
+
+ useEffect(() => {
+ if (data) {
+ setUsersData(data);
+ }
+ }, [data]);
+
+ useEffect(() => {
+ if (validDomains) {
+ setDomainsData(validDomains);
+ }
+ }, [validDomains]);
+
+ const activeData = data ?? usersData;
+ const activeDomains = validDomains ?? domainsData;
+
+ // Show loading animation only during the initial data fetch
+ if (!activeData || !activeDomains) {
return ;
}
- if (error || !data) {
+ if (error) {
return (
!accepted.map((u) => u.email).includes(user.email)
+ (user) => !accepted.some((u) => u.email === user.email)
);
return (
- <>
-
- {invited.length > 0 ? (
- finalInvited.length > 0 ? (
-
- ) : (
-
- To invite additional teammates, use the Invite Users button
- above!
-
- )
- ) : (
-
- )}
-
-
- >
+
+
+ Invited Users
+ Current Users
+ DanswerBot Users
+
+
+
+
+
+ Invited Users
+
+
+ {finalInvited.length > 0 ? (
+
+ ) : (
+ Users that have been invited will show up here
+ )}
+
+
+
+
+
+
+
+ Current Users
+
+
+ {accepted.length > 0 ? (
+
+ ) : (
+ Users that have an account will show up here
+ )}
+
+
+
+
+
+
+
+ DanswerBot Users
+
+
+ {slack_users.length > 0 ? (
+
+ ) : (
+ Slack-only users will show up here
+ )}
+
+
+
+
);
};
@@ -215,6 +257,7 @@ const Page = () => {
return (
);
diff --git a/web/src/components/admin/users/InvitedUserTable.tsx b/web/src/components/admin/users/InvitedUserTable.tsx
index ac50ac15b..43c2a8948 100644
--- a/web/src/components/admin/users/InvitedUserTable.tsx
+++ b/web/src/components/admin/users/InvitedUserTable.tsx
@@ -6,14 +6,13 @@ import {
TableBody,
TableCell,
} from "@/components/ui/table";
-import { Button } from "@/components/ui/button";
-import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
+
import CenteredPageSelector from "./CenteredPageSelector";
import { type PageSelectorProps } from "@/components/PageSelector";
import { type User } from "@/lib/types";
-import useSWRMutation from "swr/mutation";
import { TableHeader } from "@/components/ui/table";
+import { InviteUserButton } from "./buttons/InviteUserButton";
interface Props {
users: Array;
@@ -21,27 +20,6 @@ interface Props {
mutate: () => void;
}
-const RemoveUserButton = ({
- user,
- onSuccess,
- onError,
-}: {
- user: User;
- onSuccess: () => void;
- onError: (message: string) => void;
-}) => {
- const { trigger } = useSWRMutation(
- "/api/manage/admin/remove-invited-user",
- userMutationFetcher,
- { onSuccess, onError }
- );
- return (
- trigger({ user_email: user.email })}>
- Uninvite User
-
- );
-};
-
const InvitedUserTable = ({
users,
setPopup,
@@ -52,20 +30,6 @@ const InvitedUserTable = ({
}: Props & PageSelectorProps) => {
if (!users.length) return null;
- const onRemovalSuccess = () => {
- mutate();
- setPopup({
- message: "User uninvited!",
- type: "success",
- });
- };
- const onRemovalError = (errorMsg: string) => {
- setPopup({
- message: `Unable to uninvite user - ${errorMsg}`,
- type: "error",
- });
- };
-
return (
<>
@@ -83,10 +47,11 @@ const InvitedUserTable = ({
{user.email}
-
diff --git a/web/src/components/admin/users/SignedUpUserTable.tsx b/web/src/components/admin/users/SignedUpUserTable.tsx
index caae9383a..b04c0141b 100644
--- a/web/src/components/admin/users/SignedUpUserTable.tsx
+++ b/web/src/components/admin/users/SignedUpUserTable.tsx
@@ -1,16 +1,7 @@
-import {
- type User,
- UserStatus,
- UserRole,
- USER_ROLE_LABELS,
- INVALID_ROLE_HOVER_TEXT,
-} from "@/lib/types";
+import { type User, UserStatus, UserRole } from "@/lib/types";
import CenteredPageSelector from "./CenteredPageSelector";
import { type PageSelectorProps } from "@/components/PageSelector";
-import { HidableSection } from "@/app/admin/assistants/HidableSection";
import { PopupSpec } from "@/components/admin/connectors/Popup";
-import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
-import useSWRMutation from "swr/mutation";
import {
Table,
TableHead,
@@ -18,20 +9,10 @@ import {
TableBody,
TableCell,
} from "@/components/ui/table";
-
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Button } from "@/components/ui/button";
-import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
-import { useState } from "react";
-import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
-import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { TableHeader } from "@/components/ui/table";
+import { UserRoleDropdown } from "./buttons/UserRoleDropdown";
+import { DeleteUserButton } from "./buttons/DeleteUserButton";
+import { DeactivaterButton } from "./buttons/DeactivaterButton";
interface Props {
users: Array;
@@ -39,204 +20,6 @@ interface Props {
mutate: () => void;
}
-const UserRoleDropdown = ({
- user,
- onSuccess,
- onError,
-}: {
- user: User;
- onSuccess: () => void;
- onError: (message: string) => void;
-}) => {
- const [showConfirmModal, setShowConfirmModal] = useState(false);
- const [pendingRole, setPendingRole] = useState(null);
-
- const { trigger: setUserRole, isMutating: isSettingRole } = useSWRMutation(
- "/api/manage/set-user-role",
- userMutationFetcher,
- { onSuccess, onError }
- );
- const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
-
- const handleChange = (value: string) => {
- if (value === user.role) return;
- if (user.role === UserRole.CURATOR) {
- setShowConfirmModal(true);
- setPendingRole(value);
- } else {
- setUserRole({
- user_email: user.email,
- new_role: value,
- });
- }
- };
-
- const handleConfirm = () => {
- if (pendingRole) {
- setUserRole({
- user_email: user.email,
- new_role: pendingRole,
- });
- }
- setShowConfirmModal(false);
- setPendingRole(null);
- };
-
- return (
- <>
-
-
-
-
-
- {(Object.entries(USER_ROLE_LABELS) as [UserRole, string][]).map(
- ([role, label]) => {
- // Dont want to ever show external permissioned users because it's scary
- if (role === UserRole.EXT_PERM_USER) return null;
-
- // Only want to show limited users if paid enterprise features are enabled
- // Also, dont want to show these other roles in general
- const isNotVisibleRole =
- (!isPaidEnterpriseFeaturesEnabled &&
- role === UserRole.GLOBAL_CURATOR) ||
- role === UserRole.CURATOR ||
- role === UserRole.LIMITED ||
- role === UserRole.SLACK_USER;
-
- // Always show the current role
- const isCurrentRole = user.role === role;
-
- return isNotVisibleRole && !isCurrentRole ? null : (
-
- {label}
-
- );
- }
- )}
-
-
- {showConfirmModal && (
- setShowConfirmModal(false)}
- onConfirm={handleConfirm}
- />
- )}
- >
- );
-};
-
-const DeactivaterButton = ({
- user,
- deactivate,
- setPopup,
- mutate,
-}: {
- user: User;
- deactivate: boolean;
- setPopup: (spec: PopupSpec) => void;
- mutate: () => void;
-}) => {
- const { trigger, isMutating } = useSWRMutation(
- deactivate
- ? "/api/manage/admin/deactivate-user"
- : "/api/manage/admin/activate-user",
- userMutationFetcher,
- {
- onSuccess: () => {
- mutate();
- setPopup({
- message: `User ${deactivate ? "deactivated" : "activated"}!`,
- type: "success",
- });
- },
- onError: (errorMsg) =>
- setPopup({ message: errorMsg.message, type: "error" }),
- }
- );
- return (
- trigger({ user_email: user.email })}
- disabled={isMutating}
- size="sm"
- >
- {deactivate ? "Deactivate" : "Activate"}
-
- );
-};
-
-const DeleteUserButton = ({
- user,
- setPopup,
- mutate,
-}: {
- user: User;
- setPopup: (spec: PopupSpec) => void;
- mutate: () => void;
-}) => {
- const { trigger, isMutating } = useSWRMutation(
- "/api/manage/admin/delete-user",
- userMutationFetcher,
- {
- onSuccess: () => {
- mutate();
- setPopup({
- message: "User deleted successfully!",
- type: "success",
- });
- },
- onError: (errorMsg) =>
- setPopup({
- message: `Unable to delete user - ${errorMsg}`,
- type: "error",
- }),
- }
- );
-
- const [showDeleteModal, setShowDeleteModal] = useState(false);
- return (
- <>
- {showDeleteModal && (
- setShowDeleteModal(false)}
- onSubmit={() => trigger({ user_email: user.email, method: "DELETE" })}
- additionalDetails="All data associated with this user will be deleted (including personas, tools and chat sessions)."
- />
- )}
-
- setShowDeleteModal(true)}
- disabled={isMutating}
- size="sm"
- variant="destructive"
- >
- Delete
-
- >
- );
-};
-
const SignedUpUserTable = ({
users,
setPopup,
@@ -258,68 +41,66 @@ const SignedUpUserTable = ({
handlePopup(`Unable to update user role - ${errorMsg}`, "error");
return (
-
- <>
- {totalPages > 1 ? (
-
- ) : null}
-
-
-
- Email
- Role
- Status
-
-
-
-
-
-
- {users
- // Dont want to show external permissioned users because it's scary
- .filter((user) => user.role !== UserRole.EXT_PERM_USER)
- .map((user) => (
-
- {user.email}
-
-
+ {totalPages > 1 ? (
+
+ ) : null}
+
+
+
+ Email
+ Role
+ Status
+
+
+
+
+
+
+ {users
+ // Dont want to show external permissioned users because it's scary
+ .filter((user) => user.role !== UserRole.EXT_PERM_USER)
+ .map((user) => (
+
+ {user.email}
+
+
+
+
+ {user.status === "live" ? "Active" : "Inactive"}
+
+
+
+
-
-
- {user.status === "live" ? "Active" : "Inactive"}
-
-
-
-
- {user.status == UserStatus.deactivated && (
-
- )}
-
-
-
- ))}
-
-
- >
-
+ )}
+
+
+
+ ))}
+
+
+ >
);
};
diff --git a/web/src/components/admin/users/SlackUserTable.tsx b/web/src/components/admin/users/SlackUserTable.tsx
new file mode 100644
index 000000000..ce6d6a911
--- /dev/null
+++ b/web/src/components/admin/users/SlackUserTable.tsx
@@ -0,0 +1,78 @@
+import React from "react";
+import { User } from "@/lib/types";
+import {
+ Table,
+ TableCell,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { PopupSpec } from "../connectors/Popup";
+import { InviteUserButton } from "./buttons/InviteUserButton";
+import { PageSelectorProps } from "@/components/PageSelector";
+import CenteredPageSelector from "./CenteredPageSelector";
+
+interface SlackUserTableProps {
+ invitedUsers: User[];
+ slackusers: User[];
+ mutate: () => void;
+ setPopup: (spec: PopupSpec) => void;
+}
+
+const SlackUserTable: React.FC = ({
+ invitedUsers,
+ slackusers,
+ mutate,
+ currentPage,
+ totalPages,
+ onPageChange,
+ setPopup,
+}) => {
+ return (
+ <>
+ {totalPages > 1 ? (
+
+ ) : null}
+
+
+
+ Email
+ Status
+
+
+
+
+
+
+ {slackusers.map((user) => (
+
+ {user.email}
+
+ {user.status === "live" ? "Active" : "Inactive"}
+
+
+ u.email)
+ .includes(user.email)}
+ setPopup={setPopup}
+ mutate={mutate}
+ />
+
+
+ ))}
+
+
+ >
+ );
+};
+
+export default SlackUserTable;
diff --git a/web/src/components/admin/users/UserStatusButtons.tsx b/web/src/components/admin/users/UserStatusButtons.tsx
new file mode 100644
index 000000000..4963c3b84
--- /dev/null
+++ b/web/src/components/admin/users/UserStatusButtons.tsx
@@ -0,0 +1,250 @@
+import {
+ type User,
+ UserStatus,
+ UserRole,
+ USER_ROLE_LABELS,
+ INVALID_ROLE_HOVER_TEXT,
+} from "@/lib/types";
+import CenteredPageSelector from "./CenteredPageSelector";
+import { type PageSelectorProps } from "@/components/PageSelector";
+import { HidableSection } from "@/app/admin/assistants/HidableSection";
+import { PopupSpec } from "@/components/admin/connectors/Popup";
+import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
+import useSWRMutation from "swr/mutation";
+import {
+ Table,
+ TableHead,
+ TableRow,
+ TableBody,
+ TableCell,
+} from "@/components/ui/table";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
+import { useState } from "react";
+import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
+import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
+import { TableHeader } from "@/components/ui/table";
+
+export const InviteUserButton = ({
+ user,
+ invited,
+ setPopup,
+ mutate,
+}: {
+ user: User;
+ invited: boolean;
+ setPopup: (spec: PopupSpec) => void;
+ mutate: () => void;
+}) => {
+ const { trigger: inviteTrigger, isMutating: isInviting } = useSWRMutation(
+ "/api/manage/admin/users",
+ async (url, { arg }: { arg: { emails: string[] } }) => {
+ const response = await fetch(url, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(arg),
+ });
+ if (!response.ok) {
+ throw new Error(await response.text());
+ }
+ return response.json();
+ },
+ {
+ onSuccess: () => {
+ setShowInviteModal(false);
+ mutate();
+ setPopup({
+ message: "User invited successfully!",
+ type: "success",
+ });
+ },
+ onError: (errorMsg) =>
+ setPopup({
+ message: `Unable to invite user - ${errorMsg}`,
+ type: "error",
+ }),
+ }
+ );
+
+ const { trigger: uninviteTrigger, isMutating: isUninviting } = useSWRMutation(
+ "/api/manage/admin/remove-invited-user",
+ async (url, { arg }: { arg: { user_email: string } }) => {
+ const response = await fetch(url, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(arg),
+ });
+ if (!response.ok) {
+ throw new Error(await response.text());
+ }
+ return response.json();
+ },
+ {
+ onSuccess: () => {
+ setShowInviteModal(false);
+ mutate();
+ setPopup({
+ message: "User uninvited successfully!",
+ type: "success",
+ });
+ },
+ onError: (errorMsg) =>
+ setPopup({
+ message: `Unable to uninvite user - ${errorMsg}`,
+ type: "error",
+ }),
+ }
+ );
+
+ const [showInviteModal, setShowInviteModal] = useState(false);
+
+ const handleConfirm = () => {
+ if (invited) {
+ uninviteTrigger({ user_email: user.email });
+ } else {
+ inviteTrigger({ emails: [user.email] });
+ }
+ };
+
+ const isMutating = isInviting || isUninviting;
+
+ return (
+ <>
+ {showInviteModal && (
+ setShowInviteModal(false)}
+ onConfirm={handleConfirm}
+ />
+ )}
+
+ setShowInviteModal(true)}
+ disabled={isMutating}
+ size="sm"
+ >
+ {invited ? "Uninvite" : "Invite"}
+
+ >
+ );
+};
+
+export const UserRoleDropdown = ({
+ user,
+ onSuccess,
+ onError,
+}: {
+ user: User;
+ onSuccess: () => void;
+ onError: (message: string) => void;
+}) => {
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [pendingRole, setPendingRole] = useState(null);
+
+ const { trigger: setUserRole, isMutating: isSettingRole } = useSWRMutation(
+ "/api/manage/set-user-role",
+ userMutationFetcher,
+ { onSuccess, onError }
+ );
+ const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
+
+ const handleChange = (value: string) => {
+ if (value === user.role) return;
+ if (user.role === UserRole.CURATOR) {
+ setShowConfirmModal(true);
+ setPendingRole(value);
+ } else {
+ setUserRole({
+ user_email: user.email,
+ new_role: value,
+ });
+ }
+ };
+
+ const handleConfirm = () => {
+ if (pendingRole) {
+ setUserRole({
+ user_email: user.email,
+ new_role: pendingRole,
+ });
+ }
+ setShowConfirmModal(false);
+ setPendingRole(null);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {(Object.entries(USER_ROLE_LABELS) as [UserRole, string][]).map(
+ ([role, label]) => {
+ // Dont want to ever show external permissioned users because it's scary
+ if (role === UserRole.EXT_PERM_USER) return null;
+
+ // Only want to show limited users if paid enterprise features are enabled
+ // Also, dont want to show these other roles in general
+ const isNotVisibleRole =
+ (!isPaidEnterpriseFeaturesEnabled &&
+ role === UserRole.GLOBAL_CURATOR) ||
+ role === UserRole.CURATOR ||
+ role === UserRole.LIMITED ||
+ role === UserRole.SLACK_USER;
+
+ // Always show the current role
+ const isCurrentRole = user.role === role;
+
+ return isNotVisibleRole && !isCurrentRole ? null : (
+
+ {label}
+
+ );
+ }
+ )}
+
+
+ {showConfirmModal && (
+ setShowConfirmModal(false)}
+ onConfirm={handleConfirm}
+ />
+ )}
+ >
+ );
+};
diff --git a/web/src/components/admin/users/buttons/DeactivaterButton.tsx b/web/src/components/admin/users/buttons/DeactivaterButton.tsx
new file mode 100644
index 000000000..d1e0994db
--- /dev/null
+++ b/web/src/components/admin/users/buttons/DeactivaterButton.tsx
@@ -0,0 +1,73 @@
+import {
+ type User,
+ UserStatus,
+ UserRole,
+ USER_ROLE_LABELS,
+ INVALID_ROLE_HOVER_TEXT,
+} from "@/lib/types";
+import { type PageSelectorProps } from "@/components/PageSelector";
+import { HidableSection } from "@/app/admin/assistants/HidableSection";
+import { PopupSpec } from "@/components/admin/connectors/Popup";
+import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
+import useSWRMutation from "swr/mutation";
+import {
+ Table,
+ TableHead,
+ TableRow,
+ TableBody,
+ TableCell,
+} from "@/components/ui/table";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
+import { useState } from "react";
+import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
+import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
+import { TableHeader } from "@/components/ui/table";
+
+export const DeactivaterButton = ({
+ user,
+ deactivate,
+ setPopup,
+ mutate,
+}: {
+ user: User;
+ deactivate: boolean;
+ setPopup: (spec: PopupSpec) => void;
+ mutate: () => void;
+}) => {
+ const { trigger, isMutating } = useSWRMutation(
+ deactivate
+ ? "/api/manage/admin/deactivate-user"
+ : "/api/manage/admin/activate-user",
+ userMutationFetcher,
+ {
+ onSuccess: () => {
+ mutate();
+ setPopup({
+ message: `User ${deactivate ? "deactivated" : "activated"}!`,
+ type: "success",
+ });
+ },
+ onError: (errorMsg) =>
+ setPopup({ message: errorMsg.message, type: "error" }),
+ }
+ );
+ return (
+ trigger({ user_email: user.email })}
+ disabled={isMutating}
+ size="sm"
+ >
+ {deactivate ? "Deactivate" : "Activate"}
+
+ );
+};
diff --git a/web/src/components/admin/users/buttons/DeleteUserButton.tsx b/web/src/components/admin/users/buttons/DeleteUserButton.tsx
new file mode 100644
index 000000000..7c3cd7ef7
--- /dev/null
+++ b/web/src/components/admin/users/buttons/DeleteUserButton.tsx
@@ -0,0 +1,61 @@
+import { type User } from "@/lib/types";
+import { PopupSpec } from "@/components/admin/connectors/Popup";
+import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
+import useSWRMutation from "swr/mutation";
+import { Button } from "@/components/ui/button";
+import { useState } from "react";
+import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
+
+export const DeleteUserButton = ({
+ user,
+ setPopup,
+ mutate,
+}: {
+ user: User;
+ setPopup: (spec: PopupSpec) => void;
+ mutate: () => void;
+}) => {
+ const { trigger, isMutating } = useSWRMutation(
+ "/api/manage/admin/delete-user",
+ userMutationFetcher,
+ {
+ onSuccess: () => {
+ mutate();
+ setPopup({
+ message: "User deleted successfully!",
+ type: "success",
+ });
+ },
+ onError: (errorMsg) =>
+ setPopup({
+ message: `Unable to delete user - ${errorMsg}`,
+ type: "error",
+ }),
+ }
+ );
+
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ return (
+ <>
+ {showDeleteModal && (
+ setShowDeleteModal(false)}
+ onSubmit={() => trigger({ user_email: user.email, method: "DELETE" })}
+ additionalDetails="All data associated with this user will be deleted (including personas, tools and chat sessions)."
+ />
+ )}
+
+ setShowDeleteModal(true)}
+ disabled={isMutating}
+ size="sm"
+ variant="destructive"
+ >
+ Delete
+
+ >
+ );
+};
diff --git a/web/src/components/admin/users/buttons/InviteUserButton.tsx b/web/src/components/admin/users/buttons/InviteUserButton.tsx
new file mode 100644
index 000000000..e456cc734
--- /dev/null
+++ b/web/src/components/admin/users/buttons/InviteUserButton.tsx
@@ -0,0 +1,119 @@
+import { type User } from "@/lib/types";
+
+import { PopupSpec } from "@/components/admin/connectors/Popup";
+import useSWRMutation from "swr/mutation";
+import { Button } from "@/components/ui/button";
+import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
+import { useState } from "react";
+
+export const InviteUserButton = ({
+ user,
+ invited,
+ setPopup,
+ mutate,
+}: {
+ user: User;
+ invited: boolean;
+ setPopup: (spec: PopupSpec) => void;
+ mutate: () => void;
+}) => {
+ const { trigger: inviteTrigger, isMutating: isInviting } = useSWRMutation(
+ "/api/manage/admin/users",
+ async (url, { arg }: { arg: { emails: string[] } }) => {
+ const response = await fetch(url, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(arg),
+ });
+ if (!response.ok) {
+ throw new Error(await response.text());
+ }
+ return response.json();
+ },
+ {
+ onSuccess: () => {
+ setShowInviteModal(false);
+ mutate();
+ setPopup({
+ message: "User invited successfully!",
+ type: "success",
+ });
+ },
+ onError: (errorMsg) =>
+ setPopup({
+ message: `Unable to invite user - ${errorMsg}`,
+ type: "error",
+ }),
+ }
+ );
+
+ const { trigger: uninviteTrigger, isMutating: isUninviting } = useSWRMutation(
+ "/api/manage/admin/remove-invited-user",
+ async (url, { arg }: { arg: { user_email: string } }) => {
+ const response = await fetch(url, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(arg),
+ });
+ if (!response.ok) {
+ throw new Error(await response.text());
+ }
+ return response.json();
+ },
+ {
+ onSuccess: () => {
+ setShowInviteModal(false);
+ mutate();
+ setPopup({
+ message: "User uninvited successfully!",
+ type: "success",
+ });
+ },
+ onError: (errorMsg) =>
+ setPopup({
+ message: `Unable to uninvite user - ${errorMsg}`,
+ type: "error",
+ }),
+ }
+ );
+
+ const [showInviteModal, setShowInviteModal] = useState(false);
+
+ const handleConfirm = () => {
+ if (invited) {
+ uninviteTrigger({ user_email: user.email });
+ } else {
+ inviteTrigger({ emails: [user.email] });
+ }
+ };
+
+ const isMutating = isInviting || isUninviting;
+
+ return (
+ <>
+ {showInviteModal && (
+ setShowInviteModal(false)}
+ onConfirm={handleConfirm}
+ />
+ )}
+
+ setShowInviteModal(true)}
+ disabled={isMutating}
+ size="sm"
+ >
+ {invited ? "Uninvite" : "Invite"}
+
+ >
+ );
+};
diff --git a/web/src/components/admin/users/buttons/UserRoleDropdown.tsx b/web/src/components/admin/users/buttons/UserRoleDropdown.tsx
new file mode 100644
index 000000000..3f7ac121a
--- /dev/null
+++ b/web/src/components/admin/users/buttons/UserRoleDropdown.tsx
@@ -0,0 +1,123 @@
+import {
+ type User,
+ UserRole,
+ USER_ROLE_LABELS,
+ INVALID_ROLE_HOVER_TEXT,
+} from "@/lib/types";
+import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
+import useSWRMutation from "swr/mutation";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
+import { useState } from "react";
+import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
+
+export const UserRoleDropdown = ({
+ user,
+ onSuccess,
+ onError,
+}: {
+ user: User;
+ onSuccess: () => void;
+ onError: (message: string) => void;
+}) => {
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [pendingRole, setPendingRole] = useState(null);
+
+ const { trigger: setUserRole, isMutating: isSettingRole } = useSWRMutation(
+ "/api/manage/set-user-role",
+ userMutationFetcher,
+ { onSuccess, onError }
+ );
+ const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
+
+ const handleChange = (value: string) => {
+ if (value === user.role) return;
+ if (user.role === UserRole.CURATOR) {
+ setShowConfirmModal(true);
+ setPendingRole(value);
+ } else {
+ setUserRole({
+ user_email: user.email,
+ new_role: value,
+ });
+ }
+ };
+
+ const handleConfirm = () => {
+ if (pendingRole) {
+ setUserRole({
+ user_email: user.email,
+ new_role: pendingRole,
+ });
+ }
+ setShowConfirmModal(false);
+ setPendingRole(null);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {(Object.entries(USER_ROLE_LABELS) as [UserRole, string][]).map(
+ ([role, label]) => {
+ // Dont want to ever show external permissioned users because it's scary
+ if (role === UserRole.EXT_PERM_USER) return null;
+
+ // Only want to show limited users if paid enterprise features are enabled
+ // Also, dont want to show these other roles in general
+ const isNotVisibleRole =
+ (!isPaidEnterpriseFeaturesEnabled &&
+ role === UserRole.GLOBAL_CURATOR) ||
+ role === UserRole.CURATOR ||
+ role === UserRole.LIMITED ||
+ role === UserRole.SLACK_USER;
+
+ // Always show the current role
+ const isCurrentRole = user.role === role;
+
+ return isNotVisibleRole && !isCurrentRole ? null : (
+
+ {label}
+
+ );
+ }
+ )}
+
+
+ {showConfirmModal && (
+ setShowConfirmModal(false)}
+ onConfirm={handleConfirm}
+ />
+ )}
+ >
+ );
+};
diff --git a/web/src/lib/users/interfaces.ts b/web/src/lib/users/interfaces.ts
index 1bcd00ae2..c2963521f 100644
--- a/web/src/lib/users/interfaces.ts
+++ b/web/src/lib/users/interfaces.ts
@@ -3,6 +3,8 @@ import { User } from "../types";
export interface UsersResponse {
accepted: User[];
invited: User[];
+ slack_users: User[];
accepted_pages: number;
invited_pages: number;
+ slack_users_pages: number;
}
diff --git a/web/tests/e2e/admin_users.spec.ts b/web/tests/e2e/admin_users.spec.ts
index 6ec4c803c..2fc318884 100644
--- a/web/tests/e2e/admin_users.spec.ts
+++ b/web/tests/e2e/admin_users.spec.ts
@@ -9,11 +9,5 @@ test(
// Test simple loading
await page.goto("http://localhost:3000/admin/users");
await expect(page.locator("h1.text-3xl")).toHaveText("Manage Users");
- await expect(page.locator("div.font-bold").nth(0)).toHaveText(
- "Invited Users"
- );
- await expect(page.locator("div.font-bold").nth(1)).toHaveText(
- "Current Users"
- );
}
);