mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-05-02 07:50:21 +02:00
Improve index attempt display (#4511)
This commit is contained in:
parent
65fd8b90a8
commit
e3aab8e85e
@ -0,0 +1,57 @@
|
||||
"""Update status length
|
||||
|
||||
Revision ID: d961aca62eb3
|
||||
Revises: cf90764725d8
|
||||
Create Date: 2025-03-23 16:10:05.683965
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d961aca62eb3"
|
||||
down_revision = "cf90764725d8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Drop the existing enum type constraint
|
||||
op.execute("ALTER TABLE connector_credential_pair ALTER COLUMN status TYPE varchar")
|
||||
|
||||
# Create new enum type with all values
|
||||
op.execute(
|
||||
"ALTER TABLE connector_credential_pair ALTER COLUMN status TYPE VARCHAR(20) USING status::varchar(20)"
|
||||
)
|
||||
|
||||
# Update the enum type to include all possible values
|
||||
op.alter_column(
|
||||
"connector_credential_pair",
|
||||
"status",
|
||||
type_=sa.Enum(
|
||||
"SCHEDULED",
|
||||
"INITIAL_INDEXING",
|
||||
"ACTIVE",
|
||||
"PAUSED",
|
||||
"DELETING",
|
||||
"INVALID",
|
||||
name="connectorcredentialpairstatus",
|
||||
native_enum=False,
|
||||
),
|
||||
existing_type=sa.String(20),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"connector_credential_pair",
|
||||
sa.Column(
|
||||
"in_repeated_error_state", sa.Boolean, default=False, server_default="false"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# no need to convert back to the old enum type, since we're not using it anymore
|
||||
op.drop_column("connector_credential_pair", "in_repeated_error_state")
|
@ -26,6 +26,7 @@ from onyx.background.celery.celery_utils import httpx_init_vespa_pool
|
||||
from onyx.background.celery.memory_monitoring import emit_process_memory
|
||||
from onyx.background.celery.tasks.indexing.utils import get_unfenced_index_attempt_ids
|
||||
from onyx.background.celery.tasks.indexing.utils import IndexingCallback
|
||||
from onyx.background.celery.tasks.indexing.utils import is_in_repeated_error_state
|
||||
from onyx.background.celery.tasks.indexing.utils import should_index
|
||||
from onyx.background.celery.tasks.indexing.utils import try_creating_indexing_task
|
||||
from onyx.background.celery.tasks.indexing.utils import validate_indexing_fences
|
||||
@ -54,11 +55,12 @@ from onyx.connectors.exceptions import ConnectorValidationError
|
||||
from onyx.db.connector import mark_ccpair_with_indexing_trigger
|
||||
from onyx.db.connector_credential_pair import fetch_connector_credential_pairs
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from onyx.db.connector_credential_pair import set_cc_pair_repeated_error_state
|
||||
from onyx.db.engine import get_session_with_current_tenant
|
||||
from onyx.db.enums import ConnectorCredentialPairStatus
|
||||
from onyx.db.enums import IndexingMode
|
||||
from onyx.db.enums import IndexingStatus
|
||||
from onyx.db.index_attempt import get_index_attempt
|
||||
from onyx.db.index_attempt import get_last_attempt_for_cc_pair
|
||||
from onyx.db.index_attempt import mark_attempt_canceled
|
||||
from onyx.db.index_attempt import mark_attempt_failed
|
||||
from onyx.db.search_settings import get_active_search_settings_list
|
||||
@ -241,6 +243,16 @@ def monitor_ccpair_indexing_taskset(
|
||||
if not payload:
|
||||
return
|
||||
|
||||
# if the CC Pair is `SCHEDULED`, moved it to `INITIAL_INDEXING`. A CC Pair
|
||||
# should only ever be `SCHEDULED` if it's a new connector.
|
||||
cc_pair = get_connector_credential_pair_from_id(db_session, cc_pair_id)
|
||||
if cc_pair is None:
|
||||
raise RuntimeError(f"CC Pair {cc_pair_id} not found")
|
||||
|
||||
if cc_pair.status == ConnectorCredentialPairStatus.SCHEDULED:
|
||||
cc_pair.status = ConnectorCredentialPairStatus.INITIAL_INDEXING
|
||||
db_session.commit()
|
||||
|
||||
elapsed_started_str = None
|
||||
if payload.started:
|
||||
elapsed_started = datetime.now(timezone.utc) - payload.started
|
||||
@ -355,6 +367,22 @@ def monitor_ccpair_indexing_taskset(
|
||||
|
||||
redis_connector_index.reset()
|
||||
|
||||
# mark the CC Pair as `ACTIVE` if it's not already
|
||||
if (
|
||||
# it should never technically be in this state, but we'll handle it anyway
|
||||
cc_pair.status == ConnectorCredentialPairStatus.SCHEDULED
|
||||
or cc_pair.status == ConnectorCredentialPairStatus.INITIAL_INDEXING
|
||||
):
|
||||
cc_pair.status = ConnectorCredentialPairStatus.ACTIVE
|
||||
db_session.commit()
|
||||
|
||||
# if the index attempt is successful, clear the repeated error state
|
||||
if cc_pair.in_repeated_error_state:
|
||||
index_attempt = get_index_attempt(db_session, payload.index_attempt_id)
|
||||
if index_attempt and index_attempt.status.is_successful():
|
||||
cc_pair.in_repeated_error_state = False
|
||||
db_session.commit()
|
||||
|
||||
|
||||
@shared_task(
|
||||
name=OnyxCeleryTask.CHECK_FOR_INDEXING,
|
||||
@ -441,6 +469,21 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
|
||||
for cc_pair_entry in cc_pairs:
|
||||
cc_pair_ids.append(cc_pair_entry.id)
|
||||
|
||||
# mark CC Pairs that are repeatedly failing as in repeated error state
|
||||
with get_session_with_current_tenant() as db_session:
|
||||
current_search_settings = get_current_search_settings(db_session)
|
||||
for cc_pair_id in cc_pair_ids:
|
||||
if is_in_repeated_error_state(
|
||||
cc_pair_id=cc_pair_id,
|
||||
search_settings_id=current_search_settings.id,
|
||||
db_session=db_session,
|
||||
):
|
||||
set_cc_pair_repeated_error_state(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair_id,
|
||||
in_repeated_error_state=True,
|
||||
)
|
||||
|
||||
# kick off index attempts
|
||||
for cc_pair_id in cc_pair_ids:
|
||||
lock_beat.reacquire()
|
||||
@ -480,13 +523,8 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
|
||||
)
|
||||
continue
|
||||
|
||||
last_attempt = get_last_attempt_for_cc_pair(
|
||||
cc_pair.id, search_settings_instance.id, db_session
|
||||
)
|
||||
|
||||
if not should_index(
|
||||
cc_pair=cc_pair,
|
||||
last_index=last_attempt,
|
||||
search_settings_instance=search_settings_instance,
|
||||
secondary_index_building=len(search_settings_list) > 1,
|
||||
db_session=db_session,
|
||||
@ -494,7 +532,6 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
|
||||
task_logger.info(
|
||||
f"check_for_indexing - Not indexing cc_pair_id: {cc_pair_id} "
|
||||
f"search_settings={search_settings_instance.id}, "
|
||||
f"last_attempt={last_attempt.id if last_attempt else None}, "
|
||||
f"secondary_index_building={len(search_settings_list) > 1}"
|
||||
)
|
||||
continue
|
||||
@ -502,7 +539,6 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
|
||||
task_logger.info(
|
||||
f"check_for_indexing - Will index cc_pair_id: {cc_pair_id} "
|
||||
f"search_settings={search_settings_instance.id}, "
|
||||
f"last_attempt={last_attempt.id if last_attempt else None}, "
|
||||
f"secondary_index_building={len(search_settings_list) > 1}"
|
||||
)
|
||||
|
||||
|
@ -22,6 +22,7 @@ from onyx.configs.constants import OnyxCeleryPriority
|
||||
from onyx.configs.constants import OnyxCeleryQueues
|
||||
from onyx.configs.constants import OnyxCeleryTask
|
||||
from onyx.configs.constants import OnyxRedisConstants
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from onyx.db.engine import get_db_current_time
|
||||
from onyx.db.engine import get_session_with_current_tenant
|
||||
from onyx.db.enums import ConnectorCredentialPairStatus
|
||||
@ -31,6 +32,8 @@ from onyx.db.index_attempt import create_index_attempt
|
||||
from onyx.db.index_attempt import delete_index_attempt
|
||||
from onyx.db.index_attempt import get_all_index_attempts_by_status
|
||||
from onyx.db.index_attempt import get_index_attempt
|
||||
from onyx.db.index_attempt import get_last_attempt_for_cc_pair
|
||||
from onyx.db.index_attempt import get_recent_attempts_for_cc_pair
|
||||
from onyx.db.index_attempt import mark_attempt_failed
|
||||
from onyx.db.models import ConnectorCredentialPair
|
||||
from onyx.db.models import IndexAttempt
|
||||
@ -44,6 +47,8 @@ from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE = 5
|
||||
|
||||
|
||||
def get_unfenced_index_attempt_ids(db_session: Session, r: redis.Redis) -> list[int]:
|
||||
"""Gets a list of unfenced index attempts. Should not be possible, so we'd typically
|
||||
@ -346,9 +351,42 @@ def validate_indexing_fences(
|
||||
return
|
||||
|
||||
|
||||
def is_in_repeated_error_state(
|
||||
cc_pair_id: int, search_settings_id: int, db_session: Session
|
||||
) -> bool:
|
||||
"""Checks if the cc pair / search setting combination is in a repeated error state."""
|
||||
cc_pair = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
if not cc_pair:
|
||||
raise RuntimeError(
|
||||
f"is_in_repeated_error_state - could not find cc_pair with id={cc_pair_id}"
|
||||
)
|
||||
|
||||
# if the connector doesn't have a refresh_freq, a single failed attempt is enough
|
||||
number_of_failed_attempts_in_a_row_needed = (
|
||||
NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE
|
||||
if cc_pair.connector.refresh_freq is not None
|
||||
else 1
|
||||
)
|
||||
|
||||
most_recent_index_attempts = get_recent_attempts_for_cc_pair(
|
||||
cc_pair_id=cc_pair_id,
|
||||
search_settings_id=search_settings_id,
|
||||
limit=number_of_failed_attempts_in_a_row_needed,
|
||||
db_session=db_session,
|
||||
)
|
||||
return len(
|
||||
most_recent_index_attempts
|
||||
) >= number_of_failed_attempts_in_a_row_needed and all(
|
||||
attempt.status == IndexingStatus.FAILED
|
||||
for attempt in most_recent_index_attempts
|
||||
)
|
||||
|
||||
|
||||
def should_index(
|
||||
cc_pair: ConnectorCredentialPair,
|
||||
last_index: IndexAttempt | None,
|
||||
search_settings_instance: SearchSettings,
|
||||
secondary_index_building: bool,
|
||||
db_session: Session,
|
||||
@ -362,6 +400,16 @@ def should_index(
|
||||
Return True if we should try to index, False if not.
|
||||
"""
|
||||
connector = cc_pair.connector
|
||||
last_index_attempt = get_last_attempt_for_cc_pair(
|
||||
cc_pair_id=cc_pair.id,
|
||||
search_settings_id=search_settings_instance.id,
|
||||
db_session=db_session,
|
||||
)
|
||||
all_recent_errored = is_in_repeated_error_state(
|
||||
cc_pair_id=cc_pair.id,
|
||||
search_settings_id=search_settings_instance.id,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
# uncomment for debugging
|
||||
# task_logger.info(f"_should_index: "
|
||||
@ -388,24 +436,24 @@ def should_index(
|
||||
|
||||
# When switching over models, always index at least once
|
||||
if search_settings_instance.status == IndexModelStatus.FUTURE:
|
||||
if last_index:
|
||||
if last_index_attempt:
|
||||
# No new index if the last index attempt succeeded
|
||||
# Once is enough. The model will never be able to swap otherwise.
|
||||
if last_index.status == IndexingStatus.SUCCESS:
|
||||
if last_index_attempt.status == IndexingStatus.SUCCESS:
|
||||
# print(
|
||||
# f"Not indexing cc_pair={cc_pair.id}: FUTURE model with successful last index attempt={last_index.id}"
|
||||
# )
|
||||
return False
|
||||
|
||||
# No new index if the last index attempt is waiting to start
|
||||
if last_index.status == IndexingStatus.NOT_STARTED:
|
||||
if last_index_attempt.status == IndexingStatus.NOT_STARTED:
|
||||
# print(
|
||||
# f"Not indexing cc_pair={cc_pair.id}: FUTURE model with NOT_STARTED last index attempt={last_index.id}"
|
||||
# )
|
||||
return False
|
||||
|
||||
# No new index if the last index attempt is running
|
||||
if last_index.status == IndexingStatus.IN_PROGRESS:
|
||||
if last_index_attempt.status == IndexingStatus.IN_PROGRESS:
|
||||
# print(
|
||||
# f"Not indexing cc_pair={cc_pair.id}: FUTURE model with IN_PROGRESS last index attempt={last_index.id}"
|
||||
# )
|
||||
@ -439,18 +487,27 @@ def should_index(
|
||||
return True
|
||||
|
||||
# if no attempt has ever occurred, we should index regardless of refresh_freq
|
||||
if not last_index:
|
||||
if not last_index_attempt:
|
||||
return True
|
||||
|
||||
if connector.refresh_freq is None:
|
||||
# print(f"Not indexing cc_pair={cc_pair.id}: refresh_freq is None")
|
||||
return False
|
||||
|
||||
# if in the "initial" phase, we should always try and kick-off indexing
|
||||
# as soon as possible if there is no ongoing attempt. In other words,
|
||||
# no delay UNLESS we're repeatedly failing to index.
|
||||
if (
|
||||
cc_pair.status == ConnectorCredentialPairStatus.INITIAL_INDEXING
|
||||
and not all_recent_errored
|
||||
):
|
||||
return True
|
||||
|
||||
current_db_time = get_db_current_time(db_session)
|
||||
time_since_index = current_db_time - last_index.time_updated
|
||||
time_since_index = current_db_time - last_index_attempt.time_updated
|
||||
if time_since_index.total_seconds() < connector.refresh_freq:
|
||||
# print(
|
||||
# f"Not indexing cc_pair={cc_pair.id}: Last index attempt={last_index.id} "
|
||||
# f"Not indexing cc_pair={cc_pair.id}: Last index attempt={last_index_attempt.id} "
|
||||
# f"too recent ({time_since_index.total_seconds()}s < {connector.refresh_freq}s)"
|
||||
# )
|
||||
return False
|
||||
|
@ -483,14 +483,6 @@ CONTINUE_ON_CONNECTOR_FAILURE = os.environ.get(
|
||||
DISABLE_INDEX_UPDATE_ON_SWAP = (
|
||||
os.environ.get("DISABLE_INDEX_UPDATE_ON_SWAP", "").lower() == "true"
|
||||
)
|
||||
# Controls how many worker processes we spin up to index documents in the
|
||||
# background. This is useful for speeding up indexing, but does require a
|
||||
# fairly large amount of memory in order to increase substantially, since
|
||||
# each worker loads the embedding models into memory.
|
||||
NUM_INDEXING_WORKERS = int(os.environ.get("NUM_INDEXING_WORKERS") or 1)
|
||||
NUM_SECONDARY_INDEXING_WORKERS = int(
|
||||
os.environ.get("NUM_SECONDARY_INDEXING_WORKERS") or NUM_INDEXING_WORKERS
|
||||
)
|
||||
# More accurate results at the expense of indexing speed and index size (stores additional 4 MINI_CHUNK vectors)
|
||||
ENABLE_MULTIPASS_INDEXING = (
|
||||
os.environ.get("ENABLE_MULTIPASS_INDEXING", "").lower() == "true"
|
||||
|
@ -101,6 +101,7 @@ class ConfluenceConnector(
|
||||
self.labels_to_skip = labels_to_skip
|
||||
self.timezone_offset = timezone_offset
|
||||
self._confluence_client: OnyxConfluence | None = None
|
||||
self._low_timeout_confluence_client: OnyxConfluence | None = None
|
||||
self._fetched_titles: set[str] = set()
|
||||
self.allow_images = False
|
||||
|
||||
@ -156,6 +157,12 @@ class ConfluenceConnector(
|
||||
raise ConnectorMissingCredentialError("Confluence")
|
||||
return self._confluence_client
|
||||
|
||||
@property
|
||||
def low_timeout_confluence_client(self) -> OnyxConfluence:
|
||||
if self._low_timeout_confluence_client is None:
|
||||
raise ConnectorMissingCredentialError("Confluence")
|
||||
return self._low_timeout_confluence_client
|
||||
|
||||
def set_credentials_provider(
|
||||
self, credentials_provider: CredentialsProviderInterface
|
||||
) -> None:
|
||||
@ -163,13 +170,27 @@ class ConfluenceConnector(
|
||||
|
||||
# raises exception if there's a problem
|
||||
confluence_client = OnyxConfluence(
|
||||
self.is_cloud, self.wiki_base, credentials_provider
|
||||
is_cloud=self.is_cloud,
|
||||
url=self.wiki_base,
|
||||
credentials_provider=credentials_provider,
|
||||
)
|
||||
confluence_client._probe_connection(**self.probe_kwargs)
|
||||
confluence_client._initialize_connection(**self.final_kwargs)
|
||||
|
||||
self._confluence_client = confluence_client
|
||||
|
||||
# create a low timeout confluence client for sync flows
|
||||
low_timeout_confluence_client = OnyxConfluence(
|
||||
is_cloud=self.is_cloud,
|
||||
url=self.wiki_base,
|
||||
credentials_provider=credentials_provider,
|
||||
timeout=3,
|
||||
)
|
||||
low_timeout_confluence_client._probe_connection(**self.probe_kwargs)
|
||||
low_timeout_confluence_client._initialize_connection(**self.final_kwargs)
|
||||
|
||||
self._low_timeout_confluence_client = low_timeout_confluence_client
|
||||
|
||||
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
|
||||
raise NotImplementedError("Use set_credentials_provider with this connector.")
|
||||
|
||||
@ -521,11 +542,8 @@ class ConfluenceConnector(
|
||||
yield doc_metadata_list
|
||||
|
||||
def validate_connector_settings(self) -> None:
|
||||
if self._confluence_client is None:
|
||||
raise ConnectorMissingCredentialError("Confluence credentials not loaded.")
|
||||
|
||||
try:
|
||||
spaces = self._confluence_client.get_all_spaces(limit=1)
|
||||
spaces = self.low_timeout_confluence_client.get_all_spaces(limit=1)
|
||||
except HTTPError as e:
|
||||
status_code = e.response.status_code if e.response else None
|
||||
if status_code == 401:
|
||||
|
@ -72,12 +72,14 @@ class OnyxConfluence:
|
||||
|
||||
CREDENTIAL_PREFIX = "connector:confluence:credential"
|
||||
CREDENTIAL_TTL = 300 # 5 min
|
||||
PROBE_TIMEOUT = 5 # 5 seconds
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_cloud: bool,
|
||||
url: str,
|
||||
credentials_provider: CredentialsProviderInterface,
|
||||
timeout: int | None = None,
|
||||
) -> None:
|
||||
self._is_cloud = is_cloud
|
||||
self._url = url.rstrip("/")
|
||||
@ -100,11 +102,13 @@ class OnyxConfluence:
|
||||
|
||||
self._kwargs: Any = None
|
||||
|
||||
self.shared_base_kwargs = {
|
||||
self.shared_base_kwargs: dict[str, str | int | bool] = {
|
||||
"api_version": "cloud" if is_cloud else "latest",
|
||||
"backoff_and_retry": True,
|
||||
"cloud": is_cloud,
|
||||
}
|
||||
if timeout:
|
||||
self.shared_base_kwargs["timeout"] = timeout
|
||||
|
||||
def _renew_credentials(self) -> tuple[dict[str, Any], bool]:
|
||||
"""credential_json - the current json credentials
|
||||
@ -191,6 +195,8 @@ class OnyxConfluence:
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
merged_kwargs = {**self.shared_base_kwargs, **kwargs}
|
||||
# add special timeout to make sure that we don't hang indefinitely
|
||||
merged_kwargs["timeout"] = self.PROBE_TIMEOUT
|
||||
|
||||
with self._credentials_provider:
|
||||
credentials, _ = self._renew_credentials()
|
||||
|
@ -7,6 +7,7 @@ from sqlalchemy import desc
|
||||
from sqlalchemy import exists
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm import selectinload
|
||||
@ -394,6 +395,20 @@ def update_connector_credential_pair(
|
||||
)
|
||||
|
||||
|
||||
def set_cc_pair_repeated_error_state(
|
||||
db_session: Session,
|
||||
cc_pair_id: int,
|
||||
in_repeated_error_state: bool,
|
||||
) -> None:
|
||||
stmt = (
|
||||
update(ConnectorCredentialPair)
|
||||
.where(ConnectorCredentialPair.id == cc_pair_id)
|
||||
.values(in_repeated_error_state=in_repeated_error_state)
|
||||
)
|
||||
db_session.execute(stmt)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def delete_connector_credential_pair__no_commit(
|
||||
db_session: Session,
|
||||
connector_id: int,
|
||||
@ -457,7 +472,7 @@ def add_credential_to_connector(
|
||||
access_type: AccessType,
|
||||
groups: list[int] | None,
|
||||
auto_sync_options: dict | None = None,
|
||||
initial_status: ConnectorCredentialPairStatus = ConnectorCredentialPairStatus.ACTIVE,
|
||||
initial_status: ConnectorCredentialPairStatus = ConnectorCredentialPairStatus.SCHEDULED,
|
||||
last_successful_index_time: datetime | None = None,
|
||||
seeding_flow: bool = False,
|
||||
is_user_file: bool = False,
|
||||
|
@ -18,6 +18,12 @@ class IndexingStatus(str, PyEnum):
|
||||
}
|
||||
return self in terminal_states
|
||||
|
||||
def is_successful(self) -> bool:
|
||||
return (
|
||||
self == IndexingStatus.SUCCESS
|
||||
or self == IndexingStatus.COMPLETED_WITH_ERRORS
|
||||
)
|
||||
|
||||
|
||||
class IndexingMode(str, PyEnum):
|
||||
UPDATE = "update"
|
||||
@ -73,13 +79,19 @@ class ChatSessionSharedStatus(str, PyEnum):
|
||||
|
||||
|
||||
class ConnectorCredentialPairStatus(str, PyEnum):
|
||||
SCHEDULED = "SCHEDULED"
|
||||
INITIAL_INDEXING = "INITIAL_INDEXING"
|
||||
ACTIVE = "ACTIVE"
|
||||
PAUSED = "PAUSED"
|
||||
DELETING = "DELETING"
|
||||
INVALID = "INVALID"
|
||||
|
||||
def is_active(self) -> bool:
|
||||
return self == ConnectorCredentialPairStatus.ACTIVE
|
||||
return (
|
||||
self == ConnectorCredentialPairStatus.ACTIVE
|
||||
or self == ConnectorCredentialPairStatus.SCHEDULED
|
||||
or self == ConnectorCredentialPairStatus.INITIAL_INDEXING
|
||||
)
|
||||
|
||||
|
||||
class AccessType(str, PyEnum):
|
||||
|
@ -59,6 +59,7 @@ def get_recent_completed_attempts_for_cc_pair(
|
||||
limit: int,
|
||||
db_session: Session,
|
||||
) -> list[IndexAttempt]:
|
||||
"""Most recent to least recent."""
|
||||
return (
|
||||
db_session.query(IndexAttempt)
|
||||
.filter(
|
||||
@ -74,6 +75,25 @@ def get_recent_completed_attempts_for_cc_pair(
|
||||
)
|
||||
|
||||
|
||||
def get_recent_attempts_for_cc_pair(
|
||||
cc_pair_id: int,
|
||||
search_settings_id: int,
|
||||
limit: int,
|
||||
db_session: Session,
|
||||
) -> list[IndexAttempt]:
|
||||
"""Most recent to least recent."""
|
||||
return (
|
||||
db_session.query(IndexAttempt)
|
||||
.filter(
|
||||
IndexAttempt.connector_credential_pair_id == cc_pair_id,
|
||||
IndexAttempt.search_settings_id == search_settings_id,
|
||||
)
|
||||
.order_by(IndexAttempt.time_updated.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def get_index_attempt(
|
||||
db_session: Session, index_attempt_id: int
|
||||
) -> IndexAttempt | None:
|
||||
|
@ -437,6 +437,9 @@ class ConnectorCredentialPair(Base):
|
||||
status: Mapped[ConnectorCredentialPairStatus] = mapped_column(
|
||||
Enum(ConnectorCredentialPairStatus, native_enum=False), nullable=False
|
||||
)
|
||||
# this is separate from the `status` above, since a connector can be `INITIAL_INDEXING`, `ACTIVE`,
|
||||
# or `PAUSED` and still be in a repeated error state.
|
||||
in_repeated_error_state: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
connector_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("connector.id"), primary_key=True
|
||||
)
|
||||
|
@ -783,6 +783,7 @@ def get_connector_indexing_status(
|
||||
name=cc_pair.name,
|
||||
in_progress=in_progress,
|
||||
cc_pair_status=cc_pair.status,
|
||||
in_repeated_error_state=cc_pair.in_repeated_error_state,
|
||||
connector=ConnectorSnapshot.from_connector_db_model(
|
||||
connector, connector_to_cc_pair_ids.get(connector.id, [])
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from typing import Any
|
||||
from typing import Generic
|
||||
from typing import TypeVar
|
||||
@ -128,6 +129,7 @@ class CredentialBase(BaseModel):
|
||||
class CredentialSnapshot(CredentialBase):
|
||||
id: int
|
||||
user_id: UUID | None
|
||||
user_email: str | None = None
|
||||
time_created: datetime
|
||||
time_updated: datetime
|
||||
|
||||
@ -141,6 +143,7 @@ class CredentialSnapshot(CredentialBase):
|
||||
else credential.credential_json
|
||||
),
|
||||
user_id=credential.user_id,
|
||||
user_email=credential.user.email if credential.user else None,
|
||||
admin_public=credential.admin_public,
|
||||
time_created=credential.time_created,
|
||||
time_updated=credential.time_updated,
|
||||
@ -207,6 +210,7 @@ class CCPairFullInfo(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
status: ConnectorCredentialPairStatus
|
||||
in_repeated_error_state: bool
|
||||
num_docs_indexed: int
|
||||
connector: ConnectorSnapshot
|
||||
credential: CredentialSnapshot
|
||||
@ -220,6 +224,13 @@ class CCPairFullInfo(BaseModel):
|
||||
creator: UUID | None
|
||||
creator_email: str | None
|
||||
|
||||
# information on syncing/indexing
|
||||
last_indexed: datetime | None
|
||||
last_pruned: datetime | None
|
||||
last_permission_sync: datetime | None
|
||||
overall_indexing_speed: float | None
|
||||
latest_checkpoint_description: str | None
|
||||
|
||||
@classmethod
|
||||
def from_models(
|
||||
cls,
|
||||
@ -237,7 +248,8 @@ class CCPairFullInfo(BaseModel):
|
||||
# there is a mismatch between these two numbers which may confuse users.
|
||||
last_indexing_status = last_index_attempt.status if last_index_attempt else None
|
||||
if (
|
||||
last_indexing_status == IndexingStatus.SUCCESS
|
||||
# only need to do this if the last indexing attempt is still in progress
|
||||
last_indexing_status == IndexingStatus.IN_PROGRESS
|
||||
and number_of_index_attempts == 1
|
||||
and last_index_attempt
|
||||
and last_index_attempt.new_docs_indexed
|
||||
@ -246,10 +258,18 @@ class CCPairFullInfo(BaseModel):
|
||||
last_index_attempt.new_docs_indexed if last_index_attempt else 0
|
||||
)
|
||||
|
||||
overall_indexing_speed = num_docs_indexed / (
|
||||
(
|
||||
datetime.now(tz=timezone.utc) - cc_pair_model.connector.time_created
|
||||
).total_seconds()
|
||||
/ 60
|
||||
)
|
||||
|
||||
return cls(
|
||||
id=cc_pair_model.id,
|
||||
name=cc_pair_model.name,
|
||||
status=cc_pair_model.status,
|
||||
in_repeated_error_state=cc_pair_model.in_repeated_error_state,
|
||||
num_docs_indexed=num_docs_indexed,
|
||||
connector=ConnectorSnapshot.from_connector_db_model(
|
||||
cc_pair_model.connector
|
||||
@ -268,6 +288,15 @@ class CCPairFullInfo(BaseModel):
|
||||
creator_email=(
|
||||
cc_pair_model.creator.email if cc_pair_model.creator else None
|
||||
),
|
||||
last_indexed=(
|
||||
last_index_attempt.time_started if last_index_attempt else None
|
||||
),
|
||||
last_pruned=cc_pair_model.last_pruned,
|
||||
last_permission_sync=(
|
||||
last_index_attempt.time_started if last_index_attempt else None
|
||||
),
|
||||
overall_indexing_speed=overall_indexing_speed,
|
||||
latest_checkpoint_description=None,
|
||||
)
|
||||
|
||||
|
||||
@ -308,6 +337,9 @@ class ConnectorIndexingStatus(ConnectorStatus):
|
||||
"""Represents the full indexing status of a connector"""
|
||||
|
||||
cc_pair_status: ConnectorCredentialPairStatus
|
||||
# this is separate from the `status` above, since a connector can be `INITIAL_INDEXING`, `ACTIVE`,
|
||||
# or `PAUSED` and still be in a repeated error state.
|
||||
in_repeated_error_state: bool
|
||||
owner: str
|
||||
last_finished_status: IndexingStatus | None
|
||||
last_status: IndexingStatus | None
|
||||
|
@ -69,6 +69,7 @@ class CCPairManager:
|
||||
connector_specific_config: dict[str, Any] | None = None,
|
||||
credential_json: dict[str, Any] | None = None,
|
||||
user_performing_action: DATestUser | None = None,
|
||||
refresh_freq: int | None = None,
|
||||
) -> DATestCCPair:
|
||||
connector = ConnectorManager.create(
|
||||
name=name,
|
||||
@ -78,6 +79,7 @@ class CCPairManager:
|
||||
access_type=access_type,
|
||||
groups=groups,
|
||||
user_performing_action=user_performing_action,
|
||||
refresh_freq=refresh_freq,
|
||||
)
|
||||
credential = CredentialManager.create(
|
||||
credential_json=credential_json,
|
||||
|
@ -23,6 +23,7 @@ class ConnectorManager:
|
||||
access_type: AccessType = AccessType.PUBLIC,
|
||||
groups: list[int] | None = None,
|
||||
user_performing_action: DATestUser | None = None,
|
||||
refresh_freq: int | None = None,
|
||||
) -> DATestConnector:
|
||||
name = f"{name}-connector" if name else f"test-connector-{uuid4()}"
|
||||
|
||||
@ -36,6 +37,7 @@ class ConnectorManager:
|
||||
),
|
||||
access_type=access_type,
|
||||
groups=groups or [],
|
||||
refresh_freq=refresh_freq,
|
||||
)
|
||||
|
||||
response = requests.post(
|
||||
|
18
backend/tests/integration/tests/indexing/conftest.py
Normal file
18
backend/tests/integration/tests/indexing/conftest.py
Normal file
@ -0,0 +1,18 @@
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.integration.common_utils.constants import MOCK_CONNECTOR_SERVER_HOST
|
||||
from tests.integration.common_utils.constants import MOCK_CONNECTOR_SERVER_PORT
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_server_client() -> httpx.Client:
|
||||
print(
|
||||
f"Initializing mock server client with host: "
|
||||
f"{MOCK_CONNECTOR_SERVER_HOST} and port: "
|
||||
f"{MOCK_CONNECTOR_SERVER_PORT}"
|
||||
)
|
||||
return httpx.Client(
|
||||
base_url=f"http://{MOCK_CONNECTOR_SERVER_HOST}:{MOCK_CONNECTOR_SERVER_PORT}",
|
||||
timeout=5.0,
|
||||
)
|
@ -4,7 +4,6 @@ from datetime import timedelta
|
||||
from datetime import timezone
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.connectors.mock_connector.connector import MockConnectorCheckpoint
|
||||
@ -26,19 +25,6 @@ from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.common_utils.vespa import vespa_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_server_client() -> httpx.Client:
|
||||
print(
|
||||
f"Initializing mock server client with host: "
|
||||
f"{MOCK_CONNECTOR_SERVER_HOST} and port: "
|
||||
f"{MOCK_CONNECTOR_SERVER_PORT}"
|
||||
)
|
||||
return httpx.Client(
|
||||
base_url=f"http://{MOCK_CONNECTOR_SERVER_HOST}:{MOCK_CONNECTOR_SERVER_PORT}",
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
|
||||
def test_mock_connector_basic_flow(
|
||||
mock_server_client: httpx.Client,
|
||||
vespa_client: vespa_fixture,
|
||||
|
@ -0,0 +1,204 @@
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
|
||||
from onyx.background.celery.tasks.indexing.utils import (
|
||||
NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE,
|
||||
)
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.connectors.mock_connector.connector import MockConnectorCheckpoint
|
||||
from onyx.connectors.models import InputType
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from onyx.db.engine import get_session_context_manager
|
||||
from onyx.db.enums import IndexingStatus
|
||||
from tests.integration.common_utils.constants import MOCK_CONNECTOR_SERVER_HOST
|
||||
from tests.integration.common_utils.constants import MOCK_CONNECTOR_SERVER_PORT
|
||||
from tests.integration.common_utils.managers.cc_pair import CCPairManager
|
||||
from tests.integration.common_utils.managers.document import DocumentManager
|
||||
from tests.integration.common_utils.managers.index_attempt import IndexAttemptManager
|
||||
from tests.integration.common_utils.test_document_utils import create_test_document
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.common_utils.vespa import vespa_fixture
|
||||
|
||||
|
||||
def test_repeated_error_state_detection_and_recovery(
|
||||
mock_server_client: httpx.Client,
|
||||
vespa_client: vespa_fixture,
|
||||
admin_user: DATestUser,
|
||||
) -> None:
|
||||
"""Test that a connector is marked as in a repeated error state after
|
||||
NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE consecutive failures, and
|
||||
that it recovers after a successful indexing.
|
||||
|
||||
This test ensures we properly wait for the required number of indexing attempts
|
||||
to fail before checking that the connector is in a repeated error state."""
|
||||
|
||||
# Create test document for successful response
|
||||
test_doc = create_test_document()
|
||||
|
||||
# First, set up the mock server to consistently fail
|
||||
error_response = {
|
||||
"documents": [],
|
||||
"checkpoint": MockConnectorCheckpoint(has_more=False).model_dump(mode="json"),
|
||||
"failures": [],
|
||||
"unhandled_exception": "Simulated unhandled error for testing repeated errors",
|
||||
}
|
||||
|
||||
# Create a list of failure responses with at least the same length
|
||||
# as NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE
|
||||
failure_behaviors = [error_response] * (
|
||||
5 * NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE
|
||||
)
|
||||
|
||||
response = mock_server_client.post(
|
||||
"/set-behavior",
|
||||
json=failure_behaviors,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Create a new CC pair for testing
|
||||
cc_pair = CCPairManager.create_from_scratch(
|
||||
name=f"mock-repeated-error-{uuid.uuid4()}",
|
||||
source=DocumentSource.MOCK_CONNECTOR,
|
||||
input_type=InputType.POLL,
|
||||
connector_specific_config={
|
||||
"mock_server_host": MOCK_CONNECTOR_SERVER_HOST,
|
||||
"mock_server_port": MOCK_CONNECTOR_SERVER_PORT,
|
||||
},
|
||||
user_performing_action=admin_user,
|
||||
refresh_freq=60 * 60, # a very long time
|
||||
)
|
||||
|
||||
# Wait for the required number of failed indexing attempts
|
||||
# This shouldn't take long, since we keep retrying while we haven't
|
||||
# succeeded yet
|
||||
start_time = time.monotonic()
|
||||
while True:
|
||||
index_attempts_page = IndexAttemptManager.get_index_attempt_page(
|
||||
cc_pair_id=cc_pair.id,
|
||||
page=0,
|
||||
page_size=100,
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
index_attempts = [
|
||||
ia
|
||||
for ia in index_attempts_page.items
|
||||
if ia.status and ia.status.is_terminal()
|
||||
]
|
||||
if len(index_attempts) == NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE:
|
||||
break
|
||||
|
||||
if time.monotonic() - start_time > 180:
|
||||
raise TimeoutError(
|
||||
"Did not get required number of failed attempts within 180 seconds"
|
||||
)
|
||||
|
||||
# make sure that we don't mark the connector as in repeated error state
|
||||
# before we have the required number of failed attempts
|
||||
with get_session_context_manager() as db_session:
|
||||
cc_pair_obj = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair.id,
|
||||
)
|
||||
assert cc_pair_obj is not None
|
||||
assert not cc_pair_obj.in_repeated_error_state
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Verify we have the correct number of failed attempts
|
||||
assert len(index_attempts) == NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE
|
||||
for attempt in index_attempts:
|
||||
assert attempt.status == IndexingStatus.FAILED
|
||||
|
||||
# Check if the connector is in a repeated error state
|
||||
start_time = time.monotonic()
|
||||
while True:
|
||||
with get_session_context_manager() as db_session:
|
||||
cc_pair_obj = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair.id,
|
||||
)
|
||||
assert cc_pair_obj is not None
|
||||
if cc_pair_obj.in_repeated_error_state:
|
||||
break
|
||||
|
||||
if time.monotonic() - start_time > 30:
|
||||
assert False, "CC pair did not enter repeated error state within 30 seconds"
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Reset the mock server state
|
||||
response = mock_server_client.post("/reset")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Now set up the mock server to succeed
|
||||
success_response = {
|
||||
"documents": [test_doc.model_dump(mode="json")],
|
||||
"checkpoint": MockConnectorCheckpoint(has_more=False).model_dump(mode="json"),
|
||||
"failures": [],
|
||||
}
|
||||
|
||||
response = mock_server_client.post(
|
||||
"/set-behavior",
|
||||
json=[success_response],
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Run another indexing attempt that should succeed
|
||||
CCPairManager.run_once(
|
||||
cc_pair, from_beginning=True, user_performing_action=admin_user
|
||||
)
|
||||
|
||||
recovery_index_attempt = IndexAttemptManager.wait_for_index_attempt_start(
|
||||
cc_pair_id=cc_pair.id,
|
||||
index_attempts_to_ignore=[index_attempt.id for index_attempt in index_attempts],
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
|
||||
IndexAttemptManager.wait_for_index_attempt_completion(
|
||||
index_attempt_id=recovery_index_attempt.id,
|
||||
cc_pair_id=cc_pair.id,
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
|
||||
# Validate the indexing succeeded
|
||||
finished_recovery_attempt = IndexAttemptManager.get_index_attempt_by_id(
|
||||
index_attempt_id=recovery_index_attempt.id,
|
||||
cc_pair_id=cc_pair.id,
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
assert finished_recovery_attempt.status == IndexingStatus.SUCCESS
|
||||
|
||||
# Verify the document was indexed
|
||||
with get_session_context_manager() as db_session:
|
||||
documents = DocumentManager.fetch_documents_for_cc_pair(
|
||||
cc_pair_id=cc_pair.id,
|
||||
db_session=db_session,
|
||||
vespa_client=vespa_client,
|
||||
)
|
||||
assert len(documents) == 1
|
||||
assert documents[0].id == test_doc.id
|
||||
|
||||
# Verify the CC pair is no longer in a repeated error state
|
||||
start = time.monotonic()
|
||||
while True:
|
||||
with get_session_context_manager() as db_session:
|
||||
cc_pair_obj = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair.id,
|
||||
)
|
||||
assert cc_pair_obj is not None
|
||||
if not cc_pair_obj.in_repeated_error_state:
|
||||
break
|
||||
|
||||
elapsed = time.monotonic() - start
|
||||
if elapsed > 30:
|
||||
raise TimeoutError(
|
||||
"CC pair did not exit repeated error state within 30 seconds"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Waiting for CC pair to exit repeated error state. elapsed={elapsed:.2f}"
|
||||
)
|
||||
time.sleep(1)
|
@ -199,7 +199,6 @@ services:
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
# Indexing Configs
|
||||
- VESPA_SEARCHER_THREADS=${VESPA_SEARCHER_THREADS:-}
|
||||
- NUM_INDEXING_WORKERS=${NUM_INDEXING_WORKERS:-}
|
||||
- ENABLED_CONNECTOR_TYPES=${ENABLED_CONNECTOR_TYPES:-}
|
||||
- DISABLE_INDEX_UPDATE_ON_SWAP=${DISABLE_INDEX_UPDATE_ON_SWAP:-}
|
||||
- DASK_JOB_CLIENT_ENABLED=${DASK_JOB_CLIENT_ENABLED:-}
|
||||
|
@ -163,7 +163,6 @@ services:
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
# Indexing Configs
|
||||
- VESPA_SEARCHER_THREADS=${VESPA_SEARCHER_THREADS:-}
|
||||
- NUM_INDEXING_WORKERS=${NUM_INDEXING_WORKERS:-}
|
||||
- ENABLED_CONNECTOR_TYPES=${ENABLED_CONNECTOR_TYPES:-}
|
||||
- DISABLE_INDEX_UPDATE_ON_SWAP=${DISABLE_INDEX_UPDATE_ON_SWAP:-}
|
||||
- DASK_JOB_CLIENT_ENABLED=${DASK_JOB_CLIENT_ENABLED:-}
|
||||
|
@ -182,7 +182,6 @@ services:
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
# Indexing Configs
|
||||
- VESPA_SEARCHER_THREADS=${VESPA_SEARCHER_THREADS:-}
|
||||
- NUM_INDEXING_WORKERS=${NUM_INDEXING_WORKERS:-}
|
||||
- ENABLED_CONNECTOR_TYPES=${ENABLED_CONNECTOR_TYPES:-}
|
||||
- DISABLE_INDEX_UPDATE_ON_SWAP=${DISABLE_INDEX_UPDATE_ON_SWAP:-}
|
||||
- DASK_JOB_CLIENT_ENABLED=${DASK_JOB_CLIENT_ENABLED:-}
|
||||
|
@ -468,6 +468,10 @@ configMap:
|
||||
JIRA_API_VERSION: ""
|
||||
GONG_CONNECTOR_START_TIME: ""
|
||||
NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: ""
|
||||
# Worker Parallelism
|
||||
CELERY_WORKER_INDEXING_CONCURRENCY: ""
|
||||
CELERY_WORKER_LIGHT_CONCURRENCY: ""
|
||||
CELERY_WORKER_LIGHT_PREFETCH_MULTIPLIER: ""
|
||||
# OnyxBot SlackBot Configs
|
||||
DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: ""
|
||||
DANSWER_BOT_DISPLAY_ERROR_MSGS: ""
|
||||
|
@ -49,7 +49,6 @@ data:
|
||||
MIN_THREADS_ML_MODELS: ""
|
||||
# Indexing Configs
|
||||
VESPA_SEARCHER_THREADS: ""
|
||||
NUM_INDEXING_WORKERS: ""
|
||||
ENABLED_CONNECTOR_TYPES: ""
|
||||
DISABLE_INDEX_UPDATE_ON_SWAP: ""
|
||||
DASK_JOB_CLIENT_ENABLED: ""
|
||||
@ -62,6 +61,10 @@ data:
|
||||
NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: ""
|
||||
MAX_DOCUMENT_CHARS: ""
|
||||
MAX_FILE_SIZE_BYTES: ""
|
||||
# Worker Parallelism
|
||||
CELERY_WORKER_INDEXING_CONCURRENCY: ""
|
||||
CELERY_WORKER_LIGHT_CONCURRENCY: ""
|
||||
CELERY_WORKER_LIGHT_PREFETCH_MULTIPLIER: ""
|
||||
# OnyxBot SlackBot Configs
|
||||
DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: ""
|
||||
DANSWER_BOT_DISPLAY_ERROR_MSGS: ""
|
||||
|
@ -1,7 +1,5 @@
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { getNameFromPath } from "@/lib/fileUtils";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import Title from "@/components/ui/title";
|
||||
import { EditIcon } from "@/components/icons/icons";
|
||||
|
||||
import { useState } from "react";
|
||||
@ -44,7 +42,15 @@ function buildConfigEntries(
|
||||
return obj;
|
||||
}
|
||||
|
||||
function ConfigItem({ label, value }: { label: string; value: any }) {
|
||||
function ConfigItem({
|
||||
label,
|
||||
value,
|
||||
onEdit,
|
||||
}: {
|
||||
label: string;
|
||||
value: any;
|
||||
onEdit?: () => void;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const isExpandable = Array.isArray(value) && value.length > 5;
|
||||
|
||||
@ -52,11 +58,11 @@ function ConfigItem({ label, value }: { label: string; value: any }) {
|
||||
if (Array.isArray(value)) {
|
||||
const displayedItems = isExpanded ? value : value.slice(0, 5);
|
||||
return (
|
||||
<ul className="list-disc max-w-full pl-4 mt-2 overflow-x-auto">
|
||||
<ul className="list-disc pl-4 overflow-x-auto">
|
||||
{displayedItems.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="mb-1 max-w-full overflow-hidden text-right text-ellipsis whitespace-nowrap"
|
||||
className="mb-1 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{convertObjectToString(item)}
|
||||
</li>
|
||||
@ -65,7 +71,7 @@ function ConfigItem({ label, value }: { label: string; value: any }) {
|
||||
);
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
return (
|
||||
<div className="mt-2 overflow-x-auto">
|
||||
<div className="overflow-x-auto">
|
||||
{Object.entries(value).map(([key, val]) => (
|
||||
<div key={key} className="mb-1">
|
||||
<span className="font-semibold">{key}:</span>{" "}
|
||||
@ -75,20 +81,24 @@ function ConfigItem({ label, value }: { label: string; value: any }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// TODO: figure out a nice way to display boolean values
|
||||
else if (typeof value === "boolean") {
|
||||
return value ? "True" : "False";
|
||||
}
|
||||
return convertObjectToString(value) || "-";
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="w-full py-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="mb-2">{label}</span>
|
||||
<div className="mt-2 overflow-x-auto w-fit">
|
||||
<li className="w-full py-4 px-1">
|
||||
<div className="flex items-center w-full">
|
||||
<span className="text-sm">{label}</span>
|
||||
<div className="text-right overflow-x-auto max-w-[60%] text-sm font-normal ml-auto">
|
||||
{renderValue()}
|
||||
|
||||
{isExpandable && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="mt-2 ml-auto text-text-600 hover:text-text-800 flex items-center"
|
||||
className="mt-2 text-sm text-text-600 hover:text-text-800 flex items-center ml-auto"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
@ -104,6 +114,11 @@ function ConfigItem({ label, value }: { label: string; value: any }) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{onEdit && (
|
||||
<button onClick={onEdit} className="ml-4">
|
||||
<EditIcon size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
@ -146,63 +161,41 @@ export function AdvancedConfigDisplay({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title className="mt-8 mb-2">Advanced Configuration</Title>
|
||||
<CardSection>
|
||||
<ul className="w-full text-sm divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
{pruneFreq && (
|
||||
<li
|
||||
key={0}
|
||||
className="w-full flex justify-between items-center py-2"
|
||||
>
|
||||
<span>Pruning Frequency</span>
|
||||
<span className="ml-auto w-24">
|
||||
{formatPruneFrequency(pruneFreq)}
|
||||
</span>
|
||||
<span className="w-8 text-right">
|
||||
<button onClick={() => onPruningEdit()}>
|
||||
<EditIcon size={12} />
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{refreshFreq && (
|
||||
<li
|
||||
key={1}
|
||||
className="w-full flex justify-between items-center py-2"
|
||||
>
|
||||
<span>Refresh Frequency</span>
|
||||
<span className="ml-auto w-24">
|
||||
{formatRefreshFrequency(refreshFreq)}
|
||||
</span>
|
||||
<span className="w-8 text-right">
|
||||
<button onClick={() => onRefreshEdit()}>
|
||||
<EditIcon size={12} />
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{indexingStart && (
|
||||
<li
|
||||
key={2}
|
||||
className="w-full flex justify-between items-center py-2"
|
||||
>
|
||||
<span>Indexing Start</span>
|
||||
<span>{formatDate(indexingStart)}</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</CardSection>
|
||||
</>
|
||||
<div>
|
||||
<ul className="w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
{pruneFreq !== null && (
|
||||
<ConfigItem
|
||||
label="Pruning Frequency"
|
||||
value={formatPruneFrequency(pruneFreq)}
|
||||
onEdit={onPruningEdit}
|
||||
/>
|
||||
)}
|
||||
{refreshFreq && (
|
||||
<ConfigItem
|
||||
label="Refresh Frequency"
|
||||
value={formatRefreshFrequency(refreshFreq)}
|
||||
onEdit={onRefreshEdit}
|
||||
/>
|
||||
)}
|
||||
{indexingStart && (
|
||||
<ConfigItem
|
||||
label="Indexing Start"
|
||||
value={formatDate(indexingStart)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConfigDisplay({
|
||||
connectorSpecificConfig,
|
||||
sourceType,
|
||||
onEdit,
|
||||
}: {
|
||||
connectorSpecificConfig: any;
|
||||
sourceType: ValidSources;
|
||||
onEdit?: (key: string) => void;
|
||||
}) {
|
||||
const configEntries = Object.entries(
|
||||
buildConfigEntries(connectorSpecificConfig, sourceType)
|
||||
@ -212,15 +205,15 @@ export function ConfigDisplay({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title className="mb-2">Configuration</Title>
|
||||
<CardSection>
|
||||
<ul className="w-full text-sm divide-y divide-background-200 dark:divide-background-700">
|
||||
{configEntries.map(([key, value]) => (
|
||||
<ConfigItem key={key} label={key} value={value} />
|
||||
))}
|
||||
</ul>
|
||||
</CardSection>
|
||||
</>
|
||||
<ul className="w-full divide-y divide-background-200 dark:divide-background-700">
|
||||
{configEntries.map(([key, value]) => (
|
||||
<ConfigItem
|
||||
key={key}
|
||||
label={key}
|
||||
value={value}
|
||||
onEdit={onEdit ? () => onEdit(key) : undefined}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
@ -15,13 +15,9 @@ import { CCPairFullInfo } from "./types";
|
||||
import { IndexAttemptSnapshot } from "@/lib/types";
|
||||
import { IndexAttemptStatus } from "@/components/Status";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { buildCCPairInfoUrl } from "./lib";
|
||||
import { localizeAndPrettify } from "@/lib/time";
|
||||
import { getDocsProcessedPerMinute } from "@/lib/indexAttempt";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { InfoIcon, SearchIcon } from "@/components/icons/icons";
|
||||
import Link from "next/link";
|
||||
import { InfoIcon } from "@/components/icons/icons";
|
||||
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
|
||||
import {
|
||||
Tooltip,
|
||||
@ -29,10 +25,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import usePaginatedFetch from "@/hooks/usePaginatedFetch";
|
||||
|
||||
const ITEMS_PER_PAGE = 8;
|
||||
const PAGES_PER_BATCH = 8;
|
||||
|
||||
export interface IndexingAttemptsTableProps {
|
||||
ccPair: CCPairFullInfo;
|
||||
@ -84,14 +76,14 @@ export function IndexingAttemptsTable({
|
||||
<TableRow>
|
||||
<TableHead>Time Started</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>New Doc Cnt</TableHead>
|
||||
<TableHead className="whitespace-nowrap">New Docs</TableHead>
|
||||
<TableHead>
|
||||
<div className="w-fit">
|
||||
<div className="w-fit whitespace-nowrap">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-help flex items-center">
|
||||
Total Doc Cnt
|
||||
Total Docs
|
||||
<InfoIcon className="ml-1 w-4 h-4" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
|
@ -1,107 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
CCPairFullInfo,
|
||||
ConnectorCredentialPairStatus,
|
||||
statusIsNotCurrentlyActive,
|
||||
} from "./types";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { mutate } from "swr";
|
||||
import { buildCCPairInfoUrl } from "./lib";
|
||||
import { setCCPairStatus } from "@/lib/ccPair";
|
||||
import { useState } from "react";
|
||||
import { LoadingAnimation } from "@/components/Loading";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
|
||||
export function ModifyStatusButtonCluster({
|
||||
ccPair,
|
||||
}: {
|
||||
ccPair: CCPairFullInfo;
|
||||
}) {
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
|
||||
const handleStatusChange = async (
|
||||
newStatus: ConnectorCredentialPairStatus
|
||||
) => {
|
||||
if (isUpdating) return; // Prevent double-clicks or multiple requests
|
||||
|
||||
if (
|
||||
ccPair.status === ConnectorCredentialPairStatus.INVALID &&
|
||||
newStatus === ConnectorCredentialPairStatus.ACTIVE
|
||||
) {
|
||||
setShowConfirmModal(true);
|
||||
} else {
|
||||
await updateStatus(newStatus);
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = async (newStatus: ConnectorCredentialPairStatus) => {
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
// Call the backend to update the status
|
||||
await setCCPairStatus(ccPair.id, newStatus, setPopup);
|
||||
|
||||
// Use mutate to revalidate the status on the backend
|
||||
await mutate(buildCCPairInfoUrl(ccPair.id));
|
||||
} catch (error) {
|
||||
console.error("Failed to update status", error);
|
||||
} finally {
|
||||
// Reset local updating state and button text after mutation
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Compute the button text based on current state and backend status
|
||||
const isNotActive = statusIsNotCurrentlyActive(ccPair.status);
|
||||
const buttonText = isNotActive ? "Re-Enable" : "Pause";
|
||||
|
||||
const tooltip = isNotActive
|
||||
? "Click to start indexing again!"
|
||||
: "When paused, the connector's documents will still be visible. However, no new documents will be indexed.";
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<Button
|
||||
className="flex items-center justify-center w-auto min-w-[100px] px-4 py-2"
|
||||
variant={isNotActive ? "success-reverse" : "default"}
|
||||
disabled={isUpdating}
|
||||
onClick={() =>
|
||||
handleStatusChange(
|
||||
isNotActive
|
||||
? ConnectorCredentialPairStatus.ACTIVE
|
||||
: ConnectorCredentialPairStatus.PAUSED
|
||||
)
|
||||
}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<LoadingAnimation
|
||||
text={isNotActive ? "Resuming" : "Pausing"}
|
||||
size="text-md"
|
||||
/>
|
||||
) : (
|
||||
buttonText
|
||||
)}
|
||||
</Button>
|
||||
{showConfirmModal && (
|
||||
<ConfirmEntityModal
|
||||
entityType="Invalid Connector"
|
||||
entityName={ccPair.name}
|
||||
onClose={() => setShowConfirmModal(false)}
|
||||
onSubmit={() => {
|
||||
setShowConfirmModal(false);
|
||||
updateStatus(ConnectorCredentialPairStatus.ACTIVE);
|
||||
}}
|
||||
additionalDetails="This connector was previously marked as invalid. Please verify that your configuration is correct before re-enabling. Are you sure you want to proceed?"
|
||||
actionButtonText="Re-Enable"
|
||||
variant="action"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Text from "@/components/ui/text";
|
||||
import { triggerIndexing } from "./lib";
|
||||
import { mutate } from "swr";
|
||||
import { buildCCPairInfoUrl, getTooltipMessage } from "./lib";
|
||||
import { useState } from "react";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ConnectorCredentialPairStatus } from "./types";
|
||||
import { CCPairStatus } from "@/components/Status";
|
||||
import { getCCPairStatusMessage } from "@/lib/ccPair";
|
||||
|
||||
function ReIndexPopup({
|
||||
connectorId,
|
||||
credentialId,
|
||||
ccPairId,
|
||||
setPopup,
|
||||
hide,
|
||||
}: {
|
||||
connectorId: number;
|
||||
credentialId: number;
|
||||
ccPairId: number;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
hide: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal title="Run Indexing" onOutsideClick={hide}>
|
||||
<div>
|
||||
<Button
|
||||
variant="submit"
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
triggerIndexing(
|
||||
false,
|
||||
connectorId,
|
||||
credentialId,
|
||||
ccPairId,
|
||||
setPopup
|
||||
);
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Run Update
|
||||
</Button>
|
||||
|
||||
<Text className="mt-2">
|
||||
This will pull in and index all documents that have changed and/or
|
||||
have been added since the last successful indexing run.
|
||||
</Text>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button
|
||||
variant="submit"
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
triggerIndexing(
|
||||
true,
|
||||
connectorId,
|
||||
credentialId,
|
||||
ccPairId,
|
||||
setPopup
|
||||
);
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Run Complete Re-Indexing
|
||||
</Button>
|
||||
|
||||
<Text className="mt-2">
|
||||
This will cause a complete re-indexing of all documents from the
|
||||
source.
|
||||
</Text>
|
||||
|
||||
<Text className="mt-2">
|
||||
<b>NOTE:</b> depending on the number of documents stored in the
|
||||
source, this may take a long time.
|
||||
</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReIndexButton({
|
||||
ccPairId,
|
||||
connectorId,
|
||||
credentialId,
|
||||
isIndexing,
|
||||
isDisabled,
|
||||
ccPairStatus,
|
||||
}: {
|
||||
ccPairId: number;
|
||||
connectorId: number;
|
||||
credentialId: number;
|
||||
isIndexing: boolean;
|
||||
isDisabled: boolean;
|
||||
ccPairStatus: ConnectorCredentialPairStatus;
|
||||
}) {
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [reIndexPopupVisible, setReIndexPopupVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{reIndexPopupVisible && (
|
||||
<ReIndexPopup
|
||||
connectorId={connectorId}
|
||||
credentialId={credentialId}
|
||||
ccPairId={ccPairId}
|
||||
setPopup={setPopup}
|
||||
hide={() => setReIndexPopupVisible(false)}
|
||||
/>
|
||||
)}
|
||||
{popup}
|
||||
<Button
|
||||
variant="success-reverse"
|
||||
className="ml-auto min-w-[100px]"
|
||||
onClick={() => {
|
||||
setReIndexPopupVisible(true);
|
||||
}}
|
||||
disabled={
|
||||
isDisabled ||
|
||||
ccPairStatus == ConnectorCredentialPairStatus.DELETING ||
|
||||
ccPairStatus == ConnectorCredentialPairStatus.PAUSED ||
|
||||
ccPairStatus == ConnectorCredentialPairStatus.INVALID
|
||||
}
|
||||
tooltip={getCCPairStatusMessage(isDisabled, isIndexing, ccPairStatus)}
|
||||
>
|
||||
Re-Index
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
162
web/src/app/admin/connector/[ccPairId]/ReIndexModal.tsx
Normal file
162
web/src/app/admin/connector/[ccPairId]/ReIndexModal.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { triggerIndexing } from "./lib";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import Text from "@/components/ui/text";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
// Hook to handle re-indexing functionality
|
||||
export function useReIndexModal(
|
||||
connectorId: number | null,
|
||||
credentialId: number | null,
|
||||
ccPairId: number | null,
|
||||
setPopup: (popupSpec: PopupSpec | null) => void
|
||||
) {
|
||||
const [reIndexPopupVisible, setReIndexPopupVisible] = useState(false);
|
||||
|
||||
const showReIndexModal = () => {
|
||||
if (!connectorId || !credentialId || !ccPairId) {
|
||||
return;
|
||||
}
|
||||
setReIndexPopupVisible(true);
|
||||
};
|
||||
|
||||
const hideReIndexModal = () => {
|
||||
setReIndexPopupVisible(false);
|
||||
};
|
||||
|
||||
const triggerReIndex = async (fromBeginning: boolean) => {
|
||||
if (!connectorId || !credentialId || !ccPairId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await triggerIndexing(
|
||||
fromBeginning,
|
||||
connectorId,
|
||||
credentialId,
|
||||
ccPairId,
|
||||
setPopup
|
||||
);
|
||||
|
||||
// Show appropriate notification based on result
|
||||
if (result.success) {
|
||||
setPopup({
|
||||
message: `${fromBeginning ? "Complete re-indexing" : "Indexing update"} started successfully`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
message: result.message || "Failed to start indexing",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger indexing:", error);
|
||||
setPopup({
|
||||
message: "An unexpected error occurred while trying to start indexing",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const FinalReIndexModal =
|
||||
reIndexPopupVisible && connectorId && credentialId && ccPairId ? (
|
||||
<ReIndexModal
|
||||
setPopup={setPopup}
|
||||
hide={hideReIndexModal}
|
||||
onRunIndex={triggerReIndex}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return {
|
||||
showReIndexModal,
|
||||
ReIndexModal: FinalReIndexModal,
|
||||
};
|
||||
}
|
||||
|
||||
interface ReIndexModalProps {
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
hide: () => void;
|
||||
onRunIndex: (fromBeginning: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ReIndexModal({
|
||||
setPopup,
|
||||
hide,
|
||||
onRunIndex,
|
||||
}: ReIndexModalProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const handleRunIndex = async (fromBeginning: boolean) => {
|
||||
if (isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
// First show immediate feedback with a popup
|
||||
setPopup({
|
||||
message: `Starting ${fromBeginning ? "complete re-indexing" : "indexing update"}...`,
|
||||
type: "info",
|
||||
});
|
||||
|
||||
// Then close the modal
|
||||
hide();
|
||||
|
||||
// Then run the indexing operation
|
||||
await onRunIndex(fromBeginning);
|
||||
} catch (error) {
|
||||
console.error("Error starting indexing:", error);
|
||||
// Show error in popup if needed
|
||||
setPopup({
|
||||
message: "Failed to start indexing process",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Run Indexing" onOutsideClick={hide}>
|
||||
<div>
|
||||
<Button
|
||||
variant="submit"
|
||||
className="ml-auto"
|
||||
onClick={() => handleRunIndex(false)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Run Update
|
||||
</Button>
|
||||
|
||||
<Text className="mt-2">
|
||||
This will pull in and index all documents that have changed and/or
|
||||
have been added since the last successful indexing run.
|
||||
</Text>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button
|
||||
variant="submit"
|
||||
className="ml-auto"
|
||||
onClick={() => handleRunIndex(true)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Run Complete Re-Indexing
|
||||
</Button>
|
||||
|
||||
<Text className="mt-2">
|
||||
This will cause a complete re-indexing of all documents from the
|
||||
source.
|
||||
</Text>
|
||||
|
||||
<Text className="mt-2">
|
||||
<b>NOTE:</b> depending on the number of documents stored in the
|
||||
source, this may take a long time.
|
||||
</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
90
web/src/app/admin/connector/[ccPairId]/ReIndexPopup.tsx
Normal file
90
web/src/app/admin/connector/[ccPairId]/ReIndexPopup.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Text from "@/components/ui/text";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ReIndexPopupProps {
|
||||
connectorId: number;
|
||||
credentialId: number;
|
||||
ccPairId: number;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
hide: () => void;
|
||||
onRunIndex: (fromBeginning: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ReIndexPopup({
|
||||
connectorId,
|
||||
credentialId,
|
||||
ccPairId,
|
||||
setPopup,
|
||||
hide,
|
||||
onRunIndex,
|
||||
}: ReIndexPopupProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const handleRunIndex = async (fromBeginning: boolean) => {
|
||||
if (isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
// First close the modal to give immediate feedback
|
||||
hide();
|
||||
// Then run the indexing operation
|
||||
await onRunIndex(fromBeginning);
|
||||
} catch (error) {
|
||||
console.error("Error starting indexing:", error);
|
||||
// Show error in popup if needed
|
||||
setPopup({
|
||||
message: "Failed to start indexing process",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Run Indexing" onOutsideClick={hide}>
|
||||
<div>
|
||||
<Button
|
||||
variant="submit"
|
||||
className="ml-auto"
|
||||
onClick={() => handleRunIndex(false)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Run Update
|
||||
</Button>
|
||||
|
||||
<Text className="mt-2">
|
||||
This will pull in and index all documents that have changed and/or
|
||||
have been added since the last successful indexing run.
|
||||
</Text>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button
|
||||
variant="submit"
|
||||
className="ml-auto"
|
||||
onClick={() => handleRunIndex(true)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Run Complete Re-Indexing
|
||||
</Button>
|
||||
|
||||
<Text className="mt-2">
|
||||
This will cause a complete re-indexing of all documents from the
|
||||
source.
|
||||
</Text>
|
||||
|
||||
<Text className="mt-2">
|
||||
<b>NOTE:</b> depending on the number of documents stored in the
|
||||
source, this may take a long time.
|
||||
</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -21,24 +21,26 @@ export async function triggerIndexing(
|
||||
credentialId: number,
|
||||
ccPairId: number,
|
||||
setPopup: (popupSpec: PopupSpec | null) => void
|
||||
) {
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const errorMsg = await runConnector(
|
||||
connectorId,
|
||||
[credentialId],
|
||||
fromBeginning
|
||||
);
|
||||
if (errorMsg) {
|
||||
setPopup({
|
||||
message: errorMsg,
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
message: "Triggered connector run",
|
||||
type: "success",
|
||||
});
|
||||
}
|
||||
|
||||
mutate(buildCCPairInfoUrl(ccPairId));
|
||||
|
||||
if (errorMsg) {
|
||||
return {
|
||||
success: false,
|
||||
message: errorMsg,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
message: "Triggered connector run",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getTooltipMessage(
|
||||
|
@ -13,37 +13,53 @@ import {
|
||||
} from "@/lib/connector";
|
||||
import { credentialTemplates } from "@/lib/connectors/credentials";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import Title from "@/components/ui/title";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState, use } from "react";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { AdvancedConfigDisplay, ConfigDisplay } from "./ConfigDisplay";
|
||||
import { DeletionButton } from "./DeletionButton";
|
||||
import DeletionErrorStatus from "./DeletionErrorStatus";
|
||||
import { IndexingAttemptsTable } from "./IndexingAttemptsTable";
|
||||
import { ModifyStatusButtonCluster } from "./ModifyStatusButtonCluster";
|
||||
import { ReIndexButton } from "./ReIndexButton";
|
||||
|
||||
import { buildCCPairInfoUrl, triggerIndexing } from "./lib";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
CCPairFullInfo,
|
||||
ConnectorCredentialPairStatus,
|
||||
IndexAttemptError,
|
||||
PaginatedIndexAttemptErrors,
|
||||
statusIsNotCurrentlyActive,
|
||||
} from "./types";
|
||||
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import EditPropertyModal from "@/components/modals/EditPropertyModal";
|
||||
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
||||
import { deleteCCPair } from "@/lib/documentDeletion";
|
||||
|
||||
import * as Yup from "yup";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
Trash2Icon,
|
||||
RefreshCwIcon,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import IndexAttemptErrorsModal from "./IndexAttemptErrorsModal";
|
||||
import usePaginatedFetch from "@/hooks/usePaginatedFetch";
|
||||
import { IndexAttemptSnapshot } from "@/lib/types";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DropdownMenuItemWithTooltip } from "@/components/ui/dropdown-menu-with-tooltip";
|
||||
import { FiSettings } from "react-icons/fi";
|
||||
import { timeAgo } from "@/lib/time";
|
||||
import { useStatusChange } from "./useStatusChange";
|
||||
import { useReIndexModal } from "./ReIndexModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// synchronize these validations with the SQLAlchemy connector class until we have a
|
||||
// centralized schema for both frontend and backend
|
||||
@ -68,6 +84,8 @@ const PAGES_PER_BATCH = 8;
|
||||
|
||||
function Main({ ccPairId }: { ccPairId: number }) {
|
||||
const router = useRouter();
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const {
|
||||
data: ccPair,
|
||||
isLoading: isLoadingCCPair,
|
||||
@ -90,6 +108,17 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
endpoint: `${buildCCPairInfoUrl(ccPairId)}/index-attempts`,
|
||||
});
|
||||
|
||||
// need to always have the most recent index attempts around
|
||||
// so just kick off a separate fetch
|
||||
const {
|
||||
currentPageData: mostRecentIndexAttempts,
|
||||
isLoading: isLoadingMostRecentIndexAttempts,
|
||||
} = usePaginatedFetch<IndexAttemptSnapshot>({
|
||||
itemsPerPage: ITEMS_PER_PAGE,
|
||||
pagesPerBatch: 1,
|
||||
endpoint: `${buildCCPairInfoUrl(ccPairId)}/index-attempts`,
|
||||
});
|
||||
|
||||
const {
|
||||
currentPageData: indexAttemptErrorsPage,
|
||||
currentPage: errorsCurrentPage,
|
||||
@ -101,6 +130,20 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
endpoint: `/api/manage/admin/cc-pair/${ccPairId}/errors`,
|
||||
});
|
||||
|
||||
// Initialize hooks at top level to avoid conditional hook calls
|
||||
const { showReIndexModal, ReIndexModal } = useReIndexModal(
|
||||
ccPair?.connector?.id || null,
|
||||
ccPair?.credential?.id || null,
|
||||
ccPairId,
|
||||
setPopup
|
||||
);
|
||||
|
||||
const {
|
||||
handleStatusChange,
|
||||
isUpdating: isStatusUpdating,
|
||||
ConfirmModal,
|
||||
} = useStatusChange(ccPair || null);
|
||||
|
||||
const indexAttemptErrors = indexAttemptErrorsPage
|
||||
? {
|
||||
items: indexAttemptErrorsPage,
|
||||
@ -118,7 +161,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
const [showIndexAttemptErrors, setShowIndexAttemptErrors] = useState(false);
|
||||
const [showIsResolvingKickoffLoader, setShowIsResolvingKickoffLoader] =
|
||||
useState(false);
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const latestIndexAttempt = indexAttempts?.[0];
|
||||
const isResolvingErrors =
|
||||
@ -134,6 +177,52 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
router.push("/admin/indexing/status?message=connector-deleted");
|
||||
}, [router]);
|
||||
|
||||
const handleStatusUpdate = async (
|
||||
newStatus: ConnectorCredentialPairStatus
|
||||
) => {
|
||||
setShowIsResolvingKickoffLoader(true); // Show fullscreen spinner
|
||||
await handleStatusChange(newStatus);
|
||||
setShowIsResolvingKickoffLoader(false); // Hide fullscreen spinner
|
||||
};
|
||||
|
||||
const triggerReIndex = async (fromBeginning: boolean) => {
|
||||
if (!ccPair) return;
|
||||
|
||||
setShowIsResolvingKickoffLoader(true);
|
||||
|
||||
try {
|
||||
const result = await triggerIndexing(
|
||||
fromBeginning,
|
||||
ccPair.connector.id,
|
||||
ccPair.credential.id,
|
||||
ccPair.id,
|
||||
setPopup
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setPopup({
|
||||
message: `${
|
||||
fromBeginning ? "Complete re-indexing" : "Indexing update"
|
||||
} started successfully`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
message: result.message || "Failed to start indexing",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger indexing:", error);
|
||||
setPopup({
|
||||
message: "An unexpected error occurred while trying to start indexing",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setShowIsResolvingKickoffLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingCCPair) {
|
||||
return;
|
||||
@ -259,7 +348,11 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoadingCCPair || isLoadingIndexAttempts) {
|
||||
if (
|
||||
isLoadingCCPair ||
|
||||
isLoadingIndexAttempts ||
|
||||
isLoadingMostRecentIndexAttempts
|
||||
) {
|
||||
return <ThreeDotsLoader />;
|
||||
}
|
||||
|
||||
@ -292,6 +385,8 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
<>
|
||||
{popup}
|
||||
{showIsResolvingKickoffLoader && !isResolvingErrors && <Spinner />}
|
||||
{ReIndexModal}
|
||||
{ConfirmModal}
|
||||
|
||||
{editingRefreshFrequency && (
|
||||
<EditPropertyModal
|
||||
@ -324,18 +419,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
onResolveAll={async () => {
|
||||
setShowIndexAttemptErrors(false);
|
||||
setShowIsResolvingKickoffLoader(true);
|
||||
await triggerIndexing(
|
||||
true,
|
||||
ccPair.connector.id,
|
||||
ccPair.credential.id,
|
||||
ccPair.id,
|
||||
setPopup
|
||||
);
|
||||
|
||||
// show the loader for a max of 10 seconds
|
||||
setTimeout(() => {
|
||||
setShowIsResolvingKickoffLoader(false);
|
||||
}, 10000);
|
||||
await triggerReIndex(true);
|
||||
}}
|
||||
isResolvingErrors={isResolvingErrors}
|
||||
onPageChange={goToErrorsPage}
|
||||
@ -346,12 +430,21 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
<BackButton
|
||||
behaviorOverride={() => router.push("/admin/indexing/status")}
|
||||
/>
|
||||
<div className="flex items-center justify-between h-14">
|
||||
<div
|
||||
className="flex
|
||||
items-center
|
||||
justify-between
|
||||
h-16
|
||||
pb-2
|
||||
border-b
|
||||
border-neutral-200
|
||||
dark:border-neutral-600"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<SourceIcon iconSize={32} sourceType={ccPair.connector.source} />
|
||||
</div>
|
||||
|
||||
<div className="ml-1 overflow-hidden text-ellipsis whitespace-nowrap flex-1 mr-4">
|
||||
<div className="ml-2 overflow-hidden text-ellipsis whitespace-nowrap flex-1 mr-4">
|
||||
<EditableStringFieldDisplay
|
||||
value={ccPair.name}
|
||||
isEditable={ccPair.is_editable_for_current_user}
|
||||
@ -360,45 +453,109 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ccPair.is_editable_for_current_user && (
|
||||
<div className="ml-auto flex gap-x-2">
|
||||
<ReIndexButton
|
||||
ccPairId={ccPair.id}
|
||||
ccPairStatus={ccPair.status}
|
||||
connectorId={ccPair.connector.id}
|
||||
credentialId={ccPair.credential.id}
|
||||
isDisabled={
|
||||
ccPair.indexing ||
|
||||
ccPair.status === ConnectorCredentialPairStatus.PAUSED
|
||||
}
|
||||
isIndexing={ccPair.indexing}
|
||||
/>
|
||||
|
||||
{!isDeleting && <ModifyStatusButtonCluster ccPair={ccPair} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CCPairStatus
|
||||
status={ccPair.last_index_attempt_status || "not_started"}
|
||||
ccPairStatus={ccPair.status}
|
||||
/>
|
||||
<div className="text-sm mt-1">
|
||||
Creator:{" "}
|
||||
<b className="text-emphasis">{ccPair.creator_email ?? "Unknown"}</b>
|
||||
</div>
|
||||
<div className="text-sm mt-1">
|
||||
Total Documents Indexed:{" "}
|
||||
<b className="text-emphasis">{ccPair.num_docs_indexed}</b>
|
||||
</div>
|
||||
{!ccPair.is_editable_for_current_user && (
|
||||
<div className="text-sm mt-2 text-text-500 italic">
|
||||
{ccPair.access_type === "public"
|
||||
? "Public connectors are not editable by curators."
|
||||
: ccPair.access_type === "sync"
|
||||
? "Sync connectors are not editable by curators unless the curator is also the owner."
|
||||
: "This connector belongs to groups where you don't have curator permissions, so it's not editable."}
|
||||
<div className="ml-auto flex gap-x-2">
|
||||
{ccPair.is_editable_for_current_user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-x-1"
|
||||
>
|
||||
<FiSettings className="h-4 w-4" />
|
||||
<span className="text-sm ml-1">Manage</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItemWithTooltip
|
||||
onClick={() => {
|
||||
if (
|
||||
!ccPair.indexing &&
|
||||
ccPair.status !== ConnectorCredentialPairStatus.PAUSED &&
|
||||
ccPair.status !== ConnectorCredentialPairStatus.INVALID
|
||||
) {
|
||||
showReIndexModal();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
ccPair.indexing ||
|
||||
ccPair.status === ConnectorCredentialPairStatus.PAUSED ||
|
||||
ccPair.status === ConnectorCredentialPairStatus.INVALID
|
||||
}
|
||||
className="flex items-center gap-x-2 cursor-pointer px-3 py-2"
|
||||
tooltip={
|
||||
ccPair.indexing
|
||||
? "Cannot re-index while indexing is already in progress"
|
||||
: ccPair.status === ConnectorCredentialPairStatus.PAUSED
|
||||
? "Resume the connector before re-indexing"
|
||||
: ccPair.status ===
|
||||
ConnectorCredentialPairStatus.INVALID
|
||||
? "Fix the connector configuration before re-indexing"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<RefreshCwIcon className="h-4 w-4" />
|
||||
<span>Re-Index</span>
|
||||
</DropdownMenuItemWithTooltip>
|
||||
{!isDeleting && (
|
||||
<DropdownMenuItemWithTooltip
|
||||
onClick={() =>
|
||||
handleStatusUpdate(
|
||||
statusIsNotCurrentlyActive(ccPair.status)
|
||||
? ConnectorCredentialPairStatus.ACTIVE
|
||||
: ConnectorCredentialPairStatus.PAUSED
|
||||
)
|
||||
}
|
||||
disabled={isStatusUpdating}
|
||||
className="flex items-center gap-x-2 cursor-pointer px-3 py-2"
|
||||
tooltip={
|
||||
isStatusUpdating ? "Status update in progress" : undefined
|
||||
}
|
||||
>
|
||||
{statusIsNotCurrentlyActive(ccPair.status) ? (
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<PauseIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span>
|
||||
{statusIsNotCurrentlyActive(ccPair.status)
|
||||
? "Resume"
|
||||
: "Pause"}
|
||||
</span>
|
||||
</DropdownMenuItemWithTooltip>
|
||||
)}
|
||||
{!isDeleting && (
|
||||
<DropdownMenuItemWithTooltip
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteCCPair(
|
||||
ccPair.connector.id,
|
||||
ccPair.credential.id,
|
||||
setPopup,
|
||||
() => mutate(buildCCPairInfoUrl(ccPair.id))
|
||||
);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
console.error("Error deleting connector:", error);
|
||||
}
|
||||
}}
|
||||
disabled={!statusIsNotCurrentlyActive(ccPair.status)}
|
||||
className="flex items-center gap-x-2 cursor-pointer px-3 py-2 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
tooltip={
|
||||
!statusIsNotCurrentlyActive(ccPair.status)
|
||||
? "Pause the connector before deleting"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItemWithTooltip>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ccPair.deletion_failure_message &&
|
||||
ccPair.status === ConnectorCredentialPairStatus.DELETING && (
|
||||
@ -410,23 +567,8 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{credentialTemplates[ccPair.connector.source] &&
|
||||
ccPair.is_editable_for_current_user && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<Title className="mb-2">Credentials</Title>
|
||||
|
||||
<CredentialSection
|
||||
ccPair={ccPair}
|
||||
sourceType={ccPair.connector.source}
|
||||
refresh={() => refresh()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ccPair.status === ConnectorCredentialPairStatus.INVALID && (
|
||||
<div className="mt-2">
|
||||
<div className="mt-6">
|
||||
<Callout type="warning" title="Invalid Connector State">
|
||||
This connector is in an invalid state. Please update your
|
||||
credentials or create a new connector before re-indexing.
|
||||
@ -434,70 +576,158 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
<ConfigDisplay
|
||||
connectorSpecificConfig={ccPair.connector.connector_specific_config}
|
||||
sourceType={ccPair.connector.source}
|
||||
/>
|
||||
|
||||
{(pruneFreq || indexingStart || refreshFreq) && (
|
||||
<AdvancedConfigDisplay
|
||||
pruneFreq={pruneFreq}
|
||||
indexingStart={indexingStart}
|
||||
refreshFreq={refreshFreq}
|
||||
onRefreshEdit={handleRefreshEdit}
|
||||
onPruningEdit={handlePruningEdit}
|
||||
/>
|
||||
{indexAttemptErrors && indexAttemptErrors.total_items > 0 && (
|
||||
<Alert className="border-alert bg-yellow-50 dark:bg-yellow-800 my-2 mt-6">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-700 dark:text-yellow-500" />
|
||||
<AlertTitle className="text-yellow-950 dark:text-yellow-200 font-semibold">
|
||||
Some documents failed to index
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-yellow-900 dark:text-yellow-300">
|
||||
{isResolvingErrors ? (
|
||||
<span>
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-400 da animate-pulse">
|
||||
Resolving failures
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
We ran into some issues while processing some documents.{" "}
|
||||
<b
|
||||
className="text-link cursor-pointer dark:text-blue-300"
|
||||
onClick={() => setShowIndexAttemptErrors(true)}
|
||||
>
|
||||
View details.
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Title className="mb-2 mt-6" size="md">
|
||||
Indexing
|
||||
</Title>
|
||||
|
||||
<Card className="px-8 py-12">
|
||||
<div className="flex">
|
||||
<div className="w-[200px]">
|
||||
<div className="text-sm font-medium mb-1">Status</div>
|
||||
<CCPairStatus
|
||||
ccPairStatus={ccPair.status}
|
||||
inRepeatedErrorState={ccPair.in_repeated_error_state}
|
||||
lastIndexAttemptStatus={latestIndexAttempt?.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-[200px]">
|
||||
<div className="text-sm font-medium mb-1">Documents Indexed</div>
|
||||
<div className="text-sm text-text-default flex items-center gap-x-1">
|
||||
{ccPair.num_docs_indexed.toLocaleString()}
|
||||
{ccPair.status ===
|
||||
ConnectorCredentialPairStatus.INITIAL_INDEXING &&
|
||||
ccPair.overall_indexing_speed !== null &&
|
||||
ccPair.num_docs_indexed > 0 && (
|
||||
<div className="ml-0.5 text-xs font-medium">
|
||||
({ccPair.overall_indexing_speed.toFixed(1)} docs / min)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[200px]">
|
||||
<div className="text-sm font-medium mb-1">Last Indexed</div>
|
||||
<div className="text-sm text-text-default">
|
||||
{timeAgo(
|
||||
indexAttempts?.find((attempt) => attempt.status === "success")
|
||||
?.time_started
|
||||
) ?? "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ccPair.access_type === "sync" && (
|
||||
<div className="w-[200px]">
|
||||
<div className="text-sm font-medium mb-1">
|
||||
Last Permission Synced
|
||||
</div>
|
||||
<div className="text-sm text-text-default">
|
||||
{timeAgo(ccPair.last_permission_sync) ?? "-"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{credentialTemplates[ccPair.connector.source] &&
|
||||
ccPair.is_editable_for_current_user && (
|
||||
<>
|
||||
<Title size="md" className="mt-10 mb-2">
|
||||
Credential
|
||||
</Title>
|
||||
|
||||
<div className="mt-2">
|
||||
<CredentialSection
|
||||
ccPair={ccPair}
|
||||
sourceType={ccPair.connector.source}
|
||||
refresh={() => refresh()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Title size="md" className="mt-10 mb-2">
|
||||
Connector Configuration
|
||||
</Title>
|
||||
|
||||
<Card className="px-8 py-4">
|
||||
<ConfigDisplay
|
||||
connectorSpecificConfig={ccPair.connector.connector_specific_config}
|
||||
sourceType={ccPair.connector.source}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex">
|
||||
<Title>Indexing Attempts</Title>
|
||||
</div>
|
||||
{indexAttemptErrors && indexAttemptErrors.total_items > 0 && (
|
||||
<Alert className="border-alert bg-yellow-50 dark:bg-yellow-800 my-2">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-700 dark:text-yellow-500" />
|
||||
<AlertTitle className="text-yellow-950 dark:text-yellow-200 font-semibold">
|
||||
Some documents failed to index
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-yellow-900 dark:text-yellow-300">
|
||||
{isResolvingErrors ? (
|
||||
<span>
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-400 da animate-pulse">
|
||||
Resolving failures
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
We ran into some issues while processing some documents.{" "}
|
||||
<b
|
||||
className="text-link cursor-pointer dark:text-blue-300"
|
||||
onClick={() => setShowIndexAttemptErrors(true)}
|
||||
>
|
||||
View details.
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{indexAttempts && (
|
||||
<IndexingAttemptsTable
|
||||
ccPair={ccPair}
|
||||
indexAttempts={indexAttempts}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={goToPage}
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
title="Advanced"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex mt-4">
|
||||
<div className="mx-auto">
|
||||
{ccPair.is_editable_for_current_user && (
|
||||
<DeletionButton ccPair={ccPair} refresh={refresh} />
|
||||
)}
|
||||
</div>
|
||||
{showAdvancedOptions && (
|
||||
<div className="pb-16">
|
||||
{(pruneFreq || indexingStart || refreshFreq) && (
|
||||
<>
|
||||
<Title size="md" className="mt-3 mb-2">
|
||||
Advanced Configuration
|
||||
</Title>
|
||||
<Card className="px-8 py-4">
|
||||
<div>
|
||||
<AdvancedConfigDisplay
|
||||
pruneFreq={pruneFreq}
|
||||
indexingStart={indexingStart}
|
||||
refreshFreq={refreshFreq}
|
||||
onRefreshEdit={handleRefreshEdit}
|
||||
onPruningEdit={handlePruningEdit}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Title size="md" className="mt-6 mb-2">
|
||||
Indexing Attempts
|
||||
</Title>
|
||||
{indexAttempts && (
|
||||
<IndexingAttemptsTable
|
||||
ccPair={ccPair}
|
||||
indexAttempts={indexAttempts}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={goToPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -508,7 +738,7 @@ export default function Page(props: { params: Promise<{ ccPairId: string }> }) {
|
||||
const ccPairId = parseInt(params.ccPairId);
|
||||
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<div className="mx-auto w-[800px]">
|
||||
<Main ccPairId={ccPairId} />
|
||||
</div>
|
||||
);
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
import { UUID } from "crypto";
|
||||
|
||||
export enum ConnectorCredentialPairStatus {
|
||||
SCHEDULED = "SCHEDULED",
|
||||
INITIAL_INDEXING = "INITIAL_INDEXING",
|
||||
ACTIVE = "ACTIVE",
|
||||
PAUSED = "PAUSED",
|
||||
DELETING = "DELETING",
|
||||
@ -31,6 +33,7 @@ export interface CCPairFullInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
status: ConnectorCredentialPairStatus;
|
||||
in_repeated_error_state: boolean;
|
||||
num_docs_indexed: number;
|
||||
connector: Connector<any>;
|
||||
credential: Credential<any>;
|
||||
@ -43,6 +46,12 @@ export interface CCPairFullInfo {
|
||||
indexing: boolean;
|
||||
creator: UUID | null;
|
||||
creator_email: string | null;
|
||||
|
||||
last_indexed: string | null;
|
||||
last_pruned: string | null;
|
||||
last_permission_sync: string | null;
|
||||
overall_indexing_speed: number | null;
|
||||
latest_checkpoint_description: string | null;
|
||||
}
|
||||
|
||||
export interface PaginatedIndexAttempts {
|
||||
|
0
web/src/app/admin/connector/[ccPairId]/unused.txt
Normal file
0
web/src/app/admin/connector/[ccPairId]/unused.txt
Normal file
75
web/src/app/admin/connector/[ccPairId]/useStatusChange.tsx
Normal file
75
web/src/app/admin/connector/[ccPairId]/useStatusChange.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { CCPairFullInfo, ConnectorCredentialPairStatus } from "./types";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { mutate } from "swr";
|
||||
import { buildCCPairInfoUrl } from "./lib";
|
||||
import { setCCPairStatus } from "@/lib/ccPair";
|
||||
import { useState } from "react";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
|
||||
// Export the status change functionality separately
|
||||
export function useStatusChange(ccPair: CCPairFullInfo | null) {
|
||||
const { setPopup } = usePopup();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
|
||||
const updateStatus = async (newStatus: ConnectorCredentialPairStatus) => {
|
||||
if (!ccPair) return false;
|
||||
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
// Call the backend to update the status
|
||||
await setCCPairStatus(ccPair.id, newStatus, setPopup);
|
||||
|
||||
// Use mutate to revalidate the status on the backend
|
||||
await mutate(buildCCPairInfoUrl(ccPair.id));
|
||||
} catch (error) {
|
||||
console.error("Failed to update status", error);
|
||||
} finally {
|
||||
// Reset local updating state and button text after mutation
|
||||
setIsUpdating(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleStatusChange = async (
|
||||
newStatus: ConnectorCredentialPairStatus
|
||||
) => {
|
||||
if (isUpdating || !ccPair) return false; // Prevent double-clicks or multiple requests
|
||||
|
||||
if (
|
||||
ccPair.status === ConnectorCredentialPairStatus.INVALID &&
|
||||
newStatus === ConnectorCredentialPairStatus.ACTIVE
|
||||
) {
|
||||
setShowConfirmModal(true);
|
||||
return false;
|
||||
} else {
|
||||
return await updateStatus(newStatus);
|
||||
}
|
||||
};
|
||||
|
||||
const ConfirmModal =
|
||||
showConfirmModal && ccPair ? (
|
||||
<ConfirmEntityModal
|
||||
entityType="Invalid Connector"
|
||||
entityName={ccPair.name}
|
||||
onClose={() => setShowConfirmModal(false)}
|
||||
onSubmit={() => {
|
||||
setShowConfirmModal(false);
|
||||
updateStatus(ConnectorCredentialPairStatus.ACTIVE);
|
||||
}}
|
||||
additionalDetails="This connector was previously marked as invalid. Please verify that your configuration is correct before re-enabling. Are you sure you want to proceed?"
|
||||
actionButtonText="Re-Enable"
|
||||
variant="action"
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return {
|
||||
handleStatusChange,
|
||||
isUpdating,
|
||||
ConfirmModal,
|
||||
};
|
||||
}
|
@ -9,7 +9,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { IndexAttemptStatus } from "@/components/Status";
|
||||
import { CCPairStatus, IndexAttemptStatus } from "@/components/Status";
|
||||
import { timeAgo } from "@/lib/time";
|
||||
import {
|
||||
ConnectorIndexingStatus,
|
||||
@ -114,17 +114,6 @@ function SummaryRow({
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-300">
|
||||
Errors
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-lg gap-x-1 font-semibold">
|
||||
{summary.errors > 0 && <Warning className="text-error h-6 w-6" />}
|
||||
{summary.errors}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
);
|
||||
@ -147,59 +136,6 @@ function ConnectorRow({
|
||||
router.push(`/admin/connector/${ccPairsIndexingStatus.cc_pair_id}`);
|
||||
};
|
||||
|
||||
const getActivityBadge = () => {
|
||||
if (
|
||||
ccPairsIndexingStatus.cc_pair_status ===
|
||||
ConnectorCredentialPairStatus.DELETING
|
||||
) {
|
||||
return <Badge variant="destructive">Deleting</Badge>;
|
||||
} else if (
|
||||
ccPairsIndexingStatus.cc_pair_status ===
|
||||
ConnectorCredentialPairStatus.PAUSED
|
||||
) {
|
||||
return (
|
||||
<Badge icon={FiPauseCircle} variant="paused">
|
||||
Paused
|
||||
</Badge>
|
||||
);
|
||||
} else if (
|
||||
ccPairsIndexingStatus.cc_pair_status ===
|
||||
ConnectorCredentialPairStatus.INVALID
|
||||
) {
|
||||
return (
|
||||
<Badge
|
||||
tooltip="Connector is in an invalid state. Please update the credentials or create a new connector."
|
||||
circle
|
||||
variant="invalid"
|
||||
>
|
||||
Invalid
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// ACTIVE case
|
||||
switch (ccPairsIndexingStatus.last_status) {
|
||||
case "in_progress":
|
||||
return (
|
||||
<Badge circle variant="success">
|
||||
Indexing
|
||||
</Badge>
|
||||
);
|
||||
case "not_started":
|
||||
return (
|
||||
<Badge circle variant="not_started">
|
||||
Scheduled
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge circle variant="success">
|
||||
Active
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className={`
|
||||
@ -221,20 +157,28 @@ border border-border dark:border-neutral-700
|
||||
<TableCell>
|
||||
{timeAgo(ccPairsIndexingStatus?.last_success) || "-"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{getActivityBadge()}</TableCell>
|
||||
<TableCell>
|
||||
<CCPairStatus
|
||||
ccPairStatus={ccPairsIndexingStatus.cc_pair_status}
|
||||
inRepeatedErrorState={ccPairsIndexingStatus.in_repeated_error_state}
|
||||
lastIndexAttemptStatus={
|
||||
ccPairsIndexingStatus.latest_index_attempt?.status
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<TableCell>
|
||||
{ccPairsIndexingStatus.access_type === "public" ? (
|
||||
<Badge variant={isEditable ? "success" : "default"} icon={FiUnlock}>
|
||||
Public
|
||||
Organization Public
|
||||
</Badge>
|
||||
) : ccPairsIndexingStatus.access_type === "sync" ? (
|
||||
<Badge
|
||||
variant={isEditable ? "auto-sync" : "default"}
|
||||
icon={FiRefreshCw}
|
||||
>
|
||||
Auto-Sync
|
||||
Inherited from{" "}
|
||||
{getSourceDisplayName(ccPairsIndexingStatus.connector.source)}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={isEditable ? "private" : "default"} icon={FiLock}>
|
||||
@ -244,12 +188,6 @@ border border-border dark:border-neutral-700
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{ccPairsIndexingStatus.docs_indexed}</TableCell>
|
||||
<TableCell>
|
||||
<IndexAttemptStatus
|
||||
status={ccPairsIndexingStatus.last_finished_status || null}
|
||||
errorMsg={ccPairsIndexingStatus?.latest_index_attempt?.error_msg}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditable && (
|
||||
<TooltipProvider>
|
||||
@ -564,6 +502,7 @@ export function CCPairIndexingStatusTable({
|
||||
name: "Sample Credential",
|
||||
source: ValidSources.File,
|
||||
user_id: "1",
|
||||
user_email: "sample@example.com",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
credential_json: {},
|
||||
@ -575,6 +514,7 @@ export function CCPairIndexingStatusTable({
|
||||
last_finished_status: "success",
|
||||
latest_index_attempt: null,
|
||||
groups: [], // Add this line
|
||||
in_repeated_error_state: false,
|
||||
}}
|
||||
isEditable={false}
|
||||
/>
|
||||
@ -691,12 +631,11 @@ export function CCPairIndexingStatusTable({
|
||||
<TableRow className="border border-border dark:border-neutral-700">
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Last Indexed</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<TableHead>Permissions</TableHead>
|
||||
)}
|
||||
<TableHead>Total Docs</TableHead>
|
||||
<TableHead>Last Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
{(sourceMatches ? statuses : matchingConnectors).map(
|
||||
|
@ -95,12 +95,14 @@ export function IndexAttemptStatus({
|
||||
}
|
||||
|
||||
export function CCPairStatus({
|
||||
status,
|
||||
ccPairStatus,
|
||||
inRepeatedErrorState,
|
||||
lastIndexAttemptStatus,
|
||||
size = "md",
|
||||
}: {
|
||||
status: ValidStatuses;
|
||||
ccPairStatus: ConnectorCredentialPairStatus;
|
||||
inRepeatedErrorState: boolean;
|
||||
lastIndexAttemptStatus: ValidStatuses | undefined | null;
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
}) {
|
||||
let badge;
|
||||
@ -117,24 +119,48 @@ export function CCPairStatus({
|
||||
Paused
|
||||
</Badge>
|
||||
);
|
||||
} else if (ccPairStatus == ConnectorCredentialPairStatus.INVALID) {
|
||||
badge = (
|
||||
<Badge variant="invalid" icon={FiAlertTriangle}>
|
||||
Invalid
|
||||
</Badge>
|
||||
);
|
||||
} else if (status === "failed") {
|
||||
} else if (inRepeatedErrorState) {
|
||||
badge = (
|
||||
<Badge variant="destructive" icon={FiAlertTriangle}>
|
||||
Error
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
} else if (ccPairStatus == ConnectorCredentialPairStatus.SCHEDULED) {
|
||||
badge = (
|
||||
<Badge variant="success" icon={FiCheckCircle}>
|
||||
Active
|
||||
<Badge variant="not_started" icon={FiClock}>
|
||||
Scheduled
|
||||
</Badge>
|
||||
);
|
||||
} else if (ccPairStatus == ConnectorCredentialPairStatus.INITIAL_INDEXING) {
|
||||
badge = (
|
||||
<Badge variant="in_progress" icon={FiClock}>
|
||||
Initial Indexing
|
||||
</Badge>
|
||||
);
|
||||
} else if (ccPairStatus == ConnectorCredentialPairStatus.INVALID) {
|
||||
badge = (
|
||||
<Badge
|
||||
tooltip="Connector is in an invalid state. Please update the credentials or create a new connector."
|
||||
circle
|
||||
variant="invalid"
|
||||
>
|
||||
Invalid
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
if (lastIndexAttemptStatus && lastIndexAttemptStatus === "in_progress") {
|
||||
badge = (
|
||||
<Badge variant="in_progress" icon={FiClock}>
|
||||
Indexing
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
badge = (
|
||||
<Badge variant="success" icon={FiCheckCircle}>
|
||||
Indexed
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div>{badge}</div>;
|
||||
|
@ -3,8 +3,9 @@
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { FaSwatchbook } from "react-icons/fa";
|
||||
import { FaKey } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { FiEdit2 } from "react-icons/fi";
|
||||
import {
|
||||
deleteCredential,
|
||||
swapCredential,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
} from "@/lib/connectors/oauth";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { CreateStdOAuthCredential } from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import { Card } from "../ui/card";
|
||||
|
||||
export default function CredentialSection({
|
||||
ccPair,
|
||||
@ -154,26 +156,66 @@ export default function CredentialSection({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-start flex-col gap-y-2">
|
||||
<div
|
||||
className="flex
|
||||
flex-col
|
||||
gap-y-4
|
||||
rounded-lg
|
||||
bg-background"
|
||||
>
|
||||
{popup}
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
<p>Current credential:</p>
|
||||
<Text className="ml-1 italic font-bold my-auto">
|
||||
{ccPair.credential.name || `Credential #${ccPair.credential.id}`}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex text-sm justify-start mr-auto gap-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowModifyCredential(true);
|
||||
}}
|
||||
className="flex items-center gap-x-2 cursor-pointer bg-neutral-800 border-neutral-600 border-2 hover:bg-neutral-700 p-1.5 rounded-lg text-neutral-300"
|
||||
>
|
||||
<FaSwatchbook />
|
||||
Update Credentials
|
||||
</button>
|
||||
</div>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 mr-3">
|
||||
<FaKey className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col justify-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Text className="font-medium">
|
||||
{ccPair.credential.name ||
|
||||
`Credential #${ccPair.credential.id}`}
|
||||
</Text>
|
||||
<div className="text-xs text-muted-foreground/70">
|
||||
Created{" "}
|
||||
<i>
|
||||
{new Date(
|
||||
ccPair.credential.time_created
|
||||
).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</i>
|
||||
{ccPair.credential.user_email && (
|
||||
<>
|
||||
{" "}
|
||||
by <i>{ccPair.credential.user_email}</i>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModifyCredential(true)}
|
||||
className="inline-flex
|
||||
items-center
|
||||
justify-center
|
||||
p-2
|
||||
rounded-md
|
||||
text-muted-foreground
|
||||
hover:bg-accent
|
||||
hover:text-accent-foreground
|
||||
transition-colors"
|
||||
>
|
||||
<FiEdit2 className="h-4 w-4" />
|
||||
<span className="sr-only">Update Credentials</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{showModifyCredential && (
|
||||
<Modal
|
||||
onOutsideClick={closeModifyCredential}
|
||||
|
@ -119,6 +119,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
asChild = false,
|
||||
icon: Icon,
|
||||
tooltip,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@ -134,6 +135,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
})
|
||||
)}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{Icon && <Icon />}
|
||||
@ -145,8 +147,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div>{button}</div>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={disabled ? "cursor-not-allowed" : ""}>
|
||||
{button}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent showTick={true}>
|
||||
<p>{tooltip}</p>
|
||||
|
57
web/src/components/ui/dropdown-menu-with-tooltip.tsx
Normal file
57
web/src/components/ui/dropdown-menu-with-tooltip.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { DropdownMenuItem } from "./dropdown-menu";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DropdownMenuItemWithTooltipProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DropdownMenuItem> {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
const DropdownMenuItemWithTooltip = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuItem>,
|
||||
DropdownMenuItemWithTooltipProps
|
||||
>(({ className, tooltip, disabled, ...props }, ref) => {
|
||||
// Only show tooltip if the item is disabled and a tooltip is provided
|
||||
if (!tooltip || !disabled) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
ref={ref}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="cursor-not-allowed">
|
||||
<DropdownMenuItem
|
||||
ref={ref}
|
||||
className={cn(className)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent showTick={true}>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuItemWithTooltip.displayName = "DropdownMenuItemWithTooltip";
|
||||
|
||||
export { DropdownMenuItemWithTooltip };
|
@ -23,6 +23,7 @@ export interface CredentialBase<T> {
|
||||
export interface Credential<T> extends CredentialBase<T> {
|
||||
id: number;
|
||||
user_id: string | null;
|
||||
user_email: string | null;
|
||||
time_created: string;
|
||||
time_updated: string;
|
||||
}
|
||||
|
@ -170,6 +170,7 @@ export interface ConnectorIndexingStatus<
|
||||
last_status: ValidStatuses | null;
|
||||
last_finished_status: ValidStatuses | null;
|
||||
cc_pair_status: ConnectorCredentialPairStatus;
|
||||
in_repeated_error_state: boolean;
|
||||
latest_index_attempt: IndexAttemptSnapshot | null;
|
||||
docs_indexed: number;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user