mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-10 02:02:41 +00:00
Compare commits
1 Commits
main
...
nikg/std-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6041773f60 |
4
.github/workflows/pr-integration-tests.yml
vendored
4
.github/workflows/pr-integration-tests.yml
vendored
@@ -316,7 +316,6 @@ jobs:
|
||||
# Base config shared by both editions
|
||||
cat <<EOF > deployment/docker_compose/.env
|
||||
COMPOSE_PROFILES=s3-filestore
|
||||
OPENSEARCH_FOR_ONYX_ENABLED=false
|
||||
AUTH_TYPE=basic
|
||||
POSTGRES_POOL_PRE_PING=true
|
||||
POSTGRES_USE_NULL_POOL=true
|
||||
@@ -419,7 +418,6 @@ jobs:
|
||||
-e POSTGRES_POOL_PRE_PING=true \
|
||||
-e POSTGRES_USE_NULL_POOL=true \
|
||||
-e VESPA_HOST=index \
|
||||
-e ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=false \
|
||||
-e REDIS_HOST=cache \
|
||||
-e API_SERVER_HOST=api_server \
|
||||
-e OPENAI_API_KEY=${OPENAI_API_KEY} \
|
||||
@@ -639,7 +637,6 @@ jobs:
|
||||
ONYX_BACKEND_IMAGE=${ECR_CACHE}:integration-test-backend-test-${RUN_ID} \
|
||||
ONYX_MODEL_SERVER_IMAGE=${ECR_CACHE}:integration-test-model-server-test-${RUN_ID} \
|
||||
DEV_MODE=true \
|
||||
OPENSEARCH_FOR_ONYX_ENABLED=false \
|
||||
docker compose -f docker-compose.multitenant-dev.yml up \
|
||||
relational_db \
|
||||
index \
|
||||
@@ -694,7 +691,6 @@ jobs:
|
||||
-e POSTGRES_DB=postgres \
|
||||
-e POSTGRES_USE_NULL_POOL=true \
|
||||
-e VESPA_HOST=index \
|
||||
-e ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=false \
|
||||
-e REDIS_HOST=cache \
|
||||
-e API_SERVER_HOST=api_server \
|
||||
-e OPENAI_API_KEY=${OPENAI_API_KEY} \
|
||||
|
||||
@@ -46,9 +46,7 @@ RUN apt-get update && \
|
||||
pkg-config \
|
||||
gcc \
|
||||
nano \
|
||||
vim \
|
||||
libjemalloc2 \
|
||||
&& \
|
||||
vim && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get clean
|
||||
|
||||
@@ -167,13 +165,6 @@ ENV PYTHONPATH=/app
|
||||
ARG ONYX_VERSION=0.0.0-dev
|
||||
ENV ONYX_VERSION=${ONYX_VERSION}
|
||||
|
||||
# Use jemalloc instead of glibc malloc to reduce memory fragmentation
|
||||
# in long-running Python processes (API server, Celery workers).
|
||||
# The soname is architecture-independent; the dynamic linker resolves
|
||||
# the correct path from standard library directories.
|
||||
# Placed after all RUN steps so build-time processes are unaffected.
|
||||
ENV LD_PRELOAD=libjemalloc.so.2
|
||||
|
||||
# Default command which does nothing
|
||||
# This container is used by api server and background which specify their own CMD
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
@@ -21,13 +20,7 @@ logger = setup_logger()
|
||||
|
||||
|
||||
def verify_auth_setting() -> None:
|
||||
# All the Auth flows are valid for EE version, but warn about deprecated 'disabled'
|
||||
raw_auth_type = (os.environ.get("AUTH_TYPE") or "").lower()
|
||||
if raw_auth_type == "disabled":
|
||||
logger.warning(
|
||||
"AUTH_TYPE='disabled' is no longer supported. "
|
||||
"Using 'basic' instead. Please update your configuration."
|
||||
)
|
||||
# All the Auth flows are valid for EE version
|
||||
logger.notice(f"Using Auth Type: {AUTH_TYPE.value}")
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from onyx.db.models import HierarchyNode
|
||||
|
||||
|
||||
def _build_hierarchy_access_filter(
|
||||
user_email: str,
|
||||
user_email: str | None,
|
||||
external_group_ids: list[str],
|
||||
) -> ColumnElement[bool]:
|
||||
"""Build SQLAlchemy filter for hierarchy node access.
|
||||
@@ -43,7 +43,7 @@ def _build_hierarchy_access_filter(
|
||||
def _get_accessible_hierarchy_nodes_for_source(
|
||||
db_session: Session,
|
||||
source: DocumentSource,
|
||||
user_email: str,
|
||||
user_email: str | None,
|
||||
external_group_ids: list[str],
|
||||
) -> list[HierarchyNode]:
|
||||
"""
|
||||
|
||||
@@ -4,10 +4,11 @@ from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
|
||||
from model_server.utils import simple_log_function_time
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
from shared_configs.enums import EmbedTextType
|
||||
from shared_configs.model_server_models import Embedding
|
||||
@@ -188,7 +189,7 @@ async def process_embed_request(
|
||||
)
|
||||
|
||||
if not embed_request.texts:
|
||||
raise HTTPException(status_code=400, detail="No texts to be embedded")
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "No texts to be embedded")
|
||||
|
||||
if not all(embed_request.texts):
|
||||
raise ValueError("Empty strings are not allowed for embedding.")
|
||||
@@ -211,14 +212,12 @@ async def process_embed_request(
|
||||
)
|
||||
return EmbedResponse(embeddings=embeddings)
|
||||
except RateLimitError as e:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail=str(e),
|
||||
)
|
||||
raise OnyxError(OnyxErrorCode.RATE_LIMITED, str(e))
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error during embedding process: provider={embed_request.provider_type} model={embed_request.model_name}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error during embedding process: {e}"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR,
|
||||
f"Error during embedding process: {e}",
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
@@ -146,22 +145,10 @@ def is_user_admin(user: User) -> bool:
|
||||
|
||||
|
||||
def verify_auth_setting() -> None:
|
||||
"""Log warnings for AUTH_TYPE issues.
|
||||
|
||||
This only runs on app startup not during migrations/scripts.
|
||||
"""
|
||||
raw_auth_type = (os.environ.get("AUTH_TYPE") or "").lower()
|
||||
|
||||
if raw_auth_type == "cloud":
|
||||
if AUTH_TYPE == AuthType.CLOUD:
|
||||
raise ValueError(
|
||||
"'cloud' is not a valid auth type for self-hosted deployments."
|
||||
f"{AUTH_TYPE.value} is not a valid auth type for self-hosted deployments."
|
||||
)
|
||||
if raw_auth_type == "disabled":
|
||||
logger.warning(
|
||||
"AUTH_TYPE='disabled' is no longer supported. "
|
||||
"Using 'basic' instead. Please update your configuration."
|
||||
)
|
||||
|
||||
logger.notice(f"Using Auth Type: {AUTH_TYPE.value}")
|
||||
|
||||
|
||||
|
||||
@@ -96,12 +96,19 @@ WEB_DOMAIN = os.environ.get("WEB_DOMAIN") or "http://localhost:3000"
|
||||
#####
|
||||
# Auth Configs
|
||||
#####
|
||||
# Silently default to basic - warnings/errors logged in verify_auth_setting()
|
||||
# which only runs on app startup, not during migrations/scripts
|
||||
_auth_type_str = (os.environ.get("AUTH_TYPE") or "").lower()
|
||||
if _auth_type_str in [auth_type.value for auth_type in AuthType]:
|
||||
# Upgrades users from disabled auth to basic auth and shows warning.
|
||||
_auth_type_str = (os.environ.get("AUTH_TYPE") or "basic").lower()
|
||||
if _auth_type_str == "disabled":
|
||||
logger.warning(
|
||||
"AUTH_TYPE='disabled' is no longer supported. "
|
||||
"Defaulting to 'basic'. Please update your configuration. "
|
||||
"Your existing data will be migrated automatically."
|
||||
)
|
||||
_auth_type_str = AuthType.BASIC.value
|
||||
try:
|
||||
AUTH_TYPE = AuthType(_auth_type_str)
|
||||
else:
|
||||
except ValueError:
|
||||
logger.error(f"Invalid AUTH_TYPE: {_auth_type_str}. Defaulting to 'basic'.")
|
||||
AUTH_TYPE = AuthType.BASIC
|
||||
|
||||
PASSWORD_MIN_LENGTH = int(os.getenv("PASSWORD_MIN_LENGTH", 8))
|
||||
@@ -285,9 +292,8 @@ OPENSEARCH_TEXT_ANALYZER = os.environ.get("OPENSEARCH_TEXT_ANALYZER") or "englis
|
||||
# environments we always want to be dual indexing into both OpenSearch and Vespa
|
||||
# to stress test the new codepaths. Only enable this if there is some instance
|
||||
# of OpenSearch running for the relevant Onyx instance.
|
||||
# NOTE: Now enabled on by default, unless the env indicates otherwise.
|
||||
ENABLE_OPENSEARCH_INDEXING_FOR_ONYX = (
|
||||
os.environ.get("ENABLE_OPENSEARCH_INDEXING_FOR_ONYX", "true").lower() == "true"
|
||||
os.environ.get("ENABLE_OPENSEARCH_INDEXING_FOR_ONYX", "").lower() == "true"
|
||||
)
|
||||
# NOTE: This effectively does nothing anymore, admins can now toggle whether
|
||||
# retrieval is through OpenSearch. This value is only used as a final fallback
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import AsyncIterable
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime
|
||||
@@ -205,7 +204,7 @@ def _manage_async_retrieval(
|
||||
|
||||
end_time: datetime | None = end
|
||||
|
||||
async def _async_fetch() -> AsyncGenerator[Document, None]:
|
||||
async def _async_fetch() -> AsyncIterable[Document]:
|
||||
intents = Intents.default()
|
||||
intents.message_content = True
|
||||
async with Client(intents=intents) as discord_client:
|
||||
@@ -228,23 +227,22 @@ def _manage_async_retrieval(
|
||||
|
||||
def run_and_yield() -> Iterable[Document]:
|
||||
loop = asyncio.new_event_loop()
|
||||
async_gen = _async_fetch()
|
||||
try:
|
||||
# Get the async generator
|
||||
async_gen = _async_fetch()
|
||||
# Convert to AsyncIterator
|
||||
async_iter = async_gen.__aiter__()
|
||||
while True:
|
||||
try:
|
||||
doc = loop.run_until_complete(anext(async_gen))
|
||||
# Create a coroutine by calling anext with the async iterator
|
||||
next_coro = anext(async_iter)
|
||||
# Run the coroutine to get the next document
|
||||
doc = loop.run_until_complete(next_coro)
|
||||
yield doc
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
finally:
|
||||
# Must close the async generator before the loop so the Discord
|
||||
# client's `async with` block can await its shutdown coroutine.
|
||||
# The nested try/finally ensures the loop always closes even if
|
||||
# aclose() raises (same pattern as cursor.close() before conn.close()).
|
||||
try:
|
||||
loop.run_until_complete(async_gen.aclose())
|
||||
finally:
|
||||
loop.close()
|
||||
loop.close()
|
||||
|
||||
return run_and_yield()
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ from onyx.connectors.google_utils.shared_constants import (
|
||||
from onyx.db.credentials import update_credential_json
|
||||
from onyx.db.models import User
|
||||
from onyx.key_value_store.factory import get_kv_store
|
||||
from onyx.key_value_store.interface import unwrap_str
|
||||
from onyx.server.documents.models import CredentialBase
|
||||
from onyx.server.documents.models import GoogleAppCredentials
|
||||
from onyx.server.documents.models import GoogleServiceAccountKey
|
||||
@@ -90,7 +89,7 @@ def _get_current_oauth_user(creds: OAuthCredentials, source: DocumentSource) ->
|
||||
|
||||
|
||||
def verify_csrf(credential_id: int, state: str) -> None:
|
||||
csrf = unwrap_str(get_kv_store().load(KV_CRED_KEY.format(str(credential_id))))
|
||||
csrf = get_kv_store().load(KV_CRED_KEY.format(str(credential_id)))
|
||||
if csrf != state:
|
||||
raise PermissionError(
|
||||
"State from Google Drive Connector callback does not match expected"
|
||||
@@ -179,9 +178,7 @@ def get_auth_url(credential_id: int, source: DocumentSource) -> str:
|
||||
params = parse_qs(parsed_url.query)
|
||||
|
||||
get_kv_store().store(
|
||||
KV_CRED_KEY.format(credential_id),
|
||||
{"value": params.get("state", [None])[0]},
|
||||
encrypt=True,
|
||||
KV_CRED_KEY.format(credential_id), params.get("state", [None])[0], encrypt=True
|
||||
)
|
||||
return str(auth_url)
|
||||
|
||||
|
||||
@@ -458,7 +458,7 @@ def get_all_hierarchy_nodes_for_source(
|
||||
def _get_accessible_hierarchy_nodes_for_source(
|
||||
db_session: Session,
|
||||
source: DocumentSource,
|
||||
user_email: str, # noqa: ARG001
|
||||
user_email: str | None, # noqa: ARG001
|
||||
external_group_ids: list[str], # noqa: ARG001
|
||||
) -> list[HierarchyNode]:
|
||||
"""
|
||||
@@ -485,7 +485,7 @@ def _get_accessible_hierarchy_nodes_for_source(
|
||||
def get_accessible_hierarchy_nodes_for_source(
|
||||
db_session: Session,
|
||||
source: DocumentSource,
|
||||
user_email: str,
|
||||
user_email: str | None,
|
||||
external_group_ids: list[str],
|
||||
) -> list[HierarchyNode]:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import abc
|
||||
from typing import cast
|
||||
|
||||
from onyx.utils.special_types import JSON_ro
|
||||
|
||||
@@ -8,19 +7,6 @@ class KvKeyNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def unwrap_str(val: JSON_ro) -> str:
|
||||
"""Unwrap a string stored as {"value": str} in the encrypted KV store.
|
||||
Also handles legacy plain-string values cached in Redis."""
|
||||
if isinstance(val, dict):
|
||||
try:
|
||||
return cast(str, val["value"])
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Expected dict with 'value' key, got keys: {list(val.keys())}"
|
||||
)
|
||||
return cast(str, val)
|
||||
|
||||
|
||||
class KeyValueStore:
|
||||
# In the Multi Tenant case, the tenant context is picked up automatically, it does not need to be passed in
|
||||
# It's read from the global thread level variable
|
||||
|
||||
@@ -1905,7 +1905,7 @@ def get_connector_by_id(
|
||||
@router.post("/connector-request")
|
||||
def submit_connector_request(
|
||||
request_data: ConnectorRequestSubmission,
|
||||
user: User = Depends(current_user),
|
||||
user: User | None = Depends(current_user),
|
||||
) -> StatusResponse:
|
||||
"""
|
||||
Submit a connector request for Cloud deployments.
|
||||
@@ -1918,7 +1918,7 @@ def submit_connector_request(
|
||||
raise HTTPException(status_code=400, detail="Connector name cannot be empty")
|
||||
|
||||
# Get user identifier for telemetry
|
||||
user_email = user.email
|
||||
user_email = user.email if user else None
|
||||
distinct_id = user_email or tenant_id
|
||||
|
||||
# Track connector request via PostHog telemetry (Cloud only)
|
||||
|
||||
@@ -57,6 +57,9 @@ def list_messages(
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> MessageListResponse:
|
||||
"""Get all messages for a build session."""
|
||||
if user is None:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
session_manager = SessionManager(db_session)
|
||||
|
||||
messages = session_manager.list_messages(session_id, user.id)
|
||||
|
||||
@@ -54,14 +54,18 @@ def _require_opensearch(db_session: Session) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _get_user_access_info(user: User, db_session: Session) -> tuple[str, list[str]]:
|
||||
def _get_user_access_info(
|
||||
user: User | None, db_session: Session
|
||||
) -> tuple[str | None, list[str]]:
|
||||
if not user:
|
||||
return None, []
|
||||
return user.email, get_user_external_group_ids(db_session, user)
|
||||
|
||||
|
||||
@router.get(HIERARCHY_NODES_LIST_PATH)
|
||||
def list_accessible_hierarchy_nodes(
|
||||
source: DocumentSource,
|
||||
user: User = Depends(current_user),
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> HierarchyNodesResponse:
|
||||
_require_opensearch(db_session)
|
||||
@@ -88,7 +92,7 @@ def list_accessible_hierarchy_nodes(
|
||||
@router.post(HIERARCHY_NODE_DOCUMENTS_PATH)
|
||||
def list_accessible_hierarchy_node_documents(
|
||||
documents_request: HierarchyNodeDocumentsRequest,
|
||||
user: User = Depends(current_user),
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> HierarchyNodeDocumentsResponse:
|
||||
_require_opensearch(db_session)
|
||||
|
||||
@@ -1013,7 +1013,7 @@ def get_mcp_servers_for_assistant(
|
||||
@router.get("/servers", response_model=MCPServersResponse)
|
||||
def get_mcp_servers_for_user(
|
||||
db: Session = Depends(get_session),
|
||||
user: User = Depends(current_user),
|
||||
user: User | None = Depends(current_user),
|
||||
) -> MCPServersResponse:
|
||||
"""List all MCP servers for use in agent configuration and chat UI.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import contextvars
|
||||
import threading
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import cast
|
||||
|
||||
import requests
|
||||
|
||||
@@ -14,7 +15,6 @@ from onyx.db.engine.sql_engine import get_session_with_current_tenant
|
||||
from onyx.db.models import User
|
||||
from onyx.key_value_store.factory import get_kv_store
|
||||
from onyx.key_value_store.interface import KvKeyNotFoundError
|
||||
from onyx.key_value_store.interface import unwrap_str
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.variable_functionality import (
|
||||
fetch_versioned_implementation_with_fallback,
|
||||
@@ -25,7 +25,6 @@ from shared_configs.contextvars import get_current_tenant_id
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
_DANSWER_TELEMETRY_ENDPOINT = "https://telemetry.onyx.app/anonymous_telemetry"
|
||||
_CACHED_UUID: str | None = None
|
||||
_CACHED_INSTANCE_DOMAIN: str | None = None
|
||||
@@ -63,10 +62,10 @@ def get_or_generate_uuid() -> str:
|
||||
kv_store = get_kv_store()
|
||||
|
||||
try:
|
||||
_CACHED_UUID = unwrap_str(kv_store.load(KV_CUSTOMER_UUID_KEY))
|
||||
_CACHED_UUID = cast(str, kv_store.load(KV_CUSTOMER_UUID_KEY))
|
||||
except KvKeyNotFoundError:
|
||||
_CACHED_UUID = str(uuid.uuid4())
|
||||
kv_store.store(KV_CUSTOMER_UUID_KEY, {"value": _CACHED_UUID}, encrypt=True)
|
||||
kv_store.store(KV_CUSTOMER_UUID_KEY, _CACHED_UUID, encrypt=True)
|
||||
|
||||
return _CACHED_UUID
|
||||
|
||||
@@ -80,16 +79,14 @@ def _get_or_generate_instance_domain() -> str | None: #
|
||||
kv_store = get_kv_store()
|
||||
|
||||
try:
|
||||
_CACHED_INSTANCE_DOMAIN = unwrap_str(kv_store.load(KV_INSTANCE_DOMAIN_KEY))
|
||||
_CACHED_INSTANCE_DOMAIN = cast(str, kv_store.load(KV_INSTANCE_DOMAIN_KEY))
|
||||
except KvKeyNotFoundError:
|
||||
with get_session_with_current_tenant() as db_session:
|
||||
first_user = db_session.query(User).first()
|
||||
if first_user:
|
||||
_CACHED_INSTANCE_DOMAIN = first_user.email.split("@")[-1]
|
||||
kv_store.store(
|
||||
KV_INSTANCE_DOMAIN_KEY,
|
||||
{"value": _CACHED_INSTANCE_DOMAIN},
|
||||
encrypt=True,
|
||||
KV_INSTANCE_DOMAIN_KEY, _CACHED_INSTANCE_DOMAIN, encrypt=True
|
||||
)
|
||||
|
||||
return _CACHED_INSTANCE_DOMAIN
|
||||
|
||||
@@ -85,7 +85,7 @@ def test_group_overlap_filter(
|
||||
results = _get_accessible_hierarchy_nodes_for_source(
|
||||
db_session,
|
||||
source=DocumentSource.GOOGLE_DRIVE,
|
||||
user_email="",
|
||||
user_email=None,
|
||||
external_group_ids=["group_engineering"],
|
||||
)
|
||||
result_ids = {n.raw_node_id for n in results}
|
||||
@@ -124,7 +124,7 @@ def test_no_credentials_returns_only_public(
|
||||
results = _get_accessible_hierarchy_nodes_for_source(
|
||||
db_session,
|
||||
source=DocumentSource.GOOGLE_DRIVE,
|
||||
user_email="",
|
||||
user_email=None,
|
||||
external_group_ids=[],
|
||||
)
|
||||
result_ids = {n.raw_node_id for n in results}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import onyx.auth.users as users
|
||||
from onyx.auth.users import verify_auth_setting
|
||||
from onyx.configs.constants import AuthType
|
||||
|
||||
|
||||
def test_verify_auth_setting_raises_for_cloud(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Cloud auth type is not valid for self-hosted deployments."""
|
||||
monkeypatch.setenv("AUTH_TYPE", "cloud")
|
||||
|
||||
with pytest.raises(ValueError, match="'cloud' is not a valid auth type"):
|
||||
verify_auth_setting()
|
||||
|
||||
|
||||
def test_verify_auth_setting_warns_for_disabled(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Disabled auth type logs a deprecation warning."""
|
||||
monkeypatch.setenv("AUTH_TYPE", "disabled")
|
||||
|
||||
mock_logger = MagicMock()
|
||||
monkeypatch.setattr(users, "logger", mock_logger)
|
||||
monkeypatch.setattr(users, "AUTH_TYPE", AuthType.BASIC)
|
||||
|
||||
verify_auth_setting()
|
||||
|
||||
mock_logger.warning.assert_called_once()
|
||||
assert "no longer supported" in mock_logger.warning.call_args[0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"auth_type",
|
||||
[AuthType.BASIC, AuthType.GOOGLE_OAUTH, AuthType.OIDC, AuthType.SAML],
|
||||
)
|
||||
def test_verify_auth_setting_valid_auth_types(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
auth_type: AuthType,
|
||||
) -> None:
|
||||
"""Valid auth types work without errors or warnings."""
|
||||
monkeypatch.setenv("AUTH_TYPE", auth_type.value)
|
||||
|
||||
mock_logger = MagicMock()
|
||||
monkeypatch.setattr(users, "logger", mock_logger)
|
||||
monkeypatch.setattr(users, "AUTH_TYPE", auth_type)
|
||||
|
||||
verify_auth_setting()
|
||||
|
||||
mock_logger.warning.assert_not_called()
|
||||
mock_logger.notice.assert_called_once_with(f"Using Auth Type: {auth_type.value}")
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Unit tests for _get_user_access_info helper function.
|
||||
|
||||
These tests mock all database operations and don't require a real database.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.server.features.hierarchy.api import _get_user_access_info
|
||||
|
||||
|
||||
def test_get_user_access_info_returns_email_and_groups() -> None:
|
||||
"""_get_user_access_info returns the user's email and external group IDs."""
|
||||
mock_user = MagicMock()
|
||||
mock_user.email = "test@example.com"
|
||||
mock_db_session = MagicMock(spec=Session)
|
||||
|
||||
with patch(
|
||||
"onyx.server.features.hierarchy.api.get_user_external_group_ids",
|
||||
return_value=["group1", "group2"],
|
||||
):
|
||||
email, groups = _get_user_access_info(mock_user, mock_db_session)
|
||||
|
||||
assert email == "test@example.com"
|
||||
assert groups == ["group1", "group2"]
|
||||
|
||||
|
||||
def test_get_user_access_info_with_no_groups() -> None:
|
||||
"""User with no external groups returns empty list."""
|
||||
mock_user = MagicMock()
|
||||
mock_user.email = "solo@example.com"
|
||||
mock_db_session = MagicMock(spec=Session)
|
||||
|
||||
with patch(
|
||||
"onyx.server.features.hierarchy.api.get_user_external_group_ids",
|
||||
return_value=[],
|
||||
):
|
||||
email, groups = _get_user_access_info(mock_user, mock_db_session)
|
||||
|
||||
assert email == "solo@example.com"
|
||||
assert groups == []
|
||||
@@ -61,9 +61,6 @@ services:
|
||||
- POSTGRES_HOST=relational_db
|
||||
- POSTGRES_DEFAULT_SCHEMA=${POSTGRES_DEFAULT_SCHEMA:-}
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- WEB_DOMAIN=${WEB_DOMAIN:-}
|
||||
# MinIO configuration
|
||||
@@ -80,7 +77,6 @@ services:
|
||||
- DISABLE_RERANK_FOR_STREAMING=${DISABLE_RERANK_FOR_STREAMING:-}
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- MODEL_SERVER_PORT=${MODEL_SERVER_PORT:-}
|
||||
- CODE_INTERPRETER_BASE_URL=${CODE_INTERPRETER_BASE_URL:-http://code-interpreter:8000}
|
||||
- LOG_ONYX_MODEL_INTERACTIONS=${LOG_ONYX_MODEL_INTERACTIONS:-}
|
||||
- LOG_VESPA_TIMING_INFORMATION=${LOG_VESPA_TIMING_INFORMATION:-}
|
||||
- LOG_ENDPOINT_LATENCY=${LOG_ENDPOINT_LATENCY:-}
|
||||
@@ -172,9 +168,6 @@ services:
|
||||
- POSTGRES_DB=${POSTGRES_DB:-}
|
||||
- POSTGRES_DEFAULT_SCHEMA=${POSTGRES_DEFAULT_SCHEMA:-}
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- WEB_DOMAIN=${WEB_DOMAIN:-}
|
||||
# MinIO configuration
|
||||
@@ -431,50 +424,6 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:3.4.0
|
||||
restart: unless-stopped
|
||||
# Controls whether this service runs. In order to enable it, add
|
||||
# opensearch-enabled to COMPOSE_PROFILES in the environment for this
|
||||
# docker-compose.
|
||||
# NOTE: Now enabled on by default. To explicitly disable this service,
|
||||
# uncomment this profile and ensure COMPOSE_PROFILES in your env does not
|
||||
# list the profile, or when running docker compose, include all desired
|
||||
# service names but this one. Additionally set
|
||||
# OPENSEARCH_FOR_ONYX_ENABLED=false in your env.
|
||||
# profiles: ["opensearch-enabled"]
|
||||
environment:
|
||||
# We need discovery.type=single-node so that OpenSearch doesn't try
|
||||
# forming a cluster and waiting for other nodes to become live.
|
||||
- discovery.type=single-node
|
||||
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
# This and the JVM config below come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
# We do this to avoid unstable performance from page swaps.
|
||||
- bootstrap.memory_lock=true # Disable JVM heap memory swapping.
|
||||
# Java heap should be ~50% of memory limit. For now we assume a limit of
|
||||
# 4g although in practice the container can request more than this.
|
||||
# See https://opster.com/guides/opensearch/opensearch-basics/opensearch-heap-size-usage-and-jvm-garbage-collection/
|
||||
# Xms is the starting size, Xmx is the maximum size. These should be the
|
||||
# same.
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
volumes:
|
||||
- opensearch-data:/usr/share/opensearch/data
|
||||
# These come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
ulimits:
|
||||
# Similarly to bootstrap.memory_lock, we don't want to impose limits on
|
||||
# how much memory a process can lock from being swapped.
|
||||
memlock:
|
||||
soft: -1 # Set memlock to unlimited (no soft or hard limit).
|
||||
hard: -1
|
||||
nofile:
|
||||
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536.
|
||||
hard: 65536
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
nginx:
|
||||
image: nginx:1.25.5-alpine
|
||||
restart: unless-stopped
|
||||
@@ -559,5 +508,3 @@ volumes:
|
||||
model_cache_huggingface:
|
||||
indexing_huggingface_model_cache:
|
||||
# mcp_server_logs:
|
||||
# Persistent data for OpenSearch.
|
||||
opensearch-data:
|
||||
|
||||
@@ -21,9 +21,6 @@ services:
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
# MinIO configuration
|
||||
@@ -58,9 +55,6 @@ services:
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
@@ -234,50 +228,6 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:3.4.0
|
||||
restart: unless-stopped
|
||||
# Controls whether this service runs. In order to enable it, add
|
||||
# opensearch-enabled to COMPOSE_PROFILES in the environment for this
|
||||
# docker-compose.
|
||||
# NOTE: Now enabled on by default. To explicitly disable this service,
|
||||
# uncomment this profile and ensure COMPOSE_PROFILES in your env does not
|
||||
# list the profile, or when running docker compose, include all desired
|
||||
# service names but this one. Additionally set
|
||||
# OPENSEARCH_FOR_ONYX_ENABLED=false in your env.
|
||||
# profiles: ["opensearch-enabled"]
|
||||
environment:
|
||||
# We need discovery.type=single-node so that OpenSearch doesn't try
|
||||
# forming a cluster and waiting for other nodes to become live.
|
||||
- discovery.type=single-node
|
||||
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
# This and the JVM config below come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
# We do this to avoid unstable performance from page swaps.
|
||||
- bootstrap.memory_lock=true # Disable JVM heap memory swapping.
|
||||
# Java heap should be ~50% of memory limit. For now we assume a limit of
|
||||
# 4g although in practice the container can request more than this.
|
||||
# See https://opster.com/guides/opensearch/opensearch-basics/opensearch-heap-size-usage-and-jvm-garbage-collection/
|
||||
# Xms is the starting size, Xmx is the maximum size. These should be the
|
||||
# same.
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
volumes:
|
||||
- opensearch-data:/usr/share/opensearch/data
|
||||
# These come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
ulimits:
|
||||
# Similarly to bootstrap.memory_lock, we don't want to impose limits on
|
||||
# how much memory a process can lock from being swapped.
|
||||
memlock:
|
||||
soft: -1 # Set memlock to unlimited (no soft or hard limit).
|
||||
hard: -1
|
||||
nofile:
|
||||
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536.
|
||||
hard: 65536
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
nginx:
|
||||
image: nginx:1.25.5-alpine
|
||||
restart: unless-stopped
|
||||
@@ -365,5 +315,3 @@ volumes:
|
||||
model_cache_huggingface:
|
||||
indexing_huggingface_model_cache:
|
||||
# mcp_server_logs:
|
||||
# Persistent data for OpenSearch.
|
||||
opensearch-data:
|
||||
|
||||
@@ -21,12 +21,8 @@ services:
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- CODE_INTERPRETER_BASE_URL=${CODE_INTERPRETER_BASE_URL:-http://code-interpreter:8000}
|
||||
- USE_IAM_AUTH=${USE_IAM_AUTH}
|
||||
- AWS_REGION_NAME=${AWS_REGION_NAME-}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-}
|
||||
@@ -72,9 +68,6 @@ services:
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
@@ -258,50 +251,6 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:3.4.0
|
||||
restart: unless-stopped
|
||||
# Controls whether this service runs. In order to enable it, add
|
||||
# opensearch-enabled to COMPOSE_PROFILES in the environment for this
|
||||
# docker-compose.
|
||||
# NOTE: Now enabled on by default. To explicitly disable this service,
|
||||
# uncomment this profile and ensure COMPOSE_PROFILES in your env does not
|
||||
# list the profile, or when running docker compose, include all desired
|
||||
# service names but this one. Additionally set
|
||||
# OPENSEARCH_FOR_ONYX_ENABLED=false in your env.
|
||||
# profiles: ["opensearch-enabled"]
|
||||
environment:
|
||||
# We need discovery.type=single-node so that OpenSearch doesn't try
|
||||
# forming a cluster and waiting for other nodes to become live.
|
||||
- discovery.type=single-node
|
||||
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
# This and the JVM config below come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
# We do this to avoid unstable performance from page swaps.
|
||||
- bootstrap.memory_lock=true # Disable JVM heap memory swapping.
|
||||
# Java heap should be ~50% of memory limit. For now we assume a limit of
|
||||
# 4g although in practice the container can request more than this.
|
||||
# See https://opster.com/guides/opensearch/opensearch-basics/opensearch-heap-size-usage-and-jvm-garbage-collection/
|
||||
# Xms is the starting size, Xmx is the maximum size. These should be the
|
||||
# same.
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
volumes:
|
||||
- opensearch-data:/usr/share/opensearch/data
|
||||
# These come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
ulimits:
|
||||
# Similarly to bootstrap.memory_lock, we don't want to impose limits on
|
||||
# how much memory a process can lock from being swapped.
|
||||
memlock:
|
||||
soft: -1 # Set memlock to unlimited (no soft or hard limit).
|
||||
hard: -1
|
||||
nofile:
|
||||
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536.
|
||||
hard: 65536
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
nginx:
|
||||
image: nginx:1.25.5-alpine
|
||||
restart: unless-stopped
|
||||
@@ -394,5 +343,3 @@ volumes:
|
||||
# mcp_server_logs:
|
||||
# Shared volume for persistent document storage (Craft file-system mode)
|
||||
file-system:
|
||||
# Persistent data for OpenSearch.
|
||||
opensearch-data:
|
||||
|
||||
@@ -22,12 +22,8 @@ services:
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- CODE_INTERPRETER_BASE_URL=${CODE_INTERPRETER_BASE_URL:-http://code-interpreter:8000}
|
||||
- USE_IAM_AUTH=${USE_IAM_AUTH}
|
||||
- AWS_REGION_NAME=${AWS_REGION_NAME-}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-}
|
||||
@@ -77,9 +73,6 @@ services:
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- REDIS_HOST=cache
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
@@ -277,50 +270,6 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:3.4.0
|
||||
restart: unless-stopped
|
||||
# Controls whether this service runs. In order to enable it, add
|
||||
# opensearch-enabled to COMPOSE_PROFILES in the environment for this
|
||||
# docker-compose.
|
||||
# NOTE: Now enabled on by default. To explicitly disable this service,
|
||||
# uncomment this profile and ensure COMPOSE_PROFILES in your env does not
|
||||
# list the profile, or when running docker compose, include all desired
|
||||
# service names but this one. Additionally set
|
||||
# OPENSEARCH_FOR_ONYX_ENABLED=false in your env.
|
||||
# profiles: ["opensearch-enabled"]
|
||||
environment:
|
||||
# We need discovery.type=single-node so that OpenSearch doesn't try
|
||||
# forming a cluster and waiting for other nodes to become live.
|
||||
- discovery.type=single-node
|
||||
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
# This and the JVM config below come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
# We do this to avoid unstable performance from page swaps.
|
||||
- bootstrap.memory_lock=true # Disable JVM heap memory swapping.
|
||||
# Java heap should be ~50% of memory limit. For now we assume a limit of
|
||||
# 4g although in practice the container can request more than this.
|
||||
# See https://opster.com/guides/opensearch/opensearch-basics/opensearch-heap-size-usage-and-jvm-garbage-collection/
|
||||
# Xms is the starting size, Xmx is the maximum size. These should be the
|
||||
# same.
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
volumes:
|
||||
- opensearch-data:/usr/share/opensearch/data
|
||||
# These come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
ulimits:
|
||||
# Similarly to bootstrap.memory_lock, we don't want to impose limits on
|
||||
# how much memory a process can lock from being swapped.
|
||||
memlock:
|
||||
soft: -1 # Set memlock to unlimited (no soft or hard limit).
|
||||
hard: -1
|
||||
nofile:
|
||||
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536.
|
||||
hard: 65536
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "6"
|
||||
|
||||
nginx:
|
||||
image: nginx:1.25.5-alpine
|
||||
restart: unless-stopped
|
||||
@@ -431,5 +380,3 @@ volumes:
|
||||
# mcp_server_logs:
|
||||
# Shared volume for persistent document storage (Craft file-system mode)
|
||||
file-system:
|
||||
# Persistent data for OpenSearch.
|
||||
opensearch-data:
|
||||
|
||||
@@ -57,9 +57,6 @@ services:
|
||||
condition: service_started
|
||||
index:
|
||||
condition: service_started
|
||||
opensearch:
|
||||
condition: service_started
|
||||
required: false
|
||||
cache:
|
||||
condition: service_started
|
||||
inference_model_server:
|
||||
@@ -81,10 +78,9 @@ services:
|
||||
- VESPA_HOST=${VESPA_HOST:-index}
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-false}
|
||||
- REDIS_HOST=${REDIS_HOST:-cache}
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- CODE_INTERPRETER_BASE_URL=${CODE_INTERPRETER_BASE_URL:-http://code-interpreter:8000}
|
||||
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000}
|
||||
- S3_AWS_ACCESS_KEY_ID=${S3_AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
- S3_AWS_SECRET_ACCESS_KEY=${S3_AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
@@ -143,19 +139,11 @@ services:
|
||||
- path: .env
|
||||
required: false
|
||||
depends_on:
|
||||
relational_db:
|
||||
condition: service_started
|
||||
index:
|
||||
condition: service_started
|
||||
opensearch:
|
||||
condition: service_started
|
||||
required: false
|
||||
cache:
|
||||
condition: service_started
|
||||
inference_model_server:
|
||||
condition: service_started
|
||||
indexing_model_server:
|
||||
condition: service_started
|
||||
- relational_db
|
||||
- index
|
||||
- cache
|
||||
- inference_model_server
|
||||
- indexing_model_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- FILE_STORE_BACKEND=${FILE_STORE_BACKEND:-s3}
|
||||
@@ -163,7 +151,7 @@ services:
|
||||
- VESPA_HOST=${VESPA_HOST:-index}
|
||||
- OPENSEARCH_HOST=${OPENSEARCH_HOST:-opensearch}
|
||||
- OPENSEARCH_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD:-StrongPassword123!}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-true}
|
||||
- ENABLE_OPENSEARCH_INDEXING_FOR_ONYX=${OPENSEARCH_FOR_ONYX_ENABLED:-false}
|
||||
- REDIS_HOST=${REDIS_HOST:-cache}
|
||||
- MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server}
|
||||
- INDEXING_MODEL_SERVER_HOST=${INDEXING_MODEL_SERVER_HOST:-indexing_model_server}
|
||||
@@ -418,12 +406,7 @@ services:
|
||||
# Controls whether this service runs. In order to enable it, add
|
||||
# opensearch-enabled to COMPOSE_PROFILES in the environment for this
|
||||
# docker-compose.
|
||||
# NOTE: Now enabled on by default. To explicitly disable this service,
|
||||
# uncomment this profile and ensure COMPOSE_PROFILES in your env does not
|
||||
# list the profile, or when running docker compose, include all desired
|
||||
# service names but this one. Additionally set
|
||||
# OPENSEARCH_FOR_ONYX_ENABLED=false in your env.
|
||||
# profiles: ["opensearch-enabled"]
|
||||
profiles: ["opensearch-enabled"]
|
||||
environment:
|
||||
# We need discovery.type=single-node so that OpenSearch doesn't try
|
||||
# forming a cluster and waiting for other nodes to become live.
|
||||
@@ -433,11 +416,11 @@ services:
|
||||
# We do this to avoid unstable performance from page swaps.
|
||||
- bootstrap.memory_lock=true # Disable JVM heap memory swapping.
|
||||
# Java heap should be ~50% of memory limit. For now we assume a limit of
|
||||
# 4g although in practice the container can request more than this.
|
||||
# 2g although in practice the container can request more than this.
|
||||
# See https://opster.com/guides/opensearch/opensearch-basics/opensearch-heap-size-usage-and-jvm-garbage-collection/
|
||||
# Xms is the starting size, Xmx is the maximum size. These should be the
|
||||
# same.
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g"
|
||||
volumes:
|
||||
- opensearch-data:/usr/share/opensearch/data
|
||||
# These come from the example in https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
|
||||
|
||||
@@ -67,8 +67,10 @@ POSTGRES_PASSWORD=password
|
||||
## remove s3-filestore from COMPOSE_PROFILES and set FILE_STORE_BACKEND=postgres.
|
||||
COMPOSE_PROFILES=s3-filestore
|
||||
FILE_STORE_BACKEND=s3
|
||||
## Setting for enabling OpenSearch.
|
||||
OPENSEARCH_FOR_ONYX_ENABLED=true
|
||||
## Settings for enabling OpenSearch. Uncomment these and comment out
|
||||
## COMPOSE_PROFILES above.
|
||||
# COMPOSE_PROFILES=s3-filestore,opensearch-enabled
|
||||
# OPENSEARCH_FOR_ONYX_ENABLED=true
|
||||
|
||||
## MinIO/S3 Configuration (only needed when FILE_STORE_BACKEND=s3)
|
||||
S3_ENDPOINT_URL=http://minio:9000
|
||||
|
||||
@@ -5,7 +5,7 @@ home: https://www.onyx.app/
|
||||
sources:
|
||||
- "https://github.com/onyx-dot-app/onyx"
|
||||
type: application
|
||||
version: 0.4.33
|
||||
version: 0.4.32
|
||||
appVersion: latest
|
||||
annotations:
|
||||
category: Productivity
|
||||
|
||||
@@ -76,10 +76,7 @@ vespa:
|
||||
memory: 32000Mi
|
||||
|
||||
opensearch:
|
||||
# Enabled by default. Override to false and set the appropriate env vars in
|
||||
# the instance-specific values yaml if using AWS-managed OpenSearch, or simply
|
||||
# override to false to entirely disable.
|
||||
enabled: true
|
||||
enabled: false
|
||||
# These values are passed to the opensearch subchart.
|
||||
# See https://github.com/opensearch-project/helm-charts/blob/main/charts/opensearch/values.yaml
|
||||
|
||||
@@ -1161,10 +1158,8 @@ auth:
|
||||
opensearch:
|
||||
# Enable or disable this secret entirely. Will remove from env var
|
||||
# configurations and remove any created secrets.
|
||||
# Enabled by default. Override to false and set the appropriate env vars in
|
||||
# the instance-specific values yaml if using AWS-managed OpenSearch, or
|
||||
# simply override to false to entirely disable.
|
||||
enabled: true
|
||||
# Set to true when opensearch.enabled is true.
|
||||
enabled: false
|
||||
# Overwrite the default secret name, ignored if existingSecret is defined.
|
||||
secretName: 'onyx-opensearch'
|
||||
# Use a secret specified elsewhere.
|
||||
|
||||
3
web/.gitignore
vendored
3
web/.gitignore
vendored
@@ -47,6 +47,3 @@ next-env.d.ts
|
||||
# generated clients ... in particular, the API to the Onyx backend itself!
|
||||
/src/lib/generated
|
||||
.jest-cache
|
||||
|
||||
# storybook
|
||||
storybook-static
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Onyx Storybook
|
||||
|
||||
Storybook is an isolated development environment for UI components. It renders each component in a standalone "story" outside of the main app, so you can visually verify appearance, interact with props, and catch regressions without navigating through the full application.
|
||||
|
||||
The Onyx Storybook covers the full component library — from low-level `@opal/core` primitives up through `refresh-components` — giving designers and engineers a shared reference for every visual state.
|
||||
|
||||
**Production:** [onyx-storybook.vercel.app](https://onyx-storybook.vercel.app)
|
||||
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm run storybook # dev server on http://localhost:6006
|
||||
npm run storybook:build # static build to storybook-static/
|
||||
```
|
||||
|
||||
The dev server hot-reloads when you edit a component or story file.
|
||||
|
||||
## Writing Stories
|
||||
|
||||
Stories are **co-located** next to their component source:
|
||||
|
||||
```
|
||||
lib/opal/src/core/interactive/
|
||||
├── components.tsx ← the component
|
||||
├── Interactive.stories.tsx ← the story
|
||||
└── styles.css
|
||||
|
||||
src/refresh-components/buttons/
|
||||
├── Button.tsx
|
||||
└── Button.stories.tsx
|
||||
```
|
||||
|
||||
### Minimal Template
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { MyComponent } from "./MyComponent";
|
||||
|
||||
const meta: Meta<typeof MyComponent> = {
|
||||
title: "Category/MyComponent", // sidebar path
|
||||
component: MyComponent,
|
||||
tags: ["autodocs"], // generates a docs page from props
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof MyComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { label: "Hello" },
|
||||
};
|
||||
```
|
||||
|
||||
### Conventions
|
||||
|
||||
- **Title format:** `Core/Name`, `Components/Name`, `Layouts/Name`, or `refresh-components/category/Name`
|
||||
- **Tags:** Add `tags: ["autodocs"]` to auto-generate a props docs page
|
||||
- **Decorators:** Components that use Radix tooltips need a `TooltipPrimitive.Provider` decorator
|
||||
- **Layout:** Use `parameters: { layout: "fullscreen" }` for modals/popovers that use portals
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Use the theme toggle (paint roller icon) in the Storybook toolbar to switch between light and dark modes. This adds/removes the `dark` class on the preview body, matching the app's `darkMode: "class"` Tailwind config. All color tokens from `colors.css` adapt automatically.
|
||||
|
||||
## Deployment
|
||||
|
||||
The production Storybook is deployed as a static site on Vercel. The build runs `npm run storybook:build` which outputs to `storybook-static/`, and Vercel serves that directory.
|
||||
|
||||
Deploys are triggered on merges to `main` when files in `web/lib/opal/`, `web/src/refresh-components/`, or `web/.storybook/` change.
|
||||
|
||||
## Component Layers
|
||||
|
||||
The sidebar organizes components by their layer in the design system:
|
||||
|
||||
| Layer | Path | Examples |
|
||||
|-------|------|----------|
|
||||
| **Core** | `lib/opal/src/core/` | Interactive, Hoverable |
|
||||
| **Components** | `lib/opal/src/components/` | Button, OpenButton, Tag |
|
||||
| **Layouts** | `lib/opal/src/layouts/` | Content, ContentAction, IllustrationContent |
|
||||
| **refresh-components** | `src/refresh-components/` | Inputs, tables, modals, text, cards, tiles, etc. |
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
import path from "path";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../lib/opal/src/**/*.stories.@(ts|tsx)",
|
||||
"../src/refresh-components/**/*.stories.@(ts|tsx)",
|
||||
],
|
||||
addons: ["@storybook/addon-essentials", "@storybook/addon-themes"],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
typescript: {
|
||||
reactDocgen: "react-docgen-typescript",
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
config.resolve = config.resolve ?? {};
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
"@": path.resolve(__dirname, "../src"),
|
||||
"@opal": path.resolve(__dirname, "../lib/opal/src"),
|
||||
"@public": path.resolve(__dirname, "../public"),
|
||||
// Next.js module stubs for Vite
|
||||
"next/link": path.resolve(__dirname, "mocks/next-link.tsx"),
|
||||
"next/navigation": path.resolve(__dirname, "mocks/next-navigation.tsx"),
|
||||
"next/image": path.resolve(__dirname, "mocks/next-image.tsx"),
|
||||
};
|
||||
|
||||
// Process CSS with Tailwind via PostCSS
|
||||
config.css = config.css ?? {};
|
||||
config.css.postcss = path.resolve(__dirname, "..");
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface ImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fill?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function Image({ src, alt, width, height, fill, ...props }: ImageProps) {
|
||||
const fillStyle: React.CSSProperties = fill
|
||||
? { position: "absolute", inset: 0, width: "100%", height: "100%" }
|
||||
: {};
|
||||
return (
|
||||
<img
|
||||
{...(props as React.ImgHTMLAttributes<HTMLImageElement>)}
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={fill ? undefined : width}
|
||||
height={fill ? undefined : height}
|
||||
style={{ ...(props.style as React.CSSProperties), ...fillStyle }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Image;
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface LinkProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function Link({
|
||||
href,
|
||||
children,
|
||||
prefetch: _prefetch,
|
||||
scroll: _scroll,
|
||||
shallow: _shallow,
|
||||
replace: _replace,
|
||||
passHref: _passHref,
|
||||
locale: _locale,
|
||||
legacyBehavior: _legacyBehavior,
|
||||
...props
|
||||
}: LinkProps) {
|
||||
return (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
@@ -1,30 +0,0 @@
|
||||
export function useRouter() {
|
||||
return {
|
||||
push: (_url: string) => {},
|
||||
replace: (_url: string) => {},
|
||||
back: () => {},
|
||||
forward: () => {},
|
||||
refresh: () => {},
|
||||
prefetch: (_url: string) => Promise.resolve(),
|
||||
};
|
||||
}
|
||||
|
||||
export function usePathname() {
|
||||
return "/";
|
||||
}
|
||||
|
||||
export function useSearchParams() {
|
||||
return new URLSearchParams() as ReadonlyURLSearchParams;
|
||||
}
|
||||
|
||||
export function useParams() {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function redirect(_url: string): never {
|
||||
throw new Error("redirect() called in Storybook");
|
||||
}
|
||||
|
||||
export function notFound(): never {
|
||||
throw new Error("notFound() called in Storybook");
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- Preconnect for fonts loaded via globals.css @import -->
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.googleapis.com"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
import "../src/app/globals.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
backgrounds: { disable: true },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
withThemeByClassName({
|
||||
themes: {
|
||||
light: "",
|
||||
dark: "dark",
|
||||
},
|
||||
defaultTheme: "light",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Tag } from "@opal/components";
|
||||
import { SvgAlertCircle } from "@opal/icons";
|
||||
|
||||
const TAG_COLORS = ["green", "purple", "blue", "gray", "amber"] as const;
|
||||
|
||||
const meta: Meta<typeof Tag> = {
|
||||
title: "opal/components/Tag",
|
||||
component: Tag,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Tag>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "Label",
|
||||
},
|
||||
};
|
||||
|
||||
export const AllColors: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
{TAG_COLORS.map((color) => (
|
||||
<Tag key={color} title={color} color={color} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
title: "Alert",
|
||||
icon: SvgAlertCircle,
|
||||
},
|
||||
};
|
||||
|
||||
export const AllColorsWithIcon: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
{TAG_COLORS.map((color) => (
|
||||
<Tag key={color} title={color} color={color} icon={SvgAlertCircle} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
1571
web/package-lock.json
generated
1571
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,7 @@
|
||||
"test:ci": "jest --ci --maxWorkers=2 --silent --bail",
|
||||
"test:changed": "jest --onlyChanged",
|
||||
"test:diff": "jest --changedSince=main",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build -o storybook-static"
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
@@ -112,11 +110,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@storybook/addon-essentials": "^8.6.18",
|
||||
"@storybook/addon-themes": "^8.6.18",
|
||||
"@storybook/blocks": "^8.6.18",
|
||||
"@storybook/react": "^8.6.18",
|
||||
"@storybook/react-vite": "^8.6.18",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -142,7 +135,6 @@
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"prettier": "3.1.0",
|
||||
"stats.js": "^0.17.0",
|
||||
"storybook": "^8.6.18",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-unused-exports": "^11.0.1",
|
||||
|
||||
@@ -85,6 +85,23 @@ export default function LoginPage({
|
||||
{authTypeMetadata?.authType === AuthType.BASIC && (
|
||||
<div className="flex flex-col w-full gap-6">
|
||||
<LoginText />
|
||||
|
||||
{authTypeMetadata?.oauthEnabled && authUrl && (
|
||||
<>
|
||||
<SignInButton
|
||||
authorizeUrl={authUrl}
|
||||
authType={AuthType.GOOGLE_OAUTH}
|
||||
/>
|
||||
<div className="flex flex-row items-center w-full gap-2">
|
||||
<div className="flex-1 border-t border-text-01" />
|
||||
<Text as="p" text03 mainUiMuted>
|
||||
or
|
||||
</Text>
|
||||
<div className="flex-1 border-t border-text-01" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EmailPasswordForm nextUrl={effectiveNextUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -72,7 +72,15 @@ export default async function Page(props: PageProps) {
|
||||
let authUrl: string | null = null;
|
||||
if (authTypeMetadata) {
|
||||
try {
|
||||
authUrl = await getAuthUrlSS(authTypeMetadata.authType, nextUrl);
|
||||
// For BASIC auth with OAuth enabled, fetch the OAuth URL
|
||||
if (
|
||||
authTypeMetadata.authType === AuthType.BASIC &&
|
||||
authTypeMetadata.oauthEnabled
|
||||
) {
|
||||
authUrl = await getAuthUrlSS(AuthType.GOOGLE_OAUTH, nextUrl);
|
||||
} else {
|
||||
authUrl = await getAuthUrlSS(authTypeMetadata.authType, nextUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Some fetch failed for the login page - ${e}`);
|
||||
}
|
||||
|
||||
@@ -198,15 +198,16 @@ export default function SearchUI({ onDocumentClick }: SearchResultsProps) {
|
||||
const showEmpty = !error && results.length === 0;
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-h-0 w-full flex flex-col gap-3">
|
||||
{/* ── Top row: Filters + Result count ── */}
|
||||
<div className="flex-shrink-0 flex flex-row gap-x-4">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col justify-end gap-3",
|
||||
showEmpty ? "flex-1" : "flex-[3]"
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className="flex-1 min-h-0 w-full grid gap-x-4"
|
||||
style={{
|
||||
gridTemplateColumns: showEmpty ? "1fr" : "3fr 1fr",
|
||||
gridTemplateRows: "auto 1fr auto",
|
||||
}}
|
||||
>
|
||||
{/* Top-left: Search filters */}
|
||||
<div className="row-start-1 col-start-1 flex flex-col justify-end gap-3">
|
||||
<div className="flex flex-row gap-2">
|
||||
{/* Time filter */}
|
||||
<Popover open={timeFilterOpen} onOpenChange={setTimeFilterOpen}>
|
||||
@@ -306,8 +307,9 @@ export default function SearchUI({ onDocumentClick }: SearchResultsProps) {
|
||||
<Separator noPadding />
|
||||
</div>
|
||||
|
||||
{/* Top-right: Number of results */}
|
||||
{!showEmpty && (
|
||||
<div className="flex-1 flex flex-col justify-end gap-3">
|
||||
<div className="row-start-1 col-start-2 flex flex-col justify-end gap-3">
|
||||
<Section alignItems="start">
|
||||
<Text text03 mainUiMuted>
|
||||
{results.length} Results
|
||||
@@ -317,14 +319,12 @@ export default function SearchUI({ onDocumentClick }: SearchResultsProps) {
|
||||
<Separator noPadding />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Middle row: Results + Source filter ── */}
|
||||
<div className="flex-1 min-h-0 flex flex-row gap-x-4">
|
||||
{/* Bottom-left: Search results */}
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-0 overflow-y-scroll flex flex-col gap-2",
|
||||
showEmpty ? "flex-1 justify-center" : "flex-[3]"
|
||||
"row-start-2 col-start-1 min-h-0 overflow-y-scroll py-3 flex flex-col gap-2",
|
||||
showEmpty && "justify-center"
|
||||
)}
|
||||
>
|
||||
{error ? (
|
||||
@@ -332,16 +332,12 @@ export default function SearchUI({ onDocumentClick }: SearchResultsProps) {
|
||||
) : paginatedResults.length > 0 ? (
|
||||
<>
|
||||
{paginatedResults.map((doc) => (
|
||||
<div
|
||||
<SearchCard
|
||||
key={`${doc.document_id}-${doc.chunk_ind}`}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<SearchCard
|
||||
document={doc}
|
||||
isLlmSelected={llmSelectedSet.has(doc.document_id)}
|
||||
onDocumentClick={onDocumentClick}
|
||||
/>
|
||||
</div>
|
||||
document={doc}
|
||||
isLlmSelected={llmSelectedSet.has(doc.document_id)}
|
||||
onDocumentClick={onDocumentClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
@@ -353,8 +349,20 @@ export default function SearchUI({ onDocumentClick }: SearchResultsProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{!showEmpty && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col gap-4 px-1">
|
||||
<div className="row-start-3 col-start-1 col-span-2 pt-3">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom-right: Source filter */}
|
||||
{!showEmpty && (
|
||||
<div className="row-start-2 col-start-2 min-h-0 overflow-y-auto flex flex-col gap-4 px-1 py-3">
|
||||
<Section gap={0.25} height="fit">
|
||||
{sourcesWithMeta.map(({ source, meta, count }) => (
|
||||
<LineItem
|
||||
@@ -378,15 +386,6 @@ export default function SearchUI({ onDocumentClick }: SearchResultsProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Bottom row: Pagination ── */}
|
||||
{!showEmpty && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function Pagination({
|
||||
const pageNumbers = getPageNumbers();
|
||||
|
||||
return (
|
||||
<Section flexDirection="row" height="auto" gap={0.25}>
|
||||
<Section flexDirection="row" gap={0.25}>
|
||||
{/* Previous button */}
|
||||
<Disabled disabled={currentPage === 1}>
|
||||
<Button
|
||||
@@ -77,7 +77,7 @@ export default function Pagination({
|
||||
</Disabled>
|
||||
|
||||
{/* Page numbers */}
|
||||
<Section flexDirection="row" height="auto" gap={0} width="fit">
|
||||
<Section flexDirection="row" gap={0} width="fit">
|
||||
{pageNumbers.map((page, index) => {
|
||||
if (page === "...") {
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,6 @@ import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { useAvailableTools } from "@/hooks/useAvailableTools";
|
||||
import useCCPairs from "@/hooks/useCCPairs";
|
||||
import { useLLMProviders } from "@/hooks/useLLMProviders";
|
||||
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import { useToolOAuthStatus } from "@/lib/hooks/useToolOAuthStatus";
|
||||
@@ -179,10 +178,6 @@ export default function ActionsPopover({
|
||||
// const [showTopShadow, setShowTopShadow] = useState(false);
|
||||
const { selectedSources, setSelectedSources } = filterManager;
|
||||
const [mcpServers, setMcpServers] = useState<MCPServer[]>([]);
|
||||
const { llmProviders, isLoading: isLLMLoading } = useLLMProviders(
|
||||
selectedAgent.id
|
||||
);
|
||||
const hasAnyProvider = !isLLMLoading && (llmProviders?.length ?? 0) > 0;
|
||||
|
||||
// Use the OAuth hook
|
||||
const { getToolAuthStatus, authenticateTool } = useToolOAuthStatus(
|
||||
@@ -498,8 +493,7 @@ export default function ActionsPopover({
|
||||
|
||||
// Fetch MCP servers for the agent on mount
|
||||
useEffect(() => {
|
||||
if (selectedAgent == null || selectedAgent.id == null || !hasAnyProvider)
|
||||
return;
|
||||
if (selectedAgent == null || selectedAgent.id == null) return;
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -540,7 +534,7 @@ export default function ActionsPopover({
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [selectedAgent?.id, hasAnyProvider]);
|
||||
}, [selectedAgent?.id]);
|
||||
|
||||
// No separate MCP tool loading; tools already exist in selectedAgent.tools
|
||||
|
||||
|
||||
@@ -676,7 +676,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
>
|
||||
{/* Main content grid — 3 rows, animated */}
|
||||
<div
|
||||
className="flex-1 w-full grid min-h-0 transition-[grid-template-rows] duration-150 ease-in-out"
|
||||
className="flex-1 w-full grid min-h-0 px-4 transition-[grid-template-rows] duration-150 ease-in-out"
|
||||
style={gridStyle}
|
||||
>
|
||||
{/* ── Top row: ChatUI / WelcomeMessage / ProjectUI ── */}
|
||||
@@ -741,7 +741,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
</div>
|
||||
|
||||
{/* ── Middle-center: AppInputBar ── */}
|
||||
<div className="row-start-2 flex flex-col items-center px-4">
|
||||
<div className="row-start-2 flex flex-col items-center">
|
||||
<div className="relative w-full max-w-[var(--app-page-main-content-width)] flex flex-col">
|
||||
{/* Scroll to bottom button - positioned absolutely above AppInputBar */}
|
||||
{appFocus.isChat() && showScrollButton && (
|
||||
@@ -841,7 +841,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
</div>
|
||||
|
||||
{/* ── Bottom: SearchResults + SourceFilter / Suggestions / ProjectChatList ── */}
|
||||
<div className="row-start-3 min-h-0 overflow-hidden flex flex-col items-center w-full px-4">
|
||||
<div className="row-start-3 min-h-0 overflow-hidden flex flex-col items-center w-full">
|
||||
{/* Agent description below input */}
|
||||
{(appFocus.isNewSession() || appFocus.isAgent()) &&
|
||||
!isDefaultAgent && (
|
||||
|
||||
@@ -366,7 +366,7 @@ const ChatScrollContainer = React.memo(
|
||||
>
|
||||
<div
|
||||
ref={contentWrapperRef}
|
||||
className="w-full flex-1 flex flex-col items-center px-4"
|
||||
className="w-full flex-1 flex flex-col items-center"
|
||||
data-scroll-ready={isScrollReady}
|
||||
style={{
|
||||
visibility: isScrollReady ? "visible" : "hidden",
|
||||
|
||||
Reference in New Issue
Block a user