From c21b0ee3f528cd96941c146958169b020a5abe51 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sat, 24 Aug 2024 18:10:24 -0700 Subject: [PATCH] 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 --- backend/danswer/auth/users.py | 2 +- .../danswer/db/connector_credential_pair.py | 21 +- backend/danswer/db/credentials.py | 3 +- backend/danswer/db/document_set.py | 190 +++++++++---- backend/danswer/db/index_attempt.py | 3 +- backend/danswer/db/users.py | 9 - backend/danswer/server/documents/connector.py | 12 + backend/danswer/server/documents/models.py | 1 + .../server/features/document_set/api.py | 15 +- backend/ee/danswer/db/user_group.py | 10 +- web/src/app/admin/assistants/PersonaTable.tsx | 23 +- .../[connector]/AddConnectorPage.tsx | 15 +- .../connectors/[connector]/pages/Create.tsx | 213 +++++++------- .../pages/gdrive/GoogleDrivePage.tsx | 29 +- .../[connector]/pages/gmail/GmailPage.tsx | 30 +- .../sets/DocumentSetCreationForm.tsx | 268 ++++++++++++------ web/src/app/admin/documents/sets/page.tsx | 17 -- .../status/CCPairIndexingStatusTable.tsx | 1 + web/src/app/admin/indexing/status/page.tsx | 13 +- web/src/app/chat/ChatPage.tsx | 5 +- .../app/chat/modal/SetDefaultModelModal.tsx | 3 + web/src/app/chat/page.tsx | 1 - .../admin/groups/[groupId]/GroupDisplay.tsx | 90 +++--- web/src/app/ee/admin/groups/page.tsx | 22 +- web/src/app/layout.tsx | 9 +- web/src/components/IsPublicGroupSelector.tsx | 79 +++--- web/src/components/context/ChatContext.tsx | 9 +- .../credentials/actions/CreateCredential.tsx | 27 +- web/src/components/user/UserProvider.tsx | 55 ++++ web/src/lib/hooks.ts | 8 +- web/src/lib/types.ts | 1 + web/tailwind-themes/tailwind.config.js | 3 + 32 files changed, 675 insertions(+), 512 deletions(-) create mode 100644 web/src/components/user/UserProvider.tsx diff --git a/backend/danswer/auth/users.py b/backend/danswer/auth/users.py index ef6e0be1b843..72fe45ee39e1 100644 --- a/backend/danswer/auth/users.py +++ b/backend/danswer/auth/users.py @@ -80,7 +80,7 @@ def validate_curator_request(groups: list | None, is_public: bool) -> None: logger.error(detail) raise HTTPException( status_code=401, - detail="Curators must specify 1+ groups", + detail=detail, ) diff --git a/backend/danswer/db/connector_credential_pair.py b/backend/danswer/db/connector_credential_pair.py index 7bf3324a77c4..6284832ae4d6 100644 --- a/backend/danswer/db/connector_credential_pair.py +++ b/backend/danswer/db/connector_credential_pair.py @@ -84,17 +84,36 @@ def get_connector_credential_pairs( include_disabled: bool = True, user: User | None = None, get_editable: bool = True, + ids: list[int] | None = None, ) -> list[ConnectorCredentialPair]: - stmt = select(ConnectorCredentialPair) + stmt = select(ConnectorCredentialPair).distinct() stmt = _add_user_filters(stmt, user, get_editable) if not include_disabled: stmt = stmt.where( ConnectorCredentialPair.status == ConnectorCredentialPairStatus.ACTIVE ) # noqa + if ids: + stmt = stmt.where(ConnectorCredentialPair.id.in_(ids)) results = db_session.scalars(stmt) return list(results.all()) +def get_cc_pair_groups_for_ids( + db_session: Session, + cc_pair_ids: list[int], + user: User | None = None, + get_editable: bool = True, +) -> list[UserGroup__ConnectorCredentialPair]: + stmt = select(UserGroup__ConnectorCredentialPair).distinct() + stmt = stmt.outerjoin( + ConnectorCredentialPair, + UserGroup__ConnectorCredentialPair.cc_pair_id == ConnectorCredentialPair.id, + ) + stmt = _add_user_filters(stmt, user, get_editable) + stmt = stmt.where(UserGroup__ConnectorCredentialPair.cc_pair_id.in_(cc_pair_ids)) + return list(db_session.scalars(stmt).all()) + + def get_connector_credential_pair( connector_id: int, credential_id: int, diff --git a/backend/danswer/db/credentials.py b/backend/danswer/db/credentials.py index cf9af2c2e545..abab904cc48f 100644 --- a/backend/danswer/db/credentials.py +++ b/backend/danswer/db/credentials.py @@ -155,7 +155,8 @@ def fetch_credential_by_id( db_session: Session, assume_admin: bool = False, ) -> Credential | None: - stmt = select(Credential).where(Credential.id == credential_id) + stmt = select(Credential).distinct() + stmt = stmt.where(Credential.id == credential_id) stmt = _add_user_filters(stmt, user, assume_admin=assume_admin) result = db_session.execute(stmt) credential = result.scalar_one_or_none() diff --git a/backend/danswer/db/document_set.py b/backend/danswer/db/document_set.py index 130c4b0a0caf..3cc7646faa01 100644 --- a/backend/danswer/db/document_set.py +++ b/backend/danswer/db/document_set.py @@ -12,6 +12,8 @@ from sqlalchemy import select from sqlalchemy.orm import aliased from sqlalchemy.orm import Session +from danswer.db.connector_credential_pair import get_cc_pair_groups_for_ids +from danswer.db.connector_credential_pair import get_connector_credential_pairs from danswer.db.enums import ConnectorCredentialPairStatus from danswer.db.models import ConnectorCredentialPair from danswer.db.models import Document @@ -27,6 +29,52 @@ from danswer.server.features.document_set.models import DocumentSetUpdateRequest from danswer.utils.variable_functionality import fetch_versioned_implementation +def _add_user_filters( + stmt: Select, user: User | None, get_editable: bool = True +) -> Select: + # If user is None, assume the user is an admin or auth is disabled + if user is None or user.role == UserRole.ADMIN: + return stmt + + DocumentSet__UG = aliased(DocumentSet__UserGroup) + User__UG = aliased(User__UserGroup) + """ + Here we select cc_pairs by relation: + User -> User__UserGroup -> DocumentSet__UserGroup -> DocumentSet + """ + stmt = stmt.outerjoin(DocumentSet__UG).outerjoin( + User__UserGroup, + User__UserGroup.user_group_id == DocumentSet__UG.user_group_id, + ) + """ + Filter DocumentSets by: + - if the user is in the user_group that owns the DocumentSet + - if the user is not a global_curator, they must also have a curator relationship + to the user_group + - if editing is being done, we also filter out DocumentSets that are owned by groups + that the user isn't a curator for + - if we are not editing, we show all DocumentSets in the groups the user is a curator + for (as well as public DocumentSets) + """ + where_clause = User__UserGroup.user_id == user.id + if user.role == UserRole.CURATOR and get_editable: + where_clause &= User__UserGroup.is_curator == True # noqa: E712 + if get_editable: + user_groups = select(User__UG.user_group_id).where(User__UG.user_id == user.id) + if user.role == UserRole.CURATOR: + user_groups = user_groups.where(User__UG.is_curator == True) # noqa: E712 + where_clause &= ( + ~exists() + .where(DocumentSet__UG.document_set_id == DocumentSetDBModel.id) + .where(~DocumentSet__UG.user_group_id.in_(user_groups)) + .correlate(DocumentSetDBModel) + ) + else: + where_clause |= DocumentSetDBModel.is_public == True # noqa: E712 + + return stmt.where(where_clause) + + def _delete_document_set_cc_pairs__no_commit( db_session: Session, document_set_id: int, is_current: bool | None = None ) -> None: @@ -57,11 +105,15 @@ def delete_document_set_privacy__no_commit( def get_document_set_by_id( - db_session: Session, document_set_id: int + db_session: Session, + document_set_id: int, + user: User | None = None, + get_editable: bool = True, ) -> DocumentSetDBModel | None: - return db_session.scalar( - select(DocumentSetDBModel).where(DocumentSetDBModel.id == document_set_id) - ) + stmt = select(DocumentSetDBModel).distinct() + stmt = stmt.where(DocumentSetDBModel.id == document_set_id) + stmt = _add_user_filters(stmt=stmt, user=user, get_editable=get_editable) + return db_session.scalar(stmt) def get_document_set_by_name( @@ -93,6 +145,45 @@ def make_doc_set_private( raise NotImplementedError("Danswer MIT does not support private Document Sets") +def _check_if_cc_pairs_are_owned_by_groups( + db_session: Session, + cc_pair_ids: list[int], + group_ids: list[int], +) -> None: + """ + This function checks if the CC pairs are owned by the specified groups or public. + If not, it raises a ValueError. + """ + group_cc_pair_relationships = get_cc_pair_groups_for_ids( + db_session=db_session, + cc_pair_ids=cc_pair_ids, + ) + + group_cc_pair_relationships_set = { + (relationship.cc_pair_id, relationship.user_group_id) + for relationship in group_cc_pair_relationships + } + + missing_cc_pair_ids = [] + for cc_pair_id in cc_pair_ids: + for group_id in group_ids: + if (cc_pair_id, group_id) not in group_cc_pair_relationships_set: + missing_cc_pair_ids.append(cc_pair_id) + break + + if missing_cc_pair_ids: + cc_pairs = get_connector_credential_pairs( + db_session=db_session, + ids=missing_cc_pair_ids, + ) + for cc_pair in cc_pairs: + if not cc_pair.is_public: + raise ValueError( + f"Connector Credential Pair with ID: '{cc_pair.id}'" + " is not owned by the specified groups" + ) + + def insert_document_set( document_set_creation_request: DocumentSetCreationRequest, user_id: UUID | None, @@ -102,8 +193,12 @@ def insert_document_set( # It's cc-pairs in actuality but the UI displays this error raise ValueError("Cannot create a document set with no Connectors") - # start a transaction - db_session.begin() + if not document_set_creation_request.is_public: + _check_if_cc_pairs_are_owned_by_groups( + db_session=db_session, + cc_pair_ids=document_set_creation_request.cc_pair_ids, + group_ids=document_set_creation_request.groups or [], + ) try: new_document_set_row = DocumentSetDBModel( @@ -146,19 +241,28 @@ def insert_document_set( def update_document_set( - document_set_update_request: DocumentSetUpdateRequest, db_session: Session + db_session: Session, + document_set_update_request: DocumentSetUpdateRequest, + user: User | None = None, ) -> tuple[DocumentSetDBModel, list[DocumentSet__ConnectorCredentialPair]]: if not document_set_update_request.cc_pair_ids: # It's cc-pairs in actuality but the UI displays this error raise ValueError("Cannot create a document set with no Connectors") - # start a transaction - db_session.begin() + if not document_set_update_request.is_public: + _check_if_cc_pairs_are_owned_by_groups( + db_session=db_session, + cc_pair_ids=document_set_update_request.cc_pair_ids, + group_ids=document_set_update_request.groups, + ) try: # update the description document_set_row = get_document_set_by_id( - db_session=db_session, document_set_id=document_set_update_request.id + db_session=db_session, + document_set_id=document_set_update_request.id, + user=user, + get_editable=True, ) if document_set_row is None: raise ValueError( @@ -236,20 +340,26 @@ def delete_document_set( def mark_document_set_as_to_be_deleted( - document_set_id: int, db_session: Session + db_session: Session, + document_set_id: int, + user: User | None = None, ) -> None: """Cleans up all document_set -> cc_pair relationships and marks the document set as needing an update. The actual document set row will be deleted by the background job which syncs these changes to Vespa.""" - # start a transaction - db_session.begin() try: document_set_row = get_document_set_by_id( - db_session=db_session, document_set_id=document_set_id + db_session=db_session, + document_set_id=document_set_id, + user=user, + get_editable=True, ) if document_set_row is None: - raise ValueError(f"No document set with ID: '{document_set_id}'") + error_msg = f"Document set with ID: '{document_set_id}' does not exist " + if user is not None: + error_msg += f"or is not editable by user with email: '{user.email}'" + raise ValueError(error_msg) if not document_set_row.is_up_to_date: raise ValueError( "Cannot delete document set while it is syncing. Please wait " @@ -348,54 +458,10 @@ def fetch_document_sets( ] -def _add_user_filters( - stmt: Select, user: User | None, get_editable: bool = True -) -> Select: - # If user is None, assume the user is an admin or auth is disabled - if user is None or user.role == UserRole.ADMIN: - return stmt - - DocumentSet__UG = aliased(DocumentSet__UserGroup) - User__UG = aliased(User__UserGroup) - """ - Here we select cc_pairs by relation: - User -> User__UserGroup -> DocumentSet__UserGroup -> DocumentSet - """ - stmt = stmt.outerjoin(DocumentSet__UG).outerjoin( - User__UserGroup, - User__UserGroup.user_group_id == DocumentSet__UG.user_group_id, - ) - """ - Filter DocumentSets by: - - if the user is in the user_group that owns the DocumentSet - - if the user is not a global_curator, they must also have a curator relationship - to the user_group - - if editing is being done, we also filter out DocumentSets that are owned by groups - that the user isn't a curator for - - if we are not editing, we show all DocumentSets in the groups the user is a curator - for (as well as public DocumentSets) - """ - where_clause = User__UserGroup.user_id == user.id - if user.role == UserRole.CURATOR and get_editable: - where_clause &= User__UserGroup.is_curator == True # noqa: E712 - if get_editable: - user_groups = select(User__UG.user_group_id).where(User__UG.user_id == user.id) - if user.role == UserRole.CURATOR: - user_groups = user_groups.where(User__UG.is_curator == True) # noqa: E712 - where_clause &= ( - ~exists() - .where(DocumentSet__UG.document_set_id == DocumentSetDBModel.id) - .where(~DocumentSet__UG.user_group_id.in_(user_groups)) - .correlate(DocumentSetDBModel) - ) - else: - where_clause |= DocumentSetDBModel.is_public == True # noqa: E712 - - return stmt.where(where_clause) - - def fetch_all_document_sets_for_user( - db_session: Session, user: User | None = None, get_editable: bool = True + db_session: Session, + user: User | None = None, + get_editable: bool = True, ) -> Sequence[DocumentSetDBModel]: stmt = select(DocumentSetDBModel).distinct() stmt = _add_user_filters(stmt, user, get_editable=get_editable) diff --git a/backend/danswer/db/index_attempt.py b/backend/danswer/db/index_attempt.py index 3d8668427b41..22ca6c393893 100644 --- a/backend/danswer/db/index_attempt.py +++ b/backend/danswer/db/index_attempt.py @@ -242,7 +242,8 @@ def get_latest_finished_index_attempt_for_cc_pair( secondary_index: bool, db_session: Session, ) -> IndexAttempt | None: - stmt = select(IndexAttempt).where( + stmt = select(IndexAttempt).distinct() + stmt = stmt.where( IndexAttempt.connector_credential_pair_id == connector_credential_pair_id, IndexAttempt.status.not_in( [IndexingStatus.NOT_STARTED, IndexingStatus.IN_PROGRESS] diff --git a/backend/danswer/db/users.py b/backend/danswer/db/users.py index 515cbe070a32..d824ccfd9211 100644 --- a/backend/danswer/db/users.py +++ b/backend/danswer/db/users.py @@ -5,8 +5,6 @@ from sqlalchemy import select from sqlalchemy.orm import Session from danswer.db.models import User -from danswer.db.models import User__UserGroup -from danswer.db.models import UserRole def list_users( @@ -19,13 +17,6 @@ def list_users( if email_filter_string: stmt = stmt.where(User.email.ilike(f"%{email_filter_string}%")) # type: ignore - if user and user.role != UserRole.ADMIN: - stmt = stmt.join(User__UserGroup) - where_clause = User__UserGroup.user_id == user.id - if user.role == UserRole.CURATOR: - where_clause &= User__UserGroup.is_curator == True # noqa: E712 - stmt = stmt.where(where_clause) - return db_session.scalars(stmt).unique().all() diff --git a/backend/danswer/server/documents/connector.py b/backend/danswer/server/documents/connector.py index 05c18e65f81f..46b3083a2029 100644 --- a/backend/danswer/server/documents/connector.py +++ b/backend/danswer/server/documents/connector.py @@ -54,6 +54,7 @@ from danswer.db.connector import fetch_connectors from danswer.db.connector import get_connector_credential_ids from danswer.db.connector import update_connector from danswer.db.connector_credential_pair import add_credential_to_connector +from danswer.db.connector_credential_pair import get_cc_pair_groups_for_ids from danswer.db.connector_credential_pair import get_connector_credential_pair from danswer.db.connector_credential_pair import get_connector_credential_pairs from danswer.db.credentials import create_credential @@ -422,6 +423,16 @@ def get_connector_indexing_status( for connector_id, credential_id, cnt in document_count_info } + group_cc_pair_relationships = get_cc_pair_groups_for_ids( + db_session=db_session, + cc_pair_ids=[cc_pair.id for cc_pair in cc_pairs], + ) + group_cc_pair_relationships_dict: dict[int, list[int]] = {} + for relationship in group_cc_pair_relationships: + group_cc_pair_relationships_dict.setdefault(relationship.cc_pair_id, []).append( + relationship.user_group_id + ) + for cc_pair in cc_pairs: # TODO remove this to enable ingestion API if cc_pair.name == "DefaultCCPair": @@ -448,6 +459,7 @@ def get_connector_indexing_status( credential=CredentialSnapshot.from_credential_db_model(credential), public_doc=cc_pair.is_public, owner=credential.user.email if credential.user else "", + groups=group_cc_pair_relationships_dict.get(cc_pair.id, []), last_finished_status=( latest_finished_attempt.status if latest_finished_attempt else None ), diff --git a/backend/danswer/server/documents/models.py b/backend/danswer/server/documents/models.py index 32a882979ef5..ad23fd3b9000 100644 --- a/backend/danswer/server/documents/models.py +++ b/backend/danswer/server/documents/models.py @@ -236,6 +236,7 @@ class ConnectorIndexingStatus(BaseModel): connector: ConnectorSnapshot credential: CredentialSnapshot owner: str + groups: list[int] public_doc: bool last_finished_status: IndexingStatus | None last_status: IndexingStatus | None diff --git a/backend/danswer/server/features/document_set/api.py b/backend/danswer/server/features/document_set/api.py index cbce90997d12..b3f01fb0a476 100644 --- a/backend/danswer/server/features/document_set/api.py +++ b/backend/danswer/server/features/document_set/api.py @@ -4,7 +4,6 @@ from fastapi import HTTPException from fastapi import Query from sqlalchemy.orm import Session -from danswer.auth.users import current_admin_user from danswer.auth.users import current_curator_or_admin_user from danswer.auth.users import current_user from danswer.auth.users import validate_curator_request @@ -55,13 +54,19 @@ def create_document_set( @router.patch("/admin/document-set") def patch_document_set( document_set_update_request: DocumentSetUpdateRequest, - _: User = Depends(current_admin_user), + user: User = Depends(current_curator_or_admin_user), db_session: Session = Depends(get_session), ) -> None: + if user and user.role != UserRole.ADMIN: + validate_curator_request( + groups=document_set_update_request.groups, + is_public=document_set_update_request.is_public, + ) try: update_document_set( document_set_update_request=document_set_update_request, db_session=db_session, + user=user, ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @@ -70,12 +75,14 @@ def patch_document_set( @router.delete("/admin/document-set/{document_set_id}") def delete_document_set( document_set_id: int, - _: User = Depends(current_admin_user), + user: User = Depends(current_curator_or_admin_user), db_session: Session = Depends(get_session), ) -> None: try: mark_document_set_as_to_be_deleted( - document_set_id=document_set_id, db_session=db_session + db_session=db_session, + document_set_id=document_set_id, + user=user, ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/ee/danswer/db/user_group.py b/backend/ee/danswer/db/user_group.py index 93cfff36e167..9d172c5d716c 100644 --- a/backend/ee/danswer/db/user_group.py +++ b/backend/ee/danswer/db/user_group.py @@ -328,10 +328,12 @@ def update_user_group( added_user_ids = list(updated_user_ids - current_user_ids) removed_user_ids = list(current_user_ids - updated_user_ids) - if (removed_user_ids or added_user_ids) and ( - not user or user.role != UserRole.ADMIN - ): - raise ValueError("Only admins can add or remove users from user groups") + # LEAVING THIS HERE FOR NOW FOR GIVING DIFFERENT ROLES + # ACCESS TO DIFFERENT PERMISSIONS + # if (removed_user_ids or added_user_ids) and ( + # not user or user.role != UserRole.ADMIN + # ): + # raise ValueError("Only admins can add or remove users from user groups") if removed_user_ids: _cleanup_user__user_group_relationships__no_commit( diff --git a/web/src/app/admin/assistants/PersonaTable.tsx b/web/src/app/admin/assistants/PersonaTable.tsx index 9c41f100f306..aa35a0f17a91 100644 --- a/web/src/app/admin/assistants/PersonaTable.tsx +++ b/web/src/app/admin/assistants/PersonaTable.tsx @@ -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(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 (
{popup} diff --git a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx index b13a5af96814..0afc30f472d4 100644 --- a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx +++ b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx @@ -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(defaultRefresh || 0); const [pruneFreq, setPruneFreq] = useState(defaultPrune); const [indexingStart, setIndexingStart] = useState(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([]); const [createConnectorToggle, setCreateConnectorToggle] = useState(false); const formRef = useRef>(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; diff --git a/web/src/app/admin/connectors/[connector]/pages/Create.tsx b/web/src/app/admin/connectors/[connector]/pages/Create.tsx index e3f4b51e0e78..8935307f0962 100644 --- a/web/src/app/admin/connectors/[connector]/pages/Create.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/Create.tsx @@ -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 = ({ const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled(); const { setAllowAdvanced } = useFormContext(); const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups(); - const [currentUser, setCurrentUser] = useState(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 = ({ currentValue={isPublic} /> )} - {userGroups && - (!isAdmin || (!isPublic && userGroups.length > 0)) && ( -
-
-
- Assign group access for this Connector -
-
- - {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 - - )} - - ( -
- {!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 ( -
{ - if (setGroups) { - if ( - isSelected && - (isAdmin || userGroups.length > 1) - ) { - setGroups( - groups?.filter( - (id) => id !== userGroup.id - ) || [] - ); - } else if (!isSelected) { - setGroups([ - ...(groups || []), - userGroup.id, - ]); - } - } - }} - > -
- {" "} - {userGroup.name} -
-
- ); - })} + {userGroups ? ( + <> + {!isPublic && + ((isAdmin && userGroups.length > 0) || + (!isAdmin && userGroups.length > 1)) ? ( +
+
+
+ Assign group access for this Connector
- )} - /> -
- )} +
+ + {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 + + )} + + ( +
+ {!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 ( +
{ + if (setGroups) { + if ( + isSelected && + (isAdmin || userGroups.length > 1) + ) { + setGroups( + groups?.filter( + (id) => id !== userGroup.id + ) || [] + ); + } else if (!isSelected) { + setGroups([ + ...(groups || []), + userGroup.id, + ]); + } + } + }} + > +
+ {" "} + {userGroup.name} +
+
+ ); + })} +
+ )} + /> +
+ ) : userGroups && userGroups.length > 0 ? ( + + These documents will be assigned to group:{" "} + {userGroups[0].name}. + + ) : ( + <> + )} + + ) : ( + <> + )} )} diff --git a/web/src/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage.tsx b/web/src/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage.tsx index 8f84105e2098..4494e4b22eee 100644 --- a/web/src/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage.tsx @@ -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(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[], 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) || diff --git a/web/src/app/admin/connectors/[connector]/pages/gmail/GmailPage.tsx b/web/src/app/admin/connectors/[connector]/pages/gmail/GmailPage.tsx index ebfdbe832353..5f52eb310137 100644 --- a/web/src/app/admin/connectors/[connector]/pages/gmail/GmailPage.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/gmail/GmailPage.tsx @@ -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(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[]>( - "/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) || diff --git a/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx b/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx index 615b2cc0ae0f..e8f3d519944a 100644 --- a/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx +++ b/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx @@ -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[]; @@ -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 (
@@ -95,100 +102,177 @@ export const DocumentSetCreationForm = ({ } }} > - {(props) => ( -
- - + {(props) => { + return ( + + + + {isPaidEnterpriseFeaturesEnabled && + userGroups && + userGroups.length > 0 && ( + + )} - + -

- Pick your connectors: -

-

- All documents indexed by the selected connectors will be a part of - this document set. -

- ( -
- {ccPairs.map((ccPair) => { - const ind = props.values.cc_pair_ids.indexOf( - ccPair.cc_pair_id - ); - let isSelected = ind !== -1; - return ( -
{ - if (isSelected) { - arrayHelpers.remove(ind); - } else { - arrayHelpers.push(ccPair.cc_pair_id); - } - }} - > -
- -
+

+ These are the connectors available to{" "} + {userGroups && userGroups.length > 1 + ? "the selected group" + : "the group you curate"} + : +

+

+ All documents indexed by these selected connectors will be a + part of this document set. +

+ { + // 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 ( +
+ {visibleCcPairs.map((ccPair) => { + const ind = props.values.cc_pair_ids.indexOf( + ccPair.cc_pair_id + ); + let isSelected = ind !== -1; + return ( +
{ + if (isSelected) { + arrayHelpers.remove(ind); + } else { + arrayHelpers.push(ccPair.cc_pair_id); + } + }} + > +
+ +
+
+ ); + })} +
+ ); + }} + /> + + { + // 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 ? ( + <> + +

+ These connectors are not available to the{" "} + {userGroups && userGroups.length > 1 + ? `group${props.values.groups.length > 1 ? "s" : ""} you have selected` + : "group you curate"} + : +

+

+ Only connectors that are directly assigned to the group + you are trying to add the document set to will be + available. +

+
+ {nonVisibleCcPairs.map((ccPair) => ( +
+
+ +
+
+ ))}
- ); - })} -
- )} - /> + + ) : null; + }} + /> - {isPaidEnterpriseFeaturesEnabled && - userGroups && - userGroups.length > 0 && ( - - )} -
- -
- - )} +
+ +
+ + ); + }}
); diff --git a/web/src/app/admin/documents/sets/page.tsx b/web/src/app/admin/documents/sets/page.tsx index 2c12125ba8ea..718b81ab0b68 100644 --- a/web/src/app/admin/documents/sets/page.tsx +++ b/web/src/app/admin/documents/sets/page.tsx @@ -111,23 +111,6 @@ const DocumentSetTable = ({ setPopup, }: DocumentFeedbackTableProps) => { const [page, setPage] = useState(1); - const [currentUser, setCurrentUser] = useState(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) => { diff --git a/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx b/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx index 2c81e4d53e80..ce319df7a8ad 100644 --- a/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx +++ b/web/src/app/admin/indexing/status/CCPairIndexingStatusTable.tsx @@ -443,6 +443,7 @@ export function CCPairIndexingStatusTable({ error_msg: "", deletion_attempt: null, is_deletable: true, + groups: [], // Add this line }} isEditable={false} /> diff --git a/web/src/app/admin/indexing/status/page.tsx b/web/src/app/admin/indexing/status/page.tsx index cf55df314362..f5d64d3ac3ac 100644 --- a/web/src/app/admin/indexing/status/page.tsx +++ b/web/src/app/admin/indexing/status/page.tsx @@ -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[]>( - "/api/manage/admin/connector/indexing-status", - errorHandlingFetcher, - { refreshInterval: 10000 } // 10 seconds - ); + } = useConnectorCredentialIndexingStatus(); const { data: editableIndexAttemptData, isLoading: editableIndexAttemptIsLoading, error: editableIndexAttemptError, - } = useSWR[]>( - "/api/manage/admin/connector/indexing-status?get_editable=true", - errorHandlingFetcher, - { refreshInterval: 10000 } // 10 seconds - ); + } = useConnectorCredentialIndexingStatus(undefined, true); if (indexAttemptIsLoading || editableIndexAttemptIsLoading) { return ; diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 8805c4657383..207825e922a7 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -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({ setSettingsToggled(false)} /> diff --git a/web/src/app/chat/modal/SetDefaultModelModal.tsx b/web/src/app/chat/modal/SetDefaultModelModal.tsx index ee56fdd4ce87..e5536f19c613 100644 --- a/web/src/app/chat/modal/SetDefaultModelModal.tsx +++ b/web/src/app/chat/modal/SetDefaultModelModal.tsx @@ -14,11 +14,13 @@ export function SetDefaultModelModal({ onClose, setLlmOverride, defaultModel, + refreshUser, }: { llmProviders: LLMProviderDescriptor[]; setLlmOverride: Dispatch>; onClose: () => void; defaultModel: string | null; + refreshUser: () => void; }) { const { popup, setPopup } = usePopup(); const containerRef = useRef(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"); diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx index f5dce4617d1e..e391b79dae7e 100644 --- a/web/src/app/chat/page.tsx +++ b/web/src/app/chat/page.tsx @@ -48,7 +48,6 @@ export default async function Page({ )} ); } else { - return
{user.role}
; + return
{localRole}
; } }; @@ -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(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 = ({ Email Role - {isAdmin && ( - -
Remove User
-
- )} + +
Remove User
+
- {userGroup.users.map((user) => { + {userGroup.users.map((groupMember) => { return ( - + - {user.email} + {groupMember.email}
- {isAdmin && ( + {(isAdmin || + !userGroup.curator_ids.includes( + groupMember.id + )) && ( { 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 = ({ )}
- {isAdmin && ( - - )} + {addMemberFormVisible && ( - {isAdmin && ( - - )} + {addConnectorFormVisible && ( { const { popup, setPopup } = usePopup(); @@ -34,23 +35,10 @@ const Main = () => { error: usersError, } = useUsers(); - const [currentUser, setCurrentUser] = useState(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 ; diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index d1f9f61ccb49..8219dbb9a618 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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" : "" }`} > - - {children} - + + + {children} + +
diff --git a/web/src/components/IsPublicGroupSelector.tsx b/web/src/components/IsPublicGroupSelector.tsx index c08ec0b69e13..1409beedf8b2 100644 --- a/web/src/components/IsPublicGroupSelector.tsx +++ b/web/src/components/IsPublicGroupSelector.tsx @@ -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 = ({ enforceGroupSelection?: boolean; }) => { const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups(); - const [isLoading, setIsLoading] = useState(true); - const [currentUser, setCurrentUser] = useState(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
Loading...
; + } + + if (shouldHideContent && enforceGroupSelection) { + return ( + <> + {userGroups && ( +
+ This {objectName} will be assigned to group{" "} + {userGroups[0].name}. +
+ )} + + ); } return ( @@ -70,17 +73,19 @@ export const IsPublicGroupSelector = ({ disabled={!isAdmin} subtext={ - If set, then {objectName}s indexed by this {objectName} will be - visible to all users. 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{" "} + all users. If turned off, then only users who explicitly + have been given access to this {objectName} (e.g. through a User + Group) will have access. } /> )} - {(!formikProps.values.is_public || !isAdmin) && ( + {(!formikProps.values.is_public || + !isAdmin || + formikProps.values.groups.length > 0) && ( <>
diff --git a/web/src/components/context/ChatContext.tsx b/web/src/components/context/ChatContext.tsx index 76cdf8adc2ae..9ab0c1e2eac9 100644 --- a/web/src/components/context/ChatContext.tsx +++ b/web/src/components/context/ChatContext.tsx @@ -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[]; diff --git a/web/src/components/credentials/actions/CreateCredential.tsx b/web/src/components/credentials/actions/CreateCredential.tsx index c489ca5ce298..d9bd6cb19a75 100644 --- a/web/src/components/credentials/actions/CreateCredential.tsx +++ b/web/src/components/credentials/actions/CreateCredential.tsx @@ -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(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 ; } - 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 && (
{isPaidEnterpriseFeaturesEnabled && ( diff --git a/web/src/components/user/UserProvider.tsx b/web/src/components/user/UserProvider.tsx new file mode 100644 index 000000000000..d2cf0f2c94f6 --- /dev/null +++ b/web/src/components/user/UserProvider.tsx @@ -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; +} + +const UserContext = createContext(undefined); + +export function UserProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(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 ( + + {children} + + ); +} + +export function useUser() { + const context = useContext(UserContext); + if (context === undefined) { + throw new Error("useUser must be used within a UserProvider"); + } + return context; +} diff --git a/web/src/lib/hooks.ts b/web/src/lib/hooks.ts index 5bbc9c5f4625..3a7b398101a1 100644 --- a/web/src/lib/hooks.ts +++ b/web/src/lib/hooks.ts @@ -65,18 +65,20 @@ export const useObjectState = ( 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[]>( - INDEXING_STATUS_URL, + url, errorHandlingFetcher, { refreshInterval: refreshInterval } ); return { ...swrResponse, - refreshIndexingStatus: () => mutate(INDEXING_STATUS_URL), + refreshIndexingStatus: () => mutate(url), }; }; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b6deb65f4f63..93370c1daa54 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -82,6 +82,7 @@ export interface ConnectorIndexingStatus< credential: Credential; public_doc: boolean; owner: string; + groups: number[]; last_finished_status: ValidStatuses | null; last_status: ValidStatuses | null; last_success: string | null; diff --git a/web/tailwind-themes/tailwind.config.js b/web/tailwind-themes/tailwind.config.js index 208143f353dc..82021789ab9b 100644 --- a/web/tailwind-themes/tailwind.config.js +++ b/web/tailwind-themes/tailwind.config.js @@ -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