mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-21 14:12:42 +02:00
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:
@@ -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(
|
||||||
|
@@ -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:
|
||||||
|
@@ -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,
|
||||||
),
|
),
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
)
|
@@ -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
25
web/package-lock.json
generated
@@ -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",
|
||||||
|
@@ -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",
|
||||||
|
@@ -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");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -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");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -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)));
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -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");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
146
web/src/app/user/connectors/GoogleDriveCard.tsx
Normal file
146
web/src/app/user/connectors/GoogleDriveCard.tsx
Normal 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'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'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't worry, only{" "}
|
||||||
|
<b>you</b> will be able to see your private documents. Currently,
|
||||||
|
you'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>
|
||||||
|
);
|
||||||
|
};
|
12
web/src/app/user/connectors/interface.ts
Normal file
12
web/src/app/user/connectors/interface.ts
Normal 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;
|
||||||
|
}
|
136
web/src/app/user/connectors/page.tsx
Normal file
136
web/src/app/user/connectors/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
42
web/src/app/user/layout.tsx
Normal file
42
web/src/app/user/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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 " +
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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",
|
||||||
|
@@ -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",
|
||||||
|
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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";
|
||||||
|
@@ -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",
|
||||||
|
@@ -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";
|
||||||
|
@@ -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: {
|
||||||
|
43
web/src/lib/googleDrive.ts
Normal file
43
web/src/lib/googleDrive.ts
Normal 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, ""];
|
||||||
|
};
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user