mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-02 22:25:47 +00:00
Compare commits
6 Commits
refactor/a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c1d29d3cf | ||
|
|
709e3f4ca7 | ||
|
|
dfa27c08ef | ||
|
|
13d60dcb0e | ||
|
|
30704f427f | ||
|
|
4f3c54f282 |
26
.github/workflows/deployment.yml
vendored
26
.github/workflows/deployment.yml
vendored
@@ -426,8 +426,9 @@ jobs:
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
NODE_OPTIONS=--max-old-space-size=8192
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-amd64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-amd64,mode=max
|
||||
@@ -499,8 +500,9 @@ jobs:
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
NODE_OPTIONS=--max-old-space-size=8192
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-arm64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-arm64,mode=max
|
||||
@@ -646,8 +648,8 @@ jobs:
|
||||
NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=true
|
||||
NODE_OPTIONS=--max-old-space-size=8192
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:cloudweb-cache-amd64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:cloudweb-cache-amd64,mode=max
|
||||
@@ -728,8 +730,8 @@ jobs:
|
||||
NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=true
|
||||
NODE_OPTIONS=--max-old-space-size=8192
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:cloudweb-cache-arm64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:cloudweb-cache-arm64,mode=max
|
||||
@@ -862,8 +864,9 @@ jobs:
|
||||
build-args: |
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-cache-amd64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-cache-amd64,mode=max
|
||||
@@ -934,8 +937,9 @@ jobs:
|
||||
build-args: |
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-cache-arm64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-cache-arm64,mode=max
|
||||
@@ -1072,8 +1076,8 @@ jobs:
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
ENABLE_CRAFT=true
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-craft-cache-amd64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-craft-cache-amd64,mode=max
|
||||
@@ -1145,8 +1149,8 @@ jobs:
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
ENABLE_CRAFT=true
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-craft-cache-arm64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:backend-craft-cache-arm64,mode=max
|
||||
@@ -1287,8 +1291,9 @@ jobs:
|
||||
build-args: |
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:model-server-cache-amd64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:model-server-cache-amd64,mode=max
|
||||
@@ -1366,8 +1371,9 @@ jobs:
|
||||
build-args: |
|
||||
ONYX_VERSION=${{ github.ref_name }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:model-server-cache-arm64
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:edge
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:latest
|
||||
cache-to: |
|
||||
type=inline
|
||||
type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:model-server-cache-arm64,mode=max
|
||||
|
||||
45
backend/onyx/cache/factory.py
vendored
Normal file
45
backend/onyx/cache/factory.py
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from onyx.cache.interface import CacheBackend
|
||||
from onyx.cache.interface import CacheBackendType
|
||||
from onyx.configs.app_configs import CACHE_BACKEND
|
||||
|
||||
|
||||
def _build_redis_backend(tenant_id: str) -> CacheBackend:
|
||||
from onyx.cache.redis_backend import RedisCacheBackend
|
||||
from onyx.redis.redis_pool import redis_pool
|
||||
|
||||
return RedisCacheBackend(redis_pool.get_client(tenant_id))
|
||||
|
||||
|
||||
_BACKEND_BUILDERS: dict[CacheBackendType, Callable[[str], CacheBackend]] = {
|
||||
CacheBackendType.REDIS: _build_redis_backend,
|
||||
# CacheBackendType.POSTGRES will be added in a follow-up PR.
|
||||
}
|
||||
|
||||
|
||||
def get_cache_backend(*, tenant_id: str | None = None) -> CacheBackend:
|
||||
"""Return a tenant-aware ``CacheBackend``.
|
||||
|
||||
If *tenant_id* is ``None``, the current tenant is read from the
|
||||
thread-local context variable (same behaviour as ``get_redis_client``).
|
||||
"""
|
||||
if tenant_id is None:
|
||||
from shared_configs.contextvars import get_current_tenant_id
|
||||
|
||||
tenant_id = get_current_tenant_id()
|
||||
|
||||
builder = _BACKEND_BUILDERS.get(CACHE_BACKEND)
|
||||
if builder is None:
|
||||
raise ValueError(
|
||||
f"Unsupported CACHE_BACKEND={CACHE_BACKEND!r}. "
|
||||
f"Supported values: {[t.value for t in CacheBackendType]}"
|
||||
)
|
||||
return builder(tenant_id)
|
||||
|
||||
|
||||
def get_shared_cache_backend() -> CacheBackend:
|
||||
"""Return a ``CacheBackend`` in the shared (cross-tenant) namespace."""
|
||||
from shared_configs.configs import DEFAULT_REDIS_PREFIX
|
||||
|
||||
return get_cache_backend(tenant_id=DEFAULT_REDIS_PREFIX)
|
||||
89
backend/onyx/cache/interface.py
vendored
Normal file
89
backend/onyx/cache/interface.py
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import abc
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CacheBackendType(str, Enum):
|
||||
REDIS = "redis"
|
||||
POSTGRES = "postgres"
|
||||
|
||||
|
||||
class CacheLock(abc.ABC):
|
||||
"""Abstract distributed lock returned by CacheBackend.lock()."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def acquire(
|
||||
self,
|
||||
blocking: bool = True,
|
||||
blocking_timeout: float | None = None,
|
||||
) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def release(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def owned(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CacheBackend(abc.ABC):
|
||||
"""Thin abstraction over a key-value cache with TTL, locks, and blocking lists.
|
||||
|
||||
Covers the subset of Redis operations used outside of Celery. When
|
||||
CACHE_BACKEND=postgres, a PostgreSQL-backed implementation is used instead.
|
||||
"""
|
||||
|
||||
# -- basic key/value ---------------------------------------------------
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, key: str) -> bytes | None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(
|
||||
self,
|
||||
key: str,
|
||||
value: str | bytes | int | float,
|
||||
ex: int | None = None,
|
||||
) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete(self, key: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def exists(self, key: str) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
# -- TTL ---------------------------------------------------------------
|
||||
|
||||
@abc.abstractmethod
|
||||
def expire(self, key: str, seconds: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def ttl(self, key: str) -> int:
|
||||
"""Return remaining TTL in seconds. -1 if no expiry, -2 if key missing."""
|
||||
raise NotImplementedError
|
||||
|
||||
# -- distributed lock --------------------------------------------------
|
||||
|
||||
@abc.abstractmethod
|
||||
def lock(self, name: str, timeout: float | None = None) -> CacheLock:
|
||||
raise NotImplementedError
|
||||
|
||||
# -- blocking list (used by MCP OAuth BLPOP pattern) -------------------
|
||||
|
||||
@abc.abstractmethod
|
||||
def rpush(self, key: str, value: str | bytes) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def blpop(self, keys: list[str], timeout: int = 0) -> tuple[bytes, bytes] | None:
|
||||
"""Block until a value is available on one of *keys*, or *timeout* expires.
|
||||
|
||||
Returns ``(key, value)`` or ``None`` on timeout.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
92
backend/onyx/cache/redis_backend.py
vendored
Normal file
92
backend/onyx/cache/redis_backend.py
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
from typing import cast
|
||||
|
||||
from redis.client import Redis
|
||||
from redis.lock import Lock as RedisLock
|
||||
|
||||
from onyx.cache.interface import CacheBackend
|
||||
from onyx.cache.interface import CacheLock
|
||||
|
||||
|
||||
class RedisCacheLock(CacheLock):
|
||||
"""Wraps ``redis.lock.Lock`` behind the ``CacheLock`` interface."""
|
||||
|
||||
def __init__(self, lock: RedisLock) -> None:
|
||||
self._lock = lock
|
||||
|
||||
def acquire(
|
||||
self,
|
||||
blocking: bool = True,
|
||||
blocking_timeout: float | None = None,
|
||||
) -> bool:
|
||||
return bool(
|
||||
self._lock.acquire(
|
||||
blocking=blocking,
|
||||
blocking_timeout=blocking_timeout,
|
||||
)
|
||||
)
|
||||
|
||||
def release(self) -> None:
|
||||
self._lock.release()
|
||||
|
||||
def owned(self) -> bool:
|
||||
return bool(self._lock.owned())
|
||||
|
||||
|
||||
class RedisCacheBackend(CacheBackend):
|
||||
"""``CacheBackend`` implementation that delegates to a ``redis.Redis`` client.
|
||||
|
||||
This is a thin pass-through — every method maps 1-to-1 to the underlying
|
||||
Redis command. ``TenantRedis`` key-prefixing is handled by the client
|
||||
itself (provided by ``get_redis_client``).
|
||||
"""
|
||||
|
||||
def __init__(self, redis_client: Redis) -> None:
|
||||
self._r = redis_client
|
||||
|
||||
# -- basic key/value ---------------------------------------------------
|
||||
|
||||
def get(self, key: str) -> bytes | None:
|
||||
val = self._r.get(key)
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, bytes):
|
||||
return val
|
||||
return str(val).encode()
|
||||
|
||||
def set(
|
||||
self,
|
||||
key: str,
|
||||
value: str | bytes | int | float,
|
||||
ex: int | None = None,
|
||||
) -> None:
|
||||
self._r.set(key, value, ex=ex)
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
self._r.delete(key)
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
return bool(self._r.exists(key))
|
||||
|
||||
# -- TTL ---------------------------------------------------------------
|
||||
|
||||
def expire(self, key: str, seconds: int) -> None:
|
||||
self._r.expire(key, seconds)
|
||||
|
||||
def ttl(self, key: str) -> int:
|
||||
return cast(int, self._r.ttl(key))
|
||||
|
||||
# -- distributed lock --------------------------------------------------
|
||||
|
||||
def lock(self, name: str, timeout: float | None = None) -> CacheLock:
|
||||
return RedisCacheLock(self._r.lock(name, timeout=timeout))
|
||||
|
||||
# -- blocking list (MCP OAuth BLPOP pattern) ---------------------------
|
||||
|
||||
def rpush(self, key: str, value: str | bytes) -> None:
|
||||
self._r.rpush(key, value)
|
||||
|
||||
def blpop(self, keys: list[str], timeout: int = 0) -> tuple[bytes, bytes] | None:
|
||||
result = cast(list[bytes] | None, self._r.blpop(keys, timeout=timeout))
|
||||
if result is None:
|
||||
return None
|
||||
return (result[0], result[1])
|
||||
@@ -6,6 +6,7 @@ from datetime import timezone
|
||||
from typing import cast
|
||||
|
||||
from onyx.auth.schemas import AuthBackend
|
||||
from onyx.cache.interface import CacheBackendType
|
||||
from onyx.configs.constants import AuthType
|
||||
from onyx.configs.constants import QueryHistoryType
|
||||
from onyx.file_processing.enums import HtmlBasedConnectorTransformLinksStrategy
|
||||
@@ -54,6 +55,12 @@ DISABLE_USER_KNOWLEDGE = os.environ.get("DISABLE_USER_KNOWLEDGE", "").lower() ==
|
||||
# are disabled but core chat, tools, user file uploads, and Projects still work.
|
||||
DISABLE_VECTOR_DB = os.environ.get("DISABLE_VECTOR_DB", "").lower() == "true"
|
||||
|
||||
# Which backend to use for caching, locks, and ephemeral state.
|
||||
# "redis" (default) or "postgres" (only valid when DISABLE_VECTOR_DB=true).
|
||||
CACHE_BACKEND = CacheBackendType(
|
||||
os.environ.get("CACHE_BACKEND", CacheBackendType.REDIS)
|
||||
)
|
||||
|
||||
# Maximum token count for a single uploaded file. Files exceeding this are rejected.
|
||||
# Defaults to 100k tokens (or 10M when vector DB is disabled).
|
||||
_DEFAULT_FILE_TOKEN_LIMIT = 10_000_000 if DISABLE_VECTOR_DB else 100_000
|
||||
|
||||
@@ -6,7 +6,6 @@ import httpx
|
||||
from opensearchpy import NotFoundError
|
||||
|
||||
from onyx.access.models import DocumentAccess
|
||||
from onyx.configs.app_configs import USING_AWS_MANAGED_OPENSEARCH
|
||||
from onyx.configs.app_configs import VERIFY_CREATE_OPENSEARCH_INDEX_ON_INIT_MT
|
||||
from onyx.configs.chat_configs import NUM_RETURNED_HITS
|
||||
from onyx.configs.chat_configs import TITLE_CONTENT_RATIO
|
||||
@@ -563,12 +562,7 @@ class OpenSearchDocumentIndex(DocumentIndex):
|
||||
)
|
||||
|
||||
if not self._client.index_exists():
|
||||
if USING_AWS_MANAGED_OPENSEARCH:
|
||||
index_settings = (
|
||||
DocumentSchema.get_index_settings_for_aws_managed_opensearch()
|
||||
)
|
||||
else:
|
||||
index_settings = DocumentSchema.get_index_settings()
|
||||
index_settings = DocumentSchema.get_index_settings_based_on_environment()
|
||||
self._client.create_index(
|
||||
mappings=expected_mappings,
|
||||
settings=index_settings,
|
||||
|
||||
@@ -12,6 +12,7 @@ from pydantic import model_validator
|
||||
from pydantic import SerializerFunctionWrapHandler
|
||||
|
||||
from onyx.configs.app_configs import OPENSEARCH_TEXT_ANALYZER
|
||||
from onyx.configs.app_configs import USING_AWS_MANAGED_OPENSEARCH
|
||||
from onyx.document_index.interfaces_new import TenantState
|
||||
from onyx.document_index.opensearch.constants import DEFAULT_MAX_CHUNK_SIZE
|
||||
from onyx.document_index.opensearch.constants import EF_CONSTRUCTION
|
||||
@@ -525,7 +526,7 @@ class DocumentSchema:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_index_settings_for_aws_managed_opensearch() -> dict[str, Any]:
|
||||
def get_index_settings_for_aws_managed_opensearch_st_dev() -> dict[str, Any]:
|
||||
"""
|
||||
Settings for AWS-managed OpenSearch.
|
||||
|
||||
@@ -546,3 +547,41 @@ class DocumentSchema:
|
||||
"knn.algo_param.ef_search": EF_SEARCH,
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_index_settings_for_aws_managed_opensearch_mt_cloud() -> dict[str, Any]:
|
||||
"""
|
||||
Settings for AWS-managed OpenSearch in multi-tenant cloud.
|
||||
|
||||
324 shards very roughly targets a storage load of ~30Gb per shard, which
|
||||
according to AWS OpenSearch documentation is within a good target range.
|
||||
|
||||
As documented above we need 2 replicas for a total of 3 copies of the
|
||||
data because the cluster is configured with 3-AZ awareness.
|
||||
"""
|
||||
return {
|
||||
"index": {
|
||||
"number_of_shards": 324,
|
||||
"number_of_replicas": 2,
|
||||
# Required for vector search.
|
||||
"knn": True,
|
||||
"knn.algo_param.ef_search": EF_SEARCH,
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_index_settings_based_on_environment() -> dict[str, Any]:
|
||||
"""
|
||||
Returns the index settings based on the environment.
|
||||
"""
|
||||
if USING_AWS_MANAGED_OPENSEARCH:
|
||||
if MULTI_TENANT:
|
||||
return (
|
||||
DocumentSchema.get_index_settings_for_aws_managed_opensearch_mt_cloud()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
DocumentSchema.get_index_settings_for_aws_managed_opensearch_st_dev()
|
||||
)
|
||||
else:
|
||||
return DocumentSchema.get_index_settings()
|
||||
|
||||
@@ -32,11 +32,13 @@ from onyx.auth.schemas import UserUpdate
|
||||
from onyx.auth.users import auth_backend
|
||||
from onyx.auth.users import create_onyx_oauth_router
|
||||
from onyx.auth.users import fastapi_users
|
||||
from onyx.cache.interface import CacheBackendType
|
||||
from onyx.configs.app_configs import APP_API_PREFIX
|
||||
from onyx.configs.app_configs import APP_HOST
|
||||
from onyx.configs.app_configs import APP_PORT
|
||||
from onyx.configs.app_configs import AUTH_RATE_LIMITING_ENABLED
|
||||
from onyx.configs.app_configs import AUTH_TYPE
|
||||
from onyx.configs.app_configs import CACHE_BACKEND
|
||||
from onyx.configs.app_configs import DISABLE_VECTOR_DB
|
||||
from onyx.configs.app_configs import LOG_ENDPOINT_LATENCY
|
||||
from onyx.configs.app_configs import OAUTH_CLIENT_ID
|
||||
@@ -255,6 +257,20 @@ def include_auth_router_with_prefix(
|
||||
)
|
||||
|
||||
|
||||
def validate_cache_backend_settings() -> None:
|
||||
"""Validate that CACHE_BACKEND=postgres is only used with DISABLE_VECTOR_DB.
|
||||
|
||||
The Postgres cache backend eliminates the Redis dependency, but only works
|
||||
when Celery is not running (which requires DISABLE_VECTOR_DB=true).
|
||||
"""
|
||||
if CACHE_BACKEND == CacheBackendType.POSTGRES and not DISABLE_VECTOR_DB:
|
||||
raise RuntimeError(
|
||||
"CACHE_BACKEND=postgres requires DISABLE_VECTOR_DB=true. "
|
||||
"The Postgres cache backend is only supported in no-vector-DB "
|
||||
"deployments where Celery is replaced by the in-process task runner."
|
||||
)
|
||||
|
||||
|
||||
def validate_no_vector_db_settings() -> None:
|
||||
"""Validate that DISABLE_VECTOR_DB is not combined with incompatible settings.
|
||||
|
||||
@@ -286,6 +302,7 @@ def validate_no_vector_db_settings() -> None:
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001
|
||||
validate_no_vector_db_settings()
|
||||
validate_cache_backend_settings()
|
||||
|
||||
# Set recursion limit
|
||||
if SYSTEM_RECURSION_LIMIT is not None:
|
||||
|
||||
552
backend/tests/integration/tests/scim/test_scim_groups.py
Normal file
552
backend/tests/integration/tests/scim/test_scim_groups.py
Normal file
@@ -0,0 +1,552 @@
|
||||
"""Integration tests for SCIM group provisioning endpoints.
|
||||
|
||||
Covers the full group lifecycle as driven by an IdP (Okta / Azure AD):
|
||||
1. Create a group via POST /Groups
|
||||
2. Retrieve a group via GET /Groups/{id}
|
||||
3. List, filter, and paginate groups via GET /Groups
|
||||
4. Replace a group via PUT /Groups/{id}
|
||||
5. Patch a group (add/remove members, rename) via PATCH /Groups/{id}
|
||||
6. Delete a group via DELETE /Groups/{id}
|
||||
7. Error cases: duplicate name, not-found, invalid member IDs
|
||||
|
||||
All tests are parameterized across IdP request styles (Okta sends lowercase
|
||||
PATCH ops; Entra sends capitalized ops like ``"Replace"``). The server
|
||||
normalizes both — these tests verify that.
|
||||
|
||||
Auth tests live in test_scim_tokens.py.
|
||||
User lifecycle tests live in test_scim_users.py.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from onyx.auth.schemas import UserRole
|
||||
from tests.integration.common_utils.managers.scim_client import ScimClient
|
||||
from tests.integration.common_utils.managers.scim_token import ScimTokenManager
|
||||
|
||||
|
||||
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
SCIM_PATCH_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:PatchOp"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=["okta", "entra"])
|
||||
def idp_style(request: pytest.FixtureRequest) -> str:
|
||||
"""Parameterized IdP style — runs every test with both Okta and Entra request formats."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def scim_token(idp_style: str) -> str:
|
||||
"""Create a single SCIM token shared across all tests in this module.
|
||||
|
||||
Creating a new token revokes the previous one, so we create exactly once
|
||||
per IdP-style run and reuse. Uses UserManager directly to avoid
|
||||
fixture-scope conflicts with the function-scoped admin_user fixture.
|
||||
"""
|
||||
from tests.integration.common_utils.constants import ADMIN_USER_NAME
|
||||
from tests.integration.common_utils.constants import GENERAL_HEADERS
|
||||
from tests.integration.common_utils.managers.user import build_email
|
||||
from tests.integration.common_utils.managers.user import DEFAULT_PASSWORD
|
||||
from tests.integration.common_utils.managers.user import UserManager
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
|
||||
try:
|
||||
admin = UserManager.create(name=ADMIN_USER_NAME)
|
||||
except Exception:
|
||||
admin = UserManager.login_as_user(
|
||||
DATestUser(
|
||||
id="",
|
||||
email=build_email(ADMIN_USER_NAME),
|
||||
password=DEFAULT_PASSWORD,
|
||||
headers=GENERAL_HEADERS,
|
||||
role=UserRole.ADMIN,
|
||||
is_active=True,
|
||||
)
|
||||
)
|
||||
|
||||
token = ScimTokenManager.create(
|
||||
name=f"scim-group-tests-{idp_style}",
|
||||
user_performing_action=admin,
|
||||
).raw_token
|
||||
assert token is not None
|
||||
return token
|
||||
|
||||
|
||||
def _make_group_resource(
|
||||
display_name: str,
|
||||
external_id: str | None = None,
|
||||
members: list[dict] | None = None,
|
||||
) -> dict:
|
||||
"""Build a minimal SCIM GroupResource payload."""
|
||||
resource: dict = {
|
||||
"schemas": [SCIM_GROUP_SCHEMA],
|
||||
"displayName": display_name,
|
||||
}
|
||||
if external_id is not None:
|
||||
resource["externalId"] = external_id
|
||||
if members is not None:
|
||||
resource["members"] = members
|
||||
return resource
|
||||
|
||||
|
||||
def _make_user_resource(email: str, external_id: str) -> dict:
|
||||
"""Build a minimal SCIM UserResource payload for member creation."""
|
||||
return {
|
||||
"schemas": [SCIM_USER_SCHEMA],
|
||||
"userName": email,
|
||||
"externalId": external_id,
|
||||
"name": {"givenName": "Test", "familyName": "User"},
|
||||
"active": True,
|
||||
}
|
||||
|
||||
|
||||
def _make_patch_request(operations: list[dict], idp_style: str = "okta") -> dict:
|
||||
"""Build a SCIM PatchOp payload, applying IdP-specific operation casing.
|
||||
|
||||
Entra sends capitalized operations (e.g. ``"Replace"`` instead of
|
||||
``"replace"``). The server's ``normalize_operation`` validator lowercases
|
||||
them — these tests verify that both casings are accepted.
|
||||
"""
|
||||
cased_operations = []
|
||||
for operation in operations:
|
||||
cased = dict(operation)
|
||||
if idp_style == "entra":
|
||||
cased["op"] = operation["op"].capitalize()
|
||||
cased_operations.append(cased)
|
||||
return {
|
||||
"schemas": [SCIM_PATCH_SCHEMA],
|
||||
"Operations": cased_operations,
|
||||
}
|
||||
|
||||
|
||||
def _create_scim_user(token: str, email: str, external_id: str) -> requests.Response:
|
||||
return ScimClient.post(
|
||||
"/Users", token, json=_make_user_resource(email, external_id)
|
||||
)
|
||||
|
||||
|
||||
def _create_scim_group(
|
||||
token: str,
|
||||
display_name: str,
|
||||
external_id: str | None = None,
|
||||
members: list[dict] | None = None,
|
||||
) -> requests.Response:
|
||||
return ScimClient.post(
|
||||
"/Groups",
|
||||
token,
|
||||
json=_make_group_resource(display_name, external_id, members),
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle: create → get → list → replace → patch → delete
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_group(scim_token: str, idp_style: str) -> None:
|
||||
"""POST /Groups creates a group and returns 201."""
|
||||
name = f"Engineering {idp_style}"
|
||||
resp = _create_scim_group(scim_token, name, external_id=f"ext-eng-{idp_style}")
|
||||
assert resp.status_code == 201
|
||||
|
||||
body = resp.json()
|
||||
assert body["displayName"] == name
|
||||
assert body["externalId"] == f"ext-eng-{idp_style}"
|
||||
assert body["id"] # integer ID assigned by server
|
||||
assert body["meta"]["resourceType"] == "Group"
|
||||
|
||||
|
||||
def test_create_group_with_members(scim_token: str, idp_style: str) -> None:
|
||||
"""POST /Groups with members populates the member list."""
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_member1_{idp_style}@example.com", f"ext-gm-{idp_style}"
|
||||
).json()
|
||||
|
||||
resp = _create_scim_group(
|
||||
scim_token,
|
||||
f"Backend Team {idp_style}",
|
||||
external_id=f"ext-backend-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
body = resp.json()
|
||||
member_ids = [m["value"] for m in body["members"]]
|
||||
assert user["id"] in member_ids
|
||||
|
||||
|
||||
def test_get_group(scim_token: str, idp_style: str) -> None:
|
||||
"""GET /Groups/{id} returns the group resource including members."""
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_get_m_{idp_style}@example.com", f"ext-ggm-{idp_style}"
|
||||
).json()
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Frontend Team {idp_style}",
|
||||
external_id=f"ext-fe-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
).json()
|
||||
|
||||
resp = ScimClient.get(f"/Groups/{created['id']}", scim_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.json()
|
||||
assert body["id"] == created["id"]
|
||||
assert body["displayName"] == f"Frontend Team {idp_style}"
|
||||
assert body["externalId"] == f"ext-fe-{idp_style}"
|
||||
member_ids = [m["value"] for m in body["members"]]
|
||||
assert user["id"] in member_ids
|
||||
|
||||
|
||||
def test_list_groups(scim_token: str, idp_style: str) -> None:
|
||||
"""GET /Groups returns a ListResponse containing provisioned groups."""
|
||||
name = f"DevOps Team {idp_style}"
|
||||
_create_scim_group(scim_token, name, external_id=f"ext-devops-{idp_style}")
|
||||
|
||||
resp = ScimClient.get("/Groups", scim_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.json()
|
||||
assert body["totalResults"] >= 1
|
||||
names = [r["displayName"] for r in body["Resources"]]
|
||||
assert name in names
|
||||
|
||||
|
||||
def test_list_groups_pagination(scim_token: str, idp_style: str) -> None:
|
||||
"""GET /Groups with startIndex and count returns correct pagination."""
|
||||
_create_scim_group(
|
||||
scim_token, f"Page Group A {idp_style}", external_id=f"ext-page-a-{idp_style}"
|
||||
)
|
||||
_create_scim_group(
|
||||
scim_token, f"Page Group B {idp_style}", external_id=f"ext-page-b-{idp_style}"
|
||||
)
|
||||
|
||||
resp = ScimClient.get("/Groups?startIndex=1&count=1", scim_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.json()
|
||||
assert body["startIndex"] == 1
|
||||
assert body["itemsPerPage"] == 1
|
||||
assert body["totalResults"] >= 2
|
||||
assert len(body["Resources"]) == 1
|
||||
|
||||
|
||||
def test_filter_groups_by_display_name(scim_token: str, idp_style: str) -> None:
|
||||
"""GET /Groups?filter=displayName eq '...' returns only matching groups."""
|
||||
name = f"Unique QA Team {idp_style}"
|
||||
_create_scim_group(scim_token, name, external_id=f"ext-qa-filter-{idp_style}")
|
||||
|
||||
resp = ScimClient.get(f'/Groups?filter=displayName eq "{name}"', scim_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.json()
|
||||
assert body["totalResults"] == 1
|
||||
assert body["Resources"][0]["displayName"] == name
|
||||
|
||||
|
||||
def test_filter_groups_by_external_id(scim_token: str, idp_style: str) -> None:
|
||||
"""GET /Groups?filter=externalId eq '...' returns the matching group."""
|
||||
ext_id = f"ext-unique-group-id-{idp_style}"
|
||||
_create_scim_group(
|
||||
scim_token, f"ExtId Filter Group {idp_style}", external_id=ext_id
|
||||
)
|
||||
|
||||
resp = ScimClient.get(f'/Groups?filter=externalId eq "{ext_id}"', scim_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.json()
|
||||
assert body["totalResults"] == 1
|
||||
assert body["Resources"][0]["externalId"] == ext_id
|
||||
|
||||
|
||||
def test_replace_group(scim_token: str, idp_style: str) -> None:
|
||||
"""PUT /Groups/{id} replaces the group resource."""
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Original Name {idp_style}",
|
||||
external_id=f"ext-replace-g-{idp_style}",
|
||||
).json()
|
||||
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_replace_m_{idp_style}@example.com", f"ext-grm-{idp_style}"
|
||||
).json()
|
||||
|
||||
updated_resource = _make_group_resource(
|
||||
display_name=f"Renamed Group {idp_style}",
|
||||
external_id=f"ext-replace-g-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
)
|
||||
resp = ScimClient.put(f"/Groups/{created['id']}", scim_token, json=updated_resource)
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.json()
|
||||
assert body["displayName"] == f"Renamed Group {idp_style}"
|
||||
member_ids = [m["value"] for m in body["members"]]
|
||||
assert user["id"] in member_ids
|
||||
|
||||
|
||||
def test_replace_group_clears_members(scim_token: str, idp_style: str) -> None:
|
||||
"""PUT /Groups/{id} with empty members removes all memberships."""
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_clear_m_{idp_style}@example.com", f"ext-gcm-{idp_style}"
|
||||
).json()
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Clear Members Group {idp_style}",
|
||||
external_id=f"ext-clear-g-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
).json()
|
||||
|
||||
assert len(created["members"]) == 1
|
||||
|
||||
resp = ScimClient.put(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_group_resource(
|
||||
f"Clear Members Group {idp_style}", f"ext-clear-g-{idp_style}", members=[]
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["members"] == []
|
||||
|
||||
|
||||
def test_patch_add_member(scim_token: str, idp_style: str) -> None:
|
||||
"""PATCH /Groups/{id} with op=add adds a member."""
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Patch Add Group {idp_style}",
|
||||
external_id=f"ext-patch-add-{idp_style}",
|
||||
).json()
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_patch_add_{idp_style}@example.com", f"ext-gpa-{idp_style}"
|
||||
).json()
|
||||
|
||||
resp = ScimClient.patch(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_patch_request(
|
||||
[{"op": "add", "path": "members", "value": [{"value": user["id"]}]}],
|
||||
idp_style,
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
member_ids = [m["value"] for m in resp.json()["members"]]
|
||||
assert user["id"] in member_ids
|
||||
|
||||
|
||||
def test_patch_remove_member(scim_token: str, idp_style: str) -> None:
|
||||
"""PATCH /Groups/{id} with op=remove removes a specific member."""
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_patch_rm_{idp_style}@example.com", f"ext-gpr-{idp_style}"
|
||||
).json()
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Patch Remove Group {idp_style}",
|
||||
external_id=f"ext-patch-rm-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
).json()
|
||||
assert len(created["members"]) == 1
|
||||
|
||||
resp = ScimClient.patch(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_patch_request(
|
||||
[
|
||||
{
|
||||
"op": "remove",
|
||||
"path": f'members[value eq "{user["id"]}"]',
|
||||
}
|
||||
],
|
||||
idp_style,
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["members"] == []
|
||||
|
||||
|
||||
def test_patch_replace_members(scim_token: str, idp_style: str) -> None:
|
||||
"""PATCH /Groups/{id} with op=replace on members swaps the entire list."""
|
||||
user_a = _create_scim_user(
|
||||
scim_token, f"grp_repl_a_{idp_style}@example.com", f"ext-gra-{idp_style}"
|
||||
).json()
|
||||
user_b = _create_scim_user(
|
||||
scim_token, f"grp_repl_b_{idp_style}@example.com", f"ext-grb-{idp_style}"
|
||||
).json()
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Patch Replace Group {idp_style}",
|
||||
external_id=f"ext-patch-repl-{idp_style}",
|
||||
members=[{"value": user_a["id"]}],
|
||||
).json()
|
||||
|
||||
# Replace member list: swap A for B
|
||||
resp = ScimClient.patch(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_patch_request(
|
||||
[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "members",
|
||||
"value": [{"value": user_b["id"]}],
|
||||
}
|
||||
],
|
||||
idp_style,
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
member_ids = [m["value"] for m in resp.json()["members"]]
|
||||
assert user_b["id"] in member_ids
|
||||
assert user_a["id"] not in member_ids
|
||||
|
||||
|
||||
def test_patch_rename_group(scim_token: str, idp_style: str) -> None:
|
||||
"""PATCH /Groups/{id} with op=replace on displayName renames the group."""
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Old Group Name {idp_style}",
|
||||
external_id=f"ext-rename-g-{idp_style}",
|
||||
).json()
|
||||
|
||||
new_name = f"New Group Name {idp_style}"
|
||||
resp = ScimClient.patch(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_patch_request(
|
||||
[{"op": "replace", "path": "displayName", "value": new_name}],
|
||||
idp_style,
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["displayName"] == new_name
|
||||
|
||||
# Confirm via GET
|
||||
get_resp = ScimClient.get(f"/Groups/{created['id']}", scim_token)
|
||||
assert get_resp.json()["displayName"] == new_name
|
||||
|
||||
|
||||
def test_delete_group(scim_token: str, idp_style: str) -> None:
|
||||
"""DELETE /Groups/{id} removes the group."""
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Delete Me Group {idp_style}",
|
||||
external_id=f"ext-del-g-{idp_style}",
|
||||
).json()
|
||||
|
||||
resp = ScimClient.delete(f"/Groups/{created['id']}", scim_token)
|
||||
assert resp.status_code == 204
|
||||
|
||||
# Second DELETE returns 404 (group hard-deleted)
|
||||
resp2 = ScimClient.delete(f"/Groups/{created['id']}", scim_token)
|
||||
assert resp2.status_code == 404
|
||||
|
||||
|
||||
def test_delete_group_preserves_members(scim_token: str, idp_style: str) -> None:
|
||||
"""DELETE /Groups/{id} removes memberships but does not deactivate users."""
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_del_member_{idp_style}@example.com", f"ext-gdm-{idp_style}"
|
||||
).json()
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Delete With Members {idp_style}",
|
||||
external_id=f"ext-del-wm-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
).json()
|
||||
|
||||
resp = ScimClient.delete(f"/Groups/{created['id']}", scim_token)
|
||||
assert resp.status_code == 204
|
||||
|
||||
# User should still be active and retrievable
|
||||
user_resp = ScimClient.get(f"/Users/{user['id']}", scim_token)
|
||||
assert user_resp.status_code == 200
|
||||
assert user_resp.json()["active"] is True
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Error cases
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_group_duplicate_name(scim_token: str, idp_style: str) -> None:
|
||||
"""POST /Groups with an already-taken displayName returns 409."""
|
||||
name = f"Dup Name Group {idp_style}"
|
||||
resp1 = _create_scim_group(scim_token, name, external_id=f"ext-dup-g1-{idp_style}")
|
||||
assert resp1.status_code == 201
|
||||
|
||||
resp2 = _create_scim_group(scim_token, name, external_id=f"ext-dup-g2-{idp_style}")
|
||||
assert resp2.status_code == 409
|
||||
|
||||
|
||||
def test_get_nonexistent_group(scim_token: str) -> None:
|
||||
"""GET /Groups/{bad-id} returns 404."""
|
||||
resp = ScimClient.get("/Groups/999999999", scim_token)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_create_group_with_invalid_member(scim_token: str, idp_style: str) -> None:
|
||||
"""POST /Groups with a non-existent member UUID returns 400."""
|
||||
resp = _create_scim_group(
|
||||
scim_token,
|
||||
f"Bad Member Group {idp_style}",
|
||||
external_id=f"ext-bad-m-{idp_style}",
|
||||
members=[{"value": "00000000-0000-0000-0000-000000000000"}],
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "not found" in resp.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_patch_add_nonexistent_member(scim_token: str, idp_style: str) -> None:
|
||||
"""PATCH /Groups/{id} adding a non-existent member returns 400."""
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Patch Bad Member Group {idp_style}",
|
||||
external_id=f"ext-pbm-{idp_style}",
|
||||
).json()
|
||||
|
||||
resp = ScimClient.patch(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_patch_request(
|
||||
[
|
||||
{
|
||||
"op": "add",
|
||||
"path": "members",
|
||||
"value": [{"value": "00000000-0000-0000-0000-000000000000"}],
|
||||
}
|
||||
],
|
||||
idp_style,
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "not found" in resp.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_patch_add_duplicate_member_is_idempotent(
|
||||
scim_token: str, idp_style: str
|
||||
) -> None:
|
||||
"""PATCH /Groups/{id} adding an already-present member succeeds silently."""
|
||||
user = _create_scim_user(
|
||||
scim_token, f"grp_dup_add_{idp_style}@example.com", f"ext-gda-{idp_style}"
|
||||
).json()
|
||||
created = _create_scim_group(
|
||||
scim_token,
|
||||
f"Idempotent Add Group {idp_style}",
|
||||
external_id=f"ext-idem-g-{idp_style}",
|
||||
members=[{"value": user["id"]}],
|
||||
).json()
|
||||
assert len(created["members"]) == 1
|
||||
|
||||
# Add same member again
|
||||
resp = ScimClient.patch(
|
||||
f"/Groups/{created['id']}",
|
||||
scim_token,
|
||||
json=_make_patch_request(
|
||||
[{"op": "add", "path": "members", "value": [{"value": user["id"]}]}],
|
||||
idp_style,
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()["members"]) == 1 # still just one member
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgCreditCard = ({ size, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -11,13 +10,7 @@ const SvgCreditCard = ({ size, ...props }: IconProps) => (
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14 3.33334H2C1.26362 3.33334 0.666664 3.9303 0.666664 4.66668V11.3333C0.666664 12.0697 1.26362 12.6667 2 12.6667H14C14.7364 12.6667 15.3333 12.0697 15.3333 11.3333V4.66668C15.3333 3.9303 14.7364 3.33334 14 3.33334Z"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M0.666664 6.66666H15.3333"
|
||||
d="M14.6667 6V4.00008C14.6667 3.26675 14.0667 2.66675 13.3333 2.66675H2.66668C1.93334 2.66675 1.33334 3.26675 1.33334 4.00008V6M14.6667 6V12.0001C14.6667 12.7334 14.0667 13.3334 13.3333 13.3334H2.66668C1.93334 13.3334 1.33334 12.7334 1.33334 12.0001V6M14.6667 6H1.33334"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgNetworkGraph = ({ size, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -10,16 +9,19 @@ const SvgNetworkGraph = ({ size, ...props }: IconProps) => (
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8 6V10M5.17157 6.82843L3.5 8.5M10.8284 6.82843L12.5 8.5"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="8" cy="4.5" r="2" strokeWidth={1.5} />
|
||||
<circle cx="2.5" cy="11.5" r="2" strokeWidth={1.5} />
|
||||
<circle cx="8" cy="12" r="1.5" strokeWidth={1.5} />
|
||||
<circle cx="13.5" cy="11.5" r="2" strokeWidth={1.5} />
|
||||
<g clipPath="url(#clip0_2828_22555)">
|
||||
<path
|
||||
d="M9.23744 4.48744C9.92086 3.80402 9.92086 2.69598 9.23744 2.01256C8.55402 1.32915 7.44598 1.32915 6.76256 2.01256C6.07915 2.69598 6.07915 3.80402 6.76256 4.48744M9.23744 4.48744C8.89573 4.82915 8.44787 5 8 5M9.23744 4.48744L11.7626 8.01256M6.76256 4.48744C7.10427 4.82915 7.55214 5 8 5M6.76256 4.48744L4.23744 8.01256M8 11C7.0335 11 6.25001 11.7835 6.25001 12.75C6.25001 13.7165 7.03351 14.5 8.00001 14.5C8.9665 14.5 9.75 13.7165 9.75 12.75C9.75 11.7835 8.9665 11 8 11ZM8 11V5M4.23744 8.01256C4.92085 8.69598 4.92422 9.81658 4.2408 10.5C3.55739 11.1834 2.44598 11.1709 1.76256 10.4874C1.07915 9.80402 1.07915 8.69598 1.76256 8.01256C2.44598 7.32915 3.55402 7.32915 4.23744 8.01256ZM11.7626 8.01256C11.0791 8.69598 11.0791 9.80402 11.7626 10.4874C12.446 11.1709 13.554 11.1709 14.2374 10.4874C14.9209 9.80402 14.9209 8.69598 14.2374 8.01256C13.554 7.32915 12.446 7.32915 11.7626 8.01256Z"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2828_22555">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
export default SvgNetworkGraph;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { SourceCategory, SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { listSourceMetadata } from "@/lib/sources";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
@@ -248,37 +248,61 @@ export default function Page() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
<>
|
||||
<AdminPageTitle
|
||||
icon={SvgUploadCloud}
|
||||
title="Add Connector"
|
||||
rightChildren={
|
||||
farRightElement={
|
||||
<Button href="/admin/indexing/status" primary>
|
||||
See Connectors
|
||||
</Button>
|
||||
}
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<InputTypeIn
|
||||
type="text"
|
||||
placeholder="Search Connectors"
|
||||
ref={searchInputRef}
|
||||
value={rawSearchTerm} // keep the input bound to immediate state
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
className="w-96 flex-none"
|
||||
/>
|
||||
|
||||
{dedupedPopular.length > 0 && (
|
||||
<div className="pt-8">
|
||||
<InputTypeIn
|
||||
type="text"
|
||||
placeholder="Search Connectors"
|
||||
ref={searchInputRef}
|
||||
value={rawSearchTerm} // keep the input bound to immediate state
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
className="w-96 flex-none"
|
||||
/>
|
||||
|
||||
{dedupedPopular.length > 0 && (
|
||||
<div className="pt-8">
|
||||
<Text as="p" headingH3>
|
||||
Popular
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-4 p-4">
|
||||
{dedupedPopular.map((source) => (
|
||||
<SourceTileTooltipWrapper
|
||||
preSelect={false}
|
||||
key={source.internalName}
|
||||
sourceMetadata={source}
|
||||
federatedConnectors={federatedConnectors}
|
||||
slackCredentials={slackCredentials}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.entries(categorizedSources)
|
||||
.filter(([_, sources]) => sources.length > 0)
|
||||
.map(([category, sources], categoryInd) => (
|
||||
<div key={category} className="pt-8">
|
||||
<Text as="p" headingH3>
|
||||
Popular
|
||||
{category}
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-4 p-4">
|
||||
{dedupedPopular.map((source) => (
|
||||
{sources.map((source, sourceInd) => (
|
||||
<SourceTileTooltipWrapper
|
||||
preSelect={false}
|
||||
preSelect={
|
||||
(searchTerm?.length ?? 0) > 0 &&
|
||||
categoryInd == 0 &&
|
||||
sourceInd == 0
|
||||
}
|
||||
key={source.internalName}
|
||||
sourceMetadata={source}
|
||||
federatedConnectors={federatedConnectors}
|
||||
@@ -287,33 +311,7 @@ export default function Page() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.entries(categorizedSources)
|
||||
.filter(([_, sources]) => sources.length > 0)
|
||||
.map(([category, sources], categoryInd) => (
|
||||
<div key={category} className="pt-8">
|
||||
<Text as="p" headingH3>
|
||||
{category}
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-4 p-4">
|
||||
{sources.map((source, sourceInd) => (
|
||||
<SourceTileTooltipWrapper
|
||||
preSelect={
|
||||
(searchTerm?.length ?? 0) > 0 &&
|
||||
categoryInd == 0 &&
|
||||
sourceInd == 0
|
||||
}
|
||||
key={source.internalName}
|
||||
sourceMetadata={source}
|
||||
federatedConnectors={federatedConnectors}
|
||||
slackCredentials={slackCredentials}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { PersonasTable } from "./PersonaTable";
|
||||
import Text from "@/components/ui/text";
|
||||
import Title from "@/components/ui/title";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { SubLabel } from "@/components/Field";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
import { useAdminPersonas } from "@/hooks/useAdminPersonas";
|
||||
import { Persona } from "./interfaces";
|
||||
@@ -127,33 +127,31 @@ export default function Page() {
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header icon={SvgOnyxOctagon} title="Agents" separator />
|
||||
<>
|
||||
<AdminPageTitle icon={SvgOnyxOctagon} title="Agents" />
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
{isLoading && <ThreeDotsLoader />}
|
||||
{isLoading && <ThreeDotsLoader />}
|
||||
|
||||
{error && (
|
||||
<ErrorCallout
|
||||
errorTitle="Failed to load agents"
|
||||
errorMsg={
|
||||
error?.info?.message ||
|
||||
error?.info?.detail ||
|
||||
"An unknown error occurred"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<ErrorCallout
|
||||
errorTitle="Failed to load agents"
|
||||
errorMsg={
|
||||
error?.info?.message ||
|
||||
error?.info?.detail ||
|
||||
"An unknown error occurred"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && (
|
||||
<MainContent
|
||||
personas={personas}
|
||||
totalItems={totalItems}
|
||||
currentPage={currentPage}
|
||||
onPageChange={setCurrentPage}
|
||||
refreshPersonas={refresh}
|
||||
/>
|
||||
)}
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
{!isLoading && !error && (
|
||||
<MainContent
|
||||
personas={personas}
|
||||
totalItems={totalItems}
|
||||
currentPage={currentPage}
|
||||
onPageChange={setCurrentPage}
|
||||
refreshPersonas={refresh}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
@@ -233,11 +233,10 @@ function Main() {
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header title="API Keys" icon={SvgKey} separator />
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle title="API Keys" icon={SvgKey} />
|
||||
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import CardSection from "@/components/admin/CardSection";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { SlackTokensForm } from "./SlackTokensForm";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgSlack } from "@opal/icons";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
|
||||
export const NewSlackBotForm = () => {
|
||||
const [formValues] = useState({
|
||||
@@ -18,19 +19,20 @@ export const NewSlackBotForm = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header icon={SvgSlack} title="New Slack Bot" separator />
|
||||
<SettingsLayouts.Body>
|
||||
<CardSection>
|
||||
<div className="p-4">
|
||||
<SlackTokensForm
|
||||
isUpdate={false}
|
||||
initialValues={formValues}
|
||||
router={router}
|
||||
/>
|
||||
</div>
|
||||
</CardSection>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<div>
|
||||
<AdminPageTitle
|
||||
icon={<SourceIcon iconSize={36} sourceType={ValidSources.Slack} />}
|
||||
title="New Slack Bot"
|
||||
/>
|
||||
<CardSection>
|
||||
<div className="p-4">
|
||||
<SlackTokensForm
|
||||
isUpdate={false}
|
||||
initialValues={formValues}
|
||||
router={router}
|
||||
/>
|
||||
</div>
|
||||
</CardSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSetSummary, SlackChannelConfig } from "@/lib/types";
|
||||
import {
|
||||
DocumentSetSummary,
|
||||
SlackChannelConfig,
|
||||
ValidSources,
|
||||
} from "@/lib/types";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgSlack } from "@opal/icons";
|
||||
import { FetchAgentsResponse, fetchAgentsSS } from "@/lib/agentsSS";
|
||||
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||
|
||||
@@ -72,28 +77,27 @@ async function EditslackChannelConfigPage(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<div className="max-w-4xl container">
|
||||
<InstantSSRAutoRefresh />
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgSlack}
|
||||
|
||||
<BackButton />
|
||||
<AdminPageTitle
|
||||
icon={<SourceIcon sourceType={ValidSources.Slack} iconSize={32} />}
|
||||
title={
|
||||
slackChannelConfig.is_default
|
||||
? "Edit Default Slack Config"
|
||||
: "Edit Slack Channel Config"
|
||||
}
|
||||
separator
|
||||
backButton
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<SlackChannelConfigCreationForm
|
||||
slack_bot_id={slackChannelConfig.slack_bot_id}
|
||||
documentSets={documentSets}
|
||||
personas={assistants}
|
||||
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
|
||||
existingSlackChannelConfig={slackChannelConfig}
|
||||
/>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<SlackChannelConfigCreationForm
|
||||
slack_bot_id={slackChannelConfig.slack_bot_id}
|
||||
documentSets={documentSets}
|
||||
personas={assistants}
|
||||
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
|
||||
existingSlackChannelConfig={slackChannelConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSetSummary } from "@/lib/types";
|
||||
import { DocumentSetSummary, ValidSources } from "@/lib/types";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { fetchAgentsSS } from "@/lib/agentsSS";
|
||||
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||
import { redirect } from "next/navigation";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgSlack } from "@opal/icons";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
|
||||
async function NewChannelConfigPage(props: {
|
||||
params: Promise<{ "bot-id": string }>;
|
||||
@@ -49,22 +50,20 @@ async function NewChannelConfigPage(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgSlack}
|
||||
<>
|
||||
<BackButton />
|
||||
<AdminPageTitle
|
||||
icon={<SourceIcon iconSize={32} sourceType={ValidSources.Slack} />}
|
||||
title="Configure OnyxBot for Slack Channel"
|
||||
separator
|
||||
backButton
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<SlackChannelConfigCreationForm
|
||||
slack_bot_id={slack_bot_id}
|
||||
documentSets={documentSets}
|
||||
personas={agentsResponse[0]}
|
||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||
/>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<SlackChannelConfigCreationForm
|
||||
slack_bot_id={slack_bot_id}
|
||||
documentSets={documentSets}
|
||||
personas={agentsResponse[0]}
|
||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { SlackBotTable } from "./SlackBotTable";
|
||||
import { useSlackBots } from "./[bot-id]/hooks";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgSlack } from "@opal/icons";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
import { DOCS_ADMINS_PATH } from "@/lib/constants";
|
||||
|
||||
@@ -76,13 +77,15 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header icon={SvgSlack} title="Slack Bots" separator />
|
||||
<SettingsLayouts.Body>
|
||||
<InstantSSRAutoRefresh />
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle
|
||||
icon={<SourceIcon iconSize={36} sourceType={ValidSources.Slack} />}
|
||||
title="Slack Bots"
|
||||
/>
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useState } from "react";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import { DocumentIcon2 } from "@/components/icons/icons";
|
||||
import useSWR from "swr";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SvgFileText, SvgLock } from "@opal/icons";
|
||||
import { SvgLock } from "@opal/icons";
|
||||
|
||||
function Main() {
|
||||
const {
|
||||
@@ -148,15 +149,12 @@ function Main() {
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgFileText}
|
||||
<>
|
||||
<AdminPageTitle
|
||||
title="Document Processing"
|
||||
separator
|
||||
icon={<DocumentIcon2 size={32} className="my-auto" />}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import Text from "@/components/ui/text";
|
||||
import Title from "@/components/ui/title";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
@@ -141,15 +141,9 @@ function Main() {
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
title="Search Settings"
|
||||
icon={SvgSearch}
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle title="Search Settings" icon={SvgSearch} />
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { FiDownload } from "react-icons/fi";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import {
|
||||
Table,
|
||||
@@ -15,7 +16,7 @@ import Button from "@/refresh-components/buttons/Button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import Text from "@/components/ui/text";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { SvgDownload, SvgDownloadCloud } from "@opal/icons";
|
||||
import { SvgDownloadCloud } from "@opal/icons";
|
||||
function Main() {
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -115,12 +116,10 @@ function Main() {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header icon={SvgDownload} title="Debug Logs" separator />
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle icon={<FiDownload size={32} />} title="Debug Logs" />
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Explorer } from "./Explorer";
|
||||
import { fetchValidFilterInfo } from "@/lib/search/utilsSS";
|
||||
import { SvgZoomIn } from "@opal/icons";
|
||||
@@ -9,20 +9,17 @@ export default async function Page(props: {
|
||||
const { connectors, documentSets } = await fetchValidFilterInfo();
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgZoomIn}
|
||||
<>
|
||||
<AdminPageTitle
|
||||
icon={<SvgZoomIn className="stroke-text-04 h-8 w-8" />}
|
||||
title="Document Explorer"
|
||||
separator
|
||||
/>
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
<Explorer
|
||||
initialSearchValue={searchParams.query}
|
||||
connectors={connectors}
|
||||
documentSets={documentSets}
|
||||
/>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<Explorer
|
||||
initialSearchValue={searchParams.query}
|
||||
connectors={connectors}
|
||||
documentSets={documentSets}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { LoadingAnimation } from "@/components/Loading";
|
||||
import { useMostReactedToDocuments } from "@/lib/hooks";
|
||||
import { DocumentFeedbackTable } from "./DocumentFeedbackTable";
|
||||
import { numPages, numToDisplay } from "./constants";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import Title from "@/components/ui/title";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgThumbsUp } from "@opal/icons";
|
||||
const Main = () => {
|
||||
const {
|
||||
@@ -61,16 +61,11 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgThumbsUp}
|
||||
title="Document Feedback"
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle icon={SvgThumbsUp} title="Document Feedback" />
|
||||
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { refreshDocumentSets, useDocumentSets } from "../hooks";
|
||||
import { useConnectorStatus, useUserGroups } from "@/lib/hooks";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgBookOpen } from "@opal/icons";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { BookmarkIcon } from "@/components/icons/icons";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { DocumentSetCreationForm } from "../DocumentSetCreationForm";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -68,27 +69,24 @@ function Main({ documentSetId }: { documentSetId: number }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgBookOpen}
|
||||
<div>
|
||||
<AdminPageTitle
|
||||
icon={<BookmarkIcon size={32} />}
|
||||
title={documentSet.name}
|
||||
separator
|
||||
backButton
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<CardSection>
|
||||
<DocumentSetCreationForm
|
||||
ccPairs={ccPairs}
|
||||
userGroups={userGroups}
|
||||
onClose={() => {
|
||||
refreshDocumentSets();
|
||||
router.push("/admin/documents/sets");
|
||||
}}
|
||||
existingDocumentSet={documentSet}
|
||||
/>
|
||||
</CardSection>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<CardSection>
|
||||
<DocumentSetCreationForm
|
||||
ccPairs={ccPairs}
|
||||
userGroups={userGroups}
|
||||
onClose={() => {
|
||||
refreshDocumentSets();
|
||||
router.push("/admin/documents/sets");
|
||||
}}
|
||||
existingDocumentSet={documentSet}
|
||||
/>
|
||||
</CardSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,5 +96,11 @@ export default function Page(props: {
|
||||
const params = use(props.params);
|
||||
const documentSetId = parseInt(params.documentSetId);
|
||||
|
||||
return <Main documentSetId={documentSetId} />;
|
||||
return (
|
||||
<>
|
||||
<BackButton />
|
||||
|
||||
<Main documentSetId={documentSetId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgBookOpen } from "@opal/icons";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { BookmarkIcon } from "@/components/icons/icons";
|
||||
import { DocumentSetCreationForm } from "../DocumentSetCreationForm";
|
||||
import { useConnectorStatus, useUserGroups } from "@/lib/hooks";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { refreshDocumentSets } from "../hooks";
|
||||
@@ -57,17 +58,16 @@ function Main() {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgBookOpen}
|
||||
<>
|
||||
<BackButton />
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<BookmarkIcon size={32} />}
|
||||
title="New Document Set"
|
||||
separator
|
||||
backButton
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { InfoIcon } from "@/components/icons/icons";
|
||||
import { BookmarkIcon, InfoIcon } from "@/components/icons/icons";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
@@ -19,8 +19,7 @@ import { useDocumentSets } from "./hooks";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { deleteDocumentSet } from "./lib";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgBookOpen } from "@opal/icons";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import {
|
||||
FiAlertTriangle,
|
||||
FiCheckCircle,
|
||||
@@ -423,16 +422,11 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgBookOpen}
|
||||
title="Document Sets"
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle icon={<BookmarkIcon size={32} />} title="Document Sets" />
|
||||
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { toast } from "@/hooks/useToast";
|
||||
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import EmbeddingModelSelection from "../EmbeddingModelSelectionForm";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -481,6 +481,9 @@ export default function EmbeddingForm() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto mb-8 w-full">
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
{formStep == 0 && (
|
||||
<>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { NotebookIcon } from "@/components/icons/icons";
|
||||
import { CCPairIndexingStatusTable } from "./CCPairIndexingStatusTable";
|
||||
import { SearchAndFilterControls } from "./SearchAndFilterControls";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import Link from "next/link";
|
||||
import { SvgPlug } from "@opal/icons";
|
||||
import Text from "@/components/ui/text";
|
||||
import { useConnectorIndexingStatusWithPagination } from "@/lib/hooks";
|
||||
import { useToastFromQuery } from "@/hooks/useToast";
|
||||
@@ -213,18 +213,16 @@ export default function Status() {
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgPlug}
|
||||
<>
|
||||
<AdminPageTitle
|
||||
icon={<NotebookIcon size={32} />}
|
||||
title="Existing Connectors"
|
||||
rightChildren={
|
||||
farRightElement={
|
||||
<Button href="/admin/add-connector">Add Connector</Button>
|
||||
}
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import {
|
||||
DatePickerField,
|
||||
FieldLabel,
|
||||
TextArrayField,
|
||||
TextFormField,
|
||||
} from "@/components/Field";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { BrainIcon } from "@/components/icons/icons";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import SwitchField from "@/refresh-components/form/SwitchField";
|
||||
@@ -29,7 +30,7 @@ import { useIsKGExposed } from "@/app/admin/kg/utils";
|
||||
import KGEntityTypes from "@/app/admin/kg/KGEntityTypes";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SvgNetworkGraph, SvgSettings } from "@opal/icons";
|
||||
import { SvgSettings } from "@opal/icons";
|
||||
|
||||
function createDomainField(
|
||||
name: string,
|
||||
@@ -323,15 +324,12 @@ export default function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgNetworkGraph}
|
||||
<>
|
||||
<AdminPageTitle
|
||||
title="Knowledge Graph"
|
||||
separator
|
||||
icon={<BrainIcon size={32} className="my-auto" />}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import SimpleTabs from "@/refresh-components/SimpleTabs";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import Text from "@/components/ui/text";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
@@ -208,15 +208,9 @@ function Main() {
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
title="Token Rate Limits"
|
||||
icon={SvgShield}
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle title="Token Rate Limits" icon={SvgShield} />
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import SignedUpUserTable from "@/components/admin/users/SignedUpUserTable";
|
||||
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
@@ -327,12 +327,10 @@ function AddUserButton() {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header title="Manage Users" icon={SvgUser} separator />
|
||||
<SettingsLayouts.Body>
|
||||
<SearchableTables />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<>
|
||||
<AdminPageTitle title="Manage Users" icon={SvgUser} />
|
||||
<SearchableTables />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { redirect, useRouter } from "next/navigation";
|
||||
import type { Route } from "next";
|
||||
@@ -61,6 +61,10 @@ export default function ImpersonatePage() {
|
||||
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<div className="absolute top-10x w-full">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
<div className="w-full flex flex-col items-center justify-center">
|
||||
<Text as="p" headingH3 className="mb-6 text-center">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { User } from "@/lib/types";
|
||||
import {
|
||||
getCurrentUserSS,
|
||||
@@ -64,6 +65,7 @@ const Page = async (props: {
|
||||
|
||||
return (
|
||||
<AuthFlowContainer authState="join">
|
||||
<HealthCheckBanner />
|
||||
<AuthErrorDisplay searchParams={searchParams} />
|
||||
|
||||
<>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { User } from "@/lib/types";
|
||||
import {
|
||||
getCurrentUserSS,
|
||||
@@ -104,6 +105,10 @@ export default async function Page(props: PageProps) {
|
||||
authState="login"
|
||||
footerContent={ssoLoginFooterContent}
|
||||
>
|
||||
<div className="absolute top-10x w-full">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
||||
<LoginPage
|
||||
authUrl={authUrl}
|
||||
authTypeMetadata={authTypeMetadata}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { User } from "@/lib/types";
|
||||
import {
|
||||
getCurrentUserSS,
|
||||
@@ -62,6 +63,7 @@ const Page = async (props: {
|
||||
|
||||
return (
|
||||
<AuthFlowContainer authState="signup">
|
||||
<HealthCheckBanner />
|
||||
<AuthErrorDisplay searchParams={searchParams} />
|
||||
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Text from "@/components/ui/text";
|
||||
@@ -62,6 +63,9 @@ export default function Verify({ user }: VerifyProps) {
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="absolute top-10x w-full">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<Logo folded size={64} className="mx-auto w-fit animate-pulse" />
|
||||
{!error ? (
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getCurrentUserSS,
|
||||
} from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { User } from "@/lib/types";
|
||||
import Text from "@/components/ui/text";
|
||||
import { RequestNewVerificationEmail } from "./RequestNewVerificationEmail";
|
||||
@@ -35,6 +35,9 @@ export default async function Page() {
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="absolute top-10x w-full">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<Logo folded size={64} className="mx-auto w-fit" />
|
||||
<div className="flex">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import BillingInformationPage from "./BillingInformationPage";
|
||||
import { SvgCreditCard } from "@opal/icons";
|
||||
import { MdOutlineCreditCard } from "react-icons/md";
|
||||
|
||||
export interface BillingInformation {
|
||||
stripe_subscription_id: string;
|
||||
@@ -18,15 +18,12 @@ export interface BillingInformation {
|
||||
|
||||
export default function page() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgCreditCard}
|
||||
<>
|
||||
<AdminPageTitle
|
||||
title="Billing Information"
|
||||
separator
|
||||
icon={<MdOutlineCreditCard size={32} className="my-auto" />}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<BillingInformationPage />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<BillingInformationPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { useConnectorStatus } from "@/lib/hooks";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { SvgUsers } from "@opal/icons";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
const Page = (props: { params: Promise<{ groupId: string }> }) => {
|
||||
const params = use(props.params);
|
||||
const router = useRouter();
|
||||
@@ -51,27 +52,22 @@ const Page = (props: { params: Promise<{ groupId: string }> }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgUsers}
|
||||
title={userGroup.name || "Unknown"}
|
||||
separator
|
||||
backButton
|
||||
/>
|
||||
<>
|
||||
<BackButton />
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
{userGroup ? (
|
||||
<GroupDisplay
|
||||
users={users.accepted}
|
||||
ccPairs={ccPairs}
|
||||
userGroup={userGroup}
|
||||
refreshUserGroup={refreshUserGroup}
|
||||
/>
|
||||
) : (
|
||||
<div>Unable to fetch User Group :(</div>
|
||||
)}
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<AdminPageTitle title={userGroup.name || "Unknown"} icon={SvgUsers} />
|
||||
|
||||
{userGroup ? (
|
||||
<GroupDisplay
|
||||
users={users.accepted}
|
||||
ccPairs={ccPairs}
|
||||
userGroup={userGroup}
|
||||
refreshUserGroup={refreshUserGroup}
|
||||
/>
|
||||
) : (
|
||||
<div>Unable to fetch User Group :(</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import UserGroupCreationForm from "./UserGroupCreationForm";
|
||||
import { useState } from "react";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { useConnectorStatus, useUserGroups } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
import { SvgUsers } from "@opal/icons";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
const Main = () => {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
@@ -74,17 +74,11 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgUsers}
|
||||
title="Manage User Groups"
|
||||
separator
|
||||
/>
|
||||
<>
|
||||
<AdminPageTitle title="Manage User Groups" icon={SvgUsers} />
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { CUSTOM_ANALYTICS_ENABLED } from "@/lib/constants";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { SvgBarChart } from "@opal/icons";
|
||||
import { FiBarChart2 } from "react-icons/fi";
|
||||
import Text from "@/components/ui/text";
|
||||
import { CustomAnalyticsUpdateForm } from "./CustomAnalyticsUpdateForm";
|
||||
|
||||
@@ -35,15 +35,13 @@ function Main() {
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgBarChart}
|
||||
<main className="pt-4 mx-auto container">
|
||||
<AdminPageTitle
|
||||
title="Custom Analytics"
|
||||
separator
|
||||
icon={<FiBarChart2 size={32} />}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<Main />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { QueryHistoryTable } from "@/app/ee/admin/performance/query-history/QueryHistoryTable";
|
||||
import { SvgServer } from "@opal/icons";
|
||||
export default function QueryHistoryPage() {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgServer}
|
||||
title="Query History"
|
||||
separator
|
||||
/>
|
||||
<>
|
||||
<AdminPageTitle title="Query History" icon={SvgServer} />
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
<QueryHistoryTable />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<QueryHistoryTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,38 +6,32 @@ import { FeedbackChart } from "@/app/ee/admin/performance/usage/FeedbackChart";
|
||||
import { QueryPerformanceChart } from "@/app/ee/admin/performance/usage/QueryPerformanceChart";
|
||||
import { PersonaMessagesChart } from "@/app/ee/admin/performance/usage/PersonaMessagesChart";
|
||||
import { useTimeRange } from "@/app/ee/admin/performance/lib";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import UsageReports from "@/app/ee/admin/performance/usage/UsageReports";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import { useAdminPersonas } from "@/hooks/useAdminPersonas";
|
||||
import { SvgActivity } from "@opal/icons";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [timeRange, setTimeRange] = useTimeRange();
|
||||
const { personas } = useAdminPersonas();
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgActivity}
|
||||
title="Usage Statistics"
|
||||
separator
|
||||
<>
|
||||
<AdminPageTitle title="Usage Statistics" icon={SvgActivity} />
|
||||
<AdminDateRangeSelector
|
||||
value={timeRange}
|
||||
onValueChange={(value) => setTimeRange(value as any)}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<AdminDateRangeSelector
|
||||
value={timeRange}
|
||||
onValueChange={(value) => setTimeRange(value as any)}
|
||||
/>
|
||||
<QueryPerformanceChart timeRange={timeRange} />
|
||||
<FeedbackChart timeRange={timeRange} />
|
||||
<OnyxBotChart timeRange={timeRange} />
|
||||
<PersonaMessagesChart
|
||||
availablePersonas={personas}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<Separator />
|
||||
<UsageReports />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<QueryPerformanceChart timeRange={timeRange} />
|
||||
<FeedbackChart timeRange={timeRange} />
|
||||
<OnyxBotChart timeRange={timeRange} />
|
||||
<PersonaMessagesChart
|
||||
availablePersonas={personas}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<Separator />
|
||||
<UsageReports />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { StandardAnswerCreationForm } from "@/app/ee/admin/standard-answer/StandardAnswerCreationForm";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgClipboard } from "@opal/icons";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { ClipboardIcon } from "@/components/icons/icons";
|
||||
import { StandardAnswer, StandardAnswerCategory } from "@/lib/types";
|
||||
|
||||
async function Page(props: { params: Promise<{ id: string }> }) {
|
||||
@@ -67,20 +68,18 @@ async function Page(props: { params: Promise<{ id: string }> }) {
|
||||
const standardAnswerCategories =
|
||||
(await standardAnswerCategoriesResponse.json()) as StandardAnswerCategory[];
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgClipboard}
|
||||
<>
|
||||
<BackButton />
|
||||
<AdminPageTitle
|
||||
title="Edit Standard Answer"
|
||||
backButton
|
||||
separator
|
||||
icon={<ClipboardIcon size={32} />}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<StandardAnswerCreationForm
|
||||
standardAnswerCategories={standardAnswerCategories}
|
||||
existingStandardAnswer={standardAnswer}
|
||||
/>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<StandardAnswerCreationForm
|
||||
standardAnswerCategories={standardAnswerCategories}
|
||||
existingStandardAnswer={standardAnswer}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { StandardAnswerCreationForm } from "@/app/ee/admin/standard-answer/StandardAnswerCreationForm";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SvgClipboard } from "@opal/icons";
|
||||
import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { ClipboardIcon } from "@/components/icons/icons";
|
||||
import { StandardAnswerCategory } from "@/lib/types";
|
||||
|
||||
async function Page() {
|
||||
@@ -22,19 +23,17 @@ async function Page() {
|
||||
(await standardAnswerCategoriesResponse.json()) as StandardAnswerCategory[];
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgClipboard}
|
||||
<>
|
||||
<BackButton />
|
||||
<AdminPageTitle
|
||||
title="New Standard Answer"
|
||||
backButton
|
||||
separator
|
||||
icon={<ClipboardIcon size={32} />}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<StandardAnswerCreationForm
|
||||
standardAnswerCategories={standardAnswerCategories}
|
||||
/>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
|
||||
<StandardAnswerCreationForm
|
||||
standardAnswerCategories={standardAnswerCategories}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { EditIcon } from "@/components/icons/icons";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { ClipboardIcon, EditIcon } from "@/components/icons/icons";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { useStandardAnswers, useStandardAnswerCategories } from "./hooks";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
@@ -29,7 +29,7 @@ import { PageSelector } from "@/components/PageSelector";
|
||||
import Text from "@/components/ui/text";
|
||||
import { TableHeader } from "@/components/ui/table";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
import { SvgClipboard, SvgTrash } from "@opal/icons";
|
||||
import { SvgTrash } from "@opal/icons";
|
||||
import { Button } from "@opal/components";
|
||||
const NUM_RESULTS_PER_PAGE = 10;
|
||||
|
||||
@@ -417,16 +417,13 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
<SettingsLayouts.Header
|
||||
icon={SvgClipboard}
|
||||
<>
|
||||
<AdminPageTitle
|
||||
icon={<ClipboardIcon size={32} />}
|
||||
title="Standard Answers"
|
||||
separator
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
<Main />
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
<Main />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import GatedContentWrapper from "@/components/GatedContentWrapper";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { fetchAppSidebarMetadata } from "@/lib/appSidebarSS";
|
||||
import StatsOverlayLoader from "@/components/dev/StatsOverlayLoader";
|
||||
import AppHealthBanner from "@/sections/AppHealthBanner";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -129,10 +128,7 @@ export default async function RootLayout({
|
||||
>
|
||||
<div className="text-text min-h-screen bg-background">
|
||||
<TooltipProvider>
|
||||
<PHProvider>
|
||||
<AppHealthBanner />
|
||||
{content}
|
||||
</PHProvider>
|
||||
<PHProvider>{content}</PHProvider>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -28,23 +28,6 @@ const SETTINGS_LAYOUT_PREFIXES = [
|
||||
"/admin/discord-bot",
|
||||
"/admin/theme",
|
||||
"/admin/configuration/llm",
|
||||
"/admin/agents",
|
||||
"/admin/users",
|
||||
"/admin/token-rate-limits",
|
||||
"/admin/configuration/search",
|
||||
"/admin/configuration/document-processing",
|
||||
"/admin/configuration/code-interpreter",
|
||||
"/admin/api-key",
|
||||
"/admin/add-connector",
|
||||
"/admin/indexing/status",
|
||||
"/admin/documents",
|
||||
"/admin/debug",
|
||||
"/admin/kg",
|
||||
"/admin/bots",
|
||||
"/ee/admin/standard-answer",
|
||||
"/ee/admin/groups",
|
||||
"/ee/admin/billing",
|
||||
"/ee/admin/performance",
|
||||
];
|
||||
|
||||
export function ClientLayout({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { JSX } from "react";
|
||||
import { HealthCheckBanner } from "../health/healthcheck";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import type { IconProps } from "@opal/types";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -20,6 +21,9 @@ export function AdminPageTitle({
|
||||
}: AdminPageTitleProps) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between">
|
||||
<div className="flex flex-row gap-2">
|
||||
{typeof Icon === "function" ? (
|
||||
|
||||
@@ -5,16 +5,14 @@ import useSWR from "swr";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { getSecondsUntilExpiration } from "@/lib/time";
|
||||
import { refreshToken } from "@/lib/user";
|
||||
import { User } from "@/lib/types";
|
||||
import { refreshToken } from "./refreshUtils";
|
||||
import { NEXT_PUBLIC_CUSTOM_REFRESH_URL } from "@/lib/constants";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { logout } from "@/lib/user";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { SvgAlertTriangle, SvgLogOut } from "@opal/icons";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { useCurrentUser } from "@/hooks/useCurrentUser";
|
||||
|
||||
export default function AppHealthBanner() {
|
||||
import { SvgLogOut } from "@opal/icons";
|
||||
export const HealthCheckBanner = () => {
|
||||
const router = useRouter();
|
||||
const { error } = useSWR("/api/health", errorHandlingFetcher);
|
||||
const [expired, setExpired] = useState(false);
|
||||
@@ -23,7 +21,16 @@ export default function AppHealthBanner() {
|
||||
const expirationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const refreshIntervalRef = useRef<NodeJS.Timer | null>(null);
|
||||
|
||||
const { user, mutateUser, userError } = useCurrentUser();
|
||||
// Reduce revalidation frequency with dedicated SWR config
|
||||
const {
|
||||
data: user,
|
||||
mutate: mutateUser,
|
||||
error: userError,
|
||||
} = useSWR<User>("/api/me", errorHandlingFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 30000, // 30 seconds
|
||||
});
|
||||
|
||||
// Handle 403 errors from the /api/me endpoint
|
||||
useEffect(() => {
|
||||
@@ -37,10 +44,10 @@ export default function AppHealthBanner() {
|
||||
}, [userError, pathname]);
|
||||
|
||||
// Function to handle the "Log in" button click
|
||||
function handleLogin() {
|
||||
const handleLogin = () => {
|
||||
setShowLoggedOutModal(false);
|
||||
router.push("/auth/login");
|
||||
}
|
||||
};
|
||||
|
||||
// Function to set up expiration timeout
|
||||
const setupExpirationTimeout = useCallback(
|
||||
@@ -204,15 +211,16 @@ export default function AppHealthBanner() {
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-[101] w-full bg-status-error-01 p-3">
|
||||
<Content
|
||||
icon={SvgAlertTriangle}
|
||||
title="The backend is currently unavailable"
|
||||
description="If this is your initial setup or you just updated your Onyx deployment, this is likely because the backend is still starting up. Give it a minute or two, and then refresh the page. If that does not work, make sure the backend is setup and/or contact an administrator."
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<div className="fixed top-0 left-0 z-[101] w-full text-xs mx-auto bg-gradient-to-r from-red-900 to-red-700 p-2 rounded-sm border-hidden text-neutral-50 dark:text-neutral-100">
|
||||
<p className="font-bold pb-1">The backend is currently unavailable.</p>
|
||||
|
||||
<p className="px-1">
|
||||
If this is your initial setup or you just updated your Onyx
|
||||
deployment, this is likely because the backend is still starting up.
|
||||
Give it a minute or two, and then refresh the page. If that does not
|
||||
work, make sure the backend is setup and/or contact an administrator.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
64
web/src/components/health/refreshUtils.ts
Normal file
64
web/src/components/health/refreshUtils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export interface CustomRefreshTokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
session: {
|
||||
exp: number;
|
||||
};
|
||||
userinfo: {
|
||||
sub: string;
|
||||
familyName: string;
|
||||
givenName: string;
|
||||
fullName: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function mockedRefreshToken(): CustomRefreshTokenResponse {
|
||||
/**
|
||||
* This function mocks the response from a token refresh endpoint.
|
||||
* It generates a mock access token, refresh token, and user information
|
||||
* with an expiration time set to 1 hour from now.
|
||||
* This is useful for testing or development when the actual refresh endpoint is not available.
|
||||
*/
|
||||
const mockExp = Date.now() + 3600000; // 1 hour from now in milliseconds
|
||||
const data: CustomRefreshTokenResponse = {
|
||||
access_token: "Mock access token",
|
||||
refresh_token: "Mock refresh token",
|
||||
session: { exp: mockExp },
|
||||
userinfo: {
|
||||
sub: "Mock email",
|
||||
familyName: "Mock name",
|
||||
givenName: "Mock name",
|
||||
fullName: "Mock name",
|
||||
userId: "Mock User ID",
|
||||
email: "email@onyx.app",
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function refreshToken(
|
||||
customRefreshUrl: string
|
||||
): Promise<CustomRefreshTokenResponse | null> {
|
||||
try {
|
||||
console.debug("Sending request to custom refresh URL");
|
||||
// support both absolute and relative
|
||||
const url = customRefreshUrl.startsWith("http")
|
||||
? new URL(customRefreshUrl)
|
||||
: new URL(customRefreshUrl, window.location.origin);
|
||||
url.searchParams.append("info", "json");
|
||||
url.searchParams.append("access_token_refresh_interval", "3600");
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to refresh token: ${await response.text()}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error refreshing token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import useSWR, { type KeyedMutator } from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* Fetches the current authenticated user via SWR (`/api/me`).
|
||||
*
|
||||
* This hook is intentionally configured with conservative revalidation
|
||||
* settings to avoid hammering the backend on every focus/reconnect event:
|
||||
*
|
||||
* - `revalidateOnFocus: false` — tab switches won't trigger a refetch
|
||||
* - `revalidateOnReconnect: false` — network recovery won't trigger a refetch
|
||||
* - `dedupingInterval: 30_000` — duplicate requests within 30 s are deduped
|
||||
*
|
||||
* The returned `mutateUser` handle lets callers imperatively refetch (e.g.
|
||||
* after a token refresh) without changing the global SWR config.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { user, mutateUser, userError } = useCurrentUser();
|
||||
* ```
|
||||
*/
|
||||
export function useCurrentUser(): {
|
||||
/** The authenticated user, or `undefined` while loading. */
|
||||
user: User | undefined;
|
||||
/** Imperatively revalidate / update the cached user. */
|
||||
mutateUser: KeyedMutator<User>;
|
||||
/** The error thrown by the fetcher, if any. */
|
||||
userError: (Error & { status?: number }) | undefined;
|
||||
} {
|
||||
const { data, mutate, error } = useSWR<User>(
|
||||
"/api/me",
|
||||
errorHandlingFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 30_000,
|
||||
}
|
||||
);
|
||||
|
||||
return { user: data, mutateUser: mutate, userError: error };
|
||||
}
|
||||
@@ -72,44 +72,3 @@ export const basicSignup = async (
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
export interface CustomRefreshTokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
session: {
|
||||
exp: number;
|
||||
};
|
||||
userinfo: {
|
||||
sub: string;
|
||||
familyName: string;
|
||||
givenName: string;
|
||||
fullName: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshToken(
|
||||
customRefreshUrl: string
|
||||
): Promise<CustomRefreshTokenResponse | null> {
|
||||
try {
|
||||
console.debug("Sending request to custom refresh URL");
|
||||
// support both absolute and relative
|
||||
const url = customRefreshUrl.startsWith("http")
|
||||
? new URL(customRefreshUrl)
|
||||
: new URL(customRefreshUrl, window.location.origin);
|
||||
url.searchParams.append("info", "json");
|
||||
url.searchParams.append("access_token_refresh_interval", "3600");
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to refresh token: ${await response.text()}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error refreshing token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { redirect, useRouter, useSearchParams } from "next/navigation";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import {
|
||||
personaIncludesRetrieval,
|
||||
getAvailableContextTokens,
|
||||
@@ -627,7 +628,12 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
// handle error case where no assistants are available
|
||||
// Only show this after agents have loaded to prevent flash during initial load
|
||||
if (noAgents && !isLoadingAgents) {
|
||||
return <NoAgentModal />;
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
<NoAgentModal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const hasStarterMessages = (liveAgent?.starter_messages?.length ?? 0) > 0;
|
||||
@@ -648,6 +654,8 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
|
||||
<AppPopup />
|
||||
|
||||
{retrievalEnabled && documentSidebarVisible && settings.isMobile && (
|
||||
|
||||
@@ -705,6 +705,7 @@ const AppInputBar = React.memo(
|
||||
|
||||
{/* Controls that load in when data is ready */}
|
||||
<div
|
||||
data-testid="actions-container"
|
||||
className={cn(
|
||||
"flex flex-row items-center",
|
||||
controlsLoading && "invisible"
|
||||
|
||||
@@ -29,7 +29,11 @@ const DEFAULT_MASK_SELECTORS: string[] = [
|
||||
* Default selectors to hide (visibility: hidden) across all screenshots.
|
||||
* These elements are overlays or ephemeral UI that would cause spurious diffs.
|
||||
*/
|
||||
const DEFAULT_HIDE_SELECTORS: string[] = ['[data-testid="toast-container"]'];
|
||||
const DEFAULT_HIDE_SELECTORS: string[] = [
|
||||
'[data-testid="toast-container"]',
|
||||
// TODO: Remove once it loads consistently.
|
||||
'[data-testid="actions-container"]',
|
||||
];
|
||||
|
||||
interface ScreenshotOptions {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user