mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-08 03:48:14 +02:00
base role setting fix (#3381)
* base role setting fix * update user tables * finalize * minor cleanup * fix chromatic
This commit is contained in:
parent
c667d28e7a
commit
d95959fb41
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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 (
|
||||
<div className="text-sm">
|
||||
No invited users. Anyone can sign up with a valid email address. To
|
||||
restrict access you can:
|
||||
<div className="flex flex-wrap ml-2 mt-1">
|
||||
(1) Invite users above. Once a user has been invited, only emails that
|
||||
have explicitly been invited will be able to sign-up.
|
||||
</div>
|
||||
<div className="mt-1 ml-2">
|
||||
(2) Set the{" "}
|
||||
<b className="font-mono w-fit h-fit">VALID_EMAIL_DOMAINS</b>{" "}
|
||||
environment variable to a comma separated list of email domains. This
|
||||
will restrict access to users with email addresses from these domains.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
No invited users. Anyone with an email address with any of the following
|
||||
domains can sign up: <i>{validDomains.join(", ")}</i>.
|
||||
<div className="mt-2">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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<UsersResponse>(
|
||||
`/api/manage/users?q=${encodeURI(q)}&accepted_page=${
|
||||
const [slackUsersPage, setSlackUsersPage] = useState(1);
|
||||
|
||||
const [usersData, setUsersData] = useState<UsersResponse | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [domainsData, setDomainsData] = useState<string[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const { data, error, mutate } = useSWR<UsersResponse>(
|
||||
`/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<string[]>("/api/manage/admin/valid-domains", errorHandlingFetcher);
|
||||
|
||||
if (isLoading || isLoadingDomains) {
|
||||
const { data: validDomains, error: domainsError } = useSWR<string[]>(
|
||||
"/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 <LoadingAnimation text="Loading" />;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Error loading users"
|
||||
@ -86,7 +79,7 @@ const UsersTables = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (domainsError || !validDomains) {
|
||||
if (domainsError) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Error loading valid domains"
|
||||
@ -95,45 +88,94 @@ const UsersTables = ({
|
||||
);
|
||||
}
|
||||
|
||||
const { accepted, invited, accepted_pages, invited_pages } = data;
|
||||
const {
|
||||
accepted,
|
||||
invited,
|
||||
accepted_pages,
|
||||
invited_pages,
|
||||
slack_users,
|
||||
slack_users_pages,
|
||||
} = activeData;
|
||||
|
||||
// remove users that are already accepted
|
||||
const finalInvited = invited.filter(
|
||||
(user) => !accepted.map((u) => u.email).includes(user.email)
|
||||
(user) => !accepted.some((u) => u.email === user.email)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HidableSection sectionTitle="Invited Users">
|
||||
{invited.length > 0 ? (
|
||||
finalInvited.length > 0 ? (
|
||||
<InvitedUserTable
|
||||
users={finalInvited}
|
||||
setPopup={setPopup}
|
||||
currentPage={invitedPage}
|
||||
onPageChange={setInvitedPage}
|
||||
totalPages={invited_pages}
|
||||
mutate={mutate}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
To invite additional teammates, use the <b>Invite Users</b> button
|
||||
above!
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<ValidDomainsDisplay validDomains={validDomains} />
|
||||
)}
|
||||
</HidableSection>
|
||||
<SignedUpUserTable
|
||||
users={accepted}
|
||||
setPopup={setPopup}
|
||||
currentPage={acceptedPage}
|
||||
onPageChange={setAcceptedPage}
|
||||
totalPages={accepted_pages}
|
||||
mutate={mutate}
|
||||
/>
|
||||
</>
|
||||
<Tabs defaultValue="invited">
|
||||
<TabsList>
|
||||
<TabsTrigger value="invited">Invited Users</TabsTrigger>
|
||||
<TabsTrigger value="current">Current Users</TabsTrigger>
|
||||
<TabsTrigger value="danswerbot">DanswerBot Users</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="invited">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invited Users</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{finalInvited.length > 0 ? (
|
||||
<InvitedUserTable
|
||||
users={finalInvited}
|
||||
setPopup={setPopup}
|
||||
currentPage={invitedPage}
|
||||
onPageChange={setInvitedPage}
|
||||
totalPages={invited_pages}
|
||||
mutate={mutate}
|
||||
/>
|
||||
) : (
|
||||
<p>Users that have been invited will show up here</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="current">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Current Users</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{accepted.length > 0 ? (
|
||||
<SignedUpUserTable
|
||||
users={accepted}
|
||||
setPopup={setPopup}
|
||||
currentPage={acceptedPage}
|
||||
onPageChange={setAcceptedPage}
|
||||
totalPages={accepted_pages}
|
||||
mutate={mutate}
|
||||
/>
|
||||
) : (
|
||||
<p>Users that have an account will show up here</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="danswerbot">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>DanswerBot Users</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{slack_users.length > 0 ? (
|
||||
<SlackUserTable
|
||||
setPopup={setPopup}
|
||||
currentPage={slackUsersPage}
|
||||
onPageChange={setSlackUsersPage}
|
||||
totalPages={slack_users_pages}
|
||||
invitedUsers={finalInvited}
|
||||
slackusers={slack_users}
|
||||
mutate={mutate}
|
||||
/>
|
||||
) : (
|
||||
<p>Slack-only users will show up here</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@ -215,6 +257,7 @@ const Page = () => {
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
|
||||
|
||||
<SearchableTables />
|
||||
</div>
|
||||
);
|
||||
|
@ -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<User>;
|
||||
@ -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 (
|
||||
<Button onClick={() => trigger({ user_email: user.email })}>
|
||||
Uninvite User
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Table className="overflow-visible">
|
||||
@ -83,10 +47,11 @@ const InvitedUserTable = ({
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end">
|
||||
<RemoveUserButton
|
||||
<InviteUserButton
|
||||
user={user}
|
||||
onSuccess={onRemovalSuccess}
|
||||
onError={onRemovalError}
|
||||
invited={true}
|
||||
setPopup={setPopup}
|
||||
mutate={mutate}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
@ -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<User>;
|
||||
@ -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<string | null>(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 (
|
||||
<>
|
||||
<Select
|
||||
value={user.role}
|
||||
onValueChange={handleChange}
|
||||
disabled={isSettingRole}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(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 : (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
title={INVALID_ROLE_HOVER_TEXT[role] ?? ""}
|
||||
data-tooltip-delay="0"
|
||||
>
|
||||
{label}
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showConfirmModal && (
|
||||
<GenericConfirmModal
|
||||
title="Change Curator Role"
|
||||
message={`Warning: Switching roles from Curator to ${
|
||||
USER_ROLE_LABELS[pendingRole as UserRole] ??
|
||||
USER_ROLE_LABELS[user.role]
|
||||
} will remove their status as individual curators from all groups.`}
|
||||
confirmText={`Switch Role to ${
|
||||
USER_ROLE_LABELS[pendingRole as UserRole] ??
|
||||
USER_ROLE_LABELS[user.role]
|
||||
}`}
|
||||
onClose={() => 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 (
|
||||
<Button
|
||||
className="w-min"
|
||||
onClick={() => trigger({ user_email: user.email })}
|
||||
disabled={isMutating}
|
||||
size="sm"
|
||||
>
|
||||
{deactivate ? "Deactivate" : "Activate"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
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 && (
|
||||
<DeleteEntityModal
|
||||
entityType="user"
|
||||
entityName={user.email}
|
||||
onClose={() => 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)."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-min"
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
disabled={isMutating}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SignedUpUserTable = ({
|
||||
users,
|
||||
setPopup,
|
||||
@ -258,68 +41,66 @@ const SignedUpUserTable = ({
|
||||
handlePopup(`Unable to update user role - ${errorMsg}`, "error");
|
||||
|
||||
return (
|
||||
<HidableSection sectionTitle="Current Users">
|
||||
<>
|
||||
{totalPages > 1 ? (
|
||||
<CenteredPageSelector
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
) : null}
|
||||
<Table className="overflow-visible">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead className="text-center">Role</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
<TableHead>
|
||||
<div className="flex">
|
||||
<div className="ml-auto">Actions</div>
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users
|
||||
// Dont want to show external permissioned users because it's scary
|
||||
.filter((user) => user.role !== UserRole.EXT_PERM_USER)
|
||||
.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell className="w-40 ">
|
||||
<UserRoleDropdown
|
||||
<>
|
||||
{totalPages > 1 ? (
|
||||
<CenteredPageSelector
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
) : null}
|
||||
<Table className="overflow-visible">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead className="text-center">Role</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
<TableHead>
|
||||
<div className="flex">
|
||||
<div className="ml-auto">Actions</div>
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users
|
||||
// Dont want to show external permissioned users because it's scary
|
||||
.filter((user) => user.role !== UserRole.EXT_PERM_USER)
|
||||
.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell className="w-40 ">
|
||||
<UserRoleDropdown
|
||||
user={user}
|
||||
onSuccess={onRoleChangeSuccess}
|
||||
onError={onRoleChangeError}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<i>{user.status === "live" ? "Active" : "Inactive"}</i>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<DeactivaterButton
|
||||
user={user}
|
||||
onSuccess={onRoleChangeSuccess}
|
||||
onError={onRoleChangeError}
|
||||
deactivate={user.status === UserStatus.live}
|
||||
setPopup={setPopup}
|
||||
mutate={mutate}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<i>{user.status === "live" ? "Active" : "Inactive"}</i>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<DeactivaterButton
|
||||
{user.status == UserStatus.deactivated && (
|
||||
<DeleteUserButton
|
||||
user={user}
|
||||
deactivate={user.status === UserStatus.live}
|
||||
setPopup={setPopup}
|
||||
mutate={mutate}
|
||||
/>
|
||||
{user.status == UserStatus.deactivated && (
|
||||
<DeleteUserButton
|
||||
user={user}
|
||||
setPopup={setPopup}
|
||||
mutate={mutate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
</HidableSection>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
78
web/src/components/admin/users/SlackUserTable.tsx
Normal file
78
web/src/components/admin/users/SlackUserTable.tsx
Normal file
@ -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<SlackUserTableProps & PageSelectorProps> = ({
|
||||
invitedUsers,
|
||||
slackusers,
|
||||
mutate,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
setPopup,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{totalPages > 1 ? (
|
||||
<CenteredPageSelector
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
<TableHead>
|
||||
<div className="flex">
|
||||
<div className="ml-auto">Actions</div>
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{slackusers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<i>{user.status === "live" ? "Active" : "Inactive"}</i>
|
||||
</TableCell>
|
||||
<TableCell className="flex justify-end">
|
||||
<InviteUserButton
|
||||
user={user}
|
||||
invited={invitedUsers
|
||||
.map((u) => u.email)
|
||||
.includes(user.email)}
|
||||
setPopup={setPopup}
|
||||
mutate={mutate}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlackUserTable;
|
250
web/src/components/admin/users/UserStatusButtons.tsx
Normal file
250
web/src/components/admin/users/UserStatusButtons.tsx
Normal file
@ -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 && (
|
||||
<GenericConfirmModal
|
||||
title={`${invited ? "Uninvite" : "Invite"} User`}
|
||||
message={`Are you sure you want to ${
|
||||
invited ? "uninvite" : "invite"
|
||||
} ${user.email}?`}
|
||||
onClose={() => setShowInviteModal(false)}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-min"
|
||||
onClick={() => setShowInviteModal(true)}
|
||||
disabled={isMutating}
|
||||
size="sm"
|
||||
>
|
||||
{invited ? "Uninvite" : "Invite"}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserRoleDropdown = ({
|
||||
user,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
user: User;
|
||||
onSuccess: () => void;
|
||||
onError: (message: string) => void;
|
||||
}) => {
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [pendingRole, setPendingRole] = useState<string | null>(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 (
|
||||
<>
|
||||
<Select
|
||||
value={user.role}
|
||||
onValueChange={handleChange}
|
||||
disabled={isSettingRole}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(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 : (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
title={INVALID_ROLE_HOVER_TEXT[role] ?? ""}
|
||||
data-tooltip-delay="0"
|
||||
>
|
||||
{label}
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showConfirmModal && (
|
||||
<GenericConfirmModal
|
||||
title="Change Curator Role"
|
||||
message={`Warning: Switching roles from Curator to ${
|
||||
USER_ROLE_LABELS[pendingRole as UserRole] ??
|
||||
USER_ROLE_LABELS[user.role]
|
||||
} will remove their status as individual curators from all groups.`}
|
||||
confirmText={`Switch Role to ${
|
||||
USER_ROLE_LABELS[pendingRole as UserRole] ??
|
||||
USER_ROLE_LABELS[user.role]
|
||||
}`}
|
||||
onClose={() => setShowConfirmModal(false)}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
73
web/src/components/admin/users/buttons/DeactivaterButton.tsx
Normal file
73
web/src/components/admin/users/buttons/DeactivaterButton.tsx
Normal file
@ -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 (
|
||||
<Button
|
||||
className="w-min"
|
||||
onClick={() => trigger({ user_email: user.email })}
|
||||
disabled={isMutating}
|
||||
size="sm"
|
||||
>
|
||||
{deactivate ? "Deactivate" : "Activate"}
|
||||
</Button>
|
||||
);
|
||||
};
|
61
web/src/components/admin/users/buttons/DeleteUserButton.tsx
Normal file
61
web/src/components/admin/users/buttons/DeleteUserButton.tsx
Normal file
@ -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 && (
|
||||
<DeleteEntityModal
|
||||
entityType="user"
|
||||
entityName={user.email}
|
||||
onClose={() => 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)."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-min"
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
disabled={isMutating}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
119
web/src/components/admin/users/buttons/InviteUserButton.tsx
Normal file
119
web/src/components/admin/users/buttons/InviteUserButton.tsx
Normal file
@ -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 && (
|
||||
<GenericConfirmModal
|
||||
title={`${invited ? "Uninvite" : "Invite"} User`}
|
||||
message={`Are you sure you want to ${
|
||||
invited ? "uninvite" : "invite"
|
||||
} ${user.email}?`}
|
||||
onClose={() => setShowInviteModal(false)}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-min"
|
||||
onClick={() => setShowInviteModal(true)}
|
||||
disabled={isMutating}
|
||||
size="sm"
|
||||
>
|
||||
{invited ? "Uninvite" : "Invite"}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
123
web/src/components/admin/users/buttons/UserRoleDropdown.tsx
Normal file
123
web/src/components/admin/users/buttons/UserRoleDropdown.tsx
Normal file
@ -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<string | null>(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 (
|
||||
<>
|
||||
<Select
|
||||
value={user.role}
|
||||
onValueChange={handleChange}
|
||||
disabled={isSettingRole}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(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 : (
|
||||
<SelectItem
|
||||
key={role}
|
||||
value={role}
|
||||
title={INVALID_ROLE_HOVER_TEXT[role] ?? ""}
|
||||
data-tooltip-delay="0"
|
||||
>
|
||||
{label}
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showConfirmModal && (
|
||||
<GenericConfirmModal
|
||||
title="Change Curator Role"
|
||||
message={`Warning: Switching roles from Curator to ${
|
||||
USER_ROLE_LABELS[pendingRole as UserRole] ??
|
||||
USER_ROLE_LABELS[user.role]
|
||||
} will remove their status as individual curators from all groups.`}
|
||||
confirmText={`Switch Role to ${
|
||||
USER_ROLE_LABELS[pendingRole as UserRole] ??
|
||||
USER_ROLE_LABELS[user.role]
|
||||
}`}
|
||||
onClose={() => setShowConfirmModal(false)}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user