From 6a2ce321e844ecf44997f7cf2f7a0def3537977f Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:27:57 +0000 Subject: [PATCH] feat(web): Manage Index menu, activity cards, and runs card per the indexing mock (#16172) Co-authored-by: Nik Co-authored-by: nik --- ...1a2f4b9d3_record_failed_prune_doc_count.py | 28 ++ .../background/celery/tasks/pruning/tasks.py | 3 + .../background/celery/tasks/shared/tasks.py | 14 +- backend/onyx/db/cc_pair_runs.py | 18 +- backend/onyx/db/models.py | 1 + backend/onyx/db/sync_record.py | 4 + backend/onyx/redis/redis_connector_prune.py | 19 + backend/onyx/server/documents/models.py | 2 + .../indexing/test_cc_pair_run_history.py | 21 + .../components/cards/card/Card.stories.tsx | 1 + .../opal/src/components/cards/card/styles.css | 8 + web/lib/opal/src/components/tag/README.md | 3 +- web/lib/opal/src/components/tag/colors.ts | 5 +- web/lib/opal/src/types.ts | 1 + web/src/i18n/messages/ar.json | 24 +- web/src/i18n/messages/de.json | 28 +- web/src/i18n/messages/en.json | 24 +- web/src/i18n/messages/es.json | 24 +- web/src/i18n/messages/fr.json | 24 +- web/src/i18n/messages/ja.json | 30 +- web/src/i18n/messages/ko.json | 24 +- web/src/i18n/messages/pt.json | 24 +- web/src/i18n/messages/zh.json | 30 +- web/src/lib/connectors/types/ccPair.ts | 1 + .../components/CredentialChecksCard.tsx | 25 +- .../ConnectorDetailPage/ActivityCard.tsx | 129 +++-- .../ConnectorDetailPage/ActivityPanel.tsx | 151 +++--- .../ConnectorDetailPage/CheckRunPopover.tsx | 28 +- .../ConnectorDetailPage/FailedDocsPopover.tsx | 57 ++- .../connectors/ConnectorDetailPage/RunRow.tsx | 444 ++++++++++++------ .../ConnectorDetailPage/RunsCard.tsx | 161 ++++++- .../StageDetailsPopover.tsx | 14 +- .../ConnectorDetailPage/StatusCard.tsx | 54 +-- .../ConnectorDetailPage/ThroughputChart.tsx | 52 +- .../ConnectorDetailPage/focusMode.ts | 6 + .../connectors/ConnectorDetailPage/index.tsx | 215 ++++++--- .../ConnectorDetailPage/statusTag.ts | 7 +- 37 files changed, 1193 insertions(+), 511 deletions(-) create mode 100644 backend/alembic/versions/c7e1a2f4b9d3_record_failed_prune_doc_count.py create mode 100644 web/src/views/admin/connectors/ConnectorDetailPage/focusMode.ts diff --git a/backend/alembic/versions/c7e1a2f4b9d3_record_failed_prune_doc_count.py b/backend/alembic/versions/c7e1a2f4b9d3_record_failed_prune_doc_count.py new file mode 100644 index 0000000000..8470956990 --- /dev/null +++ b/backend/alembic/versions/c7e1a2f4b9d3_record_failed_prune_doc_count.py @@ -0,0 +1,28 @@ +"""record the documents a prune could not remove + +Revision ID: c7e1a2f4b9d3 +Revises: c3d9a1f4b7e2 +Create Date: 2026-10-16 20:40:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "c7e1a2f4b9d3" +down_revision = "c3d9a1f4b7e2" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "sync_record", + sa.Column("num_docs_failed", sa.Integer(), nullable=False, server_default="0"), + ) + + +def downgrade() -> None: + op.drop_column("sync_record", "num_docs_failed") diff --git a/backend/onyx/background/celery/tasks/pruning/tasks.py b/backend/onyx/background/celery/tasks/pruning/tasks.py index 6c542f8ea9..2a6ff1f4dd 100644 --- a/backend/onyx/background/celery/tasks/pruning/tasks.py +++ b/backend/onyx/background/celery/tasks/pruning/tasks.py @@ -909,6 +909,8 @@ def monitor_ccpair_pruning_taskset( # A generator that threw after fan-out still drains its tasks; record FAILED # so the prune retries after the backoff instead of advancing last_pruned. failed = redis_connector.prune.generator_failed + # Read before any commit, so a Redis error here leaves nothing half done. + failed_docs = redis_connector.prune.failed_docs if failed: task_logger.warning( f"Connector pruning failed after fan-out: cc_pair={cc_pair_id} num_pruned={initial}" @@ -931,6 +933,7 @@ def monitor_ccpair_pruning_taskset( sync_type=SyncType.PRUNING, sync_status=SyncStatus.FAILED if failed else SyncStatus.SUCCESS, num_docs_synced=initial, + num_docs_failed=failed_docs, ) redis_connector.prune.taskset_clear() diff --git a/backend/onyx/background/celery/tasks/shared/tasks.py b/backend/onyx/background/celery/tasks/shared/tasks.py index 84bd8ddc4e..c9ecd5421a 100644 --- a/backend/onyx/background/celery/tasks/shared/tasks.py +++ b/backend/onyx/background/celery/tasks/shared/tasks.py @@ -34,6 +34,7 @@ from onyx.db.relationships import delete_document_references_from_kg from onyx.db.search_settings import get_active_search_settings from onyx.document_index.factory import get_default_document_index from onyx.document_index.interfaces import MetadataUpdateRequest +from onyx.redis.redis_connector import RedisConnector from onyx.redis.redis_pool import get_redis_client from onyx.server.documents.models import ConnectorCredentialPairIdentifier @@ -111,7 +112,8 @@ def document_by_cc_pair_cleanup_task( document_id: str, connector_id: int, credential_id: int, - tenant_id: str, # noqa: ARG001 — kept on the celery task signature + tenant_id: str, + prune_cc_pair_id: int | None = None, ) -> bool: """Remove one document relationship and reconcile its index metadata. @@ -303,6 +305,16 @@ def document_by_cc_pair_cleanup_task( db_session, connector_id, credential_id, document_id ) db_session.commit() + if prune_cc_pair_id is not None: + # The count is advisory, so a Redis failure must not change the outcome. + try: + RedisConnector( + tenant_id, prune_cc_pair_id + ).prune.increment_failed_docs() + except Exception: + task_logger.exception( + f"Could not count the failed prune doc: cc_pair={prune_cc_pair_id} doc={document_id}" + ) completion_status = OnyxCeleryTaskCompletionStatus.NON_RETRYABLE_EXCEPTION else: # Exponential backoff from 2^4 to 2^6 ... i.e. 16, 32, 64 diff --git a/backend/onyx/db/cc_pair_runs.py b/backend/onyx/db/cc_pair_runs.py index 102f96ef3d..3eaddd2509 100644 --- a/backend/onyx/db/cc_pair_runs.py +++ b/backend/onyx/db/cc_pair_runs.py @@ -383,6 +383,21 @@ def _doc_permission_sync_runs( ) +def _prune_errors(run_filter: CCPairRunFilter) -> ColumnElement[bool] | None: + """The ``errors`` filter for a prune, which counts the documents it could not + remove. Nothing resolves these.""" + failed = SyncRecord.num_docs_failed + match run_filter.errors: + case CCPairRunErrors.UNRESOLVED: + return failed > 0 + case CCPairRunErrors.RESOLVED: + return false() + case CCPairRunErrors.NONE: + return failed == 0 + case None: + return None + + def _group_sync_runs(run_filter: CCPairRunFilter, with_status_filter: bool) -> Select: stmt = select(ExternalGroupPermissionSyncAttempt).where( _group_sync_scope(run_filter) @@ -423,7 +438,6 @@ def _prune_runs(run_filter: CCPairRunFilter, with_status_filter: bool) -> Select newest.sync_type == SyncType.PRUNING, ) ) - # A prune records no per-document errors, so it either fails or succeeds. return _narrowed( stmt, run_filter, @@ -432,7 +446,7 @@ def _prune_runs(run_filter: CCPairRunFilter, with_status_filter: bool) -> Select SyncRecord.sync_status, SyncStatus, with_status_filter, - _no_item_errors(run_filter), + _prune_errors(run_filter), has_newer_run, run_is_resolved(CCPairRunKind.PRUNING, SyncRecord.id), ) diff --git a/backend/onyx/db/models.py b/backend/onyx/db/models.py index 9b7a268515..4bc29dc843 100644 --- a/backend/onyx/db/models.py +++ b/backend/onyx/db/models.py @@ -3180,6 +3180,7 @@ class SyncRecord(Base): sync_status: Mapped[SyncStatus] = mapped_column(Enum(SyncStatus, native_enum=False)) num_docs_synced: Mapped[int] = mapped_column(Integer, default=0) + num_docs_failed: Mapped[int] = mapped_column(Integer, default=0, server_default="0") sync_start_time: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True)) sync_end_time: Mapped[datetime.datetime | None] = mapped_column( diff --git a/backend/onyx/db/sync_record.py b/backend/onyx/db/sync_record.py index 8c385e675a..66236a90b3 100644 --- a/backend/onyx/db/sync_record.py +++ b/backend/onyx/db/sync_record.py @@ -133,6 +133,7 @@ def update_sync_record_status( sync_type: SyncType, sync_status: SyncStatus, num_docs_synced: int | None = None, + num_docs_failed: int | None = None, ) -> None: """Update the status of a sync record. @@ -142,6 +143,7 @@ def update_sync_record_status( sync_type: The type of sync operation sync_status: The new status to set num_docs_synced: Optional number of documents synced to update + num_docs_failed: Optional number of documents that failed to sync """ sync_record = fetch_latest_sync_record(db_session, entity_id, sync_type) if sync_record is None: @@ -155,6 +157,8 @@ def update_sync_record_status( sync_record.sync_status = sync_status if num_docs_synced is not None: sync_record.num_docs_synced = num_docs_synced + if num_docs_failed is not None: + sync_record.num_docs_failed = num_docs_failed if sync_status.is_terminal(): sync_record.sync_end_time = func.now() diff --git a/backend/onyx/redis/redis_connector_prune.py b/backend/onyx/redis/redis_connector_prune.py index 7e5d4a0a8a..79c3f318cc 100644 --- a/backend/onyx/redis/redis_connector_prune.py +++ b/backend/onyx/redis/redis_connector_prune.py @@ -69,6 +69,9 @@ class RedisConnectorPrune: # it to record the prune FAILED instead of falsely advancing last_pruned. GENERATOR_FAILED_PREFIX = PREFIX + "_generator_failed" + # Number of documents whose cleanup still failed after its last retry. + FAILED_DOCS_PREFIX = PREFIX + "_failed_docs" + def __init__(self, tenant_id: str, id: int, redis: TenantRedisClient) -> None: self.tenant_id: str = tenant_id self.id = id @@ -85,6 +88,7 @@ class RedisConnectorPrune: self.active_key = f"{self.ACTIVE_PREFIX}_{id}" self.failure_backoff_key = f"{self.FAILURE_BACKOFF_PREFIX}_{id}" self.generator_failed_key = f"{self.GENERATOR_FAILED_PREFIX}_{id}" + self.failed_docs_key = f"{self.FAILED_DOCS_PREFIX}_{id}" def taskset_clear(self) -> None: self.redis.delete(self.taskset_key) @@ -93,6 +97,7 @@ class RedisConnectorPrune: self.redis.delete(self.generator_progress_key) self.redis.delete(self.generator_complete_key) self.redis.delete(self.generator_failed_key) + self.redis.delete(self.failed_docs_key) def get_remaining(self) -> int: # todo: move into fence @@ -169,6 +174,15 @@ class RedisConnectorPrune: def generator_failed(self) -> bool: return bool(self.redis.exists(self.generator_failed_key)) + def increment_failed_docs(self) -> None: + self.redis.incr(self.failed_docs_key) + self.redis.expire(self.failed_docs_key, self.FENCE_TTL) + + @property + def failed_docs(self) -> int: + value = self.redis.get(self.failed_docs_key) + return int(value) if value is not None else 0 + @property def generator_complete(self) -> int | None: """the fence payload is an int representing the starting number of @@ -232,6 +246,7 @@ class RedisConnectorPrune: "connector_id": cc_pair.connector_id, "credential_id": cc_pair.credential_id, "tenant_id": self.tenant_id, + "prune_cc_pair_id": cc_pair.id, }, queue=OnyxCeleryQueues.CONNECTOR_DELETION, task_id=custom_task_id, @@ -249,6 +264,7 @@ class RedisConnectorPrune: self.redis.delete(self.generator_progress_key) self.redis.delete(self.generator_complete_key) self.redis.delete(self.generator_failed_key) + self.redis.delete(self.failed_docs_key) self.redis.delete(self.taskset_key) self.redis.delete(self.fence_key) @@ -281,3 +297,6 @@ class RedisConnectorPrune: for key in r.scan_iter(RedisConnectorPrune.GENERATOR_FAILED_PREFIX + "*"): r.delete(key) + + for key in r.scan_iter(RedisConnectorPrune.FAILED_DOCS_PREFIX + "*"): + r.delete(key) diff --git a/backend/onyx/server/documents/models.py b/backend/onyx/server/documents/models.py index a809ba60b3..909d8fbfd9 100644 --- a/backend/onyx/server/documents/models.py +++ b/backend/onyx/server/documents/models.py @@ -569,6 +569,7 @@ class PruneRunSnapshot(BaseModel): id: int status: SyncStatus docs_removed: int + docs_failed: int error_message: str | None time_created: str time_started: str | None @@ -580,6 +581,7 @@ class PruneRunSnapshot(BaseModel): id=record.id, status=record.sync_status, docs_removed=record.num_docs_synced, + docs_failed=record.num_docs_failed, error_message=record.error_message, time_created=record.sync_start_time.isoformat(), time_started=record.sync_start_time.isoformat(), diff --git a/backend/tests/external_dependency_unit/indexing/test_cc_pair_run_history.py b/backend/tests/external_dependency_unit/indexing/test_cc_pair_run_history.py index 3b2ac2ef79..4ba4a56b2e 100644 --- a/backend/tests/external_dependency_unit/indexing/test_cc_pair_run_history.py +++ b/backend/tests/external_dependency_unit/indexing/test_cc_pair_run_history.py @@ -69,6 +69,7 @@ from onyx.server.documents.models import ( CCPairRunsResponse, ConnectorSnapshot, ConnectorUpdateRequest, + PruneRunSnapshot, ) from tests.external_dependency_unit.conftest import create_test_user @@ -119,12 +120,14 @@ def _prune( status: SyncStatus, docs_removed: int = 0, error_message: str | None = None, + docs_failed: int = 0, ) -> SyncRecord: record = SyncRecord( entity_id=cc_pair.id, sync_type=SyncType.PRUNING, sync_status=status, num_docs_synced=docs_removed, + num_docs_failed=docs_failed, sync_start_time=_T0 + timedelta(minutes=minutes), sync_end_time=( None @@ -315,6 +318,24 @@ def test_prunes_and_check_runs_join_the_feed(db_session: Session) -> None: assert _order(with_doc_errors) == [] +def test_a_prune_with_failed_docs_lists_with_the_errors(db_session: Session) -> None: + admin = create_test_user(db_session, "history_admin", is_admin=True) + cc_pair = _create_cc_pair(db_session) + _prune(db_session, cc_pair, 0, SyncStatus.SUCCESS, docs_removed=5) + failed_docs = _prune( + db_session, cc_pair, 1, SyncStatus.SUCCESS, docs_removed=3, docs_failed=2 + ) + + with_errors = _runs(db_session, admin, cc_pair, errors=CCPairRunErrors.UNRESOLVED) + resolved = _runs(db_session, admin, cc_pair, errors=CCPairRunErrors.RESOLVED) + + assert _order(with_errors) == [(CCPairRunKind.PRUNING, failed_docs.id)] + attempt = with_errors.items[0].attempt + assert isinstance(attempt, PruneRunSnapshot) + assert attempt.docs_failed == 2 + assert _order(resolved) == [] + + def test_check_run_lifecycle_writes_one_history_row_per_run( db_session: Session, ) -> None: diff --git a/web/lib/opal/src/components/cards/card/Card.stories.tsx b/web/lib/opal/src/components/cards/card/Card.stories.tsx index 55fbaa691b..e4aed666e7 100644 --- a/web/lib/opal/src/components/cards/card/Card.stories.tsx +++ b/web/lib/opal/src/components/cards/card/Card.stories.tsx @@ -6,6 +6,7 @@ const CARD_COLORS = [ "transparent", "background-tint-00", "background-tint-01", + "background-tint-02", "status-info-00", "status-success-00", "status-warning-00", diff --git a/web/lib/opal/src/components/cards/card/styles.css b/web/lib/opal/src/components/cards/card/styles.css index a99e1a21e2..a348bc746d 100644 --- a/web/lib/opal/src/components/cards/card/styles.css +++ b/web/lib/opal/src/components/cards/card/styles.css @@ -25,6 +25,10 @@ @apply bg-background-tint-01; } +.opal-card[data-color="background-tint-02"] { + @apply bg-background-tint-02; +} + .opal-card[data-color="status-info-00"] { @apply bg-status-info-00; } @@ -118,6 +122,10 @@ @apply bg-background-tint-01; } +.opal-card-expandable-header[data-color="background-tint-02"] { + @apply bg-background-tint-02; +} + .opal-card-expandable-header[data-color="status-info-00"] { @apply bg-status-info-00; } diff --git a/web/lib/opal/src/components/tag/README.md b/web/lib/opal/src/components/tag/README.md index d4b6fe1efd..951c345a47 100644 --- a/web/lib/opal/src/components/tag/README.md +++ b/web/lib/opal/src/components/tag/README.md @@ -40,7 +40,7 @@ Status tags use `font="secondary-body"`; `sm` keeps `figure-small-value` by defa ### `TagColor` -`"green" | "blue" | "purple" | "amber" | "orange" | "red" | "gray"` +`"green" | "blue" | "purple" | "amber" | "orange" | "red" | "gray" | "white"` | Color | Background | Text | | -------- | -------------------- | ------------------------------------------ | @@ -51,6 +51,7 @@ Status tags use `font="secondary-body"`; `sm` keeps `figure-small-value` by defa | `orange` | `status-warning-01` | `status-warning-05` | | `red` | `status-error-01` | `status-error-05` | | `gray` | `background-tint-02` | `text-03` (passive) / `text-04` (editable) | +| `white` | `background-tint-00` | `text-04`, for a pill on a tinted row | ## Usage Examples diff --git a/web/lib/opal/src/components/tag/colors.ts b/web/lib/opal/src/components/tag/colors.ts index 1245d0d45e..741ade2e59 100644 --- a/web/lib/opal/src/components/tag/colors.ts +++ b/web/lib/opal/src/components/tag/colors.ts @@ -9,7 +9,8 @@ export type TagColor = | "gray" | "amber" | "orange" - | "red"; + | "red" + | "white"; export const TAG_COLORS: Record = { green: { bg: "bg-theme-green-01", text: "text-theme-green-05" }, @@ -19,4 +20,6 @@ export const TAG_COLORS: Record = { orange: { bg: "bg-status-warning-01", text: "text-status-warning-05" }, red: { bg: "bg-status-error-01", text: "text-status-error-05" }, gray: { bg: "bg-background-tint-02", text: "text-text-03" }, + // A pill on a tinted row, such as "Required" on a failed check. + white: { bg: "bg-background-tint-00", text: "text-text-04" }, }; diff --git a/web/lib/opal/src/types.ts b/web/lib/opal/src/types.ts index 6bc63f9ae8..b2fb8d1cf8 100644 --- a/web/lib/opal/src/types.ts +++ b/web/lib/opal/src/types.ts @@ -141,6 +141,7 @@ export type CardColor = | "transparent" | "background-tint-00" | "background-tint-01" + | "background-tint-02" | "status-info-00" | "status-success-00" | "status-warning-00" diff --git a/web/src/i18n/messages/ar.json b/web/src/i18n/messages/ar.json index c24d1593b9..d480c3de93 100644 --- a/web/src/i18n/messages/ar.json +++ b/web/src/i18n/messages/ar.json @@ -2714,8 +2714,7 @@ "reIndex": { "tooltip": { "indexing": "لا يمكن إعادة الفهرسة أثناء وجود فهرسة قيد التنفيذ", - "invalid": "أصلح إعدادات الموصل قبل إعادة الفهرسة", - "paused": "استأنف الموصل قبل إعادة الفهرسة" + "invalid": "أصلح إعدادات الموصل قبل إعادة الفهرسة" } }, "toggleStatus": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "إعادة فهرسة كاملة", "description": "إعادة فهرسة كل المستندات من المصدر. قد يستغرق ذلك **ساعات أو أيامًا** حسب حجم المحتوى." + }, + "startIndexing": { + "label": "بدء الفهرسة", + "description": "تخطَّ فحوصات الموصل وابدأ الفهرسة الآن." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "مستندات مفهرسة" }, "indexingSpeed": "{speed} مستند/دقيقة", + "indexingSpeedTooltip": "المستندات المفهرسة في الدقيقة منذ إنشاء الموصل", "lastIndexed": { "value": "آخر فهرسة {when}" }, @@ -2942,7 +2946,9 @@ "queued": "في قائمة الانتظار", "running": "قيد التشغيل", "rate": "{rate} مستند/دقيقة", - "finished": "{time} ({minutes, number} دقيقة)" + "finished": "{time} ({minutes, number} دقيقة)", + "rateFinished": "{rate} مستند/دقيقة ({minutes, number} دقيقة)", + "membershipRate": "{rate} عضوية/دقيقة" }, "actions": { "stages": "عرض مقاييس المراحل", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, one {تم إلغاء # تشغيل} other {تم إلغاء # تشغيلات}}", "passedChecks": "{count, plural, one {نجح فحص واحد} other {نجح # فحوصات}}", "failedChecks": "{count, plural, one {فشل فحص واحد} other {فشل # فحوصات}}", - "skippedChecks": "{count, plural, one {تم تخطي فحص واحد} other {تم تخطي # فحوصات}}" + "skippedChecks": "{count, plural, one {تم تخطي فحص واحد} other {تم تخطي # فحوصات}}", + "syncedDocs": "{count, plural, one {تمت مزامنة مستند واحد} other {تمت مزامنة # مستندات}}" }, "search": { "placeholder": "البحث في عمليات الفهرسة…" @@ -3004,7 +3011,9 @@ "empty": { "title": "لا توجد عمليات فهرسة حديثة", "filteredTitle": "لا توجد عمليات تشغيل تطابق هذه المرشحات.", - "description": "تتم جدولة عمليات التشغيل في الخلفية وقد يستغرق ظهورها بعض الوقت." + "pastRuns": "{count, plural, one {عملية تشغيل سابقة واحدة} other {# عمليات تشغيل سابقة}}", + "view": "عرض", + "viewAria": "{count, plural, one {عرض عملية التشغيل السابقة} other {عرض # عمليات تشغيل سابقة}}" }, "units": "{count, plural, one {تشغيل} other {عمليات تشغيل}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "فحوصات الموصّل", "close": "إغلاق", "count": "({count})" - } + }, + "showing": "عرض {from}~{to} من {total}" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "ابحث في المستندات…" }, "resolveAll": "حل الكل", + "resolveAllTooltip": "إعادة فهرسة كل المستندات الفاشلة في هذا التشغيل", "resolveOne": "حل هذا المستند", "more": "المزيد", "less": "أقل", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "ملغاة", "deleting": "جارٍ الحذف", - "error": "خطأ", "failed": "فشلت", "indexed": "مفهرس", "indexing": "جارٍ الفهرسة", diff --git a/web/src/i18n/messages/de.json b/web/src/i18n/messages/de.json index e738eeab03..718208c758 100644 --- a/web/src/i18n/messages/de.json +++ b/web/src/i18n/messages/de.json @@ -2714,17 +2714,16 @@ "reIndex": { "tooltip": { "indexing": "Eine Neuindizierung ist nicht möglich, während bereits eine Indizierung läuft", - "invalid": "Korrigieren Sie die Konnektorkonfiguration vor der Neuindizierung", - "paused": "Setzen Sie den Konnektor vor der Neuindizierung fort" + "invalid": "Korrigieren Sie die Konnektorkonfiguration vor der Neuindizierung" } }, "toggleStatus": { "pause": { - "label": "Indexierung & Synchronisierung pausieren", + "label": "Indizierung & Synchronisation pausieren", "description": "Laufende und künftige Indexläufe und Berechtigungssynchronisierungen abbrechen. Indexierte Dokumente sind dann nicht mehr aktuell, bleiben aber durchsuchbar." }, "resume": { - "label": "Indexierung & Synchronisierung fortsetzen", + "label": "Indizierung & Synchronisation fortsetzen", "description": "Indexläufe und Berechtigungssynchronisierungen wieder starten." }, "tooltip": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "Vollständig neu indizieren", "description": "Alle Dokumente der Quelle neu indizieren. Das kann je nach Umfang **Stunden oder Tage** dauern." + }, + "startIndexing": { + "label": "Indexierung starten", + "description": "Konnektorprüfungen überspringen und jetzt indexieren." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "Indexierte Dokumente" }, "indexingSpeed": "{speed} Dok./Min.", + "indexingSpeedTooltip": "Indexierte Dokumente pro Minute seit dem Anlegen des Connectors", "lastIndexed": { "value": "Zuletzt indiziert {when}" }, @@ -2942,7 +2946,9 @@ "queued": "In Warteschlange", "running": "Läuft", "rate": "{rate} Dok./Min.", - "finished": "{time} ({minutes, number} Min.)" + "finished": "{time} ({minutes, number} Min.)", + "rateFinished": "{rate} Dok./Min. ({minutes, number} Min.)", + "membershipRate": "{rate} Mitgliedschaften/Min." }, "actions": { "stages": "Phasenmetriken anzeigen", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, one {# Lauf abgebrochen} other {# Läufe abgebrochen}}", "passedChecks": "{count, plural, one {# Prüfung bestanden} other {# Prüfungen bestanden}}", "failedChecks": "{count, plural, one {# Prüfung fehlgeschlagen} other {# Prüfungen fehlgeschlagen}}", - "skippedChecks": "{count, plural, one {# Prüfung übersprungen} other {# Prüfungen übersprungen}}" + "skippedChecks": "{count, plural, one {# Prüfung übersprungen} other {# Prüfungen übersprungen}}", + "syncedDocs": "{count, plural, one {# Dokument synchronisiert} other {# Dokumente synchronisiert}}" }, "search": { "placeholder": "Indexläufe durchsuchen…" @@ -3004,7 +3011,9 @@ "empty": { "title": "Keine aktuellen Indexläufe", "filteredTitle": "Keine Läufe entsprechen diesen Filtern.", - "description": "Läufe werden im Hintergrund geplant und erscheinen eventuell erst nach kurzer Zeit." + "pastRuns": "{count, plural, one {# vergangener Lauf} other {# vergangene Läufe}}", + "view": "Anzeigen", + "viewAria": "{count, plural, one {# vergangenen Lauf anzeigen} other {# vergangene Läufe anzeigen}}" }, "units": "{count, plural, one {Lauf} other {Läufe}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "Connector-Prüfungen", "close": "Schließen", "count": "({count})" - } + }, + "showing": "{from}~{to} von {total}" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "Dokumente durchsuchen…" }, "resolveAll": "Alle beheben", + "resolveAllTooltip": "Alle fehlgeschlagenen Dokumente dieses Laufs neu indexieren", "resolveOne": "Dieses Dokument beheben", "more": "Mehr", "less": "Weniger", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "Abgebrochen", "deleting": "Wird gelöscht", - "error": "Fehler", "failed": "Fehlgeschlagen", "indexed": "Indexiert", "indexing": "Indexierung", diff --git a/web/src/i18n/messages/en.json b/web/src/i18n/messages/en.json index 5de61011e4..cedd8a185a 100644 --- a/web/src/i18n/messages/en.json +++ b/web/src/i18n/messages/en.json @@ -2714,8 +2714,7 @@ "reIndex": { "tooltip": { "indexing": "Cannot re-index while indexing is already in progress", - "invalid": "Fix the connector configuration before re-indexing", - "paused": "Resume the connector before re-indexing" + "invalid": "Fix the connector configuration before re-indexing" } }, "toggleStatus": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "Full Re-index", "description": "Re-index all docs from the source. This may take **hours or days** depending on corpus size." + }, + "startIndexing": { + "label": "Start Indexing", + "description": "Skip connector checks and index now." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "Indexed documents" }, "indexingSpeed": "{speed} docs/min", + "indexingSpeedTooltip": "Documents indexed per minute since the connector was created", "lastIndexed": { "value": "Last indexed {when}" }, @@ -2942,7 +2946,9 @@ "queued": "Queued", "running": "Running", "rate": "{rate} docs/min", - "finished": "{time} ({minutes, number} min)" + "finished": "{time} ({minutes, number} min)", + "rateFinished": "{rate} docs/min ({minutes, number} min)", + "membershipRate": "{rate} memberships/min" }, "actions": { "stages": "View Stage Metrics", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, one {# run canceled} other {# runs canceled}}", "passedChecks": "{count, plural, one {# check passed} other {# checks passed}}", "failedChecks": "{count, plural, one {# check failed} other {# checks failed}}", - "skippedChecks": "{count, plural, one {# check skipped} other {# checks skipped}}" + "skippedChecks": "{count, plural, one {# check skipped} other {# checks skipped}}", + "syncedDocs": "{count, plural, one {# document synced} other {# documents synced}}" }, "search": { "placeholder": "Search indexing runs…" @@ -3004,7 +3011,9 @@ "empty": { "title": "No recent indexing runs", "filteredTitle": "No runs match these filters.", - "description": "Runs are scheduled in the background and may take a moment to appear." + "pastRuns": "{count, plural, one {# Past Run} other {# Past Runs}}", + "view": "View", + "viewAria": "{count, plural, one {View # past run} other {View # past runs}}" }, "units": "{count, plural, one {run} other {runs}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "Connector Checks", "close": "Close", "count": "({count})" - } + }, + "showing": "Showing {from}~{to} of {total}" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "Search documents…" }, "resolveAll": "Resolve All", + "resolveAllTooltip": "Re-index every failed document of this run", "resolveOne": "Resolve this document", "more": "More", "less": "Less", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "Canceled", "deleting": "Deleting", - "error": "Error", "failed": "Failed", "indexed": "Indexed", "indexing": "Indexing", diff --git a/web/src/i18n/messages/es.json b/web/src/i18n/messages/es.json index 1883d1ac7a..009c72eabe 100644 --- a/web/src/i18n/messages/es.json +++ b/web/src/i18n/messages/es.json @@ -2714,8 +2714,7 @@ "reIndex": { "tooltip": { "indexing": "No se puede reindexar mientras la indexación está en curso", - "invalid": "Corrige la configuración del conector antes de reindexar", - "paused": "Reanuda el conector antes de reindexar" + "invalid": "Corrige la configuración del conector antes de reindexar" } }, "toggleStatus": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "Reindexar todo", "description": "Reindexar todos los documentos de la fuente. Puede tardar **horas o días** según el tamaño." + }, + "startIndexing": { + "label": "Iniciar indexación", + "description": "Omitir las comprobaciones del conector e indexar ahora." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "Documentos en el índice" }, "indexingSpeed": "{speed} documentos/min", + "indexingSpeedTooltip": "Documentos indexados por minuto desde que se creó el conector", "lastIndexed": { "value": "Última indexación {when}" }, @@ -2942,7 +2946,9 @@ "queued": "En cola", "running": "En curso", "rate": "{rate} documentos/min", - "finished": "{time} ({minutes, number} min)" + "finished": "{time} ({minutes, number} min)", + "rateFinished": "{rate} documentos/min ({minutes, number} min)", + "membershipRate": "{rate} membresías/min" }, "actions": { "stages": "Ver métricas por etapa", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, one {# ejecución cancelada} other {# ejecuciones canceladas}}", "passedChecks": "{count, plural, one {# comprobación superada} other {# comprobaciones superadas}}", "failedChecks": "{count, plural, one {# comprobación fallida} other {# comprobaciones fallidas}}", - "skippedChecks": "{count, plural, one {# comprobación omitida} other {# comprobaciones omitidas}}" + "skippedChecks": "{count, plural, one {# comprobación omitida} other {# comprobaciones omitidas}}", + "syncedDocs": "{count, plural, one {# documento sincronizado} other {# documentos sincronizados}}" }, "search": { "placeholder": "Buscar ejecuciones de indexación…" @@ -3004,7 +3011,9 @@ "empty": { "title": "No hay ejecuciones de indexación recientes", "filteredTitle": "Ninguna ejecución coincide con estos filtros.", - "description": "Las ejecuciones se programan en segundo plano y pueden tardar un momento en aparecer." + "pastRuns": "{count, plural, one {# ejecución anterior} other {# ejecuciones anteriores}}", + "view": "Ver", + "viewAria": "{count, plural, one {Ver # ejecución anterior} other {Ver # ejecuciones anteriores}}" }, "units": "{count, plural, one {ejecución} other {ejecuciones}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "Comprobaciones del conector", "close": "Cerrar", "count": "({count})" - } + }, + "showing": "Mostrando {from}~{to} de {total}" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "Buscar documentos…" }, "resolveAll": "Resolver todo", + "resolveAllTooltip": "Reindexar todos los documentos fallidos de esta ejecución", "resolveOne": "Resolver este documento", "more": "Más", "less": "Menos", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "Cancelado", "deleting": "Eliminando", - "error": "Error", "failed": "Fallido", "indexed": "Indexado", "indexing": "Indexando", diff --git a/web/src/i18n/messages/fr.json b/web/src/i18n/messages/fr.json index 351f6c9e2d..e555cce7c6 100644 --- a/web/src/i18n/messages/fr.json +++ b/web/src/i18n/messages/fr.json @@ -2714,8 +2714,7 @@ "reIndex": { "tooltip": { "indexing": "Impossible de réindexer pendant qu'une indexation est déjà en cours", - "invalid": "Corrigez la configuration du connecteur avant de réindexer", - "paused": "Reprenez le connecteur avant de réindexer" + "invalid": "Corrigez la configuration du connecteur avant de réindexer" } }, "toggleStatus": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "Réindexation complète", "description": "Réindexer tous les documents de la source. Cela peut prendre **des heures ou des jours** selon le volume." + }, + "startIndexing": { + "label": "Démarrer l'indexation", + "description": "Ignorer les vérifications du connecteur et indexer maintenant." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "Documents dans l'index" }, "indexingSpeed": "{speed} docs/min", + "indexingSpeedTooltip": "Documents indexés par minute depuis la création du connecteur", "lastIndexed": { "value": "Dernière indexation {when}" }, @@ -2942,7 +2946,9 @@ "queued": "En file d'attente", "running": "En cours", "rate": "{rate} docs/min", - "finished": "{time} ({minutes, number} min)" + "finished": "{time} ({minutes, number} min)", + "rateFinished": "{rate} docs/min ({minutes, number} min)", + "membershipRate": "{rate} adhésions/min" }, "actions": { "stages": "Voir les métriques par étape", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, one {# exécution annulée} other {# exécutions annulées}}", "passedChecks": "{count, plural, one {# vérification réussie} other {# vérifications réussies}}", "failedChecks": "{count, plural, one {# vérification échouée} other {# vérifications échouées}}", - "skippedChecks": "{count, plural, one {# vérification ignorée} other {# vérifications ignorées}}" + "skippedChecks": "{count, plural, one {# vérification ignorée} other {# vérifications ignorées}}", + "syncedDocs": "{count, plural, one {# document synchronisé} other {# documents synchronisés}}" }, "search": { "placeholder": "Rechercher des exécutions d'indexation…" @@ -3004,7 +3011,9 @@ "empty": { "title": "Aucune exécution d'indexation récente", "filteredTitle": "Aucune exécution ne correspond à ces filtres.", - "description": "Les exécutions sont planifiées en arrière-plan et peuvent mettre un moment à apparaître." + "pastRuns": "{count, plural, one {# exécution passée} other {# exécutions passées}}", + "view": "Afficher", + "viewAria": "{count, plural, one {Afficher # exécution passée} other {Afficher # exécutions passées}}" }, "units": "{count, plural, one {exécution} other {exécutions}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "Vérifications du connecteur", "close": "Fermer", "count": "({count})" - } + }, + "showing": "Affichage de {from}~{to} sur {total}" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "Rechercher des documents…" }, "resolveAll": "Tout résoudre", + "resolveAllTooltip": "Réindexer tous les documents en échec de cette exécution", "resolveOne": "Résoudre ce document", "more": "Plus", "less": "Moins", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "Annulé", "deleting": "Suppression", - "error": "Erreur", "failed": "Échec", "indexed": "Indexé", "indexing": "Indexation", diff --git a/web/src/i18n/messages/ja.json b/web/src/i18n/messages/ja.json index 79e1e2f0f5..4285673fac 100644 --- a/web/src/i18n/messages/ja.json +++ b/web/src/i18n/messages/ja.json @@ -2714,17 +2714,16 @@ "reIndex": { "tooltip": { "indexing": "インデックス作成の実行中は再インデックスできません", - "invalid": "再インデックスの前にコネクターの設定を修正してください", - "paused": "再インデックスの前にコネクターを再開してください" + "invalid": "再インデックスの前にコネクターの設定を修正してください" } }, "toggleStatus": { "pause": { - "label": "インデックス作成と同期を一時停止", + "label": "インデックスと同期を一時停止", "description": "進行中および今後のインデックス実行と権限の同期を取り消します。インデックス済みのドキュメントは最新ではなくなりますが、引き続き検索できます。" }, "resume": { - "label": "インデックス作成と同期を再開", + "label": "インデックスと同期を再開", "description": "インデックス実行と権限の同期を再開します。" }, "tooltip": { @@ -2740,7 +2739,11 @@ }, "fullReindex": { "label": "完全な再インデックス", - "description": "ソースの全ドキュメントを再インデックスします。規模によっては**数時間から数日**かかります。" + "description": "ソースの全ドキュメントを再インデックスします。規模によっては **数時間から数日** かかります。" + }, + "startIndexing": { + "label": "インデックス作成を開始", + "description": "コネクターのチェックをスキップして、今すぐインデックスを作成します。" } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "インデックス済みドキュメント" }, "indexingSpeed": "{speed} 件/分", + "indexingSpeedTooltip": "コネクター作成以降の1分あたりのインデックス済みドキュメント数", "lastIndexed": { "value": "最終インデックス作成 {when}" }, @@ -2942,7 +2946,9 @@ "queued": "待機中", "running": "実行中", "rate": "{rate} 件/分", - "finished": "{time}({minutes, number} 分)" + "finished": "{time}({minutes, number} 分)", + "rateFinished": "{rate} 件/分({minutes, number} 分)", + "membershipRate": "{rate} メンバーシップ/分" }, "actions": { "stages": "ステージの指標を表示", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, other {# 件の実行がキャンセルされました}}", "passedChecks": "{count, plural, one {# 件のチェックが成功} other {# 件のチェックが成功}}", "failedChecks": "{count, plural, one {# 件のチェックが失敗} other {# 件のチェックが失敗}}", - "skippedChecks": "{count, plural, one {# 件のチェックをスキップ} other {# 件のチェックをスキップ}}" + "skippedChecks": "{count, plural, one {# 件のチェックをスキップ} other {# 件のチェックをスキップ}}", + "syncedDocs": "{count, plural, other {# 件のドキュメントを同期済み}}" }, "search": { "placeholder": "インデックス実行を検索…" @@ -3004,7 +3011,9 @@ "empty": { "title": "最近のインデックス実行はありません", "filteredTitle": "このフィルターに一致する実行はありません。", - "description": "実行はバックグラウンドでスケジュールされるため、表示されるまでに少し時間がかかる場合があります。" + "pastRuns": "{count, plural, other {過去の実行 # 件}}", + "view": "表示", + "viewAria": "{count, plural, other {過去の実行 # 件を表示}}" }, "units": "{count, plural, other {件の実行}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "コネクタチェック", "close": "閉じる", "count": "({count})" - } + }, + "showing": "{total} 件中 {from}~{to} 件を表示" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "ドキュメントを検索…" }, "resolveAll": "すべて解決", + "resolveAllTooltip": "この実行で失敗したすべてのドキュメントを再インデックス", "resolveOne": "このドキュメントを解決", "more": "さらに表示", "less": "折りたたむ", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "キャンセル済み", "deleting": "削除中", - "error": "エラー", "failed": "失敗", "indexed": "インデックス済み", "indexing": "インデックス作成中", diff --git a/web/src/i18n/messages/ko.json b/web/src/i18n/messages/ko.json index 5d38b6ffc0..5a3cdb66ca 100644 --- a/web/src/i18n/messages/ko.json +++ b/web/src/i18n/messages/ko.json @@ -2714,8 +2714,7 @@ "reIndex": { "tooltip": { "indexing": "인덱싱이 이미 진행 중이면 재인덱싱할 수 없습니다", - "invalid": "재인덱싱하기 전에 커넥터 구성을 수정하세요", - "paused": "재인덱싱하기 전에 커넥터를 다시 시작하세요" + "invalid": "재인덱싱하기 전에 커넥터 구성을 수정하세요" } }, "toggleStatus": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "전체 재인덱싱", "description": "소스의 모든 문서를 다시 인덱싱합니다. 규모에 따라 **몇 시간 또는 며칠**이 걸릴 수 있습니다." + }, + "startIndexing": { + "label": "인덱싱 시작", + "description": "커넥터 검사를 건너뛰고 지금 인덱싱합니다." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "인덱싱 완료 문서" }, "indexingSpeed": "분당 {speed}개 문서", + "indexingSpeedTooltip": "커넥터 생성 이후 분당 인덱싱된 문서 수", "lastIndexed": { "value": "마지막 인덱싱 {when}" }, @@ -2942,7 +2946,9 @@ "queued": "대기 중", "running": "실행 중", "rate": "분당 {rate}개 문서", - "finished": "{time} ({minutes, number}분)" + "finished": "{time} ({minutes, number}분)", + "rateFinished": "분당 {rate}개 문서 ({minutes, number}분)", + "membershipRate": "{rate} 멤버십/분" }, "actions": { "stages": "단계별 지표 보기", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, other {실행 #개 취소됨}}", "passedChecks": "{count, plural, one {검사 #개 통과} other {검사 #개 통과}}", "failedChecks": "{count, plural, one {검사 #개 실패} other {검사 #개 실패}}", - "skippedChecks": "{count, plural, one {검사 #개 건너뜀} other {검사 #개 건너뜀}}" + "skippedChecks": "{count, plural, one {검사 #개 건너뜀} other {검사 #개 건너뜀}}", + "syncedDocs": "{count, plural, other {문서 #개 동기화됨}}" }, "search": { "placeholder": "인덱싱 실행 검색…" @@ -3004,7 +3011,9 @@ "empty": { "title": "최근 인덱싱 실행이 없습니다", "filteredTitle": "이 필터와 일치하는 실행이 없습니다.", - "description": "실행은 백그라운드에서 예약되며 표시되기까지 잠시 걸릴 수 있습니다." + "pastRuns": "{count, plural, other {지난 실행 #개}}", + "view": "보기", + "viewAria": "{count, plural, other {지난 실행 #개 보기}}" }, "units": "{count, plural, other {실행}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "커넥터 검사", "close": "닫기", "count": "({count})" - } + }, + "showing": "{total}개 중 {from}~{to}개 표시" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "문서 검색…" }, "resolveAll": "모두 해결", + "resolveAllTooltip": "이 실행에서 실패한 모든 문서를 다시 색인", "resolveOne": "이 문서 해결", "more": "더 보기", "less": "접기", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "취소됨", "deleting": "삭제 중", - "error": "오류", "failed": "실패", "indexed": "인덱싱됨", "indexing": "인덱싱 중", diff --git a/web/src/i18n/messages/pt.json b/web/src/i18n/messages/pt.json index 0019fd3bc1..1aa6345322 100644 --- a/web/src/i18n/messages/pt.json +++ b/web/src/i18n/messages/pt.json @@ -2714,8 +2714,7 @@ "reIndex": { "tooltip": { "indexing": "Não é possível reindexar enquanto uma indexação está em andamento", - "invalid": "Corrija a configuração do conector antes de reindexar", - "paused": "Retome o conector antes de reindexar" + "invalid": "Corrija a configuração do conector antes de reindexar" } }, "toggleStatus": { @@ -2741,6 +2740,10 @@ "fullReindex": { "label": "Reindexar tudo", "description": "Reindexar todos os documentos da fonte. Pode levar **horas ou dias** dependendo do tamanho." + }, + "startIndexing": { + "label": "Iniciar indexação", + "description": "Ignorar as verificações do conector e indexar agora." } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "Documentos no índice" }, "indexingSpeed": "{speed} documentos/min", + "indexingSpeedTooltip": "Documentos indexados por minuto desde a criação do conector", "lastIndexed": { "value": "Última indexação {when}" }, @@ -2942,7 +2946,9 @@ "queued": "Na fila", "running": "Em execução", "rate": "{rate} documentos/min", - "finished": "{time} ({minutes, number} min)" + "finished": "{time} ({minutes, number} min)", + "rateFinished": "{rate} documentos/min ({minutes, number} min)", + "membershipRate": "{rate} associações/min" }, "actions": { "stages": "Ver métricas por etapa", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, one {# execução cancelada} other {# execuções canceladas}}", "passedChecks": "{count, plural, one {# verificação aprovada} other {# verificações aprovadas}}", "failedChecks": "{count, plural, one {# verificação falhou} other {# verificações falharam}}", - "skippedChecks": "{count, plural, one {# verificação ignorada} other {# verificações ignoradas}}" + "skippedChecks": "{count, plural, one {# verificação ignorada} other {# verificações ignoradas}}", + "syncedDocs": "{count, plural, one {# documento sincronizado} other {# documentos sincronizados}}" }, "search": { "placeholder": "Pesquisar execuções de indexação…" @@ -3004,7 +3011,9 @@ "empty": { "title": "Nenhuma execução de indexação recente", "filteredTitle": "Nenhuma execução corresponde a estes filtros.", - "description": "As execuções são agendadas em segundo plano e podem demorar um pouco para aparecer." + "pastRuns": "{count, plural, one {# execução anterior} other {# execuções anteriores}}", + "view": "Ver", + "viewAria": "{count, plural, one {Ver # execução anterior} other {Ver # execuções anteriores}}" }, "units": "{count, plural, one {execução} other {execuções}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "Verificações do conector", "close": "Fechar", "count": "({count})" - } + }, + "showing": "Mostrando {from}~{to} de {total}" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "Pesquisar documentos…" }, "resolveAll": "Resolver tudo", + "resolveAllTooltip": "Reindexar todos os documentos com falha desta execução", "resolveOne": "Resolver este documento", "more": "Mais", "less": "Menos", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "Cancelado", "deleting": "Excluindo", - "error": "Erro", "failed": "Falhou", "indexed": "Indexado", "indexing": "Indexando", diff --git a/web/src/i18n/messages/zh.json b/web/src/i18n/messages/zh.json index 4918bd9f10..a21b6c55be 100644 --- a/web/src/i18n/messages/zh.json +++ b/web/src/i18n/messages/zh.json @@ -2714,17 +2714,16 @@ "reIndex": { "tooltip": { "indexing": "索引正在进行中,无法重新索引", - "invalid": "重新索引前请先修复连接器配置", - "paused": "重新索引前请先恢复连接器" + "invalid": "重新索引前请先修复连接器配置" } }, "toggleStatus": { "pause": { - "label": "暂停索引和同步", + "label": "暂停索引与同步", "description": "取消进行中和未来的索引运行与权限同步。已索引的文档将不再保持同步,但仍可搜索。" }, "resume": { - "label": "恢复索引和同步", + "label": "恢复索引与同步", "description": "重新开始索引运行与权限同步。" }, "tooltip": { @@ -2740,7 +2739,11 @@ }, "fullReindex": { "label": "完全重新索引", - "description": "重新索引来源中的所有文档。视语料规模可能需要**数小时或数天**。" + "description": "重新索引来源中的所有文档。视语料规模可能需要 **数小时或数天**。" + }, + "startIndexing": { + "label": "开始索引", + "description": "跳过连接器检查并立即索引。" } }, "reEnableModal": { @@ -2783,6 +2786,7 @@ "label": "已索引文档" }, "indexingSpeed": "{speed} 文档/分钟", + "indexingSpeedTooltip": "自创建连接器以来每分钟索引的文档数", "lastIndexed": { "value": "上次索引 {when}" }, @@ -2942,7 +2946,9 @@ "queued": "排队中", "running": "运行中", "rate": "{rate} 文档/分钟", - "finished": "{time}({minutes, number} 分钟)" + "finished": "{time}({minutes, number} 分钟)", + "rateFinished": "{rate} 文档/分钟({minutes, number} 分钟)", + "membershipRate": "{rate} 成员关系/分钟" }, "actions": { "stages": "查看阶段指标", @@ -2976,7 +2982,8 @@ "canceled": "{count, plural, other {已取消 # 次运行}}", "passedChecks": "{count, plural, one {# 项检查通过} other {# 项检查通过}}", "failedChecks": "{count, plural, one {# 项检查失败} other {# 项检查失败}}", - "skippedChecks": "{count, plural, one {# 项检查已跳过} other {# 项检查已跳过}}" + "skippedChecks": "{count, plural, one {# 项检查已跳过} other {# 项检查已跳过}}", + "syncedDocs": "{count, plural, other {已同步 # 个文档}}" }, "search": { "placeholder": "搜索索引运行…" @@ -3004,7 +3011,9 @@ "empty": { "title": "暂无近期索引运行", "filteredTitle": "没有符合这些筛选条件的运行。", - "description": "运行在后台调度,可能需要一点时间才会显示。" + "pastRuns": "{count, plural, other {# 次过去的运行}}", + "view": "查看", + "viewAria": "{count, plural, other {查看 # 次过去的运行}}" }, "units": "{count, plural, other {个运行}}", "errorModal": { @@ -3022,7 +3031,8 @@ "title": "连接器检查", "close": "关闭", "count": "({count})" - } + }, + "showing": "显示第 {from}~{to} 条,共 {total} 条" }, "activity": { "search": { @@ -3137,6 +3147,7 @@ "placeholder": "搜索文档…" }, "resolveAll": "全部解决", + "resolveAllTooltip": "重新索引此运行中所有失败的文档", "resolveOne": "解决此文档", "more": "更多", "less": "收起", @@ -9544,7 +9555,6 @@ "badges": { "canceled": "已取消", "deleting": "正在删除", - "error": "错误", "failed": "失败", "indexed": "已索引", "indexing": "正在索引", diff --git a/web/src/lib/connectors/types/ccPair.ts b/web/src/lib/connectors/types/ccPair.ts index cebbe608ff..54154a4227 100644 --- a/web/src/lib/connectors/types/ccPair.ts +++ b/web/src/lib/connectors/types/ccPair.ts @@ -191,6 +191,7 @@ export interface PruneRunSnapshot { id: number; status: PruneRunStatus; docs_removed: number; + docs_failed: number; error_message: string | null; time_created: string; time_started: string | null; diff --git a/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx b/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx index 27626a41f6..a97d00c524 100644 --- a/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx +++ b/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx @@ -68,9 +68,7 @@ export type CheckLine = Pick< | "docs_link" > & { state: CheckLineState }; -// Outcomes that need reading (passed, failed, unverified) show their details -// in a heavier font and their status colour; the rest stay muted. -const OUTCOME_FONT: TextFont = "main-ui-action"; +// Passed and failed read plainly, an unverified check is heavy and orange so it gets read, and the rest stay muted. const MUTED = { font: "secondary-body", color: "text-03" } as const; const CHECK_LOGS = { @@ -78,22 +76,22 @@ const CHECK_LOGS = { variant: "success-light", icon: SvgCheckCircle, detail: "status.passed", - font: OUTCOME_FONT, + font: "main-ui-body", color: "text-04", }, - // A failed required check blocks, so `CheckLog` makes it heavy. + // Only a failed required check blocks, so `CheckLog` makes it heavy and red. failed: { variant: "error-light", icon: SvgXOctagon, detail: "status.failed", - font: OUTCOME_FONT, - color: "status-error-05", + font: "main-ui-body", + color: "text-04", }, indeterminate: { variant: "warning-light", icon: SvgAlertCircle, detail: "status.indeterminate", - font: OUTCOME_FONT, + font: "main-ui-action", color: "status-warning-05", }, skipped: { @@ -177,7 +175,7 @@ function CheckLog({ check }: CheckLogProps) { {check.message || t(detail)} @@ -191,7 +189,14 @@ function CheckLog({ check }: CheckLogProps) { tooltip={guidance} /> )} - {check.required && } + {check.required && ( + + )} } /> diff --git a/web/src/views/admin/connectors/ConnectorDetailPage/ActivityCard.tsx b/web/src/views/admin/connectors/ConnectorDetailPage/ActivityCard.tsx index 109dc94cb5..111009aa5d 100644 --- a/web/src/views/admin/connectors/ConnectorDetailPage/ActivityCard.tsx +++ b/web/src/views/admin/connectors/ConnectorDetailPage/ActivityCard.tsx @@ -46,6 +46,9 @@ import { runWarnings, } from "@/views/admin/connectors/ConnectorDetailPage/RunRow"; +// A Tag is clickable only with an onClick, and the popover trigger around it handles the click. +const NOOP = () => {}; + const GROUP_STYLE: Record< ActivityGroup, { @@ -53,6 +56,8 @@ const GROUP_STYLE: Record< icon: IconFunctionComponent; stroke: string; closedStroke: string; + /** The 1px edge a hovered or selected card takes, in the level's 02 color. */ + outline: string; } > = { failures: { @@ -60,18 +65,21 @@ const GROUP_STYLE: Record< icon: SvgXOctagon, stroke: "stroke-status-error-05", closedStroke: "stroke-status-error-02", + outline: "outline-status-error-02", }, errors: { color: "status-warning-00", icon: SvgAlertTriangle, stroke: "stroke-status-warning-05", closedStroke: "stroke-status-warning-02", + outline: "outline-status-warning-02", }, other: { color: "background-tint-00", icon: SvgCheckCircle, stroke: "stroke-status-success-05", closedStroke: "stroke-status-success-02", + outline: "outline-border-01", }, }; @@ -167,7 +175,16 @@ export default function ActivityCard({ resolving, }: ActivityCardProps) { const t = useTranslations("admin.connector.activity"); - const [traceOpen, setTraceOpen] = useState(false); + const [hovered, setHovered] = useState(false); + const [focused, setFocused] = useState(false); + const [openPopover, setOpenPopover] = useState<"trace" | "items" | null>( + null + ); + const selected = openPopover !== null; + const togglePopover = (popover: "trace" | "items") => (open: boolean) => + setOpenPopover((current) => + open ? popover : current === popover ? null : current + ); const tRuns = useTranslations("admin.connector.runs"); const active = isActive(run); const style = GROUP_STYLE[group]; @@ -218,13 +235,39 @@ export default function ActivityCard({ count: listsDocs ? openDocs : warnings, }); + const pillIconColor = closed ? undefined : "orange"; + + // The outline draws the edge inside the card, so hovering does not shift the list. return ( -
+
setHovered(true)} + onPointerLeave={() => setHovered(false)} + onFocus={() => setFocused(true)} + onBlur={(event) => { + if (!event.currentTarget.contains(event.relatedTarget)) { + setFocused(false); + } + }} + >
{trace !== null && ( - + - + title={t("tags.stackTrace")} + size="md" + onClick={NOOP} + /> )} {listsDocs ? ( - - - - - - - ) : run.kind === "checks" && listsChecks ? ( - - + <> + + + + + + + {/* One click re-indexes every open failed document, without opening the list. */} + {resolvable && !closed && ( + )} + + ) : run.kind === "checks" && listsChecks ? ( + + + @@ -379,8 +440,9 @@ export default function ActivityCard({ warnings > 0 && ( ) )} @@ -396,11 +458,14 @@ export default function ActivityCard({ ) : ( resolvesRun && ( - // Only on hover or focus, so an open list reads as its runs, not a column of actions. An open popover moves focus into its portal, so its trigger's open state keeps the link shown. + // Only on hover, focus or an open pill, so an open list reads as its runs. Opacity keeps it in the tab order.
+ ); + // Beside the main column the panel pins under the settings header and // scrolls inside its own height, so a long list never lengthens the page. return ( @@ -320,6 +334,7 @@ export default function ActivityPanel({ )}
)} + {/* Beside the page the search shares its row with the counts. The side panel gives the search its own row. */}
setSearchInput(event.target.value)} />
-
- -
+ {title === undefined && countTags}
- - - setShown("all")} - > - {t(`filter.${shown}`)} - - - - setShown( - shownOptions.find( - (candidate) => candidate === option.value - ) ?? "all" - ) - } - items={shownOptions.map((candidate) => ({ - kind: "option" as const, - value: candidate, - title: t(`filter.${candidate}`), - }))} - /> - - setShowResolved((value) => !value)} + {/* The chart's span sits with the other filters, inside the row that wraps instead of overflowing. */} +
- {t("filter.resolved")} - - {canResolveAll && resolvable.count > 0 && ( - - )} + {t("filter.resolved")} + + {canResolveAll && resolvable.count > 0 && ( + + )} +
+ {title !== undefined && countTags}
@@ -430,7 +444,10 @@ export default function ActivityPanel({ ))}
) : empty ? ( - + ) : ( groups.map((group) => ( - diff --git a/web/src/views/admin/connectors/ConnectorDetailPage/ThroughputChart.tsx b/web/src/views/admin/connectors/ConnectorDetailPage/ThroughputChart.tsx index c19c21c2a4..5e6429bd23 100644 --- a/web/src/views/admin/connectors/ConnectorDetailPage/ThroughputChart.tsx +++ b/web/src/views/admin/connectors/ConnectorDetailPage/ThroughputChart.tsx @@ -182,6 +182,24 @@ function axisTicks(start: number, end: number, minutes: number): number[] { return ticks; } +/** Unlabeled ticks at a third of the label spacing, inside the window. */ +function minorTicks( + labels: number[], + start: number, + end: number, + minutes: number +): number[] { + const third = (minutes * 60_000) / 3; + const ticks = new Set(); + for (const label of labels) { + for (const offset of [-2, -1, 1, 2]) { + const tick = label + offset * third; + if (tick >= start && tick <= end) ticks.add(tick); + } + } + return Array.from(ticks); +} + interface Run { start: number; end: number; @@ -232,6 +250,8 @@ export interface ThroughputWindowPickerProps { /** A span picked on the chart. The picker shows it in place of the window, with an X that calls `onClearSelection`. */ selection?: SliceRange | null; onClearSelection?: () => void; + /** Shown instead of the window's name, for a list that widened past it. */ + label?: string; } /** Picks how far back a throughput chart, and any list that shares it, reaches, and shows the span picked inside it. */ @@ -241,6 +261,7 @@ export function ThroughputWindowPicker({ onChange, selection = null, onClearSelection, + label, }: ThroughputWindowPickerProps) { const t = useTranslations("admin.connector.statusCard.throughput"); const tWindow = useTranslations("admin.connector.runs.window"); @@ -250,7 +271,7 @@ export function ThroughputWindowPicker({ {selection === null || onClearSelection === undefined ? ( - {tWindow(windowLabelKey(value))} + {label ?? tWindow(windowLabelKey(value))} ) : ( // The span takes the picker's place, so the row keeps its shape. Its X returns to the window. @@ -355,7 +376,12 @@ export function ThroughputBars({ ); const tickMinutes = selection === null ? TICK_MINUTES[range] : spanTickMinutes(end - start); - const ticks = axisTicks(start, end, tickMinutes); + const labelTicks = axisTicks(start, end, tickMinutes); + const labelSet = new Set(labelTicks); + const ticks = [ + ...labelTicks, + ...minorTicks(labelTicks, start, end, tickMinutes), + ].sort((a, b) => a - b); // A run still going covers up to the window's end. const runs: Run[] = data.runs.map((run) => ({ start: Date.parse(run.started), @@ -432,16 +458,18 @@ export function ThroughputBars({ allowDataOverflow ticks={ticks} tickFormatter={(value: number) => - tickMinutes >= DAY_MINUTES - ? format.dateTime(new Date(value), { - month: "short", - day: "numeric", - }) - : format.dateTime(new Date(value), { - hour: "2-digit", - minute: "2-digit", - hourCycle: "h23", - }) + !labelSet.has(value) + ? "" + : tickMinutes >= DAY_MINUTES + ? format.dateTime(new Date(value), { + month: "short", + day: "numeric", + }) + : format.dateTime(new Date(value), { + hour: "2-digit", + minute: "2-digit", + hourCycle: "h23", + }) } tick={{ fontSize: 10, diff --git a/web/src/views/admin/connectors/ConnectorDetailPage/focusMode.ts b/web/src/views/admin/connectors/ConnectorDetailPage/focusMode.ts new file mode 100644 index 0000000000..6c4287a8a8 --- /dev/null +++ b/web/src/views/admin/connectors/ConnectorDetailPage/focusMode.ts @@ -0,0 +1,6 @@ +import { cn } from "@opal/utils"; + +/** Fades content back while a run row's stage or checks popover is open. */ +export function focusDimClass(dimmed: boolean): string { + return cn("transition-opacity", dimmed && "opacity-50"); +} diff --git a/web/src/views/admin/connectors/ConnectorDetailPage/index.tsx b/web/src/views/admin/connectors/ConnectorDetailPage/index.tsx index fd43de4fc0..621e660cb9 100644 --- a/web/src/views/admin/connectors/ConnectorDetailPage/index.tsx +++ b/web/src/views/admin/connectors/ConnectorDetailPage/index.tsx @@ -21,23 +21,24 @@ import { toast, } from "@opal/layouts"; import { IconLoader, PageLoader } from "@opal/loaders"; +import { markdown } from "@opal/utils"; import { SvgPlugBroken } from "@opal/illustrations"; import { SvgArrowLeft, SvgEditBig, SvgPause, + SvgPlay, SvgPlayCircle, SvgRefreshCw, SvgSettings, SvgSlowMotion, SvgTrash, } from "@opal/icons"; -import { markdown } from "@opal/utils"; import { isEditableSource } from "@/lib/connectors/edit/editable"; import { updateConnectorCredentialPairName } from "@/lib/connector"; import { errorHandlingFetcher } from "@/lib/fetcher"; import { buildCCPairInfoUrl } from "@/lib/connectors/utils"; -import { triggerIndexing } from "@/lib/connectors/svc"; +import { releaseIndexingHold, triggerIndexing } from "@/lib/connectors/svc"; import { ConnectorCredentialPairStatus } from "@/lib/connectors/types"; import type { CCPairFullInfo } from "@/lib/connectors/types"; import { statusIsNotCurrentlyActive } from "@/lib/connectors/utils"; @@ -62,6 +63,7 @@ import { type PickedWindow, type SliceRange, } from "@/views/admin/connectors/ConnectorDetailPage/ThroughputChart"; +import { focusDimClass } from "@/views/admin/connectors/ConnectorDetailPage/focusMode"; const CC_PAIR_POLL_MS = 5000; @@ -92,6 +94,7 @@ function Main({ ccPairId }: { ccPairId: number }) { useState(false); // Counts the status card's asks to open the checks card. const [checksExpandRequest, setChecksExpandRequest] = useState(0); + const [runFocus, setRunFocus] = useState(false); // One window drives the status card's chart and the runs card's list. const [timeWindow, setTimeWindow] = useState(() => pickWindow("12h") @@ -153,6 +156,22 @@ function Main({ ccPairId }: { ccPairId: number }) { } }; + const startIndexing = async () => { + if (!ccPair) return; + setBusy(true); + const failure = await releaseIndexingHold( + ccPair.id, + t("statusCard.hold.releaseFailed") + ); + setBusy(false); + if (failure !== null) { + toast.error(failure); + return; + } + toast.success(t("statusCard.hold.released")); + refresh(); + }; + useEffect(() => { if (isLoadingCCPair) return; if (ccPair && !ccPairError) setHasLoadedOnce(true); @@ -226,75 +245,106 @@ function Main({ ccPairId }: { ccPairId: number }) { const onEdit = canOpenEditForm ? openEditForm : null; const canManageInlineFiles = source === "file" && can(ccPair, "edit"); + // Unlike `paused`, this excludes INVALID, whose menu still offers the re-index rows disabled with a reason. + const strictlyPaused = ccPair.status === ConnectorCredentialPairStatus.PAUSED; + const held = ccPair.indexing_hold; + // While held, the menu offers Start Indexing in place of the re-index rows; a check run in flight releases the hold itself. + const canStartIndexing = held !== null && held.reason !== "checks_running"; // The reason a row is disabled is its tooltip. const reIndexBlocked = ccPair.indexing ? t("manageMenu.reIndex.tooltip.indexing") - : ccPair.status === ConnectorCredentialPairStatus.PAUSED - ? t("manageMenu.reIndex.tooltip.paused") - : ccPair.status === ConnectorCredentialPairStatus.INVALID - ? t("manageMenu.reIndex.tooltip.invalid") - : undefined; - const manageItems: DropdownMenuItem[] = [ - { - kind: "action", - id: "update-index", - icon: SvgRefreshCw, - title: t("manageMenu.updateIndex.label"), - description: t("manageMenu.updateIndex.description"), - tooltip: reIndexBlocked, - disabled: reIndexBlocked !== undefined, - onSelect: () => void triggerReIndex(false), - }, - { - kind: "action", - id: "full-reindex", - icon: SvgSlowMotion, - title: t("manageMenu.fullReindex.label"), - description: markdown(t("manageMenu.fullReindex.description")), - tooltip: reIndexBlocked, - disabled: reIndexBlocked !== undefined, - onSelect: () => void triggerReIndex(true), - }, - ...(!isDeleting + : ccPair.status === ConnectorCredentialPairStatus.INVALID + ? t("manageMenu.reIndex.tooltip.invalid") + : undefined; + const startItems: DropdownMenuItem[] = canStartIndexing + ? [ + { + kind: "action", + id: "start-indexing", + icon: SvgPlay, + title: t("manageMenu.startIndexing.label"), + description: t("manageMenu.startIndexing.description"), + disabled: busy, + onSelect: () => void startIndexing(), + }, + ] + : []; + const reIndexItems: DropdownMenuItem[] = + strictlyPaused || held !== null + ? [] + : [ + { + kind: "action", + id: "update-index", + icon: SvgRefreshCw, + title: t("manageMenu.updateIndex.label"), + description: t("manageMenu.updateIndex.description"), + tooltip: reIndexBlocked, + disabled: reIndexBlocked !== undefined, + onSelect: () => void triggerReIndex(false), + }, + { + kind: "action", + id: "full-reindex", + icon: SvgSlowMotion, + title: t("manageMenu.fullReindex.label"), + description: markdown(t("manageMenu.fullReindex.description")), + tooltip: reIndexBlocked, + disabled: reIndexBlocked !== undefined, + onSelect: () => void triggerReIndex(true), + }, + ]; + const toggleItems: DropdownMenuItem[] = isDeleting + ? [] + : [ + { + kind: "action", + id: "toggle-status", + icon: paused ? SvgPlayCircle : SvgPause, + title: paused + ? t("manageMenu.toggleStatus.resume.label") + : t("manageMenu.toggleStatus.pause.label"), + description: paused + ? t("manageMenu.toggleStatus.resume.description") + : t("manageMenu.toggleStatus.pause.description"), + tooltip: isStatusUpdating + ? t("manageMenu.toggleStatus.tooltip.updating") + : undefined, + disabled: isStatusUpdating, + onSelect: () => + handleStatusChange( + paused + ? ConnectorCredentialPairStatus.ACTIVE + : ConnectorCredentialPairStatus.PAUSED + ), + }, + ]; + // An untitled group, so the list draws its divider above the danger row. + const dangerItems: DropdownMenuItem[] = + !isDeleting && can(ccPair, "delete") ? [ { - kind: "action" as const, - id: "toggle-status", - icon: paused ? SvgPlayCircle : SvgPause, - title: paused - ? t("manageMenu.toggleStatus.resume.label") - : t("manageMenu.toggleStatus.pause.label"), - description: paused - ? t("manageMenu.toggleStatus.resume.description") - : t("manageMenu.toggleStatus.pause.description"), - tooltip: isStatusUpdating - ? t("manageMenu.toggleStatus.tooltip.updating") - : undefined, - disabled: isStatusUpdating, - onSelect: () => - handleStatusChange( - paused - ? ConnectorCredentialPairStatus.ACTIVE - : ConnectorCredentialPairStatus.PAUSED - ), + kind: "group", + items: [ + { + kind: "action", + id: "delete", + icon: SvgTrash, + danger: true, + title: t("manageMenu.delete.label"), + tooltip: paused + ? undefined + : t("manageMenu.delete.tooltip.active"), + disabled: !paused, + onSelect: () => setShowDeleteConnectorConfirmModal(true), + }, + ], }, ] - : []), - ...(!isDeleting && can(ccPair, "delete") - ? [ - { - kind: "action" as const, - id: "delete", - icon: SvgTrash, - danger: true, - title: t("manageMenu.delete.label"), - tooltip: paused ? undefined : t("manageMenu.delete.tooltip.active"), - disabled: !paused, - onSelect: () => setShowDeleteConnectorConfirmModal(true), - }, - ] - : []), - ]; + : []; + const manageItems: DropdownMenuItem[] = strictlyPaused + ? [...toggleItems, ...dangerItems] + : [...startItems, ...reIndexItems, ...toggleItems, ...dangerItems]; return ( @@ -332,7 +382,7 @@ function Main({ ccPairId }: { ccPairId: number }) { , ] : []), - +