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 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.db.document import prepare_to_modify_documents
from danswer.db.engine import get_sqlalchemy_engine

View File

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

View File

@ -42,8 +42,8 @@ export const UserEditor = ({
px-2
py-1
border
border-gray-700
hover:bg-gray-900
border-border
hover:bg-hover-light
cursor-pointer`}
>
{selectedUser.email} <FiX className="ml-1 my-auto" />
@ -73,7 +73,7 @@ export const UserEditor = ({
]);
}}
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" />
{option.name}
<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 { 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 { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { createUserGroup } from "./lib";
import { UserGroup } from "./types";
import { UserEditor } from "./UserEditor";
import { ConnectorEditor } from "./ConnectorEditor";
import { Modal } from "@/components/Modal";
import { XIcon } from "@/components/icons/icons";
import { Button, Divider } from "@tremor/react";
interface UserGroupCreationFormProps {
onClose: () => void;
@ -27,119 +30,123 @@ export const UserGroupCreationForm = ({
const isUpdate = existingUserGroup !== undefined;
return (
<div>
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-30"
onClick={onClose}
>
<div
className="bg-gray-800 rounded-lg border border-gray-700 shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
>
<Formik
initialValues={{
name: existingUserGroup ? existingUserGroup.name : "",
user_ids: [] as string[],
cc_pair_ids: [] as number[],
}}
validationSchema={Yup.object().shape({
name: Yup.string().required("Please enter a name for the group"),
user_ids: Yup.array().of(Yup.string().required()),
cc_pair_ids: Yup.array().of(Yup.number().required()),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
let response;
response = await createUserGroup(values);
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
message: isUpdate
? "Successfully updated user group!"
: "Successfully created user group!",
type: "success",
});
onClose();
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: isUpdate
? `Error updating user group - ${errorMsg}`
: `Error creating user group - ${errorMsg}`,
type: "error",
});
}
}}
<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
onClick={onClose}
className="ml-auto hover:bg-hover p-1.5 rounded"
>
{({ isSubmitting, values, setFieldValue }) => (
<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"}
<XIcon
size={20}
className="my-auto flex flex-shrink-0 cursor-pointer"
/>
</div>
</h2>
<Divider />
<Formik
initialValues={{
name: existingUserGroup ? existingUserGroup.name : "",
user_ids: [] as string[],
cc_pair_ids: [] as number[],
}}
validationSchema={Yup.object().shape({
name: Yup.string().required("Please enter a name for the group"),
user_ids: Yup.array().of(Yup.string().required()),
cc_pair_ids: Yup.array().of(Yup.number().required()),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
let response;
response = await createUserGroup(values);
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
message: isUpdate
? "Successfully updated user group!"
: "Successfully created user group!",
type: "success",
});
onClose();
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: isUpdate
? `Error updating user group - ${errorMsg}`
: `Error creating user group - ${errorMsg}`,
type: "error",
});
}
}}
>
{({ isSubmitting, values, setFieldValue }) => (
<Form>
<div className="p-4">
<TextFormField
name="name"
label="Name:"
placeholder="A name for the User Group"
disabled={isUpdate}
autoCompleteDisabled={true}
/>
<Divider />
<h2 className="mb-1 font-medium">
Select which connectors this group has access to:
</h2>
<div className="p-4">
<TextFormField
name="name"
label="Name:"
placeholder="A name for the User Group"
disabled={isUpdate}
autoCompleteDisabled={true}
/>
<div className="border-t border-gray-600 py-2" />
<h2 className="mb-1 font-medium">
Select which connectors this group has access to:
</h2>
<p className="mb-3 text-xs">
All documents indexed by the selected connectors will be
visible to users in this group.
</p>
<p className="mb-3 text-xs">
All documents indexed by the selected connectors will be
visible to users in this group.
</p>
<ConnectorEditor
allCCPairs={ccPairs}
selectedCCPairIds={values.cc_pair_ids}
setSetCCPairIds={(ccPairsIds) =>
setFieldValue("cc_pair_ids", ccPairsIds)
<ConnectorEditor
allCCPairs={ccPairs}
selectedCCPairIds={values.cc_pair_ids}
setSetCCPairIds={(ccPairsIds) =>
setFieldValue("cc_pair_ids", ccPairsIds)
}
/>
<Divider />
<h2 className="mb-1 font-medium">
Select which Users should be a part of this Group.
</h2>
<p className="mb-3 text-xs">
All selected users will be able to search through all
documents indexed by the selected connectors.
</p>
<div className="mb-3 gap-2">
<UserEditor
selectedUserIds={values.user_ids}
setSelectedUserIds={(userIds) =>
setFieldValue("user_ids", userIds)
}
allUsers={users}
existingUsers={[]}
/>
<div className="border-t border-gray-600 py-2" />
<h2 className="mb-1 font-medium">
Select which Users should be a part of this Group.
</h2>
<p className="mb-3 text-xs">
All selected users will be able to search through all
documents indexed by the selected connectors.
</p>
<div className="mb-3 gap-2">
<UserEditor
selectedUserIds={values.user_ids}
setSelectedUserIds={(userIds) =>
setFieldValue("user_ids", userIds)
}
allUsers={users}
existingUsers={[]}
/>
</div>
<div className="flex">
<button
type="submit"
disabled={isSubmitting}
className={
"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!"}
</button>
</div>
</div>
</Form>
)}
</Formik>
</div>
<div className="flex">
<Button
type="submit"
size="xs"
color="green"
disabled={isSubmitting}
className="mx-auto w-64"
>
{isUpdate ? "Update!" : "Create!"}
</Button>
</div>
</div>
</Form>
)}
</Formik>
</div>
</div>
</Modal>
);
};

View File

@ -1,5 +1,13 @@
"use client";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
import { UserGroup } from "./types";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { LoadingAnimation } from "@/components/Loading";
@ -8,14 +16,16 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { TrashIcon } from "@/components/icons/icons";
import { deleteUserGroup } from "./lib";
import { useRouter } from "next/navigation";
import { FiUser } from "react-icons/fi";
import { FiEdit, FiUser } from "react-icons/fi";
import { User } from "@/lib/types";
import Link from "next/link";
import { DeleteButton } from "@/components/DeleteButton";
const MAX_USERS_TO_DISPLAY = 6;
const SimpleUserDisplay = ({ user }: { user: User }) => {
return (
<div className="flex my-0.5 text-gray-200">
<div className="flex my-0.5">
<FiUser className="mr-2 my-auto" /> {user.email}
</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 (
<div>
<BasicTable

View File

@ -50,8 +50,8 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
py-1
my-1
border
border-gray-700
hover:bg-gray-900
border-border
hover:bg-hover
cursor-pointer`}
>
<ConnectorTitle
@ -99,7 +99,7 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
]);
}}
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">
<ConnectorTitle
ccPairId={option?.metadata?.ccPairId as number}

View File

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

View File

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

View File

@ -5,10 +5,11 @@ import { UserGroupsTable } from "./UserGroupsTable";
import { UserGroupCreationForm } from "./UserGroupCreationForm";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import { Button } from "@/components/Button";
import { ThreeDotsLoader } from "@/components/Loading";
import { useConnectorCredentialIndexingStatus, useUsers } from "@/lib/hooks";
import { useUserGroups } from "./hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button, Divider } from "@tremor/react";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -48,13 +49,20 @@ const Main = () => {
<>
{popup}
<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>
<UserGroupsTable
userGroups={data}
setPopup={setPopup}
refresh={refreshUserGroups}
/>
{data.length > 0 && (
<div>
<Divider />
<UserGroupsTable
userGroups={data}
setPopup={setPopup}
refresh={refreshUserGroups}
/>
</div>
)}
{showForm && (
<UserGroupCreationForm
onClose={() => {
@ -73,10 +81,10 @@ const Main = () => {
const Page = () => {
return (
<div className="mx-auto container">
<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">Manage Users Groups</h1>
</div>
<AdminPageTitle
title="Manage Users Groups"
icon={<GroupsIcon size={32} />}
/>
<Main />
</div>

View File

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

View File

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