Re-style user group pages

This commit is contained in:
Weves 2023-12-16 17:22:38 -08:00 committed by Chris Weaver
parent a52711967f
commit ce870ff577
11 changed files with 442 additions and 272 deletions

View File

@ -1,8 +1,5 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from danswer.document_index.factory import get_default_document_index
from danswer.document_index.interfaces import DocumentIndex
from danswer.document_index.interfaces import UpdateRequest
from danswer.access.access import get_access_for_documents from danswer.access.access import get_access_for_documents
from danswer.db.document import prepare_to_modify_documents from danswer.db.document import prepare_to_modify_documents
from danswer.db.engine import get_sqlalchemy_engine from danswer.db.engine import get_sqlalchemy_engine

View File

@ -29,11 +29,11 @@ export const ConnectorEditor = ({
py-1 py-1
rounded-lg rounded-lg
border border
border-gray-700 border-border
w-fit w-fit
flex flex
cursor-pointer ` + cursor-pointer ` +
(isSelected ? " bg-gray-600" : " hover:bg-gray-700") (isSelected ? " bg-hover" : " hover:bg-hover-light")
} }
onClick={() => { onClick={() => {
if (isSelected) { if (isSelected) {

View File

@ -42,8 +42,8 @@ export const UserEditor = ({
px-2 px-2
py-1 py-1
border border
border-gray-700 border-border
hover:bg-gray-900 hover:bg-hover-light
cursor-pointer`} cursor-pointer`}
> >
{selectedUser.email} <FiX className="ml-1 my-auto" /> {selectedUser.email} <FiX className="ml-1 my-auto" />
@ -73,7 +73,7 @@ export const UserEditor = ({
]); ]);
}} }}
itemComponent={({ option }) => ( itemComponent={({ option }) => (
<div className="flex px-4 py-2.5 hover:bg-gray-800 cursor-pointer"> <div className="flex px-4 py-2.5 cursor-pointer hover:bg-hover">
<UsersIcon className="mr-2 my-auto" /> <UsersIcon className="mr-2 my-auto" />
{option.name} {option.name}
<div className="ml-auto my-auto"> <div className="ml-auto my-auto">

View File

@ -1,13 +1,16 @@
import { ArrayHelpers, FieldArray, Form, Formik } from "formik"; import { Form, Formik } from "formik";
import * as Yup from "yup"; import * as Yup from "yup";
import { PopupSpec } from "@/components/admin/connectors/Popup"; import { PopupSpec } from "@/components/admin/connectors/Popup";
import { ConnectorIndexingStatus, DocumentSet, User } from "@/lib/types"; import { ConnectorIndexingStatus, User } from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field"; import { TextFormField } from "@/components/admin/connectors/Field";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle"; import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { createUserGroup } from "./lib"; import { createUserGroup } from "./lib";
import { UserGroup } from "./types"; import { UserGroup } from "./types";
import { UserEditor } from "./UserEditor"; import { UserEditor } from "./UserEditor";
import { ConnectorEditor } from "./ConnectorEditor"; import { ConnectorEditor } from "./ConnectorEditor";
import { Modal } from "@/components/Modal";
import { XIcon } from "@/components/icons/icons";
import { Button, Divider } from "@tremor/react";
interface UserGroupCreationFormProps { interface UserGroupCreationFormProps {
onClose: () => void; onClose: () => void;
@ -27,15 +30,23 @@ export const UserGroupCreationForm = ({
const isUpdate = existingUserGroup !== undefined; const isUpdate = existingUserGroup !== undefined;
return ( return (
<div> <Modal onOutsideClick={onClose}>
<div className="px-8 py-6 bg-background">
<h2 className="text-xl font-bold flex">
{isUpdate ? "Update a User Group" : "Create a new User Group"}
<div <div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-30"
onClick={onClose} onClick={onClose}
className="ml-auto hover:bg-hover p-1.5 rounded"
> >
<div <XIcon
className="bg-gray-800 rounded-lg border border-gray-700 shadow-lg relative w-1/2 text-sm" size={20}
onClick={(event) => event.stopPropagation()} className="my-auto flex flex-shrink-0 cursor-pointer"
> />
</div>
</h2>
<Divider />
<Formik <Formik
initialValues={{ initialValues={{
name: existingUserGroup ? existingUserGroup.name : "", name: existingUserGroup ? existingUserGroup.name : "",
@ -74,9 +85,6 @@ export const UserGroupCreationForm = ({
> >
{({ isSubmitting, values, setFieldValue }) => ( {({ isSubmitting, values, setFieldValue }) => (
<Form> <Form>
<h2 className="text-xl font-bold mb-3 border-b border-gray-600 pt-4 pb-3 bg-gray-700 px-6">
{isUpdate ? "Update a User Group" : "Create a new User Group"}
</h2>
<div className="p-4"> <div className="p-4">
<TextFormField <TextFormField
name="name" name="name"
@ -85,7 +93,9 @@ export const UserGroupCreationForm = ({
disabled={isUpdate} disabled={isUpdate}
autoCompleteDisabled={true} autoCompleteDisabled={true}
/> />
<div className="border-t border-gray-600 py-2" />
<Divider />
<h2 className="mb-1 font-medium"> <h2 className="mb-1 font-medium">
Select which connectors this group has access to: Select which connectors this group has access to:
</h2> </h2>
@ -102,7 +112,7 @@ export const UserGroupCreationForm = ({
} }
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<h2 className="mb-1 font-medium"> <h2 className="mb-1 font-medium">
Select which Users should be a part of this Group. Select which Users should be a part of this Group.
@ -122,24 +132,21 @@ export const UserGroupCreationForm = ({
/> />
</div> </div>
<div className="flex"> <div className="flex">
<button <Button
type="submit" type="submit"
size="xs"
color="green"
disabled={isSubmitting} disabled={isSubmitting}
className={ className="mx-auto w-64"
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full max-w-sm mx-auto"
}
> >
{isUpdate ? "Update!" : "Create!"} {isUpdate ? "Update!" : "Create!"}
</button> </Button>
</div> </div>
</div> </div>
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </div>
</div> </Modal>
</div>
); );
}; };

View File

@ -1,5 +1,13 @@
"use client"; "use client";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
import { UserGroup } from "./types"; import { UserGroup } from "./types";
import { PopupSpec } from "@/components/admin/connectors/Popup"; import { PopupSpec } from "@/components/admin/connectors/Popup";
import { LoadingAnimation } from "@/components/Loading"; import { LoadingAnimation } from "@/components/Loading";
@ -8,14 +16,16 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { TrashIcon } from "@/components/icons/icons"; import { TrashIcon } from "@/components/icons/icons";
import { deleteUserGroup } from "./lib"; import { deleteUserGroup } from "./lib";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { FiUser } from "react-icons/fi"; import { FiEdit, FiUser } from "react-icons/fi";
import { User } from "@/lib/types"; import { User } from "@/lib/types";
import Link from "next/link";
import { DeleteButton } from "@/components/DeleteButton";
const MAX_USERS_TO_DISPLAY = 6; const MAX_USERS_TO_DISPLAY = 6;
const SimpleUserDisplay = ({ user }: { user: User }) => { const SimpleUserDisplay = ({ user }: { user: User }) => {
return ( return (
<div className="flex my-0.5 text-gray-200"> <div className="flex my-0.5">
<FiUser className="mr-2 my-auto" /> {user.email} <FiUser className="mr-2 my-auto" /> {user.email}
</div> </div>
); );
@ -45,6 +55,130 @@ export const UserGroupsTable = ({
} }
}); });
return (
<div>
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Connectors</TableHeaderCell>
<TableHeaderCell>Users</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Delete</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{userGroups
.filter((userGroup) => !userGroup.is_up_for_deletion)
.map((userGroup) => {
return (
<TableRow key={userGroup.id}>
<TableCell>
<Link
className="whitespace-normal break-all flex cursor-pointer p-2 rounded hover:bg-hover w-fit"
href={`/admin/groups/${userGroup.id}`}
>
<FiEdit className="my-auto mr-2" />
<p className="text font-medium">{userGroup.name}</p>
</Link>
</TableCell>
<TableCell>
{userGroup.cc_pairs.length > 0 ? (
<div>
{userGroup.cc_pairs.map((ccPairDescriptor, ind) => {
return (
<div
className={
ind !== userGroup.cc_pairs.length - 1
? "mb-3"
: ""
}
key={ccPairDescriptor.id}
>
<ConnectorTitle
connector={ccPairDescriptor.connector}
ccPairId={ccPairDescriptor.id}
ccPairName={ccPairDescriptor.name}
showMetadata={false}
/>
</div>
);
})}
</div>
) : (
"-"
)}
</TableCell>
<TableCell>
{userGroup.users.length > 0 ? (
<div>
{userGroup.users.length <= MAX_USERS_TO_DISPLAY ? (
userGroup.users.map((user) => {
return (
<SimpleUserDisplay key={user.id} user={user} />
);
})
) : (
<div>
{userGroup.users
.slice(0, MAX_USERS_TO_DISPLAY)
.map((user) => {
return (
<SimpleUserDisplay
key={user.id}
user={user}
/>
);
})}
<div>
+ {userGroup.users.length - MAX_USERS_TO_DISPLAY}{" "}
more
</div>
</div>
)}
</div>
) : (
"-"
)}
</TableCell>
<TableCell>
{userGroup.is_up_to_date ? (
<div className="text-success">Up to date!</div>
) : (
<div className="w-10">
<LoadingAnimation text="Syncing" />
</div>
)}
</TableCell>
<TableCell>
<DeleteButton
onClick={async (event) => {
event.stopPropagation();
const response = await deleteUserGroup(userGroup.id);
if (response.ok) {
setPopup({
message: `User Group "${userGroup.name}" deleted`,
type: "success",
});
} else {
const errorMsg = (await response.json()).detail;
setPopup({
message: `Failed to delete User Group - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
return ( return (
<div> <div>
<BasicTable <BasicTable

View File

@ -50,8 +50,8 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
py-1 py-1
my-1 my-1
border border
border-gray-700 border-border
hover:bg-gray-900 hover:bg-hover
cursor-pointer`} cursor-pointer`}
> >
<ConnectorTitle <ConnectorTitle
@ -99,7 +99,7 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
]); ]);
}} }}
itemComponent={({ option }) => ( itemComponent={({ option }) => (
<div className="flex px-4 py-2.5 hover:bg-gray-800 cursor-pointer"> <div className="flex px-4 py-2.5 hover:bg-hover cursor-pointer">
<div className="my-auto"> <div className="my-auto">
<ConnectorTitle <ConnectorTitle
ccPairId={option?.metadata?.ccPairId as number} ccPairId={option?.metadata?.ccPairId as number}

View File

@ -3,15 +3,24 @@
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { useState } from "react"; import { useState } from "react";
import { UserGroup } from "../types"; import { UserGroup } from "../types";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle"; import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Button } from "@/components/Button";
import { AddMemberForm } from "./AddMemberForm"; import { AddMemberForm } from "./AddMemberForm";
import { TrashIcon } from "@/components/icons/icons";
import { updateUserGroup } from "./lib"; import { updateUserGroup } from "./lib";
import { LoadingAnimation } from "@/components/Loading"; import { LoadingAnimation } from "@/components/Loading";
import { ConnectorIndexingStatus, User } from "@/lib/types"; import { ConnectorIndexingStatus, User } from "@/lib/types";
import { AddConnectorForm } from "./AddConnectorForm"; import { AddConnectorForm } from "./AddConnectorForm";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Divider,
Button,
Text,
} from "@tremor/react";
import { DeleteButton } from "@/components/DeleteButton";
interface GroupDisplayProps { interface GroupDisplayProps {
users: User[]; users: User[];
@ -35,55 +44,64 @@ export const GroupDisplay = ({
{popup} {popup}
<div className="text-sm mb-3 flex"> <div className="text-sm mb-3 flex">
<b className="mr-1">Status:</b>{" "} <Text className="mr-1">Status:</Text>{" "}
{userGroup.is_up_to_date ? ( {userGroup.is_up_to_date ? (
<div className="text-emerald-600">Up to date</div> <div className="text-success font-bold">Up to date</div>
) : ( ) : (
<div className="text-gray-300"> <div className="text-accent font-bold">
<LoadingAnimation text="Syncing" /> <LoadingAnimation text="Syncing" />
</div> </div>
)} )}
</div> </div>
<Divider />
<div className="flex w-full"> <div className="flex w-full">
<h2 className="text-xl font-bold">Users</h2> <h2 className="text-xl font-bold">Users</h2>
</div> </div>
<div className="mt-2"> <div className="mt-2">
{userGroup.users.length > 0 ? ( {userGroup.users.length > 0 ? (
<BasicTable <>
columns={[ <Table className="overflow-visible">
{ <TableHead>
header: "Email", <TableRow>
key: "email", <TableHeaderCell>Email</TableHeaderCell>
}, <TableHeaderCell className="flex w-full">
{ <div className="ml-auto">Remove User</div>
header: "Remove User", </TableHeaderCell>
key: "remove", </TableRow>
alignment: "right", </TableHead>
}, <TableBody>
]} {userGroup.users.map((user) => {
data={userGroup.users.map((user) => { return (
return { <TableRow key={user.id}>
email: <div>{user.email}</div>, <TableCell className="whitespace-normal break-all">
remove: ( {user.email}
<div className="flex"> </TableCell>
<div <TableCell>
className="cursor-pointer ml-auto mr-1" <div className="flex w-full">
<div className="ml-auto m-2">
<DeleteButton
onClick={async () => { onClick={async () => {
const response = await updateUserGroup(userGroup.id, { const response = await updateUserGroup(
userGroup.id,
{
user_ids: userGroup.users user_ids: userGroup.users
.filter( .filter(
(userGroupUser) => userGroupUser.id !== user.id (userGroupUser) =>
userGroupUser.id !== user.id
) )
.map((userGroupUser) => userGroupUser.id), .map((userGroupUser) => userGroupUser.id),
cc_pair_ids: userGroup.cc_pairs.map( cc_pair_ids: userGroup.cc_pairs.map(
(ccPair) => ccPair.id (ccPair) => ccPair.id
), ),
}); }
);
if (response.ok) { if (response.ok) {
setPopup({ setPopup({
message: "Successfully removed user from group", message:
"Successfully removed user from group",
type: "success", type: "success",
}); });
} else { } else {
@ -97,14 +115,16 @@ export const GroupDisplay = ({
} }
refreshUserGroup(); refreshUserGroup();
}} }}
>
<TrashIcon />
</div>
</div>
),
};
})}
/> />
</div>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
) : ( ) : (
<div className="text-sm">No users in this group...</div> <div className="text-sm">No users in this group...</div>
)} )}
@ -112,6 +132,8 @@ export const GroupDisplay = ({
<Button <Button
className="mt-3" className="mt-3"
size="xs"
color="green"
onClick={() => setAddMemberFormVisible(true)} onClick={() => setAddMemberFormVisible(true)}
disabled={!userGroup.is_up_to_date} disabled={!userGroup.is_up_to_date}
> >
@ -130,37 +152,40 @@ export const GroupDisplay = ({
/> />
)} )}
<h2 className="text-xl font-bold mt-4">Connectors</h2> <Divider />
<h2 className="text-xl font-bold mt-8">Connectors</h2>
<div className="mt-2"> <div className="mt-2">
{userGroup.cc_pairs.length > 0 ? ( {userGroup.cc_pairs.length > 0 ? (
<BasicTable <>
columns={[ <Table className="overflow-visible">
{ <TableHead>
header: "Connector", <TableRow>
key: "connector_name", <TableHeaderCell>Connector</TableHeaderCell>
}, <TableHeaderCell className="flex w-full">
{ <div className="ml-auto">Remove Connector</div>
header: "Remove Connector", </TableHeaderCell>
key: "delete", </TableRow>
alignment: "right", </TableHead>
}, <TableBody>
]} {userGroup.cc_pairs.map((ccPair) => {
data={userGroup.cc_pairs.map((ccPair) => { return (
return { <TableRow key={ccPair.id}>
connector_name: <TableCell className="whitespace-normal break-all">
(
<ConnectorTitle <ConnectorTitle
connector={ccPair.connector} connector={ccPair.connector}
ccPairId={ccPair.id} ccPairId={ccPair.id}
ccPairName={ccPair.name} ccPairName={ccPair.name}
/> />
) || "", </TableCell>
delete: ( <TableCell>
<div className="flex"> <div className="flex w-full">
<div <div className="ml-auto m-2">
className="cursor-pointer ml-auto mr-1" <DeleteButton
onClick={async () => { onClick={async () => {
const response = await updateUserGroup(userGroup.id, { const response = await updateUserGroup(
userGroup.id,
{
user_ids: userGroup.users.map( user_ids: userGroup.users.map(
(userGroupUser) => userGroupUser.id (userGroupUser) => userGroupUser.id
), ),
@ -170,7 +195,8 @@ export const GroupDisplay = ({
userGroupCCPair.id != ccPair.id userGroupCCPair.id != ccPair.id
) )
.map((ccPair) => ccPair.id), .map((ccPair) => ccPair.id),
}); }
);
if (response.ok) { if (response.ok) {
setPopup({ setPopup({
message: message:
@ -188,14 +214,16 @@ export const GroupDisplay = ({
} }
refreshUserGroup(); refreshUserGroup();
}} }}
>
<TrashIcon />
</div>
</div>
),
};
})}
/> />
</div>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
) : ( ) : (
<div className="text-sm">No connectors in this group...</div> <div className="text-sm">No connectors in this group...</div>
)} )}
@ -204,6 +232,8 @@ export const GroupDisplay = ({
<Button <Button
className="mt-3" className="mt-3"
onClick={() => setAddConnectorFormVisible(true)} onClick={() => setAddConnectorFormVisible(true)}
size="xs"
color="green"
disabled={!userGroup.is_up_to_date} disabled={!userGroup.is_up_to_date}
> >
Add Connectors Add Connectors

View File

@ -7,6 +7,8 @@ import { useSpecificUserGroup } from "./hook";
import { ThreeDotsLoader } from "@/components/Loading"; import { ThreeDotsLoader } from "@/components/Loading";
import { useConnectorCredentialIndexingStatus, useUsers } from "@/lib/hooks"; import { useConnectorCredentialIndexingStatus, useUsers } from "@/lib/hooks";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { BackButton } from "@/components/BackButton";
import { AdminPageTitle } from "@/components/admin/Title";
const Page = ({ params }: { params: { groupId: string } }) => { const Page = ({ params }: { params: { groupId: string } }) => {
const router = useRouter(); const router = useRouter();
@ -50,19 +52,12 @@ const Page = ({ params }: { params: { groupId: string } }) => {
return ( return (
<div className="mx-auto container"> <div className="mx-auto container">
<div <BackButton />
className="my-auto flex mb-1 hover:bg-gray-700 w-fit pr-2 cursor-pointer rounded-lg"
onClick={() => router.back()} <AdminPageTitle
> title={userGroup.name || "Unknown"}
<FiChevronLeft className="mr-1 my-auto" /> icon={<GroupsIcon size={32} />}
Back />
</div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<GroupsIcon size={32} />
<h1 className="text-3xl font-bold pl-2">
{userGroup ? userGroup.name : <FiAlertCircle />}
</h1>
</div>
{userGroup ? ( {userGroup ? (
<GroupDisplay <GroupDisplay

View File

@ -5,10 +5,11 @@ import { UserGroupsTable } from "./UserGroupsTable";
import { UserGroupCreationForm } from "./UserGroupCreationForm"; import { UserGroupCreationForm } from "./UserGroupCreationForm";
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/Button";
import { ThreeDotsLoader } from "@/components/Loading"; import { ThreeDotsLoader } from "@/components/Loading";
import { useConnectorCredentialIndexingStatus, useUsers } from "@/lib/hooks"; import { useConnectorCredentialIndexingStatus, useUsers } from "@/lib/hooks";
import { useUserGroups } from "./hooks"; import { useUserGroups } from "./hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button, Divider } from "@tremor/react";
const Main = () => { const Main = () => {
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
@ -48,13 +49,20 @@ const Main = () => {
<> <>
{popup} {popup}
<div className="my-3"> <div className="my-3">
<Button onClick={() => setShowForm(true)}>Create New User Group</Button> <Button size="xs" color="green" onClick={() => setShowForm(true)}>
Create New User Group
</Button>
</div> </div>
{data.length > 0 && (
<div>
<Divider />
<UserGroupsTable <UserGroupsTable
userGroups={data} userGroups={data}
setPopup={setPopup} setPopup={setPopup}
refresh={refreshUserGroups} refresh={refreshUserGroups}
/> />
</div>
)}
{showForm && ( {showForm && (
<UserGroupCreationForm <UserGroupCreationForm
onClose={() => { onClose={() => {
@ -73,10 +81,10 @@ const Main = () => {
const Page = () => { const Page = () => {
return ( return (
<div className="mx-auto container"> <div className="mx-auto container">
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex"> <AdminPageTitle
<GroupsIcon size={32} /> title="Manage Users Groups"
<h1 className="text-3xl font-bold pl-2">Manage Users Groups</h1> icon={<GroupsIcon size={32} />}
</div> />
<Main /> <Main />
</div> </div>

View File

@ -7,7 +7,6 @@ import { DatabaseIcon } from "@/components/icons/icons";
export default function QueryHistoryPage() { export default function QueryHistoryPage() {
return ( return (
<main className="pt-4 mx-auto container"> <main className="pt-4 mx-auto container">
<AdminPageTitle title="Query History" icon={<DatabaseIcon size={32} />} /> <AdminPageTitle title="Query History" icon={<DatabaseIcon size={32} />} />
<QueryHistoryTable /> <QueryHistoryTable />

View File

@ -4,7 +4,7 @@ export function DeleteButton({
onClick, onClick,
disabled, disabled,
}: { }: {
onClick?: () => void; onClick?: (event: React.MouseEvent<HTMLElement>) => void | Promise<void>;
disabled?: boolean; disabled?: boolean;
}) { }) {
return ( return (