Curator polish (#2229)

* add new user provider hook

* account for additional logic

* add users

* remove is loading

* Curator polish

* useeffect -> provider + effect

* squash

* use use user for user default models

* squash

* Added ability to add users to groups among other things

* final polish

* added connection button to groups

* mypy fix

* Improved document set clarity

* string fixes

---------

Co-authored-by: pablodanswer <pablo@danswer.ai>
This commit is contained in:
hagen-danswer
2024-08-24 18:10:24 -07:00
committed by GitHub
parent 1e1b2a0901
commit c21b0ee3f5
32 changed files with 675 additions and 512 deletions

View File

@@ -13,6 +13,7 @@ import { FiEdit2 } from "react-icons/fi";
import { TrashIcon } from "@/components/icons/icons";
import { getCurrentUser } from "@/lib/user";
import { UserRole, User } from "@/lib/types";
import { useUser } from "@/components/user/UserProvider";
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
if (persona.default_persona) {
@@ -56,23 +57,7 @@ export function PersonasTable({
const router = useRouter();
const { popup, setPopup } = usePopup();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, []);
const { isLoadingUser, isAdmin } = useUser();
const editablePersonaIds = new Set(
editablePersonas.map((p) => p.id.toString())
@@ -123,6 +108,10 @@ export function PersonasTable({
}
};
if (isLoadingUser) {
return <></>;
}
return (
<div>
{popup}

View File

@@ -38,6 +38,7 @@ import {
useGoogleDriveCredentials,
} from "./pages/utils/hooks";
import { FormikProps } from "formik";
import { useUser } from "@/components/user/UserProvider";
export type AdvancedConfig = {
pruneFreq: number | null;
@@ -99,7 +100,15 @@ export default function AddConnector({
const [refreshFreq, setRefreshFreq] = useState<number>(defaultRefresh || 0);
const [pruneFreq, setPruneFreq] = useState<number>(defaultPrune);
const [indexingStart, setIndexingStart] = useState<Date | null>(null);
const [isPublic, setIsPublic] = useState(true);
const { isAdmin, isLoadingUser } = useUser();
const [isPublic, setIsPublic] = useState(isAdmin);
useEffect(() => {
if (!isLoadingUser) {
setIsPublic(isAdmin);
}
}, [isLoadingUser, isAdmin]);
const [groups, setGroups] = useState<number[]>([]);
const [createConnectorToggle, setCreateConnectorToggle] = useState(false);
const formRef = useRef<FormikProps<any>>(null);
@@ -129,6 +138,10 @@ export default function AddConnector({
setFormStep(Math.min(formStep, 0));
}
if (isLoadingUser) {
return <></>;
}
const resetAdvancedConfigs = () => {
const resetRefreshFreq = defaultRefresh || 0;
const resetPruneFreq = defaultPrune;

View File

@@ -13,8 +13,9 @@ import { ConnectionConfiguration } from "@/lib/connectors/connectors";
import { useFormContext } from "@/components/context/FormContext";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { Text } from "@tremor/react";
import { getCurrentUser } from "@/lib/user";
import { FiUsers } from "react-icons/fi";
import { useUser } from "@/components/user/UserProvider";
export interface DynamicConnectionFormProps {
config: ConnectionConfiguration;
@@ -48,29 +49,12 @@ const DynamicConnectionForm: React.FC<DynamicConnectionFormProps> = ({
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const { setAllowAdvanced } = useFormContext();
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
const userIsAdmin = user.role === UserRole.ADMIN;
setIsAdmin(userIsAdmin);
if (!userIsAdmin) {
setIsPublic(false);
}
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, [setIsPublic]);
const { isLoadingUser, isAdmin, user } = useUser();
if (isLoadingUser) {
return <></>;
}
const initialValues = {
name: initialName || "",
@@ -342,92 +326,107 @@ const DynamicConnectionForm: React.FC<DynamicConnectionFormProps> = ({
currentValue={isPublic}
/>
)}
{userGroups &&
(!isAdmin || (!isPublic && userGroups.length > 0)) && (
<div>
<div className="flex gap-x-2 items-center">
<div className="block font-medium text-base">
Assign group access for this Connector
</div>
</div>
<Text className="mb-3">
{isAdmin ? (
<>
This Connector will be visible/accessible by the
groups selected below
</>
) : (
<>
Curators must select one or more groups to give
access to this Connector
</>
)}
</Text>
<FieldArray
name="groups"
render={() => (
<div className="flex gap-2 flex-wrap">
{!userGroupsIsLoading &&
userGroups.map((userGroup: UserGroup) => {
const isSelected =
groups?.includes(userGroup.id) ||
(!isAdmin && userGroups.length === 1);
// Auto-select the only group for non-admin users
if (
!isAdmin &&
userGroups.length === 1 &&
groups.length === 0
) {
setGroups([userGroup.id]);
}
return (
<div
key={userGroup.id}
className={`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer
${isSelected ? "bg-background-strong" : "hover:bg-hover"}
`}
onClick={() => {
if (setGroups) {
if (
isSelected &&
(isAdmin || userGroups.length > 1)
) {
setGroups(
groups?.filter(
(id) => id !== userGroup.id
) || []
);
} else if (!isSelected) {
setGroups([
...(groups || []),
userGroup.id,
]);
}
}
}}
>
<div className="my-auto flex">
<FiUsers className="my-auto mr-2" />{" "}
{userGroup.name}
</div>
</div>
);
})}
{userGroups ? (
<>
{!isPublic &&
((isAdmin && userGroups.length > 0) ||
(!isAdmin && userGroups.length > 1)) ? (
<div>
<div className="flex gap-x-2 items-center">
<div className="block font-medium text-base">
Assign group access for this Connector
</div>
)}
/>
</div>
)}
</div>
<Text className="mb-3">
{isAdmin ? (
<>
This Connector will be visible/accessible by the
groups selected below
</>
) : (
<>
Curators must select one or more groups to give
access to this Connector
</>
)}
</Text>
<FieldArray
name="groups"
render={() => (
<div className="flex gap-2 flex-wrap">
{!userGroupsIsLoading &&
userGroups.map((userGroup: UserGroup) => {
const isSelected =
groups?.includes(userGroup.id) ||
(!isAdmin && userGroups.length === 1);
// Auto-select the only group for non-admin users
if (
!isAdmin &&
userGroups.length === 1 &&
groups.length === 0
) {
setGroups([userGroup.id]);
}
return (
<div
key={userGroup.id}
className={`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer
${isSelected ? "bg-background-strong" : "hover:bg-hover"}
`}
onClick={() => {
if (setGroups) {
if (
isSelected &&
(isAdmin || userGroups.length > 1)
) {
setGroups(
groups?.filter(
(id) => id !== userGroup.id
) || []
);
} else if (!isSelected) {
setGroups([
...(groups || []),
userGroup.id,
]);
}
}
}}
>
<div className="my-auto flex">
<FiUsers className="my-auto mr-2" />{" "}
{userGroup.name}
</div>
</div>
);
})}
</div>
)}
/>
</div>
) : userGroups && userGroups.length > 0 ? (
<Text className="mb-3">
These documents will be assigned to group:{" "}
<b>{userGroups[0].name}</b>.
</Text>
) : (
<></>
)}
</>
) : (
<></>
)}
</>
)}
</Form>

View File

@@ -19,26 +19,12 @@ import {
GoogleDriveServiceAccountCredentialJson,
} from "@/lib/connectors/credentials";
import { GoogleDriveConfig } from "@/lib/connectors/connectors";
import { useUser } from "@/components/user/UserProvider";
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
const GDriveMain = ({}: {}) => {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
const { isLoadingUser, isAdmin } = useUser();
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, []);
const {
data: appCredentialData,
isLoading: isAppCredentialLoading,
@@ -61,10 +47,7 @@ const GDriveMain = ({}: {}) => {
data: connectorIndexingStatuses,
isLoading: isConnectorIndexingStatusesLoading,
error: connectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any, any>[], FetchError>(
"/api/manage/admin/connector/indexing-status",
errorHandlingFetcher
);
} = useConnectorCredentialIndexingStatus();
const {
data: credentialsData,
isLoading: isCredentialsLoading,
@@ -81,6 +64,10 @@ const GDriveMain = ({}: {}) => {
serviceAccountKeyData ||
(isServiceAccountKeyError && isServiceAccountKeyError.status === 404);
if (isLoadingUser) {
return <></>;
}
if (
(!appCredentialSuccessfullyFetched && isAppCredentialLoading) ||
(!serviceAccountKeySuccessfullyFetched && isServiceAccountKeyLoading) ||

View File

@@ -17,26 +17,12 @@ import { usePublicCredentials } from "@/lib/hooks";
import { Title } from "@tremor/react";
import { GmailConfig } from "@/lib/connectors/connectors";
import { useState, useEffect } from "react";
import { useUser } from "@/components/user/UserProvider";
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
export const GmailMain = () => {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
const { isLoadingUser, isAdmin } = useUser();
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, []);
const {
data: appCredentialData,
isLoading: isAppCredentialLoading,
@@ -57,10 +43,8 @@ export const GmailMain = () => {
data: connectorIndexingStatuses,
isLoading: isConnectorIndexingStatusesLoading,
error: connectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
"/api/manage/admin/connector/indexing-status",
errorHandlingFetcher
);
} = useConnectorCredentialIndexingStatus();
const {
data: credentialsData,
isLoading: isCredentialsLoading,
@@ -77,6 +61,10 @@ export const GmailMain = () => {
serviceAccountKeyData ||
(isServiceAccountKeyError && isServiceAccountKeyError.status === 404);
if (isLoadingUser) {
return <></>;
}
if (
(!appCredentialSuccessfullyFetched && isAppCredentialLoading) ||
(!serviceAccountKeySuccessfullyFetched && isServiceAccountKeyLoading) ||

View File

@@ -11,9 +11,10 @@ import {
import { ConnectorIndexingStatus, DocumentSet, UserGroup } from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Button, Divider, Text } from "@tremor/react";
import { Button, Divider } from "@tremor/react";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
import React, { useEffect, useState } from "react";
interface SetCreationPopupProps {
ccPairs: ConnectorIndexingStatus<any, any>[];
@@ -31,8 +32,14 @@ export const DocumentSetCreationForm = ({
existingDocumentSet,
}: SetCreationPopupProps) => {
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const isUpdate = existingDocumentSet !== undefined;
const [localCcPairs, setLocalCcPairs] = useState(ccPairs);
useEffect(() => {
if (existingDocumentSet?.is_public) {
return;
}
}, [existingDocumentSet?.is_public]);
return (
<div>
@@ -95,100 +102,177 @@ export const DocumentSetCreationForm = ({
}
}}
>
{(props) => (
<Form>
<TextFormField
name="name"
label="Name:"
placeholder="A name for the document set"
disabled={isUpdate}
autoCompleteDisabled={true}
/>
<TextFormField
name="description"
label="Description:"
placeholder="Describe what the document set represents"
autoCompleteDisabled={true}
/>
{(props) => {
return (
<Form>
<TextFormField
name="name"
label="Name:"
placeholder="A name for the document set"
disabled={isUpdate}
autoCompleteDisabled={true}
/>
<TextFormField
name="description"
label="Description:"
placeholder="Describe what the document set represents"
autoCompleteDisabled={true}
/>
{isPaidEnterpriseFeaturesEnabled &&
userGroups &&
userGroups.length > 0 && (
<IsPublicGroupSelector
formikProps={props}
objectName="document set"
/>
)}
<Divider />
<Divider />
<h2 className="mb-1 font-medium text-base">
Pick your connectors:
</h2>
<p className="mb-3 text-xs">
All documents indexed by the selected connectors will be a part of
this document set.
</p>
<FieldArray
name="cc_pair_ids"
render={(arrayHelpers: ArrayHelpers) => (
<div className="mb-3 flex gap-2 flex-wrap">
{ccPairs.map((ccPair) => {
const ind = props.values.cc_pair_ids.indexOf(
ccPair.cc_pair_id
);
let isSelected = ind !== -1;
return (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className={
`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-background-strong"
: " hover:bg-hover")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(ccPair.cc_pair_id);
}
}}
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
<h2 className="mb-1 font-medium text-base">
These are the connectors available to{" "}
{userGroups && userGroups.length > 1
? "the selected group"
: "the group you curate"}
:
</h2>
<p className="mb-3 text-sm">
All documents indexed by these selected connectors will be a
part of this document set.
</p>
<FieldArray
name="cc_pair_ids"
render={(arrayHelpers: ArrayHelpers) => {
// Filter visible cc pairs
const visibleCcPairs = localCcPairs.filter(
(ccPair) =>
ccPair.public_doc ||
(ccPair.groups.length > 0 &&
props.values.groups.every((group) =>
ccPair.groups.includes(group)
))
);
// Deselect filtered out cc pairs
const visibleCcPairIds = visibleCcPairs.map(
(ccPair) => ccPair.cc_pair_id
);
props.values.cc_pair_ids = props.values.cc_pair_ids.filter(
(id) => visibleCcPairIds.includes(id)
);
return (
<div className="mb-3 flex gap-2 flex-wrap">
{visibleCcPairs.map((ccPair) => {
const ind = props.values.cc_pair_ids.indexOf(
ccPair.cc_pair_id
);
let isSelected = ind !== -1;
return (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className={
`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-background-strong"
: " hover:bg-hover")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(ccPair.cc_pair_id);
}
}}
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
);
})}
</div>
);
}}
/>
<FieldArray
name="cc_pair_ids"
render={() => {
// Filter non-visible cc pairs
const nonVisibleCcPairs = localCcPairs.filter(
(ccPair) =>
!ccPair.public_doc &&
(ccPair.groups.length === 0 ||
!props.values.groups.every((group) =>
ccPair.groups.includes(group)
))
);
return nonVisibleCcPairs.length > 0 ? (
<>
<Divider />
<h2 className="mb-1 font-medium text-base">
These connectors are not available to the{" "}
{userGroups && userGroups.length > 1
? `group${props.values.groups.length > 1 ? "s" : ""} you have selected`
: "group you curate"}
:
</h2>
<p className="mb-3 text-sm">
Only connectors that are directly assigned to the group
you are trying to add the document set to will be
available.
</p>
<div className="mb-3 flex gap-2 flex-wrap">
{nonVisibleCcPairs.map((ccPair) => (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className="px-3 py-1 rounded-lg border border-non-selectable-border w-fit flex cursor-not-allowed"
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
))}
</div>
);
})}
</div>
)}
/>
</>
) : null;
}}
/>
{isPaidEnterpriseFeaturesEnabled &&
userGroups &&
userGroups.length > 0 && (
<IsPublicGroupSelector
formikProps={props}
objectName="document set"
/>
)}
<div className="flex mt-6">
<Button
type="submit"
disabled={props.isSubmitting}
className="w-64 mx-auto"
>
{isUpdate ? "Update!" : "Create!"}
</Button>
</div>
</Form>
)}
<div className="flex mt-6">
<Button
type="submit"
disabled={props.isSubmitting}
className="w-64 mx-auto"
>
{isUpdate ? "Update!" : "Create!"}
</Button>
</div>
</Form>
);
}}
</Formik>
</div>
);

View File

@@ -111,23 +111,6 @@ const DocumentSetTable = ({
setPopup,
}: DocumentFeedbackTableProps) => {
const [page, setPage] = useState(1);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, []);
// sort by name for consistent ordering
documentSets.sort((a, b) => {

View File

@@ -443,6 +443,7 @@ export function CCPairIndexingStatusTable({
error_msg: "",
deletion_attempt: null,
is_deletable: true,
groups: [], // Add this line
}}
isEditable={false}
/>

View File

@@ -10,26 +10,19 @@ import { CCPairIndexingStatusTable } from "./CCPairIndexingStatusTable";
import { AdminPageTitle } from "@/components/admin/Title";
import Link from "next/link";
import { Button, Text } from "@tremor/react";
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
function Main() {
const {
data: indexAttemptData,
isLoading: indexAttemptIsLoading,
error: indexAttemptError,
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
"/api/manage/admin/connector/indexing-status",
errorHandlingFetcher,
{ refreshInterval: 10000 } // 10 seconds
);
} = useConnectorCredentialIndexingStatus();
const {
data: editableIndexAttemptData,
isLoading: editableIndexAttemptIsLoading,
error: editableIndexAttemptError,
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
"/api/manage/admin/connector/indexing-status?get_editable=true",
errorHandlingFetcher,
{ refreshInterval: 10000 } // 10 seconds
);
} = useConnectorCredentialIndexingStatus(undefined, true);
if (indexAttemptIsLoading || editableIndexAttemptIsLoading) {
return <LoadingAnimation text="" />;

View File

@@ -85,6 +85,7 @@ import { DeleteChatModal } from "./modal/DeleteChatModal";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import { SEARCH_TOOL_NAME } from "./tools/constants";
import { useUser } from "@/components/user/UserProvider";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -105,7 +106,6 @@ export function ChatPage({
const searchParams = useSearchParams();
let {
user,
chatSessions,
availableSources,
availableDocumentSets,
@@ -116,6 +116,8 @@ export function ChatPage({
userInputPrompts,
} = useChatContext();
const { user, refreshUser } = useUser();
// chat session
const existingChatIdRaw = searchParams.get("chatId");
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
@@ -1418,6 +1420,7 @@ export function ChatPage({
<SetDefaultModelModal
setLlmOverride={llmOverrideManager.setGlobalDefault}
defaultModel={user?.preferences.default_model!}
refreshUser={refreshUser}
llmProviders={llmProviders}
onClose={() => setSettingsToggled(false)}
/>

View File

@@ -14,11 +14,13 @@ export function SetDefaultModelModal({
onClose,
setLlmOverride,
defaultModel,
refreshUser,
}: {
llmProviders: LLMProviderDescriptor[];
setLlmOverride: Dispatch<SetStateAction<LlmOverride>>;
onClose: () => void;
defaultModel: string | null;
refreshUser: () => void;
}) {
const { popup, setPopup } = usePopup();
const containerRef = useRef<HTMLDivElement>(null);
@@ -103,6 +105,7 @@ export function SetDefaultModelModal({
message: "Default model updated successfully",
type: "success",
});
refreshUser();
router.refresh();
} else {
throw new Error("Failed to update default model");

View File

@@ -48,7 +48,6 @@ export default async function Page({
)}
<ChatProvider
value={{
user,
chatSessions,
availableSources,
availableDocumentSets: documentSets,

View File

@@ -31,7 +31,7 @@ import { Bubble } from "@/components/Bubble";
import { BookmarkIcon, RobotIcon } from "@/components/icons/icons";
import { AddTokenRateLimitForm } from "./AddTokenRateLimitForm";
import { GenericTokenRateLimitTable } from "@/app/admin/token-rate-limits/TokenRateLimitTables";
import { getCurrentUser } from "@/lib/user";
import { useUser } from "@/components/user/UserProvider";
interface GroupDisplayProps {
users: User[];
@@ -106,7 +106,7 @@ const UserRoleDropdown = ({
</div>
);
} else {
return <div>{user.role}</div>;
return <div>{localRole}</div>;
}
};
@@ -120,23 +120,12 @@ export const GroupDisplay = ({
const [addMemberFormVisible, setAddMemberFormVisible] = useState(false);
const [addConnectorFormVisible, setAddConnectorFormVisible] = useState(false);
const [addRateLimitFormVisible, setAddRateLimitFormVisible] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, []);
const { isLoadingUser, isAdmin } = useUser();
if (isLoadingUser) {
return <></>;
}
const handlePopup = (message: string, type: "success" | "error") => {
setPopup({ message, type });
};
@@ -175,23 +164,21 @@ export const GroupDisplay = ({
<TableRow>
<TableHeaderCell>Email</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
{isAdmin && (
<TableHeaderCell className="flex w-full">
<div className="ml-auto">Remove User</div>
</TableHeaderCell>
)}
<TableHeaderCell className="flex w-full">
<div className="ml-auto">Remove User</div>
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{userGroup.users.map((user) => {
{userGroup.users.map((groupMember) => {
return (
<TableRow key={user.id}>
<TableRow key={groupMember.id}>
<TableCell className="whitespace-normal break-all">
{user.email}
{groupMember.email}
</TableCell>
<TableCell>
<UserRoleDropdown
user={user}
user={groupMember}
group={userGroup}
onSuccess={onRoleChangeSuccess}
onError={onRoleChangeError}
@@ -201,7 +188,10 @@ export const GroupDisplay = ({
<TableCell>
<div className="flex w-full">
<div className="ml-auto m-2">
{isAdmin && (
{(isAdmin ||
!userGroup.curator_ids.includes(
groupMember.id
)) && (
<DeleteButton
onClick={async () => {
const response = await updateUserGroup(
@@ -210,7 +200,7 @@ export const GroupDisplay = ({
user_ids: userGroup.users
.filter(
(userGroupUser) =>
userGroupUser.id !== user.id
userGroupUser.id !== groupMember.id
)
.map(
(userGroupUser) => userGroupUser.id
@@ -254,17 +244,15 @@ export const GroupDisplay = ({
)}
</div>
{isAdmin && (
<Button
className="mt-3"
size="xs"
color="green"
onClick={() => setAddMemberFormVisible(true)}
disabled={!userGroup.is_up_to_date}
>
Add Users
</Button>
)}
<Button
className="mt-3"
size="xs"
color="green"
onClick={() => setAddMemberFormVisible(true)}
disabled={!userGroup.is_up_to_date}
>
Add Users
</Button>
{addMemberFormVisible && (
<AddMemberForm
@@ -355,17 +343,15 @@ export const GroupDisplay = ({
)}
</div>
{isAdmin && (
<Button
className="mt-3"
onClick={() => setAddConnectorFormVisible(true)}
size="xs"
color="green"
disabled={!userGroup.is_up_to_date}
>
Add Connectors
</Button>
)}
<Button
className="mt-3"
onClick={() => setAddConnectorFormVisible(true)}
size="xs"
color="green"
disabled={!userGroup.is_up_to_date}
>
Add Connectors
</Button>
{addConnectorFormVisible && (
<AddConnectorForm

View File

@@ -15,6 +15,7 @@ import {
} from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button, Divider } from "@tremor/react";
import { useUser } from "@/components/user/UserProvider";
const Main = () => {
const { popup, setPopup } = usePopup();
@@ -34,23 +35,10 @@ const Main = () => {
error: usersError,
} = useUsers();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
}
};
fetchCurrentUser();
}, []);
const { isLoadingUser, isAdmin } = useUser();
if (isLoadingUser) {
return <></>;
}
if (isLoading || isCCPairsLoading || userIsLoading) {
return <ThreeDotsLoader />;

View File

@@ -20,6 +20,7 @@ import { Button, Card } from "@tremor/react";
import LogoType from "@/components/header/LogoType";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { Logo } from "@/components/Logo";
import { UserProvider } from "@/components/user/UserProvider";
const inter = Inter({
subsets: ["latin"],
@@ -111,9 +112,11 @@ export default async function RootLayout({
process.env.THEME_IS_DARK?.toLowerCase() === "true" ? "dark" : ""
}`}
>
<SettingsProvider settings={combinedSettings}>
{children}
</SettingsProvider>
<UserProvider>
<SettingsProvider settings={combinedSettings}>
{children}
</SettingsProvider>
</UserProvider>
</div>
</body>
</html>

View File

@@ -5,7 +5,7 @@ import { FiUsers } from "react-icons/fi";
import { UserGroup, User, UserRole } from "@/lib/types";
import { useUserGroups } from "@/lib/hooks";
import { BooleanFormField } from "@/components/admin/connectors/Field";
import { getCurrentUser } from "@/lib/user";
import { useUser } from "./user/UserProvider";
export type IsPublicGroupSelectorFormType = {
is_public: boolean;
@@ -22,41 +22,44 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
enforceGroupSelection?: boolean;
}) => {
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
const [isLoading, setIsLoading] = useState(true);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
const { isAdmin, user, isLoadingUser } = useUser();
const [shouldHideContent, setShouldHideContent] = useState(false);
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
formikProps.setFieldValue("is_public", user.role === UserRole.ADMIN);
// Select the only group by default if there's only one
if (
userGroups &&
userGroups.length === 1 &&
formikProps.values.groups.length === 0 &&
user.role !== UserRole.ADMIN
) {
formikProps.setFieldValue("groups", [userGroups[0].id]);
}
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
} finally {
setIsLoading(false);
if (user && userGroups) {
const isUserAdmin = user.role === UserRole.ADMIN;
if (userGroups.length === 1 && !isUserAdmin) {
formikProps.setFieldValue("groups", [userGroups[0].id]);
setShouldHideContent(true);
} else if (formikProps.values.is_public) {
formikProps.setFieldValue("groups", []);
setShouldHideContent(false);
} else {
setShouldHideContent(false);
}
};
fetchCurrentUser();
}, [userGroups]); // Add userGroups as a dependency
}
}, [
user,
userGroups,
formikProps.setFieldValue,
formikProps.values.is_public,
]);
if (isLoading || userGroupsIsLoading) {
return null; // or return a loading spinner if preferred
if (isLoadingUser || userGroupsIsLoading) {
return <div>Loading...</div>;
}
if (shouldHideContent && enforceGroupSelection) {
return (
<>
{userGroups && (
<div className="mb-1 font-medium text-base">
This {objectName} will be assigned to group{" "}
<b>{userGroups[0].name}</b>.
</div>
)}
</>
);
}
return (
@@ -70,17 +73,19 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
disabled={!isAdmin}
subtext={
<span className="block mt-2 text-sm text-gray-500">
If set, then {objectName}s indexed by this {objectName} will be
visible to <b>all users</b>. If turned off, then only users who
explicitly have been given access to the {objectName}s (e.g.
through a User Group) will have access.
If set, then this {objectName} will be visible to{" "}
<b>all users</b>. If turned off, then only users who explicitly
have been given access to this {objectName} (e.g. through a User
Group) will have access.
</span>
}
/>
</>
)}
{(!formikProps.values.is_public || !isAdmin) && (
{(!formikProps.values.is_public ||
!isAdmin ||
formikProps.values.groups.length > 0) && (
<>
<div className="flex gap-x-2 items-center">
<div className="block font-medium text-base">

View File

@@ -1,13 +1,7 @@
"use client";
import React, { createContext, useContext } from "react";
import {
CCPairBasicInfo,
DocumentSet,
Tag,
User,
ValidSources,
} from "@/lib/types";
import { DocumentSet, Tag, User, ValidSources } from "@/lib/types";
import { ChatSession } from "@/app/chat/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
@@ -15,7 +9,6 @@ import { Folder } from "@/app/chat/folders/interfaces";
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
interface ChatContextProps {
user: User | null;
chatSessions: ChatSession[];
availableSources: ValidSources[];
availableDocumentSets: DocumentSet[];

View File

@@ -26,6 +26,7 @@ import {
IsPublicGroupSelectorFormType,
IsPublicGroupSelector,
} from "@/components/IsPublicGroupSelector";
import { useUser } from "@/components/user/UserProvider";
const CreateButton = ({
onClick,
@@ -92,27 +93,12 @@ export default function CreateCredential({
refresh?: () => void;
}) {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const user = await getCurrentUser();
if (user) {
setCurrentUser(user);
} else {
console.error("Failed to fetch current user");
}
} catch (error) {
console.error("Error fetching current user:", error);
} finally {
setIsLoading(false);
}
};
fetchCurrentUser();
}, []);
const { isLoadingUser, isAdmin } = useUser();
if (isLoadingUser) {
return <></>;
}
const handleSubmit = async (
values: formType,
@@ -184,7 +170,6 @@ export default function CreateCredential({
return <GDriveMain />;
}
const isAdmin = currentUser?.role === UserRole.ADMIN;
const credentialTemplate: dictionaryType = credentialTemplates[sourceType];
const validationSchema = createValidationSchema(credentialTemplate);
@@ -236,7 +221,7 @@ export default function CreateCredential({
}
/>
))}
{!swapConnector && !isLoading && (
{!swapConnector && (
<div className="mt-4 flex flex-col sm:flex-row justify-between items-end">
<div className="w-full sm:w-3/4 mb-4 sm:mb-0">
{isPaidEnterpriseFeaturesEnabled && (

View File

@@ -0,0 +1,55 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
import { User, UserRole } from "@/lib/types";
import { getCurrentUser } from "@/lib/user";
interface UserContextType {
user: User | null;
isLoadingUser: boolean;
isAdmin: boolean;
refreshUser: () => Promise<void>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoadingUser, setIsLoadingUser] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const fetchUser = async () => {
try {
const user = await getCurrentUser();
setUser(user);
setIsAdmin(user?.role === UserRole.ADMIN);
} catch (error) {
console.error("Error fetching current user:", error);
} finally {
setIsLoadingUser(false);
}
};
useEffect(() => {
fetchUser();
}, []);
const refreshUser = async () => {
setIsLoadingUser(true);
await fetchUser();
};
return (
<UserContext.Provider value={{ user, isLoadingUser, isAdmin, refreshUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error("useUser must be used within a UserProvider");
}
return context;
}

View File

@@ -65,18 +65,20 @@ export const useObjectState = <T>(
const INDEXING_STATUS_URL = "/api/manage/admin/connector/indexing-status";
export const useConnectorCredentialIndexingStatus = (
refreshInterval = 30000 // 30 seconds
refreshInterval = 30000, // 30 seconds
getEditable = false
) => {
const { mutate } = useSWRConfig();
const url = `${INDEXING_STATUS_URL}${getEditable ? "?get_editable=true" : ""}`;
const swrResponse = useSWR<ConnectorIndexingStatus<any, any>[]>(
INDEXING_STATUS_URL,
url,
errorHandlingFetcher,
{ refreshInterval: refreshInterval }
);
return {
...swrResponse,
refreshIndexingStatus: () => mutate(INDEXING_STATUS_URL),
refreshIndexingStatus: () => mutate(url),
};
};

View File

@@ -82,6 +82,7 @@ export interface ConnectorIndexingStatus<
credential: Credential<ConnectorCredentialType>;
public_doc: boolean;
owner: string;
groups: number[];
last_finished_status: ValidStatuses | null;
last_status: ValidStatuses | null;
last_success: string | null;

View File

@@ -79,6 +79,8 @@ module.exports = {
"token-regex": "#9cdcfe", // light blue
"token-attr-name": "#9cdcfe", // light blue
"non-selectable": "#f8d7da", // red-100
// background
"background-search": "#ffffff", // white
input: "#f5f5f5",
@@ -133,6 +135,7 @@ module.exports = {
"border-medium": "#d1d5db", // gray-300
"border-strong": "#9ca3af", // gray-400
"border-dark": "#525252", // neutral-600
"non-selectable-border": "#f5c2c7", // red-200
// hover
"hover-light": "#f3f4f6", // gray-100