mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-29 17:20:44 +02:00
base role setting fix (#3381)
* base role setting fix * update user tables * finalize * minor cleanup * fix chromatic
This commit is contained in:
@ -9,7 +9,6 @@ from danswer.utils.special_types import JSON_ro
|
|||||||
def get_invited_users() -> list[str]:
|
def get_invited_users() -> list[str]:
|
||||||
try:
|
try:
|
||||||
store = get_kv_store()
|
store = get_kv_store()
|
||||||
|
|
||||||
return cast(list, store.load(KV_USER_STORE_KEY))
|
return cast(list, store.load(KV_USER_STORE_KEY))
|
||||||
except KvKeyNotFoundError:
|
except KvKeyNotFoundError:
|
||||||
return list()
|
return list()
|
||||||
|
@ -266,5 +266,7 @@ class FullModelVersionResponse(BaseModel):
|
|||||||
class AllUsersResponse(BaseModel):
|
class AllUsersResponse(BaseModel):
|
||||||
accepted: list[FullUserSnapshot]
|
accepted: list[FullUserSnapshot]
|
||||||
invited: list[InvitedUserSnapshot]
|
invited: list[InvitedUserSnapshot]
|
||||||
|
slack_users: list[FullUserSnapshot]
|
||||||
accepted_pages: int
|
accepted_pages: int
|
||||||
invited_pages: int
|
invited_pages: int
|
||||||
|
slack_users_pages: int
|
||||||
|
@ -119,6 +119,7 @@ def set_user_role(
|
|||||||
def list_all_users(
|
def list_all_users(
|
||||||
q: str | None = None,
|
q: str | None = None,
|
||||||
accepted_page: int | None = None,
|
accepted_page: int | None = None,
|
||||||
|
slack_users_page: int | None = None,
|
||||||
invited_page: int | None = None,
|
invited_page: int | None = None,
|
||||||
user: User | None = Depends(current_curator_or_admin_user),
|
user: User | None = Depends(current_curator_or_admin_user),
|
||||||
db_session: Session = Depends(get_session),
|
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)
|
for user in list_users(db_session, email_filter_string=q)
|
||||||
if not is_api_key_email_address(user.email)
|
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()
|
invited_emails = get_invited_users()
|
||||||
if q:
|
if q:
|
||||||
invited_emails = [
|
invited_emails = [
|
||||||
@ -139,10 +145,11 @@ def list_all_users(
|
|||||||
]
|
]
|
||||||
|
|
||||||
accepted_count = len(accepted_emails)
|
accepted_count = len(accepted_emails)
|
||||||
|
slack_users_count = len(slack_users_emails)
|
||||||
invited_count = len(invited_emails)
|
invited_count = len(invited_emails)
|
||||||
|
|
||||||
# If any of q, accepted_page, or invited_page is None, return all users
|
# 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(
|
return AllUsersResponse(
|
||||||
accepted=[
|
accepted=[
|
||||||
FullUserSnapshot(
|
FullUserSnapshot(
|
||||||
@ -153,11 +160,23 @@ def list_all_users(
|
|||||||
UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED
|
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],
|
invited=[InvitedUserSnapshot(email=email) for email in invited_emails],
|
||||||
accepted_pages=1,
|
accepted_pages=1,
|
||||||
invited_pages=1,
|
invited_pages=1,
|
||||||
|
slack_users_pages=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Otherwise, return paginated results
|
# Otherwise, return paginated results
|
||||||
@ -169,13 +188,27 @@ def list_all_users(
|
|||||||
role=user.role,
|
role=user.role,
|
||||||
status=UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED,
|
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],
|
][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=[InvitedUserSnapshot(email=email) for email in invited_emails][
|
||||||
invited_page * USERS_PAGE_SIZE : (invited_page + 1) * USERS_PAGE_SIZE
|
invited_page * USERS_PAGE_SIZE : (invited_page + 1) * USERS_PAGE_SIZE
|
||||||
],
|
],
|
||||||
accepted_pages=accepted_count // USERS_PAGE_SIZE + 1,
|
accepted_pages=accepted_count // USERS_PAGE_SIZE + 1,
|
||||||
invited_pages=invited_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(
|
return AllUsersResponse(
|
||||||
accepted=[FullUserSnapshot(**user) for user in data["accepted"]],
|
accepted=[FullUserSnapshot(**user) for user in data["accepted"]],
|
||||||
invited=[InvitedUserSnapshot(**user) for user in data["invited"]],
|
invited=[InvitedUserSnapshot(**user) for user in data["invited"]],
|
||||||
|
slack_users=[FullUserSnapshot(**user) for user in data["slack_users"]],
|
||||||
accepted_pages=data["accepted_pages"],
|
accepted_pages=data["accepted_pages"],
|
||||||
invited_pages=data["invited_pages"],
|
invited_pages=data["invited_pages"],
|
||||||
|
slack_users_pages=data["slack_users_pages"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -130,8 +130,10 @@ class UserManager:
|
|||||||
all_users = AllUsersResponse(
|
all_users = AllUsersResponse(
|
||||||
accepted=[FullUserSnapshot(**user) for user in data["accepted"]],
|
accepted=[FullUserSnapshot(**user) for user in data["accepted"]],
|
||||||
invited=[InvitedUserSnapshot(**user) for user in data["invited"]],
|
invited=[InvitedUserSnapshot(**user) for user in data["invited"]],
|
||||||
|
slack_users=[FullUserSnapshot(**user) for user in data["slack_users"]],
|
||||||
accepted_pages=data["accepted_pages"],
|
accepted_pages=data["accepted_pages"],
|
||||||
invited_pages=data["invited_pages"],
|
invited_pages=data["invited_pages"],
|
||||||
|
slack_users_pages=data["slack_users_pages"],
|
||||||
)
|
)
|
||||||
for accepted_user in all_users.accepted:
|
for accepted_user in all_users.accepted:
|
||||||
if accepted_user.email == user.email and accepted_user.id == user.id:
|
if accepted_user.email == user.email and accepted_user.id == user.id:
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"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 InvitedUserTable from "@/components/admin/users/InvitedUserTable";
|
||||||
import SignedUpUserTable from "@/components/admin/users/SignedUpUserTable";
|
import SignedUpUserTable from "@/components/admin/users/SignedUpUserTable";
|
||||||
import { SearchBar } from "@/components/search/SearchBar";
|
import { SearchBar } from "@/components/search/SearchBar";
|
||||||
import { useState } from "react";
|
|
||||||
import { FiPlusSquare } from "react-icons/fi";
|
import { FiPlusSquare } from "react-icons/fi";
|
||||||
import { Modal } from "@/components/Modal";
|
import { Modal } from "@/components/Modal";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import Text from "@/components/ui/text";
|
|
||||||
import { LoadingAnimation } from "@/components/Loading";
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
|
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
|
||||||
@ -15,42 +15,10 @@ import { UsersIcon } from "@/components/icons/icons";
|
|||||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
import { HidableSection } from "@/app/admin/assistants/HidableSection";
|
|
||||||
import BulkAdd from "@/components/admin/users/BulkAdd";
|
import BulkAdd from "@/components/admin/users/BulkAdd";
|
||||||
import { UsersResponse } from "@/lib/users/interfaces";
|
import { UsersResponse } from "@/lib/users/interfaces";
|
||||||
|
import SlackUserTable from "@/components/admin/users/SlackUserTable";
|
||||||
const ValidDomainsDisplay = ({ validDomains }: { validDomains: string[] }) => {
|
import Text from "@/components/ui/text";
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UsersTables = ({
|
const UsersTables = ({
|
||||||
q,
|
q,
|
||||||
@ -61,23 +29,48 @@ const UsersTables = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [invitedPage, setInvitedPage] = useState(1);
|
const [invitedPage, setInvitedPage] = useState(1);
|
||||||
const [acceptedPage, setAcceptedPage] = useState(1);
|
const [acceptedPage, setAcceptedPage] = useState(1);
|
||||||
const { data, isLoading, mutate, error } = useSWR<UsersResponse>(
|
const [slackUsersPage, setSlackUsersPage] = useState(1);
|
||||||
`/api/manage/users?q=${encodeURI(q)}&accepted_page=${
|
|
||||||
|
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
|
acceptedPage - 1
|
||||||
}&invited_page=${invitedPage - 1}`,
|
}&invited_page=${invitedPage - 1}&slack_users_page=${slackUsersPage - 1}`,
|
||||||
errorHandlingFetcher
|
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" />;
|
return <LoadingAnimation text="Loading" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !data) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorCallout
|
<ErrorCallout
|
||||||
errorTitle="Error loading users"
|
errorTitle="Error loading users"
|
||||||
@ -86,7 +79,7 @@ const UsersTables = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domainsError || !validDomains) {
|
if (domainsError) {
|
||||||
return (
|
return (
|
||||||
<ErrorCallout
|
<ErrorCallout
|
||||||
errorTitle="Error loading valid domains"
|
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(
|
const finalInvited = invited.filter(
|
||||||
(user) => !accepted.map((u) => u.email).includes(user.email)
|
(user) => !accepted.some((u) => u.email === user.email)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Tabs defaultValue="invited">
|
||||||
<HidableSection sectionTitle="Invited Users">
|
<TabsList>
|
||||||
{invited.length > 0 ? (
|
<TabsTrigger value="invited">Invited Users</TabsTrigger>
|
||||||
finalInvited.length > 0 ? (
|
<TabsTrigger value="current">Current Users</TabsTrigger>
|
||||||
<InvitedUserTable
|
<TabsTrigger value="danswerbot">DanswerBot Users</TabsTrigger>
|
||||||
users={finalInvited}
|
</TabsList>
|
||||||
setPopup={setPopup}
|
|
||||||
currentPage={invitedPage}
|
<TabsContent value="invited">
|
||||||
onPageChange={setInvitedPage}
|
<Card>
|
||||||
totalPages={invited_pages}
|
<CardHeader>
|
||||||
mutate={mutate}
|
<CardTitle>Invited Users</CardTitle>
|
||||||
/>
|
</CardHeader>
|
||||||
) : (
|
<CardContent>
|
||||||
<div className="text-sm">
|
{finalInvited.length > 0 ? (
|
||||||
To invite additional teammates, use the <b>Invite Users</b> button
|
<InvitedUserTable
|
||||||
above!
|
users={finalInvited}
|
||||||
</div>
|
setPopup={setPopup}
|
||||||
)
|
currentPage={invitedPage}
|
||||||
) : (
|
onPageChange={setInvitedPage}
|
||||||
<ValidDomainsDisplay validDomains={validDomains} />
|
totalPages={invited_pages}
|
||||||
)}
|
mutate={mutate}
|
||||||
</HidableSection>
|
/>
|
||||||
<SignedUpUserTable
|
) : (
|
||||||
users={accepted}
|
<p>Users that have been invited will show up here</p>
|
||||||
setPopup={setPopup}
|
)}
|
||||||
currentPage={acceptedPage}
|
</CardContent>
|
||||||
onPageChange={setAcceptedPage}
|
</Card>
|
||||||
totalPages={accepted_pages}
|
</TabsContent>
|
||||||
mutate={mutate}
|
|
||||||
/>
|
<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 (
|
return (
|
||||||
<div className="mx-auto container">
|
<div className="mx-auto container">
|
||||||
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
|
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
|
||||||
|
|
||||||
<SearchableTables />
|
<SearchableTables />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,14 +6,13 @@ import {
|
|||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
|
|
||||||
import CenteredPageSelector from "./CenteredPageSelector";
|
import CenteredPageSelector from "./CenteredPageSelector";
|
||||||
import { type PageSelectorProps } from "@/components/PageSelector";
|
import { type PageSelectorProps } from "@/components/PageSelector";
|
||||||
|
|
||||||
import { type User } from "@/lib/types";
|
import { type User } from "@/lib/types";
|
||||||
import useSWRMutation from "swr/mutation";
|
|
||||||
import { TableHeader } from "@/components/ui/table";
|
import { TableHeader } from "@/components/ui/table";
|
||||||
|
import { InviteUserButton } from "./buttons/InviteUserButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
users: Array<User>;
|
users: Array<User>;
|
||||||
@ -21,27 +20,6 @@ interface Props {
|
|||||||
mutate: () => void;
|
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 = ({
|
const InvitedUserTable = ({
|
||||||
users,
|
users,
|
||||||
setPopup,
|
setPopup,
|
||||||
@ -52,20 +30,6 @@ const InvitedUserTable = ({
|
|||||||
}: Props & PageSelectorProps) => {
|
}: Props & PageSelectorProps) => {
|
||||||
if (!users.length) return null;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table className="overflow-visible">
|
<Table className="overflow-visible">
|
||||||
@ -83,10 +47,11 @@ const InvitedUserTable = ({
|
|||||||
<TableCell>{user.email}</TableCell>
|
<TableCell>{user.email}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<RemoveUserButton
|
<InviteUserButton
|
||||||
user={user}
|
user={user}
|
||||||
onSuccess={onRemovalSuccess}
|
invited={true}
|
||||||
onError={onRemovalError}
|
setPopup={setPopup}
|
||||||
|
mutate={mutate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
@ -1,16 +1,7 @@
|
|||||||
import {
|
import { type User, UserStatus, UserRole } from "@/lib/types";
|
||||||
type User,
|
|
||||||
UserStatus,
|
|
||||||
UserRole,
|
|
||||||
USER_ROLE_LABELS,
|
|
||||||
INVALID_ROLE_HOVER_TEXT,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import CenteredPageSelector from "./CenteredPageSelector";
|
import CenteredPageSelector from "./CenteredPageSelector";
|
||||||
import { type PageSelectorProps } from "@/components/PageSelector";
|
import { type PageSelectorProps } from "@/components/PageSelector";
|
||||||
import { HidableSection } from "@/app/admin/assistants/HidableSection";
|
|
||||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||||
import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
|
|
||||||
import useSWRMutation from "swr/mutation";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHead,
|
TableHead,
|
||||||
@ -18,20 +9,10 @@ import {
|
|||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/ui/table";
|
} 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 { TableHeader } from "@/components/ui/table";
|
||||||
|
import { UserRoleDropdown } from "./buttons/UserRoleDropdown";
|
||||||
|
import { DeleteUserButton } from "./buttons/DeleteUserButton";
|
||||||
|
import { DeactivaterButton } from "./buttons/DeactivaterButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
users: Array<User>;
|
users: Array<User>;
|
||||||
@ -39,204 +20,6 @@ interface Props {
|
|||||||
mutate: () => void;
|
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 = ({
|
const SignedUpUserTable = ({
|
||||||
users,
|
users,
|
||||||
setPopup,
|
setPopup,
|
||||||
@ -258,68 +41,66 @@ const SignedUpUserTable = ({
|
|||||||
handlePopup(`Unable to update user role - ${errorMsg}`, "error");
|
handlePopup(`Unable to update user role - ${errorMsg}`, "error");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HidableSection sectionTitle="Current Users">
|
<>
|
||||||
<>
|
{totalPages > 1 ? (
|
||||||
{totalPages > 1 ? (
|
<CenteredPageSelector
|
||||||
<CenteredPageSelector
|
currentPage={currentPage}
|
||||||
currentPage={currentPage}
|
totalPages={totalPages}
|
||||||
totalPages={totalPages}
|
onPageChange={onPageChange}
|
||||||
onPageChange={onPageChange}
|
/>
|
||||||
/>
|
) : null}
|
||||||
) : null}
|
<Table className="overflow-visible">
|
||||||
<Table className="overflow-visible">
|
<TableHeader>
|
||||||
<TableHeader>
|
<TableRow>
|
||||||
<TableRow>
|
<TableHead>Email</TableHead>
|
||||||
<TableHead>Email</TableHead>
|
<TableHead className="text-center">Role</TableHead>
|
||||||
<TableHead className="text-center">Role</TableHead>
|
<TableHead className="text-center">Status</TableHead>
|
||||||
<TableHead className="text-center">Status</TableHead>
|
<TableHead>
|
||||||
<TableHead>
|
<div className="flex">
|
||||||
<div className="flex">
|
<div className="ml-auto">Actions</div>
|
||||||
<div className="ml-auto">Actions</div>
|
</div>
|
||||||
</div>
|
</TableHead>
|
||||||
</TableHead>
|
</TableRow>
|
||||||
</TableRow>
|
</TableHeader>
|
||||||
</TableHeader>
|
<TableBody>
|
||||||
<TableBody>
|
{users
|
||||||
{users
|
// Dont want to show external permissioned users because it's scary
|
||||||
// Dont want to show external permissioned users because it's scary
|
.filter((user) => user.role !== UserRole.EXT_PERM_USER)
|
||||||
.filter((user) => user.role !== UserRole.EXT_PERM_USER)
|
.map((user) => (
|
||||||
.map((user) => (
|
<TableRow key={user.id}>
|
||||||
<TableRow key={user.id}>
|
<TableCell>{user.email}</TableCell>
|
||||||
<TableCell>{user.email}</TableCell>
|
<TableCell className="w-40 ">
|
||||||
<TableCell className="w-40 ">
|
<UserRoleDropdown
|
||||||
<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}
|
user={user}
|
||||||
onSuccess={onRoleChangeSuccess}
|
deactivate={user.status === UserStatus.live}
|
||||||
onError={onRoleChangeError}
|
setPopup={setPopup}
|
||||||
|
mutate={mutate}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
{user.status == UserStatus.deactivated && (
|
||||||
<TableCell className="text-center">
|
<DeleteUserButton
|
||||||
<i>{user.status === "live" ? "Active" : "Inactive"}</i>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex justify-end gap-x-2">
|
|
||||||
<DeactivaterButton
|
|
||||||
user={user}
|
user={user}
|
||||||
deactivate={user.status === UserStatus.live}
|
|
||||||
setPopup={setPopup}
|
setPopup={setPopup}
|
||||||
mutate={mutate}
|
mutate={mutate}
|
||||||
/>
|
/>
|
||||||
{user.status == UserStatus.deactivated && (
|
)}
|
||||||
<DeleteUserButton
|
</div>
|
||||||
user={user}
|
</TableCell>
|
||||||
setPopup={setPopup}
|
</TableRow>
|
||||||
mutate={mutate}
|
))}
|
||||||
/>
|
</TableBody>
|
||||||
)}
|
</Table>
|
||||||
</div>
|
</>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
</HidableSection>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
export interface UsersResponse {
|
||||||
accepted: User[];
|
accepted: User[];
|
||||||
invited: User[];
|
invited: User[];
|
||||||
|
slack_users: User[];
|
||||||
accepted_pages: number;
|
accepted_pages: number;
|
||||||
invited_pages: number;
|
invited_pages: number;
|
||||||
|
slack_users_pages: number;
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,5 @@ test(
|
|||||||
// Test simple loading
|
// Test simple loading
|
||||||
await page.goto("http://localhost:3000/admin/users");
|
await page.goto("http://localhost:3000/admin/users");
|
||||||
await expect(page.locator("h1.text-3xl")).toHaveText("Manage 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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user