Enable non-admin credentials + add page for google drive (#84)

* Enable non-admin credentials + add page for google drive

* Return one indexing status entry for each connector / credential pair

* Remove some logs

* Small fixes

* Sort index status by source
This commit is contained in:
Chris Weaver
2023-06-04 11:26:50 -07:00
committed by GitHub
parent 8c9b3079aa
commit 7cc64efc3a
33 changed files with 1072 additions and 644 deletions

View File

@@ -61,9 +61,7 @@ def verify_csrf(credential_id: int, state: str) -> None:
) )
def get_auth_url( def get_auth_url(credential_id: int) -> str:
credential_id: int,
) -> str:
creds_str = str(get_dynamic_config_store().load(GOOGLE_DRIVE_CRED_KEY)) creds_str = str(get_dynamic_config_store().load(GOOGLE_DRIVE_CRED_KEY))
credential_json = json.loads(creds_str) credential_json = json.loads(creds_str)
flow = InstalledAppFlow.from_client_config( flow = InstalledAppFlow.from_client_config(

View File

@@ -47,7 +47,6 @@ def create_collection(
raise RuntimeError("Could not create Qdrant collection") raise RuntimeError("Could not create Qdrant collection")
@log_function_time()
def get_document_whitelists( def get_document_whitelists(
doc_chunk_id: str, collection_name: str, q_client: QdrantClient doc_chunk_id: str, collection_name: str, q_client: QdrantClient
) -> tuple[int, list[str], list[str]]: ) -> tuple[int, list[str], list[str]]:
@@ -66,7 +65,6 @@ def get_document_whitelists(
return len(results), payload[ALLOWED_USERS], payload[ALLOWED_GROUPS] return len(results), payload[ALLOWED_USERS], payload[ALLOWED_GROUPS]
@log_function_time()
def delete_doc_chunks( def delete_doc_chunks(
document_id: str, collection_name: str, q_client: QdrantClient document_id: str, collection_name: str, q_client: QdrantClient
) -> None: ) -> None:

View File

@@ -272,10 +272,12 @@ def fetch_latest_index_attempts_by_status(
subquery = ( subquery = (
db_session.query( db_session.query(
IndexAttempt.connector_id, IndexAttempt.connector_id,
IndexAttempt.credential_id,
IndexAttempt.status, IndexAttempt.status,
func.max(IndexAttempt.time_updated).label("time_updated"), func.max(IndexAttempt.time_updated).label("time_updated"),
) )
.group_by(IndexAttempt.connector_id) .group_by(IndexAttempt.connector_id)
.group_by(IndexAttempt.credential_id)
.group_by(IndexAttempt.status) .group_by(IndexAttempt.status)
.subquery() .subquery()
) )
@@ -286,6 +288,7 @@ def fetch_latest_index_attempts_by_status(
alias, alias,
and_( and_(
IndexAttempt.connector_id == alias.connector_id, IndexAttempt.connector_id == alias.connector_id,
IndexAttempt.credential_id == alias.credential_id,
IndexAttempt.status == alias.status, IndexAttempt.status == alias.status,
IndexAttempt.time_updated == alias.time_updated, IndexAttempt.time_updated == alias.time_updated,
), ),

View File

@@ -96,9 +96,6 @@ def update_credential_json(
user: User, user: User,
db_session: Session, db_session: Session,
) -> Credential | None: ) -> Credential | None:
logger.info("HIIII")
logger.info(credential_id)
logger.info(credential_json)
credential = fetch_credential_by_id(credential_id, user, db_session) credential = fetch_credential_by_id(credential_id, user, db_session)
if credential is None: if credential is None:
return None return None

View File

@@ -12,9 +12,9 @@ from danswer.configs.app_configs import SECRET
from danswer.configs.app_configs import WEB_DOMAIN from danswer.configs.app_configs import WEB_DOMAIN
from danswer.datastores.qdrant.indexing import list_collections from danswer.datastores.qdrant.indexing import list_collections
from danswer.db.credentials import create_initial_public_credential from danswer.db.credentials import create_initial_public_credential
from danswer.server.admin import router as admin_router
from danswer.server.event_loading import router as event_processing_router from danswer.server.event_loading import router as event_processing_router
from danswer.server.health import router as health_router from danswer.server.health import router as health_router
from danswer.server.manage import router as admin_router
from danswer.server.search_backend import router as backend_router from danswer.server.search_backend import router as backend_router
from danswer.utils.logging import setup_logger from danswer.utils.logging import setup_logger
from fastapi import FastAPI from fastapi import FastAPI

View File

@@ -2,6 +2,7 @@ from collections import defaultdict
from typing import cast from typing import cast
from danswer.auth.users import current_admin_user from danswer.auth.users import current_admin_user
from danswer.auth.users import current_user
from danswer.configs.app_configs import MASK_CREDENTIAL_PREFIX from danswer.configs.app_configs import MASK_CREDENTIAL_PREFIX
from danswer.configs.constants import DocumentSource from danswer.configs.constants import DocumentSource
from danswer.configs.constants import OPENAI_API_KEY_STORAGE_KEY from danswer.configs.constants import OPENAI_API_KEY_STORAGE_KEY
@@ -32,6 +33,7 @@ from danswer.db.credentials import mask_credential_dict
from danswer.db.credentials import update_credential from danswer.db.credentials import update_credential
from danswer.db.engine import get_session from danswer.db.engine import get_session
from danswer.db.index_attempt import create_index_attempt from danswer.db.index_attempt import create_index_attempt
from danswer.db.models import Connector
from danswer.db.models import IndexAttempt from danswer.db.models import IndexAttempt
from danswer.db.models import IndexingStatus from danswer.db.models import IndexingStatus
from danswer.db.models import User from danswer.db.models import User
@@ -61,12 +63,16 @@ from fastapi import Request
from fastapi import Response from fastapi import Response
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
router = APIRouter(prefix="/admin") router = APIRouter(prefix="/manage")
logger = setup_logger() logger = setup_logger()
_GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME = "google_drive_credential_id"
@router.get("/connector/google-drive/app-credential") """Admin only API endpoints"""
@router.get("/admin/connector/google-drive/app-credential")
def check_google_app_credentials_exist( def check_google_app_credentials_exist(
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
) -> dict[str, str]: ) -> dict[str, str]:
@@ -76,7 +82,7 @@ def check_google_app_credentials_exist(
raise HTTPException(status_code=404, detail="Google App Credentials not found") raise HTTPException(status_code=404, detail="Google App Credentials not found")
@router.put("/connector/google-drive/app-credential") @router.put("/admin/connector/google-drive/app-credential")
def update_google_app_credentials( def update_google_app_credentials(
app_credentials: GoogleAppCredentials, _: User = Depends(current_admin_user) app_credentials: GoogleAppCredentials, _: User = Depends(current_admin_user)
) -> StatusResponse: ) -> StatusResponse:
@@ -90,7 +96,7 @@ def update_google_app_credentials(
) )
@router.get("/connector/google-drive/check-auth/{credential_id}") @router.get("/admin/connector/google-drive/check-auth/{credential_id}")
def check_drive_tokens( def check_drive_tokens(
credential_id: int, credential_id: int,
user: User = Depends(current_admin_user), user: User = Depends(current_admin_user),
@@ -109,11 +115,8 @@ def check_drive_tokens(
return AuthStatus(authenticated=True) return AuthStatus(authenticated=True)
_GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME = "google_drive_credential_id" @router.get("/admin/connector/google-drive/authorize/{credential_id}")
def admin_google_drive_auth(
@router.get("/connector/google-drive/authorize/{credential_id}", response_model=AuthUrl)
def google_drive_auth(
response: Response, credential_id: str, _: User = Depends(current_admin_user) response: Response, credential_id: str, _: User = Depends(current_admin_user)
) -> AuthUrl: ) -> AuthUrl:
# set a cookie that we can read in the callback (used for `verify_csrf`) # set a cookie that we can read in the callback (used for `verify_csrf`)
@@ -123,35 +126,10 @@ def google_drive_auth(
httponly=True, httponly=True,
max_age=600, max_age=600,
) )
return AuthUrl(auth_url=get_auth_url(int(credential_id))) return AuthUrl(auth_url=get_auth_url(credential_id=int(credential_id)))
@router.get("/connector/google-drive/callback") @router.get("/admin/latest-index-attempt")
def google_drive_callback(
request: Request,
callback: GDriveCallback = Depends(),
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> StatusResponse:
credential_id_cookie = request.cookies.get(_GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME)
if credential_id_cookie is None or not credential_id_cookie.isdigit():
raise HTTPException(
status_code=401, detail="Request did not pass CSRF verification."
)
credential_id = int(credential_id_cookie)
verify_csrf(credential_id, callback.state)
if (
update_credential_access_tokens(callback.code, credential_id, user, db_session)
is None
):
raise HTTPException(
status_code=500, detail="Unable to fetch Google Drive access tokens"
)
return StatusResponse(success=True, message="Updated Google Drive access tokens")
@router.get("/latest-index-attempt", response_model=list[IndexAttemptSnapshot])
def list_all_index_attempts( def list_all_index_attempts(
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
@@ -173,7 +151,7 @@ def list_all_index_attempts(
] ]
@router.get("/latest-index-attempt/{source}", response_model=list[IndexAttemptSnapshot]) @router.get("/admin/latest-index-attempt/{source}")
def list_index_attempts( def list_index_attempts(
source: DocumentSource, source: DocumentSource,
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
@@ -196,38 +174,39 @@ def list_index_attempts(
] ]
@router.get("/connector", response_model=list[ConnectorSnapshot]) @router.get("/admin/connector/indexing-status")
def get_connectors(
_: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> list[ConnectorSnapshot]:
connectors = fetch_connectors(db_session)
return [
ConnectorSnapshot.from_connector_db_model(connector) for connector in connectors
]
@router.get("/connector/indexing-status")
def get_connector_indexing_status( def get_connector_indexing_status(
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
) -> list[ConnectorIndexingStatus]: ) -> list[ConnectorIndexingStatus]:
connector_id_to_connector = { connector_id_to_connector: dict[int, Connector] = {
connector.id: connector for connector in fetch_connectors(db_session) connector.id: connector for connector in fetch_connectors(db_session)
} }
index_attempts = fetch_latest_index_attempts_by_status(db_session) index_attempts = fetch_latest_index_attempts_by_status(db_session)
connector_to_index_attempts: dict[int, list[IndexAttempt]] = defaultdict(list) connector_credential_pair_to_index_attempts: dict[
tuple[int, int], list[IndexAttempt]
] = defaultdict(list)
for index_attempt in index_attempts: for index_attempt in index_attempts:
# don't consider index attempts where the connector has been deleted # don't consider index attempts where the connector has been deleted
if index_attempt.connector_id: # or the credential has been deleted
connector_to_index_attempts[index_attempt.connector_id].append( if index_attempt.connector_id and index_attempt.credential_id:
index_attempt connector_credential_pair_to_index_attempts[
) (index_attempt.connector_id, index_attempt.credential_id)
].append(index_attempt)
indexing_statuses: list[ConnectorIndexingStatus] = [] indexing_statuses: list[ConnectorIndexingStatus] = []
for connector_id, index_attempts in connector_to_index_attempts.items(): for (
connector_id,
credential_id,
), index_attempts in connector_credential_pair_to_index_attempts.items():
# NOTE: index_attempts is guaranteed to be length > 0 # NOTE: index_attempts is guaranteed to be length > 0
connector = connector_id_to_connector[connector_id] connector = connector_id_to_connector[connector_id]
credential = [
credential_association.credential
for credential_association in connector.credentials
if credential_association.credential_id == credential_id
][0]
index_attempts_sorted = sorted( index_attempts_sorted = sorted(
index_attempts, key=lambda x: x.time_updated, reverse=True index_attempts, key=lambda x: x.time_updated, reverse=True
) )
@@ -239,6 +218,8 @@ def get_connector_indexing_status(
indexing_statuses.append( indexing_statuses.append(
ConnectorIndexingStatus( ConnectorIndexingStatus(
connector=ConnectorSnapshot.from_connector_db_model(connector), connector=ConnectorSnapshot.from_connector_db_model(connector),
public_doc=credential.public_doc,
owner=credential.user.email if credential.user else "",
last_status=index_attempts_sorted[0].status, last_status=index_attempts_sorted[0].status,
last_success=successful_index_attempts_sorted[0].time_updated last_success=successful_index_attempts_sorted[0].time_updated
if successful_index_attempts_sorted if successful_index_attempts_sorted
@@ -250,53 +231,28 @@ def get_connector_indexing_status(
), ),
) )
# add in the connector that haven't started indexing yet # add in the connectors that haven't started indexing yet
for connector in connector_id_to_connector.values(): for connector in connector_id_to_connector.values():
if connector.id not in connector_to_index_attempts: for credential_association in connector.credentials:
indexing_statuses.append( if (
ConnectorIndexingStatus( connector.id,
connector=ConnectorSnapshot.from_connector_db_model(connector), credential_association.credential_id,
last_status=IndexingStatus.NOT_STARTED, ) not in connector_credential_pair_to_index_attempts:
last_success=None, indexing_statuses.append(
docs_indexed=0, ConnectorIndexingStatus(
), connector=ConnectorSnapshot.from_connector_db_model(connector),
) public_doc=credential_association.credential.public_doc,
owner=credential.user.email if credential.user else "",
last_status=IndexingStatus.NOT_STARTED,
last_success=None,
docs_indexed=0,
),
)
return indexing_statuses return indexing_statuses
@router.get( @router.post("/admin/connector")
"/connector/{connector_id}",
response_model=ConnectorSnapshot | StatusResponse[int],
)
def get_connector_by_id(
connector_id: int,
_: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> ConnectorSnapshot | StatusResponse[int]:
connector = fetch_connector_by_id(connector_id, db_session)
if connector is None:
raise HTTPException(
status_code=404, detail=f"Connector {connector_id} does not exist"
)
return ConnectorSnapshot(
id=connector.id,
name=connector.name,
source=connector.source,
input_type=connector.input_type,
connector_specific_config=connector.connector_specific_config,
refresh_freq=connector.refresh_freq,
credential_ids=[
association.credential.id for association in connector.credentials
],
time_created=connector.time_created,
time_updated=connector.time_updated,
disabled=connector.disabled,
)
@router.post("/connector", response_model=ObjectCreationIdResponse)
def create_connector_from_model( def create_connector_from_model(
connector_info: ConnectorBase, connector_info: ConnectorBase,
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
@@ -308,10 +264,7 @@ def create_connector_from_model(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.patch( @router.patch("/admin/connector/{connector_id}")
"/connector/{connector_id}",
response_model=ConnectorSnapshot | StatusResponse[int],
)
def update_connector_from_model( def update_connector_from_model(
connector_id: int, connector_id: int,
connector_data: ConnectorBase, connector_data: ConnectorBase,
@@ -340,7 +293,7 @@ def update_connector_from_model(
) )
@router.delete("/connector/{connector_id}", response_model=StatusResponse[int]) @router.delete("/admin/connector/{connector_id}", response_model=StatusResponse[int])
def delete_connector_by_id( def delete_connector_by_id(
connector_id: int, connector_id: int,
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
@@ -349,128 +302,7 @@ def delete_connector_by_id(
return delete_connector(connector_id, db_session) return delete_connector(connector_id, db_session)
@router.get("/credential", response_model=list[CredentialSnapshot]) @router.post("/admin/connector/run-once")
def get_credentials(
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> list[CredentialSnapshot]:
credentials = fetch_credentials(user, db_session)
return [
CredentialSnapshot(
id=credential.id,
credential_json=mask_credential_dict(credential.credential_json)
if MASK_CREDENTIAL_PREFIX
else credential.credential_json,
user_id=credential.user_id,
public_doc=credential.public_doc,
time_created=credential.time_created,
time_updated=credential.time_updated,
)
for credential in credentials
]
@router.get(
"/credential/{credential_id}",
response_model=CredentialSnapshot | StatusResponse[int],
)
def get_credential_by_id(
credential_id: int,
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> CredentialSnapshot | StatusResponse[int]:
credential = fetch_credential_by_id(credential_id, user, db_session)
if credential is None:
raise HTTPException(
status_code=401,
detail=f"Credential {credential_id} does not exist or does not belong to user",
)
return CredentialSnapshot(
id=credential.id,
credential_json=mask_credential_dict(credential.credential_json)
if MASK_CREDENTIAL_PREFIX
else credential.credential_json,
user_id=credential.user_id,
public_doc=credential.public_doc,
time_created=credential.time_created,
time_updated=credential.time_updated,
)
@router.post("/credential", response_model=ObjectCreationIdResponse)
def create_credential_from_model(
connector_info: CredentialBase,
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> ObjectCreationIdResponse:
return create_credential(connector_info, user, db_session)
@router.patch(
"/credential/{credential_id}",
response_model=CredentialSnapshot | StatusResponse[int],
)
def update_credential_from_model(
credential_id: int,
credential_data: CredentialBase,
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> CredentialSnapshot | StatusResponse[int]:
updated_credential = update_credential(
credential_id, credential_data, user, db_session
)
if updated_credential is None:
raise HTTPException(
status_code=401,
detail=f"Credential {credential_id} does not exist or does not belong to user",
)
return CredentialSnapshot(
id=updated_credential.id,
credential_json=updated_credential.credential_json,
user_id=updated_credential.user_id,
public_doc=updated_credential.public_doc,
time_created=updated_credential.time_created,
time_updated=updated_credential.time_updated,
)
@router.delete("/credential/{credential_id}", response_model=StatusResponse[int])
def delete_credential_by_id(
credential_id: int,
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> StatusResponse:
delete_credential(credential_id, user, db_session)
return StatusResponse(
success=True, message="Credential deleted successfully", data=credential_id
)
@router.put("/connector/{connector_id}/credential/{credential_id}")
def associate_credential_to_connector(
connector_id: int,
credential_id: int,
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> StatusResponse[int]:
return add_credential_to_connector(connector_id, credential_id, user, db_session)
@router.delete("/connector/{connector_id}/credential/{credential_id}")
def dissociate_credential_from_connector(
connector_id: int,
credential_id: int,
user: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> StatusResponse[int]:
return remove_credential_from_connector(
connector_id, credential_id, user, db_session
)
@router.post("/connector/run-once")
def connector_run_once( def connector_run_once(
run_info: RunConnectorRequest, run_info: RunConnectorRequest,
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
@@ -516,7 +348,7 @@ def connector_run_once(
) )
@router.head("/openai-api-key/validate") @router.head("/admin/openai-api-key/validate")
def validate_existing_openai_api_key( def validate_existing_openai_api_key(
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
) -> None: ) -> None:
@@ -532,7 +364,7 @@ def validate_existing_openai_api_key(
raise HTTPException(status_code=400, detail="Invalid API key provided") raise HTTPException(status_code=400, detail="Invalid API key provided")
@router.get("/openai-api-key", response_model=ApiKey) @router.get("/admin/openai-api-key", response_model=ApiKey)
def get_openai_api_key_from_dynamic_config_store( def get_openai_api_key_from_dynamic_config_store(
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
) -> ApiKey: ) -> ApiKey:
@@ -550,7 +382,7 @@ def get_openai_api_key_from_dynamic_config_store(
raise HTTPException(status_code=404, detail="Key not found") raise HTTPException(status_code=404, detail="Key not found")
@router.post("/openai-api-key") @router.put("/admin/openai-api-key")
def store_openai_api_key( def store_openai_api_key(
request: ApiKey, request: ApiKey,
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
@@ -564,8 +396,204 @@ def store_openai_api_key(
raise HTTPException(400, str(e)) raise HTTPException(400, str(e))
@router.delete("/openai-api-key") @router.delete("/admin/openai-api-key")
def delete_openai_api_key( def delete_openai_api_key(
_: User = Depends(current_admin_user), _: User = Depends(current_admin_user),
) -> None: ) -> None:
get_dynamic_config_store().delete(OPENAI_API_KEY_STORAGE_KEY) get_dynamic_config_store().delete(OPENAI_API_KEY_STORAGE_KEY)
"""Endpoints for all!"""
@router.get("/connector/google-drive/authorize/{credential_id}")
def google_drive_auth(
response: Response, credential_id: str, _: User = Depends(current_user)
) -> AuthUrl:
# set a cookie that we can read in the callback (used for `verify_csrf`)
response.set_cookie(
key=_GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME,
value=credential_id,
httponly=True,
max_age=600,
)
return AuthUrl(auth_url=get_auth_url(int(credential_id)))
@router.get("/connector/google-drive/callback")
def google_drive_callback(
request: Request,
callback: GDriveCallback = Depends(),
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse:
credential_id_cookie = request.cookies.get(_GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME)
if credential_id_cookie is None or not credential_id_cookie.isdigit():
raise HTTPException(
status_code=401, detail="Request did not pass CSRF verification."
)
credential_id = int(credential_id_cookie)
verify_csrf(credential_id, callback.state)
if (
update_credential_access_tokens(callback.code, credential_id, user, db_session)
is None
):
raise HTTPException(
status_code=500, detail="Unable to fetch Google Drive access tokens"
)
return StatusResponse(success=True, message="Updated Google Drive access tokens")
@router.get("/connector")
def get_connectors(
_: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> list[ConnectorSnapshot]:
connectors = fetch_connectors(db_session)
return [
ConnectorSnapshot.from_connector_db_model(connector) for connector in connectors
]
@router.get("/connector/{connector_id}")
def get_connector_by_id(
connector_id: int,
_: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> ConnectorSnapshot | StatusResponse[int]:
connector = fetch_connector_by_id(connector_id, db_session)
if connector is None:
raise HTTPException(
status_code=404, detail=f"Connector {connector_id} does not exist"
)
return ConnectorSnapshot(
id=connector.id,
name=connector.name,
source=connector.source,
input_type=connector.input_type,
connector_specific_config=connector.connector_specific_config,
refresh_freq=connector.refresh_freq,
credential_ids=[
association.credential.id for association in connector.credentials
],
time_created=connector.time_created,
time_updated=connector.time_updated,
disabled=connector.disabled,
)
@router.get("/credential")
def get_credentials(
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> list[CredentialSnapshot]:
credentials = fetch_credentials(user, db_session)
return [
CredentialSnapshot(
id=credential.id,
credential_json=mask_credential_dict(credential.credential_json)
if MASK_CREDENTIAL_PREFIX
else credential.credential_json,
user_id=credential.user_id,
public_doc=credential.public_doc,
time_created=credential.time_created,
time_updated=credential.time_updated,
)
for credential in credentials
]
@router.get("/credential/{credential_id}")
def get_credential_by_id(
credential_id: int,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> CredentialSnapshot | StatusResponse[int]:
credential = fetch_credential_by_id(credential_id, user, db_session)
if credential is None:
raise HTTPException(
status_code=401,
detail=f"Credential {credential_id} does not exist or does not belong to user",
)
return CredentialSnapshot(
id=credential.id,
credential_json=mask_credential_dict(credential.credential_json)
if MASK_CREDENTIAL_PREFIX
else credential.credential_json,
user_id=credential.user_id,
public_doc=credential.public_doc,
time_created=credential.time_created,
time_updated=credential.time_updated,
)
@router.post("/credential")
def create_credential_from_model(
connector_info: CredentialBase,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> ObjectCreationIdResponse:
return create_credential(connector_info, user, db_session)
@router.patch("/credential/{credential_id}")
def update_credential_from_model(
credential_id: int,
credential_data: CredentialBase,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> CredentialSnapshot | StatusResponse[int]:
updated_credential = update_credential(
credential_id, credential_data, user, db_session
)
if updated_credential is None:
raise HTTPException(
status_code=401,
detail=f"Credential {credential_id} does not exist or does not belong to user",
)
return CredentialSnapshot(
id=updated_credential.id,
credential_json=updated_credential.credential_json,
user_id=updated_credential.user_id,
public_doc=updated_credential.public_doc,
time_created=updated_credential.time_created,
time_updated=updated_credential.time_updated,
)
@router.delete("/credential/{credential_id}")
def delete_credential_by_id(
credential_id: int,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse:
delete_credential(credential_id, user, db_session)
return StatusResponse(
success=True, message="Credential deleted successfully", data=credential_id
)
@router.put("/connector/{connector_id}/credential/{credential_id}")
def associate_credential_to_connector(
connector_id: int,
credential_id: int,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse[int]:
return add_credential_to_connector(connector_id, credential_id, user, db_session)
@router.delete("/connector/{connector_id}/credential/{credential_id}")
def dissociate_credential_from_connector(
connector_id: int,
credential_id: int,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> StatusResponse[int]:
return remove_credential_from_connector(
connector_id, credential_id, user, db_session
)

View File

@@ -137,6 +137,8 @@ class ConnectorIndexingStatus(BaseModel):
"""Represents the latest indexing status of a connector""" """Represents the latest indexing status of a connector"""
connector: ConnectorSnapshot connector: ConnectorSnapshot
owner: str
public_doc: bool
last_status: IndexingStatus last_status: IndexingStatus
last_success: datetime | None last_success: datetime | None
docs_indexed: int docs_indexed: int

25
web/package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@phosphor-icons/react": "^2.0.8", "@phosphor-icons/react": "^2.0.8",
"@types/js-cookie": "^3.0.3",
"@types/node": "18.15.11", "@types/node": "18.15.11",
"@types/react": "18.0.32", "@types/react": "18.0.32",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
@@ -16,6 +17,7 @@
"eslint": "8.37.0", "eslint": "8.37.0",
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"formik": "^2.2.9", "formik": "^2.2.9",
"js-cookie": "^3.0.5",
"next": "^13.2.4", "next": "^13.2.4",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"react": "^18.2.0", "react": "^18.2.0",
@@ -392,6 +394,11 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/js-cookie": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
"integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww=="
},
"node_modules/@types/json5": { "node_modules/@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -2483,6 +2490,14 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-sdsl": { "node_modules/js-sdsl": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
@@ -4324,6 +4339,11 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"@types/js-cookie": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
"integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww=="
},
"@types/json5": { "@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -5791,6 +5811,11 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==" "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg=="
}, },
"js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
},
"js-sdsl": { "js-sdsl": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/react": "^2.0.8", "@phosphor-icons/react": "^2.0.8",
"@types/js-cookie": "^3.0.3",
"@types/node": "18.15.11", "@types/node": "18.15.11",
"@types/react": "18.0.32", "@types/react": "18.0.32",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
@@ -17,6 +18,7 @@
"eslint": "8.37.0", "eslint": "8.37.0",
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"formik": "^2.2.9", "formik": "^2.2.9",
"js-cookie": "^3.0.5",
"next": "^13.2.4", "next": "^13.2.4",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -25,7 +25,7 @@ const Main = () => {
isLoading: isConnectorIndexingStatusesLoading, isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError, error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any>[]>( } = useSWR<ConnectorIndexingStatus<any>[]>(
"/api/admin/connector/indexing-status", "/api/manage/admin/connector/indexing-status",
fetcher fetcher
); );
const { const {
@@ -34,7 +34,7 @@ const Main = () => {
isValidating: isCredentialsValidating, isValidating: isCredentialsValidating,
error: isCredentialsError, error: isCredentialsError,
} = useSWR<Credential<ConfluenceCredentialJson>[]>( } = useSWR<Credential<ConfluenceCredentialJson>[]>(
"/api/admin/credential", "/api/manage/credential",
fetcher fetcher
); );
@@ -85,7 +85,7 @@ const Main = () => {
className="ml-1 hover:bg-gray-700 rounded-full p-1" className="ml-1 hover:bg-gray-700 rounded-full p-1"
onClick={async () => { onClick={async () => {
await deleteCredential(confluenceCredential.id); await deleteCredential(confluenceCredential.id);
mutate("/api/admin/credential"); mutate("/api/manage/credential");
}} }}
> >
<TrashIcon /> <TrashIcon />
@@ -131,7 +131,7 @@ const Main = () => {
}} }}
onSubmit={(isSuccess) => { onSubmit={(isSuccess) => {
if (isSuccess) { if (isSuccess) {
mutate("/api/admin/credential"); mutate("/api/manage/credential");
} }
}} }}
/> />
@@ -181,7 +181,7 @@ const Main = () => {
onCredentialLink={async (connectorId) => { onCredentialLink={async (connectorId) => {
if (confluenceCredential) { if (confluenceCredential) {
await linkCredential(connectorId, confluenceCredential.id); await linkCredential(connectorId, confluenceCredential.id);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
specialColumns={[ specialColumns={[
@@ -198,7 +198,9 @@ const Main = () => {
), ),
}, },
]} ]}
onUpdate={() => mutate("/api/admin/connector/indexing-status")} onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
/> />
</div> </div>
</> </>
@@ -229,7 +231,7 @@ const Main = () => {
onSubmit={async (isSuccess, responseJson) => { onSubmit={async (isSuccess, responseJson) => {
if (isSuccess && responseJson) { if (isSuccess && responseJson) {
await linkCredential(responseJson.id, confluenceCredential.id); await linkCredential(responseJson.id, confluenceCredential.id);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
/> />

View File

@@ -25,7 +25,7 @@ const Main = () => {
isLoading: isConnectorIndexingStatusesLoading, isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError, error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any>[]>( } = useSWR<ConnectorIndexingStatus<any>[]>(
"/api/admin/connector/indexing-status", "/api/manage/admin/connector/indexing-status",
fetcher fetcher
); );
@@ -35,7 +35,7 @@ const Main = () => {
isValidating: isCredentialsValidating, isValidating: isCredentialsValidating,
error: isCredentialsError, error: isCredentialsError,
} = useSWR<Credential<GithubCredentialJson>[]>( } = useSWR<Credential<GithubCredentialJson>[]>(
"/api/admin/credential", "/api/manage/credential",
fetcher fetcher
); );
@@ -81,7 +81,7 @@ const Main = () => {
className="ml-1 hover:bg-gray-700 rounded-full p-1" className="ml-1 hover:bg-gray-700 rounded-full p-1"
onClick={async () => { onClick={async () => {
await deleteCredential(githubCredential.id); await deleteCredential(githubCredential.id);
mutate("/api/admin/credential"); mutate("/api/manage/credential");
}} }}
> >
<TrashIcon /> <TrashIcon />
@@ -121,7 +121,7 @@ const Main = () => {
}} }}
onSubmit={(isSuccess) => { onSubmit={(isSuccess) => {
if (isSuccess) { if (isSuccess) {
mutate("/api/admin/credential"); mutate("/api/manage/credential");
} }
}} }}
/> />
@@ -149,7 +149,7 @@ const Main = () => {
onCredentialLink={async (connectorId) => { onCredentialLink={async (connectorId) => {
if (githubCredential) { if (githubCredential) {
await linkCredential(connectorId, githubCredential.id); await linkCredential(connectorId, githubCredential.id);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
specialColumns={[ specialColumns={[
@@ -160,7 +160,9 @@ const Main = () => {
`${connector.connector_specific_config.repo_owner}/${connector.connector_specific_config.repo_name}`, `${connector.connector_specific_config.repo_owner}/${connector.connector_specific_config.repo_name}`,
}, },
]} ]}
onUpdate={() => mutate("/api/admin/connector/indexing-status")} onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
/> />
</div> </div>
</> </>
@@ -196,7 +198,7 @@ const Main = () => {
onSubmit={async (isSuccess, responseJson) => { onSubmit={async (isSuccess, responseJson) => {
if (isSuccess && responseJson) { if (isSuccess && responseJson) {
await linkCredential(responseJson.id, githubCredential.id); await linkCredential(responseJson.id, githubCredential.id);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
/> />

View File

@@ -2,11 +2,12 @@ import { getDomain } from "@/lib/redirectSS";
import { buildUrl } from "@/lib/utilsSS"; import { buildUrl } from "@/lib/utilsSS";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
export const GET = async (request: NextRequest) => { export const GET = async (request: NextRequest) => {
// Wrapper around the FastAPI endpoint /connectors/google-drive/callback, // Wrapper around the FastAPI endpoint /connectors/google-drive/callback,
// which adds back a redirect to the Google Drive admin page. // which adds back a redirect to the Google Drive admin page.
const url = new URL(buildUrl("/admin/connector/google-drive/callback")); const url = new URL(buildUrl("/manage/connector/google-drive/callback"));
url.search = request.nextUrl.search; url.search = request.nextUrl.search;
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
@@ -26,7 +27,14 @@ export const GET = async (request: NextRequest) => {
return NextResponse.redirect(new URL("/auth/error", getDomain(request))); return NextResponse.redirect(new URL("/auth/error", getDomain(request)));
} }
return NextResponse.redirect( if (
new URL("/admin/connectors/google-drive", getDomain(request)) cookies()
); .get(GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME)
?.value?.toLowerCase() === "true"
) {
return NextResponse.redirect(
new URL("/admin/connectors/google-drive", getDomain(request))
);
}
return NextResponse.redirect(new URL("/user/connectors", getDomain(request)));
}; };

View File

@@ -18,6 +18,10 @@ import {
} from "@/lib/types"; } from "@/lib/types";
import { deleteConnector } from "@/lib/connector"; import { deleteConnector } from "@/lib/connector";
import { StatusRow } from "@/components/admin/connectors/table/ConnectorsTable"; import { StatusRow } from "@/components/admin/connectors/table/ConnectorsTable";
import { setupGoogleDriveOAuth } from "@/lib/googleDrive";
import Cookies from "js-cookie";
import { GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
import { deleteCredential, linkCredential } from "@/lib/credential";
const AppCredentialUpload = ({ const AppCredentialUpload = ({
setPopup, setPopup,
@@ -61,7 +65,7 @@ const AppCredentialUpload = ({
disabled={!appCredentialJsonStr} disabled={!appCredentialJsonStr}
onClick={async () => { onClick={async () => {
const response = await fetch( const response = await fetch(
"/api/admin/connector/google-drive/app-credential", "/api/manage/admin/connector/google-drive/app-credential",
{ {
method: "PUT", method: "PUT",
headers: { headers: {
@@ -98,7 +102,7 @@ const Main = () => {
isLoading: isAppCredentialLoading, isLoading: isAppCredentialLoading,
error: isAppCredentialError, error: isAppCredentialError,
} = useSWR<{ client_id: string }>( } = useSWR<{ client_id: string }>(
"/api/admin/connector/google-drive/app-credential", "/api/manage/admin/connector/google-drive/app-credential",
fetcher fetcher
); );
const { const {
@@ -106,7 +110,7 @@ const Main = () => {
isLoading: isConnectorIndexingStatusesLoading, isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError, error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any>[]>( } = useSWR<ConnectorIndexingStatus<any>[]>(
"/api/admin/connector/indexing-status", "/api/manage/admin/connector/indexing-status",
fetcher fetcher
); );
const { const {
@@ -114,7 +118,7 @@ const Main = () => {
isLoading: isCredentialsLoading, isLoading: isCredentialsLoading,
error: isCredentialsError, error: isCredentialsError,
} = useSWR<Credential<GoogleDriveCredentialJson>[]>( } = useSWR<Credential<GoogleDriveCredentialJson>[]>(
"/api/admin/credential", "/api/manage/credential",
fetcher fetcher
); );
@@ -167,6 +171,10 @@ const Main = () => {
); );
} }
const googleDrivePublicCredential = credentialsData.find(
(credential) =>
credential.credential_json?.google_drive_tokens && credential.public_doc
);
const googleDriveConnectorIndexingStatuses: ConnectorIndexingStatus<{}>[] = const googleDriveConnectorIndexingStatuses: ConnectorIndexingStatus<{}>[] =
connectorIndexingStatuses.filter( connectorIndexingStatuses.filter(
(connectorIndexingStatus) => (connectorIndexingStatus) =>
@@ -174,9 +182,13 @@ const Main = () => {
); );
const googleDriveConnectorIndexingStatus = const googleDriveConnectorIndexingStatus =
googleDriveConnectorIndexingStatuses[0]; googleDriveConnectorIndexingStatuses[0];
const googleDriveCredential = credentialsData.filter(
(credential) => credential.credential_json?.google_drive_tokens const credentialIsLinked =
)[0]; googleDriveConnectorIndexingStatus !== undefined &&
googleDrivePublicCredential !== undefined &&
googleDriveConnectorIndexingStatus.connector.credential_ids.includes(
googleDrivePublicCredential.id
);
return ( return (
<> <>
@@ -198,7 +210,9 @@ const Main = () => {
<div className="mt-2"> <div className="mt-2">
<AppCredentialUpload <AppCredentialUpload
setPopup={(popup) => { setPopup={(popup) => {
mutate("/api/admin/connector/google-drive/app-credential"); mutate(
"/api/manage/admin/connector/google-drive/app-credential"
);
setPopupWithExpiration(popup); setPopupWithExpiration(popup);
}} }}
/> />
@@ -221,7 +235,9 @@ const Main = () => {
</p> </p>
<AppCredentialUpload <AppCredentialUpload
setPopup={(popup) => { setPopup={(popup) => {
mutate("/api/admin/connector/google-drive/app-credential"); mutate(
"/api/manage/admin/connector/google-drive/app-credential"
);
setPopupWithExpiration(popup); setPopupWithExpiration(popup);
}} }}
/> />
@@ -233,175 +249,199 @@ const Main = () => {
Step 2: Authenticate with Danswer Step 2: Authenticate with Danswer
</h2> </h2>
<div className="text-sm mb-4"> <div className="text-sm mb-4">
{googleDriveCredential ? ( {googleDrivePublicCredential ? (
<p> <>
<i>Existing credential already setup!</i> If you want to reset that <p className="mb-2">
credential, click the button below to go through the OAuth flow <i>Existing credential already setup!</i>
again. </p>
</p> <Button
onClick={async () => {
await deleteCredential(googleDrivePublicCredential.id);
setPopup({
message: "Successfully revoked access to Google Drive!",
type: "success",
});
mutate("/api/manage/credential");
}}
>
Revoke Access
</Button>
</>
) : ( ) : (
<> <>
<p> <p className="mb-2">
Next, you must provide credentials via OAuth. This gives us read Next, you must provide credentials via OAuth. This gives us read
access to the docs you have access to in your google drive access to the docs you have access to in your google drive
account. account.
</p> </p>
<Button
onClick={async () => {
const [authUrl, errorMsg] = await setupGoogleDriveOAuth({
isPublic: true,
});
if (authUrl) {
// cookie used by callback to determine where to finally redirect to
Cookies.set(GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME, "true", {
path: "/",
});
router.push(authUrl);
return;
}
setPopup({
message: errorMsg,
type: "error",
});
}}
>
Authenticate with Google Drive
</Button>
</> </>
)} )}
</div> </div>
<Button
onClick={async () => {
const credentialCreationResponse = await fetch(
"/api/admin/credential",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
public_doc: true,
credential_json: {},
}),
}
);
if (!credentialCreationResponse.ok) {
setPopupWithExpiration({
message: `Failed to create credential - ${credentialCreationResponse.status}`,
type: "error",
});
return;
}
const credential =
(await credentialCreationResponse.json()) as Credential<{}>;
const authorizationUrlResponse = await fetch(
`/api/admin/connector/google-drive/authorize/${credential.id}`
);
if (!authorizationUrlResponse.ok) {
setPopupWithExpiration({
message: `Failed to create credential - ${authorizationUrlResponse.status}`,
type: "error",
});
return;
}
const authorizationUrlJson =
(await authorizationUrlResponse.json()) as { auth_url: string };
router.push(authorizationUrlJson.auth_url);
}}
>
Authenticate with Google Drive
</Button>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto"> <h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
Step 3: Start Indexing! Step 3: Start Indexing!
</h2> </h2>
{googleDriveConnectorIndexingStatus ? ( {googleDrivePublicCredential ? (
<div> googleDriveConnectorIndexingStatus ? (
<div className="text-sm mb-2"> credentialIsLinked ? (
<div className="flex mb-1"> <div>
The Google Drive connector is setup!{" "} <div className="text-sm mb-2">
<b className="mx-2">Status:</b>{" "} <div className="flex mb-1">
<StatusRow The Google Drive connector is setup!{" "}
connectorIndexingStatus={googleDriveConnectorIndexingStatus} <b className="mx-2">Status:</b>{" "}
hasCredentialsIssue={ <StatusRow
googleDriveConnectorIndexingStatus.connector.credential_ids connectorIndexingStatus={googleDriveConnectorIndexingStatus}
.length === 0 hasCredentialsIssue={
} googleDriveConnectorIndexingStatus.connector
setPopup={setPopupWithExpiration} .credential_ids.length === 0
onUpdate={() => { }
mutate("/api/admin/connector/indexing-status"); setPopup={setPopupWithExpiration}
onUpdate={() => {
mutate("/api/manage/admin/connector/indexing-status");
}}
/>
</div>
<p>
Checkout the{" "}
<a href="/admin/indexing/status" className="text-blue-500">
status page
</a>{" "}
for the latest indexing status. We fetch the latest documents
from Google Drive every <b>10</b> minutes.
</p>
</div>
<Button
onClick={() => {
deleteConnector(
googleDriveConnectorIndexingStatus.connector.id
).then(() => {
setPopupWithExpiration({
message: "Successfully deleted connector!",
type: "success",
});
mutate("/api/manage/admin/connector/indexing-status");
});
}} }}
/> >
Delete Connector
</Button>
</div> </div>
<p> ) : (
Checkout the{" "} <>
<a href="/admin/indexing/status" className="text-blue-500"> <p className="text-sm mb-2">
status page Click the button below to link your credentials! Once this is
</a>{" "} done, all public documents in your Google Drive will be
for the latest indexing status. We fetch the latest documents from searchable. We will refresh the latest documents every <b>10</b>{" "}
Google Drive every <b>10</b> minutes. minutes.
</p>
<Button
onClick={async () => {
await linkCredential(
googleDriveConnectorIndexingStatus.connector.id,
googleDrivePublicCredential.id
);
setPopupWithExpiration({
message: "Successfully linked credentials!",
type: "success",
});
mutate("/api/manage/admin/connector/indexing-status");
}}
>
Link Credentials
</Button>
</>
)
) : (
<>
<p className="text-sm mb-2">
Click the button below to create a connector. We will refresh the
latest documents from Google Drive every <b>10</b> minutes.
</p> </p>
</div> <Button
<Button onClick={async () => {
onClick={() => { const connectorBase: ConnectorBase<{}> = {
deleteConnector( name: "GoogleDriveConnector",
googleDriveConnectorIndexingStatus.connector.id input_type: "load_state",
).then(() => { source: "google_drive",
connector_specific_config: {},
refresh_freq: 60 * 10, // 10 minutes
disabled: false,
};
const connectorCreationResponse = await fetch(
`/api/manage/admin/connector`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(connectorBase),
}
);
if (!connectorCreationResponse.ok) {
setPopupWithExpiration({
message: `Failed to create connector - ${connectorCreationResponse.status}`,
type: "error",
});
return;
}
const connector =
(await connectorCreationResponse.json()) as Connector<{}>;
const credentialLinkResponse = await fetch(
`/api/manage/connector/${connector.id}/credential/${googleDrivePublicCredential.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
}
);
if (!credentialLinkResponse.ok) {
setPopupWithExpiration({
message: `Failed to link connector to credential - ${credentialLinkResponse.status}`,
type: "error",
});
return;
}
setPopupWithExpiration({ setPopupWithExpiration({
message: "Successfully deleted connector!", message: "Successfully created connector!",
type: "success", type: "success",
}); });
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
}); }}
}} >
> Add
Delete Connector </Button>
</Button> </>
</div> )
) : ( ) : (
<> <p className="text-sm">
<p className="text-sm mb-2"> Please authenticate with Google Drive as described in Step 2! Once
Click the button below to create a connector. We will refresh the done with that, you can then move on to enable this connector.
latest documents from Google Drive every <b>10</b> minutes. </p>
</p>
<Button
onClick={async () => {
const connectorBase: ConnectorBase<{}> = {
name: "GoogleDriveConnector",
input_type: "load_state",
source: "google_drive",
connector_specific_config: {},
refresh_freq: 60 * 10, // 10 minutes
disabled: false,
};
const connectorCreationResponse = await fetch(
`/api/admin/connector`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(connectorBase),
}
);
if (!connectorCreationResponse.ok) {
setPopupWithExpiration({
message: `Failed to create connector - ${connectorCreationResponse.status}`,
type: "error",
});
return;
}
const connector =
(await connectorCreationResponse.json()) as Connector<{}>;
const credentialLinkResponse = await fetch(
`/api/admin/connector/${connector.id}/credential/${googleDriveCredential.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
}
);
if (!credentialLinkResponse.ok) {
setPopupWithExpiration({
message: `Failed to link connector to credential - ${credentialLinkResponse.status}`,
type: "error",
});
return;
}
setPopupWithExpiration({
message: "Successfully created connector!",
type: "success",
});
mutate("/api/admin/connector/indexing-status");
}}
>
Add
</Button>
</>
)} )}
</> </>
); );

View File

@@ -26,7 +26,7 @@ const MainSection = () => {
isLoading: isConnectorIndexingStatusesLoading, isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError, error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any>[]>( } = useSWR<ConnectorIndexingStatus<any>[]>(
"/api/admin/connector/indexing-status", "/api/manage/admin/connector/indexing-status",
fetcher fetcher
); );
@@ -36,7 +36,7 @@ const MainSection = () => {
isValidating: isCredentialsValidating, isValidating: isCredentialsValidating,
error: isCredentialsError, error: isCredentialsError,
} = useSWR<Credential<SlackCredentialJson>[]>( } = useSWR<Credential<SlackCredentialJson>[]>(
"/api/admin/credential", "/api/manage/credential",
fetcher fetcher
); );
@@ -81,7 +81,7 @@ const MainSection = () => {
className="ml-1 hover:bg-gray-700 rounded-full p-1" className="ml-1 hover:bg-gray-700 rounded-full p-1"
onClick={async () => { onClick={async () => {
await deleteCredential(slackCredential.id); await deleteCredential(slackCredential.id);
mutate("/api/admin/credential"); mutate("/api/manage/credential");
}} }}
> >
<TrashIcon /> <TrashIcon />
@@ -123,7 +123,7 @@ const MainSection = () => {
}} }}
onSubmit={(isSuccess) => { onSubmit={(isSuccess) => {
if (isSuccess) { if (isSuccess) {
mutate("/api/admin/credential"); mutate("/api/manage/credential");
} }
}} }}
/> />
@@ -156,11 +156,13 @@ const MainSection = () => {
connector.connector_specific_config.workspace, connector.connector_specific_config.workspace,
}, },
]} ]}
onUpdate={() => mutate("/api/admin/connector/indexing-status")} onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
onCredentialLink={async (connectorId) => { onCredentialLink={async (connectorId) => {
if (slackCredential) { if (slackCredential) {
await linkCredential(connectorId, slackCredential.id); await linkCredential(connectorId, slackCredential.id);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
/> />
@@ -191,7 +193,7 @@ const MainSection = () => {
onSubmit={async (isSuccess, responseJson) => { onSubmit={async (isSuccess, responseJson) => {
if (isSuccess && responseJson) { if (isSuccess && responseJson) {
await linkCredential(responseJson.id, slackCredential.id); await linkCredential(responseJson.id, slackCredential.id);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
/> />

View File

@@ -21,7 +21,7 @@ export default function Web() {
isLoading: isConnectorIndexingStatusesLoading, isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError, error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any>[]>( } = useSWR<ConnectorIndexingStatus<any>[]>(
"/api/admin/connector/indexing-status", "/api/manage/admin/connector/indexing-status",
fetcher fetcher
); );
@@ -69,7 +69,7 @@ export default function Web() {
if (isSuccess && responseJson) { if (isSuccess && responseJson) {
// assumes there is a dummy credential with id 0 // assumes there is a dummy credential with id 0
await linkCredential(responseJson.id, 0); await linkCredential(responseJson.id, 0);
mutate("/api/admin/connector/indexing-status"); mutate("/api/manage/admin/connector/indexing-status");
} }
}} }}
/> />
@@ -99,7 +99,7 @@ export default function Web() {
), ),
}, },
]} ]}
onUpdate={() => mutate("/api/admin/connector/indexing-status")} onUpdate={() => mutate("/api/manage/admin/connector/indexing-status")}
/> />
) : ( ) : (
<p className="text-sm">No indexed websites found</p> <p className="text-sm">No indexed websites found</p>

View File

@@ -9,12 +9,13 @@ import { NotebookIcon, XSquareIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher"; import { fetcher } from "@/lib/fetcher";
import { getSourceMetadata } from "@/components/source"; import { getSourceMetadata } from "@/components/source";
import { CheckCircle, XCircle } from "@phosphor-icons/react"; import { CheckCircle, XCircle } from "@phosphor-icons/react";
import { useState } from "react";
import { Popup } from "@/components/admin/connectors/Popup";
import { HealthCheckBanner } from "@/components/health/healthcheck"; import { HealthCheckBanner } from "@/components/health/healthcheck";
import { Connector, ConnectorIndexingStatus } from "@/lib/types"; import { ConnectorIndexingStatus } from "@/lib/types";
const getSourceDisplay = (connector: Connector<any>) => { const getSourceDisplay = (
connectorIndexingStatus: ConnectorIndexingStatus<any>
) => {
const connector = connectorIndexingStatus.connector;
const sourceMetadata = getSourceMetadata(connector.source); const sourceMetadata = getSourceMetadata(connector.source);
if (connector.source === "web") { if (connector.source === "web") {
return ( return (
@@ -38,28 +39,140 @@ const getSourceDisplay = (connector: Connector<any>) => {
); );
} }
if (
connector.source === "google_drive" &&
!connectorIndexingStatus.public_doc
) {
if (connectorIndexingStatus.owner) {
return `${sourceMetadata.displayName} [${connectorIndexingStatus.owner}]`;
}
return `${sourceMetadata.displayName} [private]`;
}
return sourceMetadata.displayName; return sourceMetadata.displayName;
}; };
export default function Status() { function Main() {
const { const {
data: indexAttemptData, data: indexAttemptData,
isLoading: indexAttemptIsLoading, isLoading: indexAttemptIsLoading,
error: indexAttemptIsError, error: indexAttemptIsError,
} = useSWR<ConnectorIndexingStatus<any>[]>( } = useSWR<ConnectorIndexingStatus<any>[]>(
"/api/admin/connector/indexing-status", "/api/manage/admin/connector/indexing-status",
fetcher, fetcher,
{ refreshInterval: 30000 } // 30 seconds { refreshInterval: 30000 } // 30 seconds
); );
const [popup, setPopup] = useState<{ if (indexAttemptIsLoading) {
message: string; return <LoadingAnimation text="" />;
type: "success" | "error"; }
} | null>(null);
if (indexAttemptIsError || !indexAttemptData) {
return <div className="text-red-600">Error loading indexing history.</div>;
}
// sort by source name
indexAttemptData.sort((a, b) => {
if (a.connector.source < b.connector.source) {
return -1;
} else if (a.connector.source > b.connector.source) {
return 1;
} else {
return 0;
}
});
return (
<BasicTable
columns={[
{ header: "Connector", key: "connector" },
{ header: "Status", key: "status" },
{ header: "Last Indexed", key: "indexed_at" },
{ header: "Docs Indexed", key: "docs_indexed" },
// { header: "Re-Index", key: "reindex" },
]}
data={indexAttemptData.map((connectorIndexingStatus) => {
const sourceMetadata = getSourceMetadata(
connectorIndexingStatus.connector.source
);
let statusDisplay = <div className="text-gray-400">In Progress...</div>;
if (connectorIndexingStatus.connector.disabled) {
statusDisplay = (
<div className="text-red-600 flex">
<XSquareIcon className="my-auto mr-1" size="18" />
Disabled
</div>
);
} else if (connectorIndexingStatus.last_status === "success") {
statusDisplay = (
<div className="text-green-600 flex">
<CheckCircle className="my-auto mr-1" size="18" />
Enabled
</div>
);
} else if (connectorIndexingStatus.last_status === "failed") {
statusDisplay = (
<div className="text-red-600 flex">
<XCircle className="my-auto mr-1" size="18" />
Error
</div>
);
}
return {
indexed_at: timeAgo(connectorIndexingStatus?.last_success) || "-",
docs_indexed: connectorIndexingStatus?.docs_indexed
? `${connectorIndexingStatus?.docs_indexed} documents`
: "-",
connector: (
<a
className="text-blue-500 flex"
href={sourceMetadata.adminPageLink}
>
{sourceMetadata.icon({ size: "20" })}
<div className="ml-1">
{getSourceDisplay(connectorIndexingStatus)}
</div>
</a>
),
status: statusDisplay,
// TODO: add the below back in after this is supported in the backend
// reindex: (
// <button
// className={
// "group relative " +
// "py-1 px-2 border border-transparent text-sm " +
// "font-medium rounded-md text-white bg-red-800 " +
// "hover:bg-red-900 focus:outline-none focus:ring-2 " +
// "focus:ring-offset-2 focus:ring-red-500 mx-auto"
// }
// onClick={async () => {
// const { message, isSuccess } = await submitIndexRequest(
// connectorIndexingStatus.connector.source,
// connectorIndexingStatus.connector
// .connector_specific_config
// );
// setPopup({
// message,
// type: isSuccess ? "success" : "error",
// });
// setTimeout(() => {
// setPopup(null);
// }, 4000);
// mutate("/api/manage/admin/connector/index-attempt");
// }}
// >
// Index
// </button>
// ),
};
})}
/>
);
}
export default function Status() {
return ( return (
<div className="mx-auto container"> <div className="mx-auto container">
{popup && <Popup message={popup.message} type={popup.type} />}
<div className="mb-4"> <div className="mb-4">
<HealthCheckBanner /> <HealthCheckBanner />
</div> </div>
@@ -67,99 +180,7 @@ export default function Status() {
<NotebookIcon size="32" /> <NotebookIcon size="32" />
<h1 className="text-3xl font-bold pl-2">Indexing Status</h1> <h1 className="text-3xl font-bold pl-2">Indexing Status</h1>
</div> </div>
<Main />
{indexAttemptIsLoading ? (
<LoadingAnimation text="Loading" />
) : indexAttemptIsError || !indexAttemptData ? (
<div>Error loading indexing history</div>
) : (
<BasicTable
columns={[
{ header: "Connector", key: "connector" },
{ header: "Status", key: "status" },
{ header: "Last Indexed", key: "indexed_at" },
{ header: "Docs Indexed", key: "docs_indexed" },
// { header: "Re-Index", key: "reindex" },
]}
data={indexAttemptData.map((connectorIndexingStatus) => {
const sourceMetadata = getSourceMetadata(
connectorIndexingStatus.connector.source
);
let statusDisplay = (
<div className="text-gray-400">In Progress...</div>
);
if (connectorIndexingStatus.connector.disabled) {
statusDisplay = (
<div className="text-red-600 flex">
<XSquareIcon className="my-auto mr-1" size="18" />
Disabled
</div>
);
} else if (connectorIndexingStatus.last_status === "success") {
statusDisplay = (
<div className="text-green-600 flex">
<CheckCircle className="my-auto mr-1" size="18" />
Enabled
</div>
);
} else if (connectorIndexingStatus.last_status === "failed") {
statusDisplay = (
<div className="text-red-600 flex">
<XCircle className="my-auto mr-1" size="18" />
Error
</div>
);
}
return {
indexed_at: timeAgo(connectorIndexingStatus?.last_success) || "-",
docs_indexed: connectorIndexingStatus?.docs_indexed
? `${connectorIndexingStatus?.docs_indexed} documents`
: "-",
connector: (
<a
className="text-blue-500 flex"
href={sourceMetadata.adminPageLink}
>
{sourceMetadata.icon({ size: "20" })}
<div className="ml-1">
{getSourceDisplay(connectorIndexingStatus.connector)}
</div>
</a>
),
status: statusDisplay,
// TODO: add the below back in after this is supported in the backend
// reindex: (
// <button
// className={
// "group relative " +
// "py-1 px-2 border border-transparent text-sm " +
// "font-medium rounded-md text-white bg-red-800 " +
// "hover:bg-red-900 focus:outline-none focus:ring-2 " +
// "focus:ring-offset-2 focus:ring-red-500 mx-auto"
// }
// onClick={async () => {
// const { message, isSuccess } = await submitIndexRequest(
// connectorIndexingStatus.connector.source,
// connectorIndexingStatus.connector
// .connector_specific_config
// );
// setPopup({
// message,
// type: isSuccess ? "success" : "error",
// });
// setTimeout(() => {
// setPopup(null);
// }, 4000);
// mutate("/api/admin/connector/index-attempt");
// }}
// >
// Index
// </button>
// ),
};
})}
/>
)}
</div> </div>
); );
} }

View File

@@ -0,0 +1,146 @@
import { Button } from "@/components/Button";
import { GoogleDriveIcon } from "@/components/icons/icons";
import { deleteCredential, linkCredential } from "@/lib/credential";
import { setupGoogleDriveOAuth } from "@/lib/googleDrive";
import { GoogleDriveCredentialJson, Credential } from "@/lib/types";
import { GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
import Cookies from "js-cookie";
import { CardProps } from "./interface";
import { CheckCircle, MinusCircle } from "@phosphor-icons/react";
export const GoogleDriveCard = ({
connector,
userCredentials,
setPopup,
router,
mutate,
}: CardProps) => {
if (!connector) return null;
const existingCredential: Credential<GoogleDriveCredentialJson> | undefined =
userCredentials?.find(
(credential) =>
credential.credential_json?.google_drive_tokens !== undefined &&
!credential.public_doc
);
const credentialIsLinked =
existingCredential !== undefined &&
connector.credential_ids.includes(existingCredential.id);
return (
<div className="border rounded border-gray-700 p-3 w-80">
<div className="flex items-center">
<GoogleDriveIcon size="20" />{" "}
<b className="ml-2 text-xl">Google Drive</b>
</div>
<div>
{existingCredential && credentialIsLinked ? (
<div className="text-green-600 flex text-sm mt-1">
<CheckCircle className="my-auto mr-1" size="16" />
Enabled
</div>
) : (
<div className="text-gray-400 flex text-sm mt-1">
<MinusCircle className="my-auto mr-1" size="16" />
Not Setup
</div>
)}
</div>
<div className="text-sm mt-2">
{existingCredential ? (
credentialIsLinked ? (
<>
<p>
Danswer has access to your Google Drive documents! Don&apos;t
worry, only <b>you</b> will be able to see your private
documents. You can revoke this access by clicking the button
below.
</p>
<div className="mt-2 flex">
<Button
onClick={async () => {
await deleteCredential(existingCredential.id);
setPopup({
message: "Successfully revoked access to Google Drive!",
type: "success",
});
mutate("/api/manage/connector");
mutate("/api/manage/credential");
}}
fullWidth
>
Revoke Access
</Button>
</div>
</>
) : (
<>
<p>
We&apos;ve recieved your credentials from Google! Click the
button below to activate the connector - we will pull the latest
state of your documents every <b>10</b> minutes.
</p>
<div className="mt-2 flex">
<Button
onClick={async () => {
await linkCredential(connector.id, existingCredential.id);
setPopup({
message: "Activated!",
type: "success",
});
mutate("/api/manage/connector");
}}
fullWidth
>
Activate
</Button>
</div>
</>
)
) : (
<>
<p>
If you want to make all your Google Drive documents searchable
through Danswer, click the button below! Don&apos;t worry, only{" "}
<b>you</b> will be able to see your private documents. Currently,
you&apos;ll only be able to search through documents shared with
the whole company.
</p>
<div className="mt-2 flex">
<Button
onClick={async () => {
const [authUrl, errorMsg] = await setupGoogleDriveOAuth({
isPublic: false,
});
if (authUrl) {
// cookie used by callback to determine where to finally redirect to
Cookies.set(
GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME,
"false",
{
path: "/",
}
);
router.push(authUrl);
return;
}
setPopup({
message: errorMsg,
type: "error",
});
}}
fullWidth
>
Authenticate with Google Drive
</Button>
</div>
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { Connector, Credential } from "@/lib/types";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context";
import { ScopedMutator } from "swr/_internal";
export interface CardProps {
connector: Connector<{}> | null | undefined;
userCredentials: Credential<any>[] | null | undefined;
setPopup: (popup: PopupSpec | null) => void;
router: AppRouterInstance;
mutate: ScopedMutator;
}

View File

@@ -0,0 +1,136 @@
"use client";
import { PlugIcon } from "@/components/icons/icons";
import useSWR, { useSWRConfig } from "swr";
import { fetcher } from "@/lib/fetcher";
import { LoadingAnimation } from "@/components/Loading";
import { useRouter } from "next/navigation";
import { Popup, PopupSpec } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { Connector, Credential, ValidSources } from "@/lib/types";
import { GoogleDriveCard } from "./GoogleDriveCard";
import { CardProps } from "./interface";
const connectorSourceToConnectorCard = (
source: ValidSources
): React.FC<CardProps> | null => {
switch (source) {
case "google_drive":
return GoogleDriveCard;
default:
return null;
}
};
const Main = () => {
const router = useRouter();
const { mutate } = useSWRConfig();
const {
data: appCredentialData,
isLoading: isAppCredentialLoading,
error: isAppCredentialError,
} = useSWR<{ client_id: string }>(
"/api/manage/admin/connector/google-drive/app-credential",
fetcher
);
const {
data: connectorsData,
isLoading: isConnectorDataLoading,
error: isConnectorDataError,
} = useSWR<Connector<any>[]>("/api/manage/connector", fetcher);
const {
data: credentialsData,
isLoading: isCredentialsLoading,
error: isCredentialsError,
} = useSWR<Credential<any>[]>("/api/manage/credential", fetcher);
const [popup, setPopup] = useState<{
message: string;
type: "success" | "error";
} | null>(null);
const setPopupWithExpiration = (popupSpec: PopupSpec | null) => {
setPopup(popupSpec);
setTimeout(() => {
setPopup(null);
}, 4000);
};
if (
isCredentialsLoading ||
isAppCredentialLoading ||
isConnectorDataLoading
) {
return (
<div className="mx-auto">
<LoadingAnimation text="" />
</div>
);
}
if (isCredentialsError || !credentialsData) {
return (
<div className="mx-auto">
<div className="text-red-500">Failed to load credentials.</div>
</div>
);
}
if (isConnectorDataError || !connectorsData) {
return (
<div className="mx-auto">
<div className="text-red-500">Failed to load connectors.</div>
</div>
);
}
if (isAppCredentialError || !appCredentialData) {
return (
<div className="mx-auto">
<div className="text-red-500">
Error loading Google Drive app credentials. Contact an administrator.
</div>
</div>
);
}
return (
<>
{popup && <Popup message={popup.message} type={popup.type} />}
{connectorsData.map((connector) => {
const connectorCard = connectorSourceToConnectorCard(connector.source);
if (connectorCard) {
return (
<div key={connector.id}>
{connectorCard({
connector,
userCredentials: credentialsData,
setPopup: setPopupWithExpiration,
router,
mutate,
})}
</div>
);
}
})}
</>
);
};
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<PlugIcon size="32" />
<h1 className="text-3xl font-bold pl-2">Personal Connectors</h1>
</div>
<Main />
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { Header } from "@/components/Header";
import { Sidebar } from "@/components/admin/connectors/Sidebar";
import {
NotebookIcon,
GithubIcon,
GlobeIcon,
GoogleDriveIcon,
SlackIcon,
KeyIcon,
ConfluenceIcon,
} from "@/components/icons/icons";
import { DISABLE_AUTH } from "@/lib/constants";
import { getCurrentUserSS } from "@/lib/userSS";
import { redirect } from "next/navigation";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
let user = null;
if (!DISABLE_AUTH) {
user = await getCurrentUserSS();
if (!user) {
return redirect("/auth/login");
}
if (user.role !== "admin") {
return redirect("/");
}
}
return (
<div>
<Header user={user} />
<div className="bg-gray-900 pt-8 flex">
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
{children}
</div>
</div>
</div>
);
}

View File

@@ -2,13 +2,20 @@ interface Props {
onClick: () => void; onClick: () => void;
children: JSX.Element | string; children: JSX.Element | string;
disabled?: boolean; disabled?: boolean;
fullWidth?: boolean;
} }
export const Button = ({ onClick, children, disabled = false }: Props) => { export const Button = ({
onClick,
children,
disabled = false,
fullWidth = false,
}: Props) => {
return ( return (
<button <button
className={ className={
"group relative " + "group relative " +
(fullWidth ? "w-full " : "") +
"py-1 px-2 border border-transparent text-sm " + "py-1 px-2 border border-transparent text-sm " +
"font-medium rounded-md text-white bg-red-800 " + "font-medium rounded-md text-white bg-red-800 " +
"hover:bg-red-900 focus:outline-none focus:ring-2 " + "hover:bg-red-900 focus:outline-none focus:ring-2 " +

View File

@@ -71,14 +71,19 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
<div <div
className={ className={
"absolute top-10 right-0 mt-2 bg-gray-600 rounded-sm " + "absolute top-10 right-0 mt-2 bg-gray-600 rounded-sm " +
"w-36 overflow-hidden shadow-xl z-10 text-sm text-gray-300" "w-48 overflow-hidden shadow-xl z-10 text-sm text-gray-300"
} }
> >
<Link href="/user/connectors">
<div className="flex py-2 px-3 cursor-pointer hover:bg-gray-500 border-b border-gray-500">
Personal Connectors
</div>
</Link>
{/* Show connector option if (1) auth is disabled or (2) user is an admin */} {/* Show connector option if (1) auth is disabled or (2) user is an admin */}
{(!user || user.role === "admin") && ( {(!user || user.role === "admin") && (
<Link href="/admin/indexing/status"> <Link href="/admin/indexing/status">
<div className="flex py-2 px-3 cursor-pointer hover:bg-gray-500 border-b border-gray-500"> <div className="flex py-2 px-3 cursor-pointer hover:bg-gray-500 border-b border-gray-500">
Connectors Admin Panel
</div> </div>
</Link> </Link>
)} )}

View File

@@ -14,7 +14,7 @@ export async function submitConnector<T>(
): Promise<{ message: string; isSuccess: boolean; response?: Connector<T> }> { ): Promise<{ message: string; isSuccess: boolean; response?: Connector<T> }> {
let isSuccess = false; let isSuccess = false;
try { try {
const response = await fetch(`/api/admin/connector`, { const response = await fetch(`/api/manage/admin/connector`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -9,7 +9,7 @@ export async function submitCredential<T>(
): Promise<{ message: string; isSuccess: boolean }> { ): Promise<{ message: string; isSuccess: boolean }> {
let isSuccess = false; let isSuccess = false;
try { try {
const response = await fetch(`/api/admin/credential`, { const response = await fetch(`/api/manage/credential`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -1,104 +0,0 @@
import React, { useState } from "react";
import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup";
import { Popup } from "./Popup";
import { ValidInputTypes, ValidSources } from "@/lib/types";
export const submitIndexRequest = async (
source: ValidSources,
values: Yup.AnyObject,
inputType: ValidInputTypes = "load_state"
): Promise<{ message: string; isSuccess: boolean }> => {
let isSuccess = false;
try {
const response = await fetch(
`/api/admin/connector/${source}/index-attempt`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
connector_specific_config: values,
input_type: inputType,
}),
}
);
if (response.ok) {
isSuccess = true;
return { message: "Success!", isSuccess: true };
} else {
const errorData = await response.json();
return { message: `Error: ${errorData.detail}`, isSuccess: false };
}
} catch (error) {
return { message: `Error: ${error}`, isSuccess: false };
}
};
interface IndexFormProps<YupObjectType extends Yup.AnyObject> {
source: ValidSources;
formBody: JSX.Element | null;
validationSchema: Yup.ObjectSchema<YupObjectType>;
initialValues: YupObjectType;
onSubmit: (isSuccess: boolean) => void;
additionalNonFormValues?: Yup.AnyObject;
}
export function IndexForm<YupObjectType extends Yup.AnyObject>({
source,
formBody,
validationSchema,
initialValues,
onSubmit,
additionalNonFormValues = {},
}: IndexFormProps<YupObjectType>): JSX.Element {
const [popup, setPopup] = useState<{
message: string;
type: "success" | "error";
} | null>(null);
return (
<>
{popup && <Popup message={popup.message} type={popup.type} />}
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values, formikHelpers) => {
formikHelpers.setSubmitting(true);
submitIndexRequest(source, {
...values,
...additionalNonFormValues,
}).then(({ message, isSuccess }) => {
setPopup({ message, type: isSuccess ? "success" : "error" });
formikHelpers.setSubmitting(false);
setTimeout(() => {
setPopup(null);
}, 4000);
onSubmit(isSuccess);
});
}}
>
{({ isSubmitting }) => (
<Form>
{formBody}
<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"
}
>
Index
</button>
</div>
</Form>
)}
</Formik>
</>
);
}

View File

@@ -8,6 +8,7 @@ import {
XSquare, XSquare,
LinkBreak, LinkBreak,
Link, Link,
Plug,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { SiConfluence, SiGithub, SiGoogledrive, SiSlack } from "react-icons/si"; import { SiConfluence, SiGithub, SiGoogledrive, SiSlack } from "react-icons/si";
import { FaGlobe } from "react-icons/fa"; import { FaGlobe } from "react-icons/fa";
@@ -19,6 +20,13 @@ interface IconProps {
const defaultTailwindCSS = "text-blue-400 my-auto flex flex-shrink-0"; const defaultTailwindCSS = "text-blue-400 my-auto flex flex-shrink-0";
export const PlugIcon = ({
size = "16",
className = defaultTailwindCSS,
}: IconProps) => {
return <Plug size={size} className={className} />;
};
export const NotebookIcon = ({ export const NotebookIcon = ({
size = "16", size = "16",
className = defaultTailwindCSS, className = defaultTailwindCSS,

View File

@@ -7,7 +7,7 @@ export const ApiKeyModal = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetch("/api/admin/openai-api-key/validate", { fetch("/api/manage/admin/openai-api-key/validate", {
method: "HEAD", method: "HEAD",
}).then((res) => { }).then((res) => {
// show popup if either the API key is not set or the API key is invalid // show popup if either the API key is not set or the API key is invalid

View File

@@ -1 +1 @@
export const OPENAI_API_KEY_URL = "/api/admin/openai-api-key"; export const OPENAI_API_KEY_URL = "/api/manage/admin/openai-api-key";

View File

@@ -3,7 +3,7 @@ import { Connector, ConnectorBase } from "./types";
export async function createConnector<T>( export async function createConnector<T>(
connector: ConnectorBase<T> connector: ConnectorBase<T>
): Promise<Connector<T>> { ): Promise<Connector<T>> {
const response = await fetch(`/api/admin/connector`, { const response = await fetch(`/api/manage/admin/connector`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -16,7 +16,7 @@ export async function createConnector<T>(
export async function updateConnector<T>( export async function updateConnector<T>(
connector: Connector<T> connector: Connector<T>
): Promise<Connector<T>> { ): Promise<Connector<T>> {
const response = await fetch(`/api/admin/connector/${connector.id}`, { const response = await fetch(`/api/manage/admin/connector/${connector.id}`, {
method: "PATCH", method: "PATCH",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -29,7 +29,7 @@ export async function updateConnector<T>(
export async function deleteConnector<T>( export async function deleteConnector<T>(
connectorId: number connectorId: number
): Promise<Connector<T>> { ): Promise<Connector<T>> {
const response = await fetch(`/api/admin/connector/${connectorId}`, { const response = await fetch(`/api/manage/admin/connector/${connectorId}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -1,2 +1,5 @@
export const DISABLE_AUTH = process.env.DISABLE_AUTH?.toLowerCase() === "true"; export const DISABLE_AUTH = process.env.DISABLE_AUTH?.toLowerCase() === "true";
export const INTERNAL_URL = process.env.INTERNAL_URL || "http://127.0.0.1:8080"; export const INTERNAL_URL = process.env.INTERNAL_URL || "http://127.0.0.1:8080";
export const GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME =
"google_drive_auth_is_admin";

View File

@@ -1,5 +1,5 @@
export async function deleteCredential<T>(credentialId: number) { export async function deleteCredential<T>(credentialId: number) {
const response = await fetch(`/api/admin/credential/${credentialId}`, { const response = await fetch(`/api/manage/credential/${credentialId}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -13,7 +13,7 @@ export async function linkCredential<T>(
credentialId: number credentialId: number
) { ) {
const response = await fetch( const response = await fetch(
`/api/admin/connector/${connectorId}/credential/${credentialId}`, `/api/manage/connector/${connectorId}/credential/${credentialId}`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {

View File

@@ -0,0 +1,43 @@
import { Credential } from "@/lib/types";
interface SetupGoogleDriveArgs {
isPublic: boolean;
}
export const setupGoogleDriveOAuth = async ({
isPublic,
}: SetupGoogleDriveArgs): Promise<[string | null, string]> => {
const credentialCreationResponse = await fetch("/api/manage/credential", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
public_doc: isPublic,
credential_json: {},
}),
});
if (!credentialCreationResponse.ok) {
return [
null,
`Failed to create credential - ${credentialCreationResponse.status}`,
];
}
const credential =
(await credentialCreationResponse.json()) as Credential<{}>;
const authorizationUrlResponse = await fetch(
`/api/manage/connector/google-drive/authorize/${credential.id}`
);
if (!authorizationUrlResponse.ok) {
return [
null,
`Failed to create credential - ${authorizationUrlResponse.status}`,
];
}
const authorizationUrlJson = (await authorizationUrlResponse.json()) as {
auth_url: string;
};
return [authorizationUrlJson.auth_url, ""];
};

View File

@@ -51,6 +51,8 @@ export interface SlackConfig {
export interface ConnectorIndexingStatus<T> { export interface ConnectorIndexingStatus<T> {
connector: Connector<T>; connector: Connector<T>;
public_doc: boolean;
owner: string;
last_status: "success" | "failed" | "in_progress" | "not_started"; last_status: "success" | "failed" | "in_progress" | "not_started";
last_success: string | null; last_success: string | null;
docs_indexed: number; docs_indexed: number;