Compare commits

..

1 Commits

Author SHA1 Message Date
Jamison Lahman
c1fc691b26 chore(fe): allow Modal.Body to defer scrolling to children 2026-03-13 14:57:09 -07:00
101 changed files with 793 additions and 1612 deletions

View File

@@ -10,9 +10,6 @@ inputs:
failed-jobs:
description: "Deprecated alias for details"
required: false
mention:
description: "GitHub username to resolve to a Slack @-mention. Replaces {mention} in details."
required: false
title:
description: "Title for the notification"
required: false
@@ -29,7 +26,6 @@ runs:
SLACK_WEBHOOK_URL: ${{ inputs.webhook-url }}
DETAILS: ${{ inputs.details }}
FAILED_JOBS: ${{ inputs.failed-jobs }}
MENTION_USER: ${{ inputs.mention }}
TITLE: ${{ inputs.title }}
REF_NAME: ${{ inputs.ref-name }}
REPO: ${{ github.repository }}
@@ -56,27 +52,6 @@ runs:
DETAILS="$FAILED_JOBS"
fi
# Resolve {mention} placeholder if a GitHub username was provided.
# Looks up the username in user-mappings.json (co-located with this action)
# and replaces {mention} with <@SLACK_ID> for a Slack @-mention.
# Falls back to the plain GitHub username if not found in the mapping.
if [ -n "$MENTION_USER" ]; then
MAPPINGS_FILE="${GITHUB_ACTION_PATH}/user-mappings.json"
slack_id="$(jq -r --arg gh "$MENTION_USER" 'to_entries[] | select(.value | ascii_downcase == ($gh | ascii_downcase)) | .key' "$MAPPINGS_FILE" 2>/dev/null | head -1)"
if [ -n "$slack_id" ]; then
mention_text="<@${slack_id}>"
else
mention_text="${MENTION_USER}"
fi
DETAILS="${DETAILS//\{mention\}/$mention_text}"
TITLE="${TITLE//\{mention\}/}"
else
DETAILS="${DETAILS//\{mention\}/}"
TITLE="${TITLE//\{mention\}/}"
fi
normalize_multiline() {
printf '%s' "$1" | awk 'BEGIN { ORS=""; first=1 } { if (!first) printf "\\n"; printf "%s", $0; first=0 }'
}

View File

@@ -1,18 +0,0 @@
{
"U05SAGZPEA1": "yuhongsun96",
"U05SAH6UGUD": "Weves",
"U07PWEQB7A5": "evan-onyx",
"U07V1SM68KF": "joachim-danswer",
"U08JZ9N3QNN": "raunakab",
"U08L24NCLJE": "Subash-Mohan",
"U090B9M07B2": "wenxi-onyx",
"U094RASDP0Q": "duo-onyx",
"U096L8ZQ85B": "justin-tahara",
"U09AHV8UBQX": "jessicasingh7",
"U09KAL5T3C2": "nmgarza5",
"U09KPGVQ70R": "acaprau",
"U09QR8KTSJH": "rohoswagger",
"U09RB4NTXA4": "jmelahman",
"U0A6K9VCY6A": "Danelegend",
"U0AGC4KH71A": "Bo-Onyx"
}

View File

@@ -207,7 +207,7 @@ jobs:
CHERRY_PICK_PR_URL: ${{ needs.cherry-pick-to-latest-release.outputs.cherry_pick_pr_url }}
run: |
source_pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${SOURCE_PR_NUMBER}"
details="*Cherry-pick PR opened successfully.*\\n• author: {mention}\\n• source PR: ${source_pr_url}"
details="*Cherry-pick PR opened successfully.*\\n• source PR: ${source_pr_url}"
if [ -n "${CHERRY_PICK_PR_URL}" ]; then
details="${details}\\n• cherry-pick PR: ${CHERRY_PICK_PR_URL}"
fi
@@ -221,7 +221,6 @@ jobs:
uses: ./.github/actions/slack-notify
with:
webhook-url: ${{ secrets.CHERRY_PICK_PRS_WEBHOOK }}
mention: ${{ needs.resolve-cherry-pick-request.outputs.merged_by }}
details: ${{ steps.success-summary.outputs.details }}
title: "✅ Automated Cherry-Pick PR Opened"
ref-name: ${{ github.event.pull_request.base.ref }}
@@ -276,21 +275,20 @@ jobs:
else
failed_job_label="cherry-pick-to-latest-release"
fi
details="• author: {mention}\\n• ${failed_job_label}\\n• source PR: ${source_pr_url}\\n• reason: ${reason_text}"
failed_jobs="• ${failed_job_label}\\n• source PR: ${source_pr_url}\\n• reason: ${reason_text}"
if [ -n "${MERGE_COMMIT_SHA}" ]; then
details="${details}\\n• merge SHA: ${MERGE_COMMIT_SHA}"
failed_jobs="${failed_jobs}\\n• merge SHA: ${MERGE_COMMIT_SHA}"
fi
if [ -n "${details_excerpt}" ]; then
details="${details}\\n• excerpt: ${details_excerpt}"
failed_jobs="${failed_jobs}\\n• excerpt: ${details_excerpt}"
fi
echo "details=${details}" >> "$GITHUB_OUTPUT"
echo "jobs=${failed_jobs}" >> "$GITHUB_OUTPUT"
- name: Notify #cherry-pick-prs about cherry-pick failure
uses: ./.github/actions/slack-notify
with:
webhook-url: ${{ secrets.CHERRY_PICK_PRS_WEBHOOK }}
mention: ${{ needs.resolve-cherry-pick-request.outputs.merged_by }}
details: ${{ steps.failure-summary.outputs.details }}
details: ${{ steps.failure-summary.outputs.jobs }}
title: "🚨 Automated Cherry-Pick Failed"
ref-name: ${{ github.event.pull_request.base.ref }}

View File

@@ -118,7 +118,9 @@ JWT_PUBLIC_KEY_URL: str | None = os.getenv("JWT_PUBLIC_KEY_URL", None)
SUPER_USERS = json.loads(os.environ.get("SUPER_USERS", "[]"))
SUPER_CLOUD_API_KEY = os.environ.get("SUPER_CLOUD_API_KEY", "api_key")
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY")
# The posthog client does not accept empty API keys or hosts however it fails silently
# when the capture is called. These defaults prevent Posthog issues from breaking the Onyx app
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY") or "FooBar"
POSTHOG_HOST = os.environ.get("POSTHOG_HOST") or "https://us.i.posthog.com"
POSTHOG_DEBUG_LOGS_ENABLED = (
os.environ.get("POSTHOG_DEBUG_LOGS_ENABLED", "").lower() == "true"

View File

@@ -34,9 +34,6 @@ class PostHogFeatureFlagProvider(FeatureFlagProvider):
Returns:
True if the feature is enabled for the user, False otherwise.
"""
if not posthog:
return False
try:
posthog.set(
distinct_id=user_id,

View File

@@ -9,7 +9,6 @@ from ee.onyx.configs.app_configs import POSTHOG_API_KEY
from ee.onyx.configs.app_configs import POSTHOG_DEBUG_LOGS_ENABLED
from ee.onyx.configs.app_configs import POSTHOG_HOST
from onyx.utils.logger import setup_logger
from shared_configs.configs import MULTI_TENANT
logger = setup_logger()
@@ -19,19 +18,12 @@ def posthog_on_error(error: Any, items: Any) -> None:
logger.error(f"PostHog error: {error}, items: {items}")
posthog: Posthog | None = None
if POSTHOG_API_KEY:
posthog = Posthog(
project_api_key=POSTHOG_API_KEY,
host=POSTHOG_HOST,
debug=POSTHOG_DEBUG_LOGS_ENABLED,
on_error=posthog_on_error,
)
elif MULTI_TENANT:
logger.warning(
"POSTHOG_API_KEY is not set but MULTI_TENANT is enabled — "
"PostHog telemetry and feature flags will be disabled"
)
posthog = Posthog(
project_api_key=POSTHOG_API_KEY,
host=POSTHOG_HOST,
debug=POSTHOG_DEBUG_LOGS_ENABLED,
on_error=posthog_on_error,
)
# For cross referencing between cloud and www Onyx sites
# NOTE: These clients are separate because they are separate posthog projects.
@@ -68,7 +60,7 @@ def capture_and_sync_with_alternate_posthog(
logger.error(f"Error capturing marketing posthog event: {e}")
try:
if posthog and (cloud_user_id := props.get("onyx_cloud_user_id")):
if cloud_user_id := props.get("onyx_cloud_user_id"):
cloud_props = props.copy()
cloud_props.pop("onyx_cloud_user_id", None)

View File

@@ -8,9 +8,6 @@ def event_telemetry(
distinct_id: str, event: str, properties: dict | None = None
) -> None:
"""Capture and send an event to PostHog, flushing immediately."""
if not posthog:
return
logger.info(f"Capturing PostHog event: {distinct_id} {event} {properties}")
try:
posthog.capture(distinct_id, event, properties)

View File

@@ -1,88 +0,0 @@
from datetime import datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel
from onyx.db.enums import HookFailStrategy
from onyx.db.enums import HookPoint
# ---------------------------------------------------------------------------
# Request models
# ---------------------------------------------------------------------------
class HookCreateRequest(BaseModel):
name: str
hook_point: HookPoint
endpoint_url: str
api_key: str | None = None
fail_strategy: HookFailStrategy = HookFailStrategy.HARD
timeout_seconds: float | None = None # if None, uses HookPointSpec default
class HookUpdateRequest(BaseModel):
name: str | None = None
endpoint_url: str | None = None
api_key: str | None = None
fail_strategy: HookFailStrategy | None = None
timeout_seconds: float | None = None
# ---------------------------------------------------------------------------
# Response models
# ---------------------------------------------------------------------------
class HookPointMetaResponse(BaseModel):
hook_point: HookPoint
display_name: str
description: str
docs_url: str | None
input_schema: dict[str, Any]
output_schema: dict[str, Any]
default_timeout_seconds: float
default_fail_strategy: HookFailStrategy
fail_hard_description: str
class HookResponse(BaseModel):
id: int
name: str
hook_point: HookPoint
endpoint_url: str | None
fail_strategy: HookFailStrategy
timeout_seconds: float
is_active: bool
creator_email: str | None
created_at: datetime
updated_at: datetime
class HookValidateResponse(BaseModel):
success: bool
error_message: str | None = None
# ---------------------------------------------------------------------------
# Health models
# ---------------------------------------------------------------------------
class HookHealthStatus(str, Enum):
healthy = "healthy" # green — reachable, no failures in last 1h
degraded = "degraded" # yellow — reachable, failures in last 1h
unreachable = "unreachable" # red — is_reachable=false or null
class HookFailureRecord(BaseModel):
error_message: str | None
status_code: int | None
duration_ms: int | None
created_at: datetime
class HookHealthResponse(BaseModel):
status: HookHealthStatus
recent_failures: list[HookFailureRecord] # last 10, newest first

View File

@@ -1,38 +0,0 @@
from abc import ABC
from abc import abstractmethod
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from onyx.db.enums import HookFailStrategy
from onyx.db.enums import HookPoint
@dataclass
class HookPointSpec(ABC):
"""Static metadata and contract for a pipeline hook point.
Each hook point is a concrete subclass of this class. Onyx engineers
own these definitions — customers never touch this code.
Instances are registered in onyx.hooks.registry.REGISTRY and served
via GET /api/admin/hook-points.
"""
hook_point: HookPoint
display_name: str
description: str
default_timeout_seconds: float
fail_hard_description: str
docs_url: str | None = None
default_fail_strategy: HookFailStrategy = field(default=HookFailStrategy.HARD)
@property
@abstractmethod
def input_schema(self) -> dict[str, Any]:
"""JSON schema describing the request payload sent to the customer's endpoint."""
@property
@abstractmethod
def output_schema(self) -> dict[str, Any]:
"""JSON schema describing the expected response from the customer's endpoint."""

View File

@@ -1,79 +0,0 @@
from dataclasses import dataclass
from typing import Any
from onyx.db.enums import HookPoint
from onyx.hooks.points.base import HookPointSpec
@dataclass
class QueryProcessingSpec(HookPointSpec):
"""Hook point that runs on every user query before it enters the pipeline.
Call site: inside handle_stream_message_objects() in
backend/onyx/chat/process_message.py, immediately after message_text is
assigned from the request and before create_new_chat_message() saves it.
This is the earliest possible point in the query pipeline:
- Raw query — unmodified, exactly as the user typed it
- No side effects yet — message has not been saved to DB
- User identity is available for user-specific logic
Supported use cases:
- Query rejection: block queries based on content or user context
- Query rewriting: normalize, expand, or modify the query
- PII removal: scrub sensitive data before the LLM sees it
- Access control: reject queries from certain users or groups
- Query auditing: log or track queries based on business rules
"""
hook_point: HookPoint = HookPoint.QUERY_PROCESSING
display_name: str = "Query Processing"
description: str = (
"Runs on every user query before it enters the pipeline. "
"Allows rewriting, filtering, or rejecting queries."
)
default_timeout_seconds: float = 5.0 # user is actively waiting — keep tight
fail_hard_description: str = (
"The query will be blocked and the user will see an error message."
)
docs_url: str | None = None
@property
def input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The raw query string exactly as the user typed it.",
},
"user_email": {
"type": ["string", "null"],
"description": "Email of the user submitting the query, or null if unauthenticated.",
},
},
"required": ["query", "user_email"],
}
@property
def output_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {
"type": ["string", "null"],
"description": (
"The (optionally modified) query to use. "
"Set to null to reject the query."
),
},
"rejection_message": {
"type": ["string", "null"],
"description": (
"Message shown to the user when query is null. "
"Falls back to a generic message if not provided."
),
},
},
"required": ["query"],
}

View File

@@ -1,34 +0,0 @@
from onyx.db.enums import HookPoint
from onyx.hooks.points.base import HookPointSpec
from onyx.hooks.points.query_processing import QueryProcessingSpec
REGISTRY: dict[HookPoint, HookPointSpec] = {
HookPoint.QUERY_PROCESSING: QueryProcessingSpec(),
}
_missing = set(HookPoint) - set(REGISTRY)
if _missing:
raise RuntimeError(
f"Hook point(s) have no registered spec: {_missing}. "
"Add an entry to onyx.hooks.registry.REGISTRY."
)
def get_hook_point_spec(hook_point: HookPoint) -> HookPointSpec:
"""Returns the spec for a given hook point.
Raises ValueError if the hook point has no registered spec — this is a
programmer error; every HookPoint enum value must have a corresponding spec
in REGISTRY.
"""
try:
return REGISTRY[hook_point]
except KeyError:
raise ValueError(
f"No spec registered for hook point {hook_point!r}. "
"Add an entry to onyx.hooks.registry.REGISTRY."
)
def get_all_specs() -> list[HookPointSpec]:
return list(REGISTRY.values())

View File

@@ -81,7 +81,6 @@ from onyx.server.manage.llm.models import VisionProviderResponse
from onyx.server.manage.llm.utils import generate_bedrock_display_name
from onyx.server.manage.llm.utils import generate_ollama_display_name
from onyx.server.manage.llm.utils import infer_vision_support
from onyx.server.manage.llm.utils import is_embedding_model
from onyx.server.manage.llm.utils import is_reasoning_model
from onyx.server.manage.llm.utils import is_valid_bedrock_model
from onyx.server.manage.llm.utils import ModelMetadata
@@ -1375,10 +1374,6 @@ def get_litellm_available_models(
try:
model_details = LitellmModelDetails.model_validate(model)
# Skip embedding models
if is_embedding_model(model_details.id):
continue
results.append(
LitellmFinalModelResponse(
provider_name=model_details.owned_by,

View File

@@ -366,18 +366,3 @@ def extract_vendor_from_model_name(model_name: str, provider: str) -> str | None
return None
return None
def is_embedding_model(model_name: str) -> bool:
"""Checks for if a model is an embedding model"""
from litellm import get_model_info
try:
# get_model_info raises on unknown models
# default to False
model_info = get_model_info(model_name)
except Exception:
return False
is_embedding_mode = model_info.get("mode") == "embedding"
return is_embedding_mode

View File

@@ -65,7 +65,7 @@ attrs==25.4.0
# jsonschema
# referencing
# zeep
authlib==1.6.9
authlib==1.6.7
# via fastmcp
azure-cognitiveservices-speech==1.38.0
# via onyx
@@ -737,7 +737,7 @@ pygithub==2.5.0
# via onyx
pygments==2.19.2
# via rich
pyjwt==2.12.0
pyjwt==2.11.0
# via
# fastapi-users
# mcp

View File

@@ -353,7 +353,7 @@ pygments==2.19.2
# via
# ipython
# ipython-pygments-lexers
pyjwt==2.12.0
pyjwt==2.11.0
# via mcp
pyparsing==3.2.5
# via matplotlib

View File

@@ -218,7 +218,7 @@ pydantic-core==2.33.2
# via pydantic
pydantic-settings==2.12.0
# via mcp
pyjwt==2.12.0
pyjwt==2.11.0
# via mcp
python-dateutil==2.8.2
# via

View File

@@ -308,7 +308,7 @@ pydantic-core==2.33.2
# via pydantic
pydantic-settings==2.12.0
# via mcp
pyjwt==2.12.0
pyjwt==2.11.0
# via mcp
python-dateutil==2.8.2
# via

View File

@@ -1,50 +0,0 @@
from onyx.db.enums import HookFailStrategy
from onyx.db.enums import HookPoint
from onyx.hooks.points.query_processing import QueryProcessingSpec
def test_hook_point_is_query_processing() -> None:
assert QueryProcessingSpec().hook_point == HookPoint.QUERY_PROCESSING
def test_default_fail_strategy_is_hard() -> None:
assert QueryProcessingSpec().default_fail_strategy == HookFailStrategy.HARD
def test_default_timeout_seconds() -> None:
# User is actively waiting — 5s is the documented contract for this hook point
assert QueryProcessingSpec().default_timeout_seconds == 5.0
def test_input_schema_required_fields() -> None:
schema = QueryProcessingSpec().input_schema
assert schema["type"] == "object"
required = schema["required"]
assert "query" in required
assert "user_email" in required
def test_input_schema_query_is_string() -> None:
props = QueryProcessingSpec().input_schema["properties"]
assert props["query"]["type"] == "string"
def test_input_schema_user_email_is_nullable() -> None:
props = QueryProcessingSpec().input_schema["properties"]
assert "null" in props["user_email"]["type"]
def test_output_schema_query_is_required() -> None:
schema = QueryProcessingSpec().output_schema
assert "query" in schema["required"]
def test_output_schema_query_is_nullable() -> None:
# null means "reject the query"
props = QueryProcessingSpec().output_schema["properties"]
assert "null" in props["query"]["type"]
def test_output_schema_rejection_message_is_optional() -> None:
schema = QueryProcessingSpec().output_schema
assert "rejection_message" not in schema.get("required", [])

View File

@@ -1,35 +0,0 @@
import pytest
from onyx.db.enums import HookPoint
from onyx.hooks import registry as registry_module
from onyx.hooks.registry import get_all_specs
from onyx.hooks.registry import get_hook_point_spec
from onyx.hooks.registry import REGISTRY
def test_registry_covers_all_hook_points() -> None:
"""Every HookPoint enum member must have a registered spec."""
assert set(REGISTRY.keys()) == set(
HookPoint
), f"Missing specs for: {set(HookPoint) - set(REGISTRY.keys())}"
def test_get_hook_point_spec_returns_correct_spec() -> None:
for hook_point in HookPoint:
spec = get_hook_point_spec(hook_point)
assert spec.hook_point == hook_point
def test_get_all_specs_returns_all() -> None:
specs = get_all_specs()
assert len(specs) == len(HookPoint)
assert {s.hook_point for s in specs} == set(HookPoint)
def test_get_hook_point_spec_raises_for_unregistered(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""get_hook_point_spec raises ValueError when a hook point has no spec."""
monkeypatch.setattr(registry_module, "REGISTRY", {})
with pytest.raises(ValueError, match="No spec registered for hook point"):
get_hook_point_spec(HookPoint.QUERY_PROCESSING)

View File

@@ -3,7 +3,6 @@
from onyx.server.manage.llm.utils import generate_bedrock_display_name
from onyx.server.manage.llm.utils import generate_ollama_display_name
from onyx.server.manage.llm.utils import infer_vision_support
from onyx.server.manage.llm.utils import is_embedding_model
from onyx.server.manage.llm.utils import is_reasoning_model
from onyx.server.manage.llm.utils import is_valid_bedrock_model
from onyx.server.manage.llm.utils import strip_openrouter_vendor_prefix
@@ -210,35 +209,3 @@ class TestIsReasoningModel:
is_reasoning_model("anthropic/claude-3-5-sonnet", "Claude 3.5 Sonnet")
is False
)
class TestIsEmbeddingModel:
"""Tests for embedding model detection."""
def test_openai_embedding_ada(self) -> None:
assert is_embedding_model("text-embedding-ada-002") is True
def test_openai_embedding_3_small(self) -> None:
assert is_embedding_model("text-embedding-3-small") is True
def test_openai_embedding_3_large(self) -> None:
assert is_embedding_model("text-embedding-3-large") is True
def test_cohere_embed_model(self) -> None:
assert is_embedding_model("embed-english-v3.0") is True
def test_bedrock_titan_embed(self) -> None:
assert is_embedding_model("amazon.titan-embed-text-v1") is True
def test_gpt4o_not_embedding(self) -> None:
assert is_embedding_model("gpt-4o") is False
def test_gpt4_not_embedding(self) -> None:
assert is_embedding_model("gpt-4") is False
def test_dall_e_not_embedding(self) -> None:
assert is_embedding_model("dall-e-3") is False
def test_unknown_custom_model_not_embedding(self) -> None:
"""Custom/local models not in litellm's model DB should default to False."""
assert is_embedding_model("my-custom-local-model-v1") is False

12
uv.lock generated
View File

@@ -453,14 +453,14 @@ wheels = [
[[package]]
name = "authlib"
version = "1.6.9"
version = "1.6.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" }
sdist = { url = "https://files.pythonhosted.org/packages/49/dc/ed1681bf1339dd6ea1ce56136bad4baabc6f7ad466e375810702b0237047/authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b", size = 164950, upload-time = "2026-02-06T14:04:14.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" },
{ url = "https://files.pythonhosted.org/packages/f8/00/3ed12264094ec91f534fae429945efbaa9f8c666f3aa7061cc3b2a26a0cd/authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0", size = 244115, upload-time = "2026-02-06T14:04:12.141Z" },
]
[[package]]
@@ -5643,11 +5643,11 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.12.0"
version = "2.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" },
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
]
[package.optional-dependencies]

View File

@@ -7,7 +7,6 @@ import {
type InteractiveStatefulInteraction,
} from "@opal/core";
import type { SizeVariant, WidthVariant } from "@opal/shared";
import type { InteractiveContainerRoundingVariant } from "@opal/core";
import type { TooltipSide } from "@opal/components";
import type { IconFunctionComponent, IconProps } from "@opal/types";
import { SvgChevronDownSmall } from "@opal/icons";
@@ -81,9 +80,6 @@ type OpenButtonProps = Omit<InteractiveStatefulProps, "variant"> & {
/** Which side the tooltip appears on. */
tooltipSide?: TooltipSide;
/** Override the default rounding derived from `size`. */
roundingVariant?: InteractiveContainerRoundingVariant;
};
// ---------------------------------------------------------------------------
@@ -99,7 +95,6 @@ function OpenButton({
justifyContent,
tooltip,
tooltipSide = "top",
roundingVariant: roundingVariantOverride,
interaction,
variant = "select-heavy",
...statefulProps
@@ -137,8 +132,7 @@ function OpenButton({
heightVariant={size}
widthVariant={width}
roundingVariant={
roundingVariantOverride ??
(isLarge ? "default" : size === "2xs" ? "mini" : "compact")
isLarge ? "default" : size === "2xs" ? "mini" : "compact"
}
>
<div

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgCurate = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M8 9L8 14.5M8 9C7.35971 8.35971 6.9055 8 6 8H2.5L2.5 13.5H6C6.9055 13.5 7.35971 13.8597 8 14.5M8 9C8.64029 8.35971 9.09449 8 10 8H13.5L13.5 13.5H10C9.09449 13.5 8.64029 13.8597 8 14.5M10.25 3.75C10.25 4.99264 9.24264 6 8 6C6.75736 6 5.75 4.99264 5.75 3.75C5.75 2.50736 6.75736 1.5 8 1.5C9.24264 1.5 10.25 2.50736 10.25 3.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgCurate;

View File

@@ -54,7 +54,6 @@ export { default as SvgColumn } from "@opal/icons/column";
export { default as SvgCopy } from "@opal/icons/copy";
export { default as SvgCornerRightUpDot } from "@opal/icons/corner-right-up-dot";
export { default as SvgCpu } from "@opal/icons/cpu";
export { default as SvgCurate } from "@opal/icons/curate";
export { default as SvgCreditCard } from "@opal/icons/credit-card";
export { default as SvgDashboard } from "@opal/icons/dashboard";
export { default as SvgDevKit } from "@opal/icons/dev-kit";
@@ -136,7 +135,6 @@ export { default as SvgPlayCircle } from "@opal/icons/play-circle";
export { default as SvgPlug } from "@opal/icons/plug";
export { default as SvgPlus } from "@opal/icons/plus";
export { default as SvgPlusCircle } from "@opal/icons/plus-circle";
export { default as SvgProgressBars } from "@opal/icons/progress-bars";
export { default as SvgProgressCircle } from "@opal/icons/progress-circle";
export { default as SvgQuestionMarkSmall } from "@opal/icons/question-mark-small";
export { default as SvgQuoteEnd } from "@opal/icons/quote-end";
@@ -178,16 +176,9 @@ export { default as SvgTwoLineSmall } from "@opal/icons/two-line-small";
export { default as SvgUnplug } from "@opal/icons/unplug";
export { default as SvgUploadCloud } from "@opal/icons/upload-cloud";
export { default as SvgUser } from "@opal/icons/user";
export { default as SvgUserCheck } from "@opal/icons/user-check";
export { default as SvgUserEdit } from "@opal/icons/user-edit";
export { default as SvgUserKey } from "@opal/icons/user-key";
export { default as SvgUserManage } from "@opal/icons/user-manage";
export { default as SvgUserMinus } from "@opal/icons/user-minus";
export { default as SvgUserPlus } from "@opal/icons/user-plus";
export { default as SvgUserShield } from "@opal/icons/user-shield";
export { default as SvgUserSpeaker } from "@opal/icons/user-speaker";
export { default as SvgUserSync } from "@opal/icons/user-sync";
export { default as SvgUserX } from "@opal/icons/user-x";
export { default as SvgUsers } from "@opal/icons/users";
export { default as SvgVolume } from "@opal/icons/volume";
export { default as SvgVolumeOff } from "@opal/icons/volume-off";

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgProgressBars = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M5.5 2.00003L13.25 2C13.9403 2 14.5 2.55964 14.5 3.25C14.5 3.94036 13.9403 4.5 13.25 4.5L5.5 4.50003M5.5 2.00003L2.74998 2C2.05963 2 1.49998 2.55964 1.49998 3.25C1.49998 3.94036 2.05963 4.5 2.74998 4.5L5.5 4.50003M5.5 2.00003V4.50003M10.5 11.5H13.25C13.9403 11.5 14.5 12.0596 14.5 12.75C14.5 13.4404 13.9403 14 13.25 14H10.5M10.5 11.5H2.74998C2.05963 11.5 1.49998 12.0596 1.49998 12.75C1.49998 13.4404 2.05963 14 2.74999 14H10.5M10.5 11.5V14M8 6.75H13.25C13.9403 6.75 14.5 7.30964 14.5 8C14.5 8.69036 13.9403 9.25 13.25 9.25H8M8 6.75H2.74998C2.05963 6.75 1.49998 7.30964 1.49998 8C1.49998 8.69036 2.05963 9.25 2.74998 9.25H8M8 6.75V9.25"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgProgressBars;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserCheck = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M11 14C11 13.6667 11 13.3333 11 13C11 11.3431 9.65684 10 7.99998 10H4.00002C2.34316 10 1 11.3431 1 13C1 13.3333 1 13.6667 1 14M10.75 7.49999L12.25 9L15 6.24999M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserCheck;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserEdit = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M1 14C1 13.6667 1 13.3333 1 13C1 11.3431 2.34316 10 4.00002 10H7M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75ZM12.09 8.41421C12.3552 8.149 12.7149 8 13.09 8C13.2757 8 13.4596 8.03658 13.6312 8.10765C13.8028 8.17872 13.9587 8.28289 14.09 8.41421C14.2213 8.54554 14.3255 8.70144 14.3966 8.87302C14.4676 9.0446 14.5042 9.2285 14.5042 9.41421C14.5042 9.59993 14.4676 9.78383 14.3966 9.95541C14.3255 10.127 14.2213 10.2829 14.09 10.4142L10.6667 13.8333L8 14.5L8.66667 11.8333L12.09 8.41421Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserEdit;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserKey = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M1 14C1 13.6667 1 13.3333 1 13C1 11.3431 2.34316 10 4.00002 10H8.5M12.625 10C13.6605 10 14.5 9.16053 14.5 8.125C14.5 7.08947 13.6605 6.25 12.625 6.25C11.5895 6.25 10.75 7.08947 10.75 8.125C10.75 9.16053 11.5895 10 12.625 10ZM12.625 10V12.25M12.625 14.5V13.5M12.625 13.5H13.875V12.25H12.625M12.625 13.5V12.25M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserKey;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserMinus = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M11 14C11 13.6667 11 13.3333 11 13C11 11.3431 9.65684 10 7.99998 10H4.00002C2.34316 10 1 11.3431 1 13C1 13.3333 1 13.6667 1 14M10.75 7.49999L14.75 7.50007M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserMinus;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserShield = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M1 14C1 13.6667 1 13.3333 1 13C1 11.3431 2.34316 10 4.00002 10H7M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75ZM12 14.5C12 14.5 14.5 13.25 14.5 11.375V9L12 8L9.5 9V11.375C9.5 13.25 12 14.5 12 14.5Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserShield;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserSpeaker = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M1 14C1 13.6667 1 13.3333 1 13C1 11.3431 2.34316 10 4.00002 10H7.99998C9.65684 10 11 11.3431 11 13C11 13.3333 11 13.6667 11 14H14.5V10L12.7071 8.20711M12 7.5L12.7071 8.20711M12.7071 8.20711C13.0976 7.81658 13.0976 7.18342 12.7071 6.79289C12.3166 6.40237 11.6834 6.40237 11.2929 6.79289C10.9024 7.18342 10.9024 7.81658 11.2929 8.20711C11.6834 8.59763 12.3166 8.59763 12.7071 8.20711ZM8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserSpeaker;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserX = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M11 14C11 13.6667 11 13.3333 11 13C11 11.3431 9.65684 10 7.99998 10H4.00002C2.34316 10 1 11.3431 1 13C1 13.3333 1 13.6667 1 14M11.5 8.5L13.25 6.75M13.25 6.75L15 5M13.25 6.75L15 8.5M13.25 6.75L11.5 5M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserX;

4
web/package-lock.json generated
View File

@@ -10309,9 +10309,7 @@
}
},
"node_modules/flatted": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"version": "3.3.3",
"dev": true,
"license": "ISC"
},

View File

@@ -2,9 +2,9 @@
import MCPPageContent from "@/sections/actions/MCPPageContent";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.MCP_ACTIONS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.MCP_ACTIONS]!;
export default function Main() {
return (

View File

@@ -2,9 +2,9 @@
import * as SettingsLayouts from "@/layouts/settings-layouts";
import OpenApiPageContent from "@/sections/actions/OpenApiPageContent";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.OPENAPI_ACTIONS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.OPENAPI_ACTIONS]!;
export default function Main() {
return (

View File

@@ -32,10 +32,7 @@ import { SettingsContext } from "@/providers/SettingsProvider";
import SourceTile from "@/components/SourceTile";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import Text from "@/refresh-components/texts/Text";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.ADD_CONNECTOR;
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
function SourceTileTooltipWrapper({
sourceMetadata,
preSelect,
@@ -127,6 +124,7 @@ function SourceTileTooltipWrapper({
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.ADD_CONNECTOR]!;
const sources = useMemo(() => listSourceMetadata(), []);
const [rawSearchTerm, setSearchTerm] = useState("");

View File

@@ -11,11 +11,10 @@ import { useAdminPersonas } from "@/hooks/useAdminPersonas";
import { Persona } from "./interfaces";
import { ThreeDotsLoader } from "@/components/Loading";
import { ErrorCallout } from "@/components/ErrorCallout";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { useState, useEffect } from "react";
import Pagination from "@/refresh-components/Pagination";
const route = ADMIN_ROUTES.AGENTS;
const PAGE_SIZE = 20;
function MainContent({
@@ -121,6 +120,7 @@ function MainContent({
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.AGENTS]!;
const [currentPage, setCurrentPage] = useState(1);
const { personas, totalItems, isLoading, error, refresh } = useAdminPersonas({
pageNum: currentPage - 1, // Backend uses 0-indexed pages

View File

@@ -32,9 +32,9 @@ import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
import Text from "@/refresh-components/texts/Text";
import { SvgEdit, SvgKey, SvgRefreshCw } from "@opal/icons";
import { useCloudSubscription } from "@/hooks/useCloudSubscription";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.API_KEYS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.API_KEYS]!;
function Main() {
const {

View File

@@ -6,12 +6,10 @@ import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { SlackBotTable } from "./SlackBotTable";
import { useSlackBots } from "./[bot-id]/hooks";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import CreateButton from "@/refresh-components/buttons/CreateButton";
import { DOCS_ADMINS_PATH } from "@/lib/constants";
const route = ADMIN_ROUTES.SLACK_BOTS;
function Main() {
const {
data: slackBots,
@@ -77,6 +75,8 @@ function Main() {
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.SLACK_BOTS]!;
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header icon={route.icon} title={route.title} separator />

View File

@@ -10,9 +10,9 @@ import * as SettingsLayouts from "@/layouts/settings-layouts";
import Text from "@/refresh-components/texts/Text";
import { cn } from "@/lib/utils";
import { SvgLock } from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.DOCUMENT_PROCESSING;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DOCUMENT_PROCESSING]!;
function Main() {
const {

View File

@@ -2,9 +2,9 @@
import * as SettingsLayouts from "@/layouts/settings-layouts";
import ImageGenerationContent from "./ImageGenerationContent";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.IMAGE_GENERATION;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.IMAGE_GENERATION]!;
export default function Page() {
return (

View File

@@ -19,9 +19,9 @@ import { SettingsContext } from "@/providers/SettingsProvider";
import CardSection from "@/components/admin/CardSection";
import { ErrorCallout } from "@/components/ErrorCallout";
import { useToastFromQuery } from "@/hooks/useToast";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.INDEX_SETTINGS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.SEARCH_SETTINGS]!;
export interface EmbeddingDetails {
api_key: string;
@@ -131,7 +131,7 @@ function Main() {
<div className="mt-4">
<Button variant="action" href="/admin/embeddings">
Update Index Settings
Update Search Settings
</Button>
</div>
</>

View File

@@ -23,10 +23,10 @@ import {
SvgOnyxLogo,
SvgX,
} from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { WebProviderSetupModal } from "@/app/admin/configuration/web-search/WebProviderSetupModal";
const route = ADMIN_ROUTES.WEB_SEARCH;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.WEB_SEARCH]!;
import {
SEARCH_PROVIDERS_URL,
SEARCH_PROVIDER_DETAILS,

View File

@@ -16,9 +16,9 @@ import { Card } from "@/components/ui/card";
import Text from "@/components/ui/text";
import { Spinner } from "@/components/Spinner";
import { SvgDownloadCloud } from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.DEBUG;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DEBUG]!;
function Main() {
const [categories, setCategories] = useState<string[]>([]);

View File

@@ -19,9 +19,7 @@ import {
import { createGuildConfig } from "@/app/admin/discord-bot/lib";
import { DiscordGuildsTable } from "@/app/admin/discord-bot/DiscordGuildsTable";
import { BotConfigCard } from "@/app/admin/discord-bot/BotConfigCard";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.DISCORD_BOTS;
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
function DiscordBotContent() {
const { data: guilds, isLoading, error, refreshGuilds } = useDiscordGuilds();
@@ -120,6 +118,8 @@ function DiscordBotContent() {
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DISCORD_BOTS]!;
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header

View File

@@ -3,9 +3,9 @@
import { useState } from "react";
import useSWR from "swr";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.INDEX_MIGRATION;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.INDEX_MIGRATION]!;
import Card from "@/refresh-components/cards/Card";
import { Content, ContentAction } from "@opal/layouts";

View File

@@ -1,13 +1,11 @@
"use client";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { Explorer } from "./Explorer";
import { Connector } from "@/lib/connectors/connectors";
import { DocumentSetSummary } from "@/lib/types";
const route = ADMIN_ROUTES.DOCUMENT_EXPLORER;
interface DocumentExplorerPageProps {
initialSearchValue: string | undefined;
connectors: Connector<any>[];
@@ -19,6 +17,8 @@ export default function DocumentExplorerPage({
connectors,
documentSets,
}: DocumentExplorerPageProps) {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DOCUMENT_EXPLORER]!;
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header icon={route.icon} title={route.title} separator />

View File

@@ -6,9 +6,7 @@ import { DocumentFeedbackTable } from "./DocumentFeedbackTable";
import { numPages, numToDisplay } from "./constants";
import Title from "@/components/ui/title";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.DOCUMENT_FEEDBACK;
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
function Main() {
const {
@@ -63,6 +61,8 @@ function Main() {
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DOCUMENT_FEEDBACK]!;
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header icon={route.icon} title={route.title} separator />

View File

@@ -6,14 +6,12 @@ import { refreshDocumentSets, useDocumentSets } from "../hooks";
import { useConnectorStatus, useUserGroups } from "@/lib/hooks";
import { ThreeDotsLoader } from "@/components/Loading";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import CardSection from "@/components/admin/CardSection";
import { DocumentSetCreationForm } from "../DocumentSetCreationForm";
import { useRouter } from "next/navigation";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
const route = ADMIN_ROUTES.DOCUMENT_SETS;
function Main({ documentSetId }: { documentSetId: number }) {
const router = useRouter();
const vectorDbEnabled = useVectorDbEnabled();
@@ -95,6 +93,7 @@ export default function Page(props: {
}) {
const params = use(props.params);
const documentSetId = parseInt(params.documentSetId);
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DOCUMENT_SETS]!;
return (
<SettingsLayouts.Root>

View File

@@ -1,7 +1,7 @@
"use client";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { DocumentSetCreationForm } from "../DocumentSetCreationForm";
import { useConnectorStatus, useUserGroups } from "@/lib/hooks";
import { ThreeDotsLoader } from "@/components/Loading";
@@ -11,8 +11,6 @@ import { refreshDocumentSets } from "../hooks";
import CardSection from "@/components/admin/CardSection";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
const route = ADMIN_ROUTES.DOCUMENT_SETS;
function Main() {
const router = useRouter();
const vectorDbEnabled = useVectorDbEnabled();
@@ -60,6 +58,8 @@ function Main() {
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DOCUMENT_SETS]!;
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header

View File

@@ -20,7 +20,7 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { deleteDocumentSet } from "./lib";
import { toast } from "@/hooks/useToast";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import {
FiAlertTriangle,
FiCheckCircle,
@@ -43,7 +43,6 @@ import CreateButton from "@/refresh-components/buttons/CreateButton";
import { SourceIcon } from "@/components/SourceIcon";
import Link from "next/link";
const route = ADMIN_ROUTES.DOCUMENT_SETS;
const numToDisplay = 50;
// Component to display federated connectors with consistent styling
@@ -423,6 +422,8 @@ function Main() {
}
export default function Page() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.DOCUMENT_SETS]!;
return (
<SettingsLayouts.Root>
<SettingsLayouts.Header icon={route.icon} title={route.title} separator />

View File

@@ -4,7 +4,7 @@ import { CCPairIndexingStatusTable } from "./CCPairIndexingStatusTable";
import { SearchAndFilterControls } from "./SearchAndFilterControls";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import Link from "next/link";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import Text from "@/components/ui/text";
import { useConnectorIndexingStatusWithPagination } from "@/lib/hooks";
import { useToastFromQuery } from "@/hooks/useToast";
@@ -18,8 +18,6 @@ import { TOGGLED_CONNECTORS_COOKIE_NAME } from "@/lib/constants";
import { ConnectorStaggeredSkeleton } from "./ConnectorRowSkeleton";
import { IndexingStatusRequest } from "@/lib/types";
const route = ADMIN_ROUTES.INDEXING_STATUS;
function Main() {
const vectorDbEnabled = useVectorDbEnabled();
@@ -206,6 +204,8 @@ function Main() {
}
export default function Status() {
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.INDEXING_STATUS]!;
useToastFromQuery({
"connector-created": {
message: "Connector created successfully",

View File

@@ -31,9 +31,9 @@ import KGEntityTypes from "@/app/admin/kg/KGEntityTypes";
import Text from "@/refresh-components/texts/Text";
import { cn } from "@/lib/utils";
import { SvgSettings } from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.KNOWLEDGE_GRAPH;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.KNOWLEDGE_GRAPH]!;
function createDomainField(
name: string,

View File

@@ -18,9 +18,9 @@ import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidE
import CreateButton from "@/refresh-components/buttons/CreateButton";
import { SvgGlobe, SvgUser, SvgUsers } from "@opal/icons";
import { Section } from "@/layouts/general-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.TOKEN_RATE_LIMITS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.TOKEN_RATE_LIMITS]!;
const BASE_URL = "/api/admin/token-rate-limits";
const GLOBAL_TOKEN_FETCH_URL = `${BASE_URL}/global`;
const USER_TOKEN_FETCH_URL = `${BASE_URL}/users`;

View File

@@ -6,11 +6,11 @@ import { useSpecificUserGroup } from "./hook";
import { ThreeDotsLoader } from "@/components/Loading";
import { useConnectorStatus } from "@/lib/hooks";
import useUsers from "@/hooks/useUsers";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
const route = ADMIN_ROUTES.GROUPS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.GROUPS]!;
function Main({ groupId }: { groupId: string }) {
const vectorDbEnabled = useVectorDbEnabled();

View File

@@ -8,11 +8,11 @@ import { useConnectorStatus, useUserGroups } from "@/lib/hooks";
import useUsers from "@/hooks/useUsers";
import { useUser } from "@/providers/UserProvider";
import CreateButton from "@/refresh-components/buttons/CreateButton";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
const route = ADMIN_ROUTES.GROUPS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.GROUPS]!;
function Main() {
const [showForm, setShowForm] = useState(false);

View File

@@ -1,11 +1,11 @@
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { CUSTOM_ANALYTICS_ENABLED } from "@/lib/constants";
import { Callout } from "@/components/ui/callout";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import Text from "@/components/ui/text";
import { CustomAnalyticsUpdateForm } from "./CustomAnalyticsUpdateForm";
const route = ADMIN_ROUTES.CUSTOM_ANALYTICS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.CUSTOM_ANALYTICS]!;
function Main() {
if (!CUSTOM_ANALYTICS_ENABLED) {

View File

@@ -2,9 +2,9 @@
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { QueryHistoryTable } from "@/app/ee/admin/performance/query-history/QueryHistoryTable";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const route = ADMIN_ROUTES.QUERY_HISTORY;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.QUERY_HISTORY]!;
export default function QueryHistoryPage() {
return (

View File

@@ -9,10 +9,10 @@ import { useTimeRange } from "@/app/ee/admin/performance/lib";
import UsageReports from "@/app/ee/admin/performance/usage/UsageReports";
import Separator from "@/refresh-components/Separator";
import { useAdminPersonas } from "@/hooks/useAdminPersonas";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import * as SettingsLayouts from "@/layouts/settings-layouts";
const route = ADMIN_ROUTES.USAGE;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.USAGE]!;
export default function AnalyticsPage() {
const [timeRange, setTimeRange] = useTimeRange();

View File

@@ -2,10 +2,10 @@ import { StandardAnswerCreationForm } from "@/app/ee/admin/standard-answer/Stand
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { StandardAnswer, StandardAnswerCategory } from "@/lib/types";
const route = ADMIN_ROUTES.STANDARD_ANSWERS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.STANDARD_ANSWERS]!;
async function Main({ id }: { id: string }) {
const tasks = [

View File

@@ -2,10 +2,10 @@ import { StandardAnswerCreationForm } from "@/app/ee/admin/standard-answer/Stand
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { StandardAnswerCategory } from "@/lib/types";
const route = ADMIN_ROUTES.STANDARD_ANSWERS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.STANDARD_ANSWERS]!;
async function Page() {
const standardAnswerCategoriesResponse = await fetchSS(

View File

@@ -30,10 +30,10 @@ import { TableHeader } from "@/components/ui/table";
import CreateButton from "@/refresh-components/buttons/CreateButton";
import { SvgEdit, SvgTrash } from "@opal/icons";
import { Button } from "@opal/components";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
const NUM_RESULTS_PER_PAGE = 10;
const route = ADMIN_ROUTES.STANDARD_ANSWERS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.STANDARD_ANSWERS]!;
type Displayable = JSX.Element | string;

View File

@@ -1,7 +1,7 @@
"use client";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { Button } from "@opal/components";
import { Disabled } from "@opal/core";
import {
@@ -16,7 +16,7 @@ import * as Yup from "yup";
import { EnterpriseSettings } from "@/interfaces/settings";
import { useRouter } from "next/navigation";
const route = ADMIN_ROUTES.THEME;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.THEME]!;
const CHAR_LIMITS = {
application_name: 50,

View File

@@ -6,10 +6,11 @@ import { useSettingsContext } from "@/providers/SettingsProvider";
import { ApplicationStatus } from "@/interfaces/settings";
import { Button } from "@opal/components";
import { cn } from "@/lib/utils";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_PATHS } from "@/lib/admin-routes";
export interface ClientLayoutProps {
children: React.ReactNode;
enableEnterprise: boolean;
enableCloud: boolean;
}
@@ -18,36 +19,40 @@ export interface ClientLayoutProps {
// the `py-10 px-4 md:px-12` padding below can be removed entirely and
// this prefix list can be deleted.
const SETTINGS_LAYOUT_PREFIXES = [
ADMIN_ROUTES.CHAT_PREFERENCES.path,
ADMIN_ROUTES.IMAGE_GENERATION.path,
ADMIN_ROUTES.WEB_SEARCH.path,
ADMIN_ROUTES.MCP_ACTIONS.path,
ADMIN_ROUTES.OPENAPI_ACTIONS.path,
ADMIN_ROUTES.BILLING.path,
ADMIN_ROUTES.INDEX_MIGRATION.path,
ADMIN_ROUTES.DISCORD_BOTS.path,
ADMIN_ROUTES.THEME.path,
ADMIN_ROUTES.LLM_MODELS.path,
ADMIN_ROUTES.AGENTS.path,
ADMIN_ROUTES.USERS.path,
ADMIN_ROUTES.TOKEN_RATE_LIMITS.path,
ADMIN_ROUTES.INDEX_SETTINGS.path,
ADMIN_ROUTES.DOCUMENT_PROCESSING.path,
ADMIN_ROUTES.CODE_INTERPRETER.path,
ADMIN_ROUTES.API_KEYS.path,
ADMIN_ROUTES.ADD_CONNECTOR.path,
ADMIN_ROUTES.INDEXING_STATUS.path,
ADMIN_ROUTES.DOCUMENTS.path,
ADMIN_ROUTES.DEBUG.path,
ADMIN_ROUTES.KNOWLEDGE_GRAPH.path,
ADMIN_ROUTES.SLACK_BOTS.path,
ADMIN_ROUTES.STANDARD_ANSWERS.path,
ADMIN_ROUTES.GROUPS.path,
ADMIN_ROUTES.PERFORMANCE.path,
ADMIN_ROUTES.SCIM.path,
ADMIN_PATHS.CHAT_PREFERENCES,
ADMIN_PATHS.IMAGE_GENERATION,
ADMIN_PATHS.WEB_SEARCH,
ADMIN_PATHS.MCP_ACTIONS,
ADMIN_PATHS.OPENAPI_ACTIONS,
ADMIN_PATHS.BILLING,
ADMIN_PATHS.INDEX_MIGRATION,
ADMIN_PATHS.DISCORD_BOTS,
ADMIN_PATHS.THEME,
ADMIN_PATHS.LLM_MODELS,
ADMIN_PATHS.AGENTS,
ADMIN_PATHS.USERS,
ADMIN_PATHS.TOKEN_RATE_LIMITS,
ADMIN_PATHS.SEARCH_SETTINGS,
ADMIN_PATHS.DOCUMENT_PROCESSING,
ADMIN_PATHS.CODE_INTERPRETER,
ADMIN_PATHS.API_KEYS,
ADMIN_PATHS.ADD_CONNECTOR,
ADMIN_PATHS.INDEXING_STATUS,
ADMIN_PATHS.DOCUMENTS,
ADMIN_PATHS.DEBUG,
ADMIN_PATHS.KNOWLEDGE_GRAPH,
ADMIN_PATHS.SLACK_BOTS,
ADMIN_PATHS.STANDARD_ANSWERS,
ADMIN_PATHS.GROUPS,
ADMIN_PATHS.PERFORMANCE,
ADMIN_PATHS.SCIM,
];
export function ClientLayout({ children, enableCloud }: ClientLayoutProps) {
export function ClientLayout({
children,
enableEnterprise,
enableCloud,
}: ClientLayoutProps) {
const pathname = usePathname();
const settings = useSettingsContext();
@@ -81,7 +86,10 @@ export function ClientLayout({ children, enableCloud }: ClientLayoutProps) {
<div className="flex-1 min-w-0 min-h-0 overflow-y-auto">{children}</div>
) : (
<>
<AdminSidebar enableCloudSS={enableCloud} />
<AdminSidebar
enableCloudSS={enableCloud}
enableEnterpriseSS={enableEnterprise}
/>
<div
data-main-container
className={cn(

View File

@@ -2,7 +2,10 @@ import { redirect } from "next/navigation";
import type { Route } from "next";
import { requireAdminAuth } from "@/lib/auth/requireAuth";
import { ClientLayout } from "./ClientLayout";
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
import {
NEXT_PUBLIC_CLOUD_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
} from "@/lib/constants";
import { AnnouncementBanner } from "../header/AnnouncementBanner";
export interface LayoutProps {
@@ -19,7 +22,10 @@ export default async function Layout({ children }: LayoutProps) {
}
return (
<ClientLayout enableCloud={NEXT_PUBLIC_CLOUD_ENABLED}>
<ClientLayout
enableEnterprise={SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED}
enableCloud={NEXT_PUBLIC_CLOUD_ENABLED}
>
<AnnouncementBanner />
{children}
</ClientLayout>

View File

@@ -65,7 +65,6 @@ import useAppFocus from "@/hooks/useAppFocus";
import { useQueryController } from "@/providers/QueryControllerProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import useBrowserInfo from "@/hooks/useBrowserInfo";
import { APP_SLOGAN } from "@/lib/constants";
/**
* App Header Component
@@ -462,7 +461,7 @@ function Footer() {
settings?.enterpriseSettings?.custom_lower_disclaimer_content ||
`[Onyx ${
settings?.webVersion || "dev"
}](https://www.onyx.app/) - ${APP_SLOGAN}`;
}](https://www.onyx.app/) - Open Source AI Platform`;
return (
<footer

View File

@@ -9,7 +9,7 @@ import React from "react";
export type FlexDirection = "row" | "column";
export type JustifyContent = "start" | "center" | "end" | "between";
export type AlignItems = "start" | "center" | "end" | "stretch";
export type Length = "auto" | "fit" | "full" | number;
export type Length = "auto" | "fit" | "full";
const flexDirectionClassMap: Record<FlexDirection, string> = {
row: "flex-row",
@@ -90,12 +90,11 @@ export const heightClassmap: Record<Length, string> = {
* @remarks
* - The component defaults to column layout when no direction is specified
* - Full width and height by default
* - Accepts className for additional styling; style prop is not available
* - Prevents style overrides (className and style props are not available)
* - Import using namespace import for consistent usage: `import * as GeneralLayouts from "@/layouts/general-layouts"`
*/
export interface SectionProps
extends WithoutStyles<React.HtmlHTMLAttributes<HTMLDivElement>> {
className?: string;
flexDirection?: FlexDirection;
justifyContent?: JustifyContent;
alignItems?: AlignItems;
@@ -117,7 +116,6 @@ export interface SectionProps
* wrap a `Section` without affecting layout.
*/
function Section({
className,
flexDirection = "column",
justifyContent = "center",
alignItems = "center",
@@ -139,20 +137,13 @@ function Section({
flexDirectionClassMap[flexDirection],
justifyClassMap[justifyContent],
alignClassMap[alignItems],
typeof width === "string" && widthClassmap[width],
typeof height === "string" && heightClassmap[height],
typeof height === "number" && "overflow-hidden",
widthClassmap[width],
heightClassmap[height],
wrap && "flex-wrap",
dbg && "dbg-red",
className
dbg && "dbg-red"
)}
style={{
gap: `${gap}rem`,
padding: `${padding}rem`,
...(typeof width === "number" && { width: `${width}rem` }),
...(typeof height === "number" && { height: `${height}rem` }),
}}
style={{ gap: `${gap}rem`, padding: `${padding}rem` }}
{...rest}
/>
);

View File

@@ -3,7 +3,6 @@ import {
SvgActions,
SvgActivity,
SvgArrowExchange,
SvgAudio,
SvgBarChart,
SvgBookOpen,
SvgBubbleText,
@@ -11,254 +10,243 @@ import {
SvgCpu,
SvgDiscordMono,
SvgDownload,
SvgEmpty,
SvgFileText,
SvgFiles,
SvgFolder,
SvgGlobe,
SvgHistory,
SvgImage,
SvgKey,
SvgMcp,
SvgNetworkGraph,
SvgOnyxOctagon,
SvgPaintBrush,
SvgProgressBars,
SvgSearchMenu,
SvgSearch,
SvgServer,
SvgShield,
SvgSlack,
SvgTerminal,
SvgThumbsUp,
SvgUploadCloud,
SvgUser,
SvgUserKey,
SvgUserSync,
SvgUsers,
SvgWallet,
SvgZoomIn,
} from "@opal/icons";
export interface AdminRouteEntry {
path: string;
/**
* Canonical path constants for every admin route.
*/
export const ADMIN_PATHS = {
INDEXING_STATUS: "/admin/indexing/status",
ADD_CONNECTOR: "/admin/add-connector",
DOCUMENT_SETS: "/admin/documents/sets",
DOCUMENT_EXPLORER: "/admin/documents/explorer",
DOCUMENT_FEEDBACK: "/admin/documents/feedback",
AGENTS: "/admin/agents",
SLACK_BOTS: "/admin/bots",
DISCORD_BOTS: "/admin/discord-bot",
MCP_ACTIONS: "/admin/actions/mcp",
OPENAPI_ACTIONS: "/admin/actions/open-api",
STANDARD_ANSWERS: "/admin/standard-answer",
GROUPS: "/admin/groups",
CHAT_PREFERENCES: "/admin/configuration/chat-preferences",
LLM_MODELS: "/admin/configuration/llm",
WEB_SEARCH: "/admin/configuration/web-search",
IMAGE_GENERATION: "/admin/configuration/image-generation",
CODE_INTERPRETER: "/admin/configuration/code-interpreter",
SEARCH_SETTINGS: "/admin/configuration/search",
DOCUMENT_PROCESSING: "/admin/configuration/document-processing",
KNOWLEDGE_GRAPH: "/admin/kg",
USERS: "/admin/users",
API_KEYS: "/admin/api-key",
TOKEN_RATE_LIMITS: "/admin/token-rate-limits",
USAGE: "/admin/performance/usage",
QUERY_HISTORY: "/admin/performance/query-history",
CUSTOM_ANALYTICS: "/admin/performance/custom-analytics",
THEME: "/admin/theme",
BILLING: "/admin/billing",
INDEX_MIGRATION: "/admin/document-index-migration",
SCIM: "/admin/scim",
DEBUG: "/admin/debug",
// Prefix-only entries (used in SETTINGS_LAYOUT_PREFIXES but have no
// single page header of their own)
DOCUMENTS: "/admin/documents",
PERFORMANCE: "/admin/performance",
} as const;
interface AdminRouteConfig {
icon: IconFunctionComponent;
title: string;
sidebarLabel: string;
}
/**
* Single source of truth for every admin route: path, icon, page-header
* title, and sidebar label.
* Single source of truth for icon, page-header title, and sidebar label
* for every admin route. Keyed by path from `ADMIN_PATHS`.
*/
export const ADMIN_ROUTES = {
INDEXING_STATUS: {
path: "/admin/indexing/status",
export const ADMIN_ROUTE_CONFIG: Record<string, AdminRouteConfig> = {
[ADMIN_PATHS.INDEXING_STATUS]: {
icon: SvgBookOpen,
title: "Existing Connectors",
sidebarLabel: "Existing Connectors",
},
ADD_CONNECTOR: {
path: "/admin/add-connector",
[ADMIN_PATHS.ADD_CONNECTOR]: {
icon: SvgUploadCloud,
title: "Add Connector",
sidebarLabel: "Add Connector",
},
DOCUMENT_SETS: {
path: "/admin/documents/sets",
icon: SvgFiles,
[ADMIN_PATHS.DOCUMENT_SETS]: {
icon: SvgFolder,
title: "Document Sets",
sidebarLabel: "Document Sets",
},
DOCUMENT_EXPLORER: {
path: "/admin/documents/explorer",
[ADMIN_PATHS.DOCUMENT_EXPLORER]: {
icon: SvgZoomIn,
title: "Document Explorer",
sidebarLabel: "Explorer",
},
DOCUMENT_FEEDBACK: {
path: "/admin/documents/feedback",
[ADMIN_PATHS.DOCUMENT_FEEDBACK]: {
icon: SvgThumbsUp,
title: "Document Feedback",
sidebarLabel: "Feedback",
},
AGENTS: {
path: "/admin/agents",
[ADMIN_PATHS.AGENTS]: {
icon: SvgOnyxOctagon,
title: "Agents",
sidebarLabel: "Agents",
},
SLACK_BOTS: {
path: "/admin/bots",
[ADMIN_PATHS.SLACK_BOTS]: {
icon: SvgSlack,
title: "Slack Integration",
sidebarLabel: "Slack Integration",
title: "Slack Bots",
sidebarLabel: "Slack Bots",
},
DISCORD_BOTS: {
path: "/admin/discord-bot",
[ADMIN_PATHS.DISCORD_BOTS]: {
icon: SvgDiscordMono,
title: "Discord Integration",
sidebarLabel: "Discord Integration",
title: "Discord Bots",
sidebarLabel: "Discord Bots",
},
MCP_ACTIONS: {
path: "/admin/actions/mcp",
[ADMIN_PATHS.MCP_ACTIONS]: {
icon: SvgMcp,
title: "MCP Actions",
sidebarLabel: "MCP Actions",
},
OPENAPI_ACTIONS: {
path: "/admin/actions/open-api",
[ADMIN_PATHS.OPENAPI_ACTIONS]: {
icon: SvgActions,
title: "OpenAPI Actions",
sidebarLabel: "OpenAPI Actions",
},
STANDARD_ANSWERS: {
path: "/admin/standard-answer",
[ADMIN_PATHS.STANDARD_ANSWERS]: {
icon: SvgClipboard,
title: "Standard Answers",
sidebarLabel: "Standard Answers",
},
GROUPS: {
path: "/admin/groups",
[ADMIN_PATHS.GROUPS]: {
icon: SvgUsers,
title: "Manage User Groups",
sidebarLabel: "Groups",
},
CHAT_PREFERENCES: {
path: "/admin/configuration/chat-preferences",
[ADMIN_PATHS.CHAT_PREFERENCES]: {
icon: SvgBubbleText,
title: "Chat Preferences",
sidebarLabel: "Chat Preferences",
},
LLM_MODELS: {
path: "/admin/configuration/llm",
[ADMIN_PATHS.LLM_MODELS]: {
icon: SvgCpu,
title: "Language Models",
sidebarLabel: "Language Models",
},
WEB_SEARCH: {
path: "/admin/configuration/web-search",
[ADMIN_PATHS.WEB_SEARCH]: {
icon: SvgGlobe,
title: "Web Search",
sidebarLabel: "Web Search",
},
IMAGE_GENERATION: {
path: "/admin/configuration/image-generation",
[ADMIN_PATHS.IMAGE_GENERATION]: {
icon: SvgImage,
title: "Image Generation",
sidebarLabel: "Image Generation",
},
VOICE: {
path: "/admin/configuration/voice",
icon: SvgAudio,
title: "Voice",
sidebarLabel: "Voice",
},
CODE_INTERPRETER: {
path: "/admin/configuration/code-interpreter",
[ADMIN_PATHS.CODE_INTERPRETER]: {
icon: SvgTerminal,
title: "Code Interpreter",
sidebarLabel: "Code Interpreter",
},
INDEX_SETTINGS: {
path: "/admin/configuration/search",
icon: SvgSearchMenu,
title: "Index Settings",
sidebarLabel: "Index Settings",
[ADMIN_PATHS.SEARCH_SETTINGS]: {
icon: SvgSearch,
title: "Search Settings",
sidebarLabel: "Search Settings",
},
DOCUMENT_PROCESSING: {
path: "/admin/configuration/document-processing",
[ADMIN_PATHS.DOCUMENT_PROCESSING]: {
icon: SvgFileText,
title: "Document Processing",
sidebarLabel: "Document Processing",
},
KNOWLEDGE_GRAPH: {
path: "/admin/kg",
[ADMIN_PATHS.KNOWLEDGE_GRAPH]: {
icon: SvgNetworkGraph,
title: "Knowledge Graph",
sidebarLabel: "Knowledge Graph",
},
USERS: {
path: "/admin/users",
[ADMIN_PATHS.USERS]: {
icon: SvgUser,
title: "Users & Requests",
sidebarLabel: "Users",
},
API_KEYS: {
path: "/admin/api-key",
icon: SvgUserKey,
title: "Service Accounts",
sidebarLabel: "Service Accounts",
[ADMIN_PATHS.API_KEYS]: {
icon: SvgKey,
title: "API Keys",
sidebarLabel: "API Keys",
},
TOKEN_RATE_LIMITS: {
path: "/admin/token-rate-limits",
icon: SvgProgressBars,
title: "Spending Limits",
sidebarLabel: "Spending Limits",
[ADMIN_PATHS.TOKEN_RATE_LIMITS]: {
icon: SvgShield,
title: "Token Rate Limits",
sidebarLabel: "Token Rate Limits",
},
USAGE: {
path: "/admin/performance/usage",
[ADMIN_PATHS.USAGE]: {
icon: SvgActivity,
title: "Usage Statistics",
sidebarLabel: "Usage Statistics",
},
QUERY_HISTORY: {
path: "/admin/performance/query-history",
icon: SvgHistory,
[ADMIN_PATHS.QUERY_HISTORY]: {
icon: SvgServer,
title: "Query History",
sidebarLabel: "Query History",
},
CUSTOM_ANALYTICS: {
path: "/admin/performance/custom-analytics",
[ADMIN_PATHS.CUSTOM_ANALYTICS]: {
icon: SvgBarChart,
title: "Custom Analytics",
sidebarLabel: "Custom Analytics",
},
THEME: {
path: "/admin/theme",
[ADMIN_PATHS.THEME]: {
icon: SvgPaintBrush,
title: "Appearance & Theming",
sidebarLabel: "Appearance & Theming",
},
BILLING: {
path: "/admin/billing",
[ADMIN_PATHS.BILLING]: {
icon: SvgWallet,
title: "Plans & Billing",
sidebarLabel: "Plans & Billing",
},
INDEX_MIGRATION: {
path: "/admin/document-index-migration",
[ADMIN_PATHS.INDEX_MIGRATION]: {
icon: SvgArrowExchange,
title: "Document Index Migration",
sidebarLabel: "Document Index Migration",
},
SCIM: {
path: "/admin/scim",
[ADMIN_PATHS.SCIM]: {
icon: SvgUserSync,
title: "SCIM",
sidebarLabel: "SCIM",
},
DEBUG: {
path: "/admin/debug",
[ADMIN_PATHS.DEBUG]: {
icon: SvgDownload,
title: "Debug Logs",
sidebarLabel: "Debug Logs",
},
// Prefix-only entries used for layout matching — not rendered as sidebar
// items or page headers.
DOCUMENTS: {
path: "/admin/documents",
icon: SvgEmpty,
title: "",
sidebarLabel: "",
},
PERFORMANCE: {
path: "/admin/performance",
icon: SvgEmpty,
title: "",
sidebarLabel: "",
},
} as const satisfies Record<string, AdminRouteEntry>;
};
/**
* Helper that converts a route entry into the `{ name, icon, link }`
* shape expected by the sidebar.
* Helper that converts a route config entry into the `{ name, icon, link }`
* shape expected by the sidebar. Extra fields (e.g. `error`) can be spread in.
*/
export function sidebarItem(route: AdminRouteEntry) {
return { name: route.sidebarLabel, icon: route.icon, link: route.path };
export function sidebarItem(path: string) {
const config = ADMIN_ROUTE_CONFIG[path]!;
return { name: config.sidebarLabel, icon: config.icon, link: path };
}

View File

@@ -108,6 +108,7 @@ export const CREDENTIAL_JSON = "credential_json";
export const MODAL_ROOT_ID = "modal-root";
export const ANONYMOUS_USER_NAME = "Anonymous";
export const UNNAMED_CHAT = "New Chat";
export const DEFAULT_AGENT_ID = 0;
@@ -130,5 +131,3 @@ export const LOGO_UNFOLDED_SIZE_PX = 88;
export const DEFAULT_CONTEXT_TOKENS = 120_000;
export const MAX_CHUNKS_FED_TO_CHAT = 25;
export const APP_SLOGAN = "Open Source AI Platform";

View File

@@ -1,4 +1,4 @@
import { User } from "@/lib/types";
import { User } from "./types";
export const checkUserIsNoAuthUser = (userId: string) => {
return userId === "__no_auth_user__";
@@ -113,18 +113,3 @@ export async function refreshToken(
throw error;
}
}
export function getUserDisplayName(user: User | null): string {
// Prioritize custom personal name if set
if (!!user?.personalization?.name) return user.personalization.name;
// Then, prioritize personal email
if (!!user?.email) {
const atIndex = user.email.indexOf("@");
if (atIndex > 0) {
return user.email.substring(0, atIndex);
}
}
// If nothing works, then fall back to anonymous user name
return "Anonymous";
}

View File

@@ -1,4 +1,3 @@
import { cn } from "@/lib/utils";
import Text from "@/refresh-components/texts/Text";
import { SvgX } from "@opal/icons";
import { Button } from "@opal/components";
@@ -11,8 +10,6 @@ export interface ChipProps {
rightIcon?: React.FunctionComponent<IconProps>;
onRemove?: () => void;
smallLabel?: boolean;
/** When true, applies warning-coloured styling to the right icon. */
error?: boolean;
}
/**
@@ -32,27 +29,16 @@ export default function Chip({
rightIcon: RightIcon,
onRemove,
smallLabel = true,
error = false,
}: ChipProps) {
return (
<div
className={cn(
"flex items-center gap-1 px-1.5 py-0.5 rounded-08",
"bg-background-tint-02"
)}
>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded-08 bg-background-tint-02">
{Icon && <Icon size={12} className="text-text-03" />}
{children && (
<Text figureSmallLabel={smallLabel} text03>
{children}
</Text>
)}
{RightIcon && (
<RightIcon
size={14}
className={cn(error ? "text-status-warning-05" : "text-text-03")}
/>
)}
{RightIcon && <RightIcon size={14} className="text-text-03" />}
{onRemove && (
<Button
onClick={(e) => {

View File

@@ -61,6 +61,7 @@ interface ModalContextValue {
hasAttemptedClose: boolean;
setHasAttemptedClose: (value: boolean) => void;
height: keyof typeof heightClasses;
deferScrollingToChildren: boolean;
hasDescription: boolean;
setHasDescription: (value: boolean) => void;
}
@@ -84,9 +85,9 @@ const widthClasses = {
const heightClasses = {
fit: "h-fit",
sm: "max-h-[30rem] overflow-y-auto",
lg: "max-h-[calc(100dvh-4rem)] overflow-y-auto",
full: "h-[80dvh] overflow-y-auto",
sm: "max-h-[30rem]",
lg: "max-h-[calc(100dvh-4rem)]",
full: "h-[80dvh]",
};
/**
@@ -120,6 +121,8 @@ export interface ModalContentProps
> {
width?: keyof typeof widthClasses;
height?: keyof typeof heightClasses;
/** Lets nested children like ScrollIndicatorDiv own vertical scrolling. */
deferScrollingToChildren?: boolean;
preventAccidentalClose?: boolean;
skipOverlay?: boolean;
background?: "default" | "gray";
@@ -136,6 +139,7 @@ const ModalContent = React.forwardRef<
children,
width = "md",
height = "fit",
deferScrollingToChildren = false,
preventAccidentalClose = true,
skipOverlay = false,
background = "default",
@@ -290,6 +294,11 @@ const ModalContent = React.forwardRef<
!hasContainerCenter && "left-1/2 top-1/2"
);
const contentOverflowClasses =
deferScrollingToChildren || height === "fit"
? "overflow-hidden"
: "overflow-y-auto";
const dialogEventHandlers = {
onOpenAutoFocus: (e: Event) => {
resetState();
@@ -320,6 +329,7 @@ const ModalContent = React.forwardRef<
hasAttemptedClose,
setHasAttemptedClose,
height,
deferScrollingToChildren,
hasDescription,
setHasDescription,
}}
@@ -344,7 +354,13 @@ const ModalContent = React.forwardRef<
widthClasses[width]
)}
>
<div className={cn(cardClasses, "w-full min-h-0")}>
<div
className={cn(
cardClasses,
contentOverflowClasses,
"w-full min-h-0"
)}
>
{children}
</div>
<div className="w-full flex-shrink-0">{bottomSlot}</div>
@@ -357,7 +373,6 @@ const ModalContent = React.forwardRef<
style={containerStyle}
className={cn(
positionClasses,
"overflow-hidden",
"z-modal",
background === "gray"
? "bg-background-tint-01"
@@ -367,7 +382,8 @@ const ModalContent = React.forwardRef<
"max-w-[calc(100dvw-2rem)] max-h-[calc(100dvh-2rem)]",
animationClasses,
widthClasses[width],
heightClasses[height]
heightClasses[height],
contentOverflowClasses
)}
{...dialogEventHandlers}
>
@@ -510,16 +526,20 @@ interface ModalBodyProps extends WithoutStyles<SectionProps> {
}
const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(
({ twoTone = true, children, ...props }, ref) => {
const { deferScrollingToChildren } = useModalContext();
return (
<div
ref={ref}
className={cn(
twoTone && "bg-background-tint-01",
"flex-auto min-h-0 overflow-y-auto w-full"
"flex-auto min-h-0 w-full",
deferScrollingToChildren ? "overflow-hidden" : "overflow-y-auto"
)}
>
<Section
height="auto"
height={deferScrollingToChildren ? "full" : "auto"}
justifyContent="start"
padding={1}
gap={1}
alignItems="start"

View File

@@ -107,10 +107,9 @@ const PopoverClose = PopoverPrimitive.Close;
* </Popover.Content>
* ```
*/
type PopoverWidths = "fit" | "sm" | "md" | "lg" | "xl" | "trigger";
type PopoverWidths = "fit" | "md" | "lg" | "xl" | "trigger";
const widthClasses: Record<PopoverWidths, string> = {
fit: "w-fit",
sm: "w-[10rem]",
md: "w-[12rem]",
lg: "w-[15rem]",
xl: "w-[18rem]",
@@ -121,29 +120,25 @@ interface PopoverContentProps
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
> {
width?: PopoverWidths;
/** Portal container. Set to a DOM element to render inside it (e.g. inside a modal). */
container?: HTMLElement | null;
ref?: React.Ref<React.ComponentRef<typeof PopoverPrimitive.Content>>;
}
function PopoverContent({
width = "fit",
container,
align = "center",
sideOffset = 4,
ref,
...props
}: PopoverContentProps) {
return (
<PopoverPrimitive.Portal container={container}>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
collisionPadding={8}
className={cn(
"bg-background-neutral-00 p-1 z-popover rounded-12 border shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"bg-background-neutral-00 p-1 z-popover rounded-12 overflow-hidden border shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"max-h-[var(--radix-popover-content-available-height)]",
"overflow-hidden",
widthClasses[width]
)}
{...props}

View File

@@ -119,6 +119,11 @@ export default function ScrollIndicatorDiv({
// DOM after initial render, which changes scrollHeight without firing
// resize or scroll events on the container).
const mutationObserver = new MutationObserver(handleScroll);
mutationObserver.observe(container, {
childList: true,
subtree: true,
characterData: true,
});
return () => {
container.removeEventListener("scroll", handleScroll);

View File

@@ -7,8 +7,6 @@ import { cn } from "@/lib/utils";
export interface SeparatorProps
extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
noPadding?: boolean;
/** Custom horizontal padding in rem. Overrides the default padding. */
paddingXRem?: number;
}
/**
@@ -36,7 +34,6 @@ const Separator = React.forwardRef(
(
{
noPadding,
paddingXRem,
className,
orientation = "horizontal",
@@ -49,17 +46,9 @@ const Separator = React.forwardRef(
return (
<div
style={{
...(paddingXRem != null
? {
paddingLeft: `${paddingXRem}rem`,
paddingRight: `${paddingXRem}rem`,
}
: {}),
}}
className={cn(
isHorizontal ? "w-full" : "h-full",
paddingXRem == null && !noPadding && (isHorizontal ? "py-4" : "px-4"),
!noPadding && (isHorizontal ? "py-4" : "px-4"),
className
)}
>

View File

@@ -105,7 +105,7 @@ export default function ShadowDiv({
}, [containerRef, checkScroll]);
return (
<div className="relative min-h-0">
<div className="relative">
<div
ref={containerRef}
className={cn("overflow-y-auto", className)}
@@ -118,7 +118,7 @@ export default function ShadowDiv({
{!bottomOnly && (
<div
className={cn(
"absolute top-0 left-0 right-0 pointer-events-none transition-opacity duration-150",
"absolute top-0 left-0 right-0 pointer-events-none",
showTopShadow ? "opacity-100" : "opacity-0"
)}
style={{
@@ -132,7 +132,7 @@ export default function ShadowDiv({
{!topOnly && (
<div
className={cn(
"absolute bottom-0 left-0 right-0 pointer-events-none transition-opacity duration-150",
"absolute bottom-0 left-0 right-0 pointer-events-none",
showBottomShadow ? "opacity-100" : "opacity-0"
)}
style={{

View File

@@ -97,8 +97,16 @@ function InputChipField({
<Chip
key={chip.id}
onRemove={disabled ? undefined : () => onRemoveChip(chip.id)}
rightIcon={chip.error ? SvgAlertTriangle : undefined}
error={chip.error}
rightIcon={
chip.error
? (props) => (
<SvgAlertTriangle
{...props}
className="text-status-warning-text"
/>
)
: undefined
}
smallLabel={layout === "stacked"}
>
{chip.label}
@@ -116,7 +124,7 @@ function InputChipField({
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
placeholder={chips.length === 0 ? placeholder : undefined}
className={cn(
"flex-1 min-w-[80px] h-[1.5rem] bg-transparent p-0.5 focus:outline-none",
innerClasses[variant],

View File

@@ -24,7 +24,7 @@ import {
SvgFold,
SvgExternalLink,
} from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { Content } from "@opal/layouts";
import {
useSettingsContext,
@@ -58,7 +58,7 @@ import useFilter from "@/hooks/useFilter";
import { MCPServer } from "@/lib/tools/interfaces";
import type { IconProps } from "@opal/types";
const route = ADMIN_ROUTES.CHAT_PREFERENCES;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.CHAT_PREFERENCES]!;
interface DefaultAgentConfiguration {
tool_ids: number[];

View File

@@ -11,7 +11,7 @@ import {
SvgUnplug,
SvgXOctagon,
} from "@opal/icons";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import { Section } from "@/layouts/general-layouts";
import { Button } from "@opal/components";
import { Disabled } from "@opal/core";
@@ -23,7 +23,7 @@ import { updateCodeInterpreter } from "@/lib/admin/code-interpreter/svc";
import { ContentAction } from "@opal/layouts";
import { toast } from "@/hooks/useToast";
const route = ADMIN_ROUTES.CODE_INTERPRETER;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.CODE_INTERPRETER]!;
interface CodeInterpreterCardProps {
variant?: CardProps["variant"];

View File

@@ -13,7 +13,7 @@ import { Button } from "@opal/components";
import { Hoverable } from "@opal/core";
import { SvgArrowExchange, SvgSettings, SvgTrash } from "@opal/icons";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import * as GeneralLayouts from "@/layouts/general-layouts";
import {
getProviderDisplayName,
@@ -47,7 +47,7 @@ import { LMStudioForm } from "@/sections/modals/llmConfig/LMStudioForm";
import { LiteLLMProxyModal } from "@/sections/modals/llmConfig/LiteLLMProxyModal";
import { Section } from "@/layouts/general-layouts";
const route = ADMIN_ROUTES.LLM_MODELS;
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.LLM_MODELS]!;
// ============================================================================
// Provider form mapping (keyed by provider name from the API)

View File

@@ -1,26 +1,23 @@
"use client";
import { useState, useMemo, useCallback } from "react";
import { useState, useMemo, useRef, useCallback } from "react";
import { Button } from "@opal/components";
import { SvgUsers, SvgUser, SvgLogOut, SvgCheck } from "@opal/icons";
import { Disabled } from "@opal/core";
import { ContentAction } from "@opal/layouts";
import Modal from "@/refresh-components/Modal";
import Text from "@/refresh-components/texts/Text";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import InputSelect from "@/refresh-components/inputs/InputSelect";
import Popover from "@/refresh-components/Popover";
import LineItem from "@/refresh-components/buttons/LineItem";
import Separator from "@/refresh-components/Separator";
import ShadowDiv from "@/refresh-components/ShadowDiv";
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
import { Section } from "@/layouts/general-layouts";
import { toast } from "@/hooks/useToast";
import { UserRole, USER_ROLE_LABELS } from "@/lib/types";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import useGroups from "@/hooks/useGroups";
import { addUserToGroup, removeUserFromGroup, setUserRole } from "./svc";
import type { UserRow } from "./interfaces";
import { cn } from "../../../lib/utils";
// ---------------------------------------------------------------------------
// Constants
@@ -36,7 +33,7 @@ const ASSIGNABLE_ROLES: UserRole[] = [
// Types
// ---------------------------------------------------------------------------
interface EditUserModalProps {
interface EditGroupsModalProps {
user: UserRow & { id: string };
onClose: () => void;
onMutate: () => void;
@@ -46,16 +43,25 @@ interface EditUserModalProps {
// Component
// ---------------------------------------------------------------------------
export default function EditUserModal({
export default function EditGroupsModal({
user,
onClose,
onMutate,
}: EditUserModalProps) {
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
}: EditGroupsModalProps) {
const { data: allGroups, isLoading: groupsLoading } = useGroups();
const [searchTerm, setSearchTerm] = useState("");
const [dropdownOpen, setDropdownOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const closeDropdown = useCallback(() => {
// Delay to allow click events on dropdown items to fire before closing
setTimeout(() => {
if (!containerRef.current?.contains(document.activeElement)) {
setDropdownOpen(false);
}
}, 0);
}, []);
const [selectedRole, setSelectedRole] = useState<UserRole | "">(
user.role ?? ""
);
@@ -89,10 +95,6 @@ export default function EditUserModal({
);
}, [memberGroupIds, initialMemberGroupIds]);
const visibleRoles = isPaidEnterpriseFeaturesEnabled
? ASSIGNABLE_ROLES
: ASSIGNABLE_ROLES.filter((r) => r !== UserRole.GLOBAL_CURATOR);
const hasRoleChange =
user.role !== null && selectedRole !== "" && selectedRole !== user.role;
const hasChanges = hasGroupChanges || hasRoleChange;
@@ -158,17 +160,10 @@ export default function EditUserModal({
};
const displayName = user.personal_name ?? user.email;
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
const contentRef = useCallback((node: HTMLDivElement | null) => {
setContentEl(node);
}, []);
return (
<Modal
open
onOpenChange={(isOpen) => !isOpen && !isSubmitting && onClose()}
>
<Modal.Content width="sm" ref={contentRef}>
<Modal open onOpenChange={(isOpen) => !isOpen && onClose()}>
<Modal.Content width="sm">
<Modal.Header
icon={SvgUsers}
title="Edit User's Groups & Roles"
@@ -177,119 +172,108 @@ export default function EditUserModal({
? `${user.personal_name} (${user.email})`
: user.email
}
onClose={isSubmitting ? undefined : onClose}
onClose={onClose}
/>
<Modal.Body twoTone>
<Section padding={0} height="auto" alignItems="stretch">
<Section
gap={0.5}
padding={0.25}
height={joinedGroups.length === 0 && !popoverOpen ? "auto" : 14.5}
alignItems="stretch"
justifyContent="start"
className="bg-background-tint-02 rounded-08"
>
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<Popover.Trigger asChild>
{/* asChild merges trigger props onto this div instead of rendering a <button>.
Without it, the trigger <button> would nest around InputTypeIn's
internal IconButton <button>, causing a hydration error. */}
<div>
<InputTypeIn
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search groups to join..."
leftSearchIcon
/>
</div>
</Popover.Trigger>
<Popover.Content
width="trigger"
align="start"
container={contentEl}
>
{groupsLoading ? (
<LineItem skeleton description="Loading groups...">
Loading...
</LineItem>
) : dropdownGroups.length === 0 ? (
<LineItem
skeleton
description="Try a different search term."
>
No groups found
</LineItem>
) : (
<ShadowDiv
shadowHeight="0.75rem"
className={cn(
"flex flex-col gap-1 max-h-[15rem] rounded-08"
)}
>
{dropdownGroups.map((group) => {
const isMember = memberGroupIds.has(group.id);
return (
<LineItem
key={group.id}
icon={isMember ? SvgCheck : SvgUsers}
description={`${group.users.length} ${
group.users.length === 1 ? "user" : "users"
}`}
selected={isMember}
emphasized={isMember}
onClick={() => toggleGroup(group.id)}
>
{group.name}
</LineItem>
);
})}
</ShadowDiv>
)}
</Popover.Content>
</Popover>
<ShadowDiv
className={cn(" max-h-[11rem] flex flex-col gap-1 rounded-08")}
shadowHeight="0.75rem"
<Section
gap={1}
height="auto"
alignItems="stretch"
justifyContent="start"
>
{/* Subsection: white card behind search + groups */}
<div className="relative">
<div className="absolute -inset-2 bg-background-neutral-00 rounded-12" />
<Section
gap={0.5}
height="auto"
alignItems="stretch"
justifyContent="start"
>
<div ref={containerRef} className="relative">
<InputTypeIn
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
if (!dropdownOpen) setDropdownOpen(true);
}}
onFocus={() => setDropdownOpen(true)}
onBlur={closeDropdown}
placeholder="Search groups to join..."
leftSearchIcon
/>
{dropdownOpen && (
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-background-neutral-00 border border-border-02 rounded-12 shadow-md p-1">
{groupsLoading ? (
<Text as="p" text03 secondaryBody className="px-3 py-2">
Loading groups...
</Text>
) : dropdownGroups.length === 0 ? (
<Text as="p" text03 secondaryBody className="px-3 py-2">
No groups found
</Text>
) : (
<ShadowDiv className="max-h-[200px] flex flex-col gap-1">
{dropdownGroups.map((group) => {
const isMember = memberGroupIds.has(group.id);
return (
<LineItem
key={group.id}
icon={isMember ? SvgCheck : SvgUsers}
description={`${group.users.length} ${
group.users.length === 1 ? "user" : "users"
}`}
selected={isMember}
emphasized={isMember}
onMouseDown={(e: React.MouseEvent) =>
e.preventDefault()
}
onClick={() => toggleGroup(group.id)}
>
{group.name}
</LineItem>
);
})}
</ShadowDiv>
)}
</div>
)}
</div>
{joinedGroups.length === 0 ? (
<LineItem
icon={SvgUsers}
skeleton
interactive={false}
description={`${displayName} is not in any groups.`}
muted
>
No groups found
No groups joined
</LineItem>
) : (
joinedGroups.map((group) => (
<div
key={group.id}
className="bg-background-tint-01 rounded-08"
>
<LineItem
<ShadowDiv className="flex flex-col gap-1 max-h-[200px]">
{joinedGroups.map((group) => (
<div
key={group.id}
icon={SvgUsers}
description={`${group.users.length} ${
group.users.length === 1 ? "user" : "users"
}`}
rightChildren={
<SimpleTooltip
tooltip="Remove from group"
side="left"
>
<SvgLogOut height={16} width={16} />
</SimpleTooltip>
}
onClick={() => toggleGroup(group.id)}
className="bg-background-tint-01 rounded-08"
>
{group.name}
</LineItem>
</div>
))
<LineItem
icon={SvgUsers}
description={`${group.users.length} ${
group.users.length === 1 ? "user" : "users"
}`}
rightChildren={
<SvgLogOut className="w-4 h-4 text-text-03" />
}
onClick={() => toggleGroup(group.id)}
>
{group.name}
</LineItem>
</div>
))}
</ShadowDiv>
)}
</ShadowDiv>
</Section>
</Section>
</div>
{user.role && (
<>
<Separator noPadding />
@@ -307,7 +291,7 @@ export default function EditUserModal({
>
<InputSelect.Trigger />
<InputSelect.Content>
{user.role && !visibleRoles.includes(user.role) && (
{user.role && !ASSIGNABLE_ROLES.includes(user.role) && (
<InputSelect.Item
key={user.role}
value={user.role}
@@ -316,7 +300,7 @@ export default function EditUserModal({
{USER_ROLE_LABELS[user.role]}
</InputSelect.Item>
)}
{visibleRoles.map((role) => (
{ASSIGNABLE_ROLES.map((role) => (
<InputSelect.Item
key={role}
value={role}
@@ -335,10 +319,7 @@ export default function EditUserModal({
</Modal.Body>
<Modal.Footer>
<Button
prominence="secondary"
onClick={isSubmitting ? undefined : onClose}
>
<Button prominence="secondary" onClick={onClose}>
Cancel
</Button>
<Disabled disabled={isSubmitting || !hasChanges}>

View File

@@ -12,7 +12,7 @@ import { Tag } from "@opal/components";
import IconButton from "@/refresh-components/buttons/IconButton";
import Text from "@/refresh-components/texts/Text";
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
import EditUserModal from "./EditUserModal";
import EditGroupsModal from "./EditGroupsModal";
import type { UserRow, UserGroupInfo } from "./interfaces";
interface GroupsCellProps {
@@ -184,7 +184,7 @@ export default function GroupsCell({
)}
</div>
{showModal && user.id != null && (
<EditUserModal
<EditGroupsModal
user={{ ...user, id: user.id }}
onClose={() => setShowModal(false)}
onMutate={onMutate}

View File

@@ -2,12 +2,11 @@
import { useState, useCallback } from "react";
import { Button } from "@opal/components";
import { SvgUsers, SvgAlertTriangle } from "@opal/icons";
import { SvgUsers } from "@opal/icons";
import { Disabled } from "@opal/core";
import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
import InputChipField from "@/refresh-components/inputs/InputChipField";
import type { ChipItem } from "@/refresh-components/inputs/InputChipField";
import Text from "@/refresh-components/texts/Text";
import { toast } from "@/hooks/useToast";
import { inviteUsers } from "./svc";
@@ -146,20 +145,9 @@ export default function InviteUsersModal({
onAdd={addEmail}
value={inputValue}
onChange={setInputValue}
placeholder="Add an email and press enter"
placeholder="Add emails to invite, comma separated"
layout="stacked"
/>
{chips.some((c) => c.error) && (
<div className="flex items-center gap-1 pt-1">
<SvgAlertTriangle
size={14}
className="text-status-warning-05 shrink-0"
/>
<Text secondaryBody text03>
Some email addresses are invalid and will be skipped.
</Text>
</div>
)}
</Modal.Body>
<Modal.Footer>

View File

@@ -166,7 +166,6 @@ export default function UserFilters({
<Popover>
<Popover.Trigger asChild>
<FilterButton
data-testid="filter-role"
leftIcon={SvgUsers}
active={hasRoleFilter}
onClear={() => onRolesChange([])}
@@ -214,7 +213,6 @@ export default function UserFilters({
>
<Popover.Trigger asChild>
<FilterButton
data-testid="filter-group"
leftIcon={SvgUsers}
active={hasGroupFilter}
onClear={() => onGroupsChange([])}
@@ -269,7 +267,6 @@ export default function UserFilters({
<Popover>
<Popover.Trigger asChild>
<FilterButton
data-testid="filter-status"
leftIcon={SvgUsers}
active={hasStatusFilter}
onClear={() => onStatusesChange([])}

View File

@@ -51,6 +51,16 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
);
}
if (user.is_scim_synced) {
return (
<div className="flex items-center gap-1.5">
<Text as="span" mainUiBody text03>
{USER_ROLE_LABELS[user.role] ?? user.role}
</Text>
</div>
);
}
const applyRole = async (newRole: UserRole) => {
if (isUpdatingRef.current) return;
isUpdatingRef.current = true;
@@ -79,46 +89,47 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
const currentIcon = ROLE_ICONS[user.role] ?? SvgUser;
const visibleRoles = isPaidEnterpriseFeaturesEnabled
? SELECTABLE_ROLES
: SELECTABLE_ROLES.filter((r) => r !== UserRole.GLOBAL_CURATOR);
const roleItems = visibleRoles.map((role) => {
const isSelected = user.role === role;
const icon = ROLE_ICONS[role] ?? SvgUser;
return (
<LineItem
key={role}
icon={isSelected ? SvgCheck : icon}
selected={isSelected}
emphasized={isSelected}
onClick={() => handleSelect(role)}
>
{USER_ROLE_LABELS[role]}
</LineItem>
);
});
return (
<Disabled disabled={isUpdating}>
<Popover open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<OpenButton
icon={currentIcon}
variant="select-tinted"
width="full"
justifyContent="between"
roundingVariant="compact"
>
{USER_ROLE_LABELS[user.role]}
</OpenButton>
</Popover.Trigger>
<Popover.Content align="start">
<div className="flex flex-col gap-1 p-1 min-w-[160px]">
{roleItems}
</div>
</Popover.Content>
</Popover>
</Disabled>
<div className="[&_button]:rounded-08">
<Disabled disabled={isUpdating}>
<Popover open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<OpenButton
icon={currentIcon}
variant="select-tinted"
width="full"
justifyContent="between"
>
{USER_ROLE_LABELS[user.role]}
</OpenButton>
</Popover.Trigger>
<Popover.Content align="start">
<div className="flex flex-col gap-1 p-1 min-w-[160px]">
{SELECTABLE_ROLES.map((role) => {
if (
role === UserRole.GLOBAL_CURATOR &&
!isPaidEnterpriseFeaturesEnabled
) {
return null;
}
const isSelected = user.role === role;
const icon = ROLE_ICONS[role] ?? SvgUser;
return (
<LineItem
key={role}
icon={isSelected ? SvgCheck : icon}
selected={isSelected}
emphasized={isSelected}
onClick={() => handleSelect(role)}
>
{USER_ROLE_LABELS[role]}
</LineItem>
);
})}
</div>
</Popover.Content>
</Popover>
</Disabled>
</div>
);
}

View File

@@ -6,16 +6,12 @@ import {
SvgMoreHorizontal,
SvgUsers,
SvgXCircle,
SvgUserCheck,
SvgUserPlus,
SvgUserX,
SvgKey,
SvgTrash,
SvgCheck,
} from "@opal/icons";
import { Disabled } from "@opal/core";
import Popover from "@/refresh-components/Popover";
import Separator from "@/refresh-components/Separator";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
import { Section } from "@/layouts/general-layouts";
import Text from "@/refresh-components/texts/Text";
import { UserStatus } from "@/lib/types";
import { toast } from "@/hooks/useToast";
@@ -25,23 +21,21 @@ import {
deleteUser,
cancelInvite,
approveRequest,
resetPassword,
} from "./svc";
import EditUserModal from "./EditUserModal";
import EditGroupsModal from "./EditGroupsModal";
import type { UserRow } from "./interfaces";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
enum Modal {
DEACTIVATE = "deactivate",
ACTIVATE = "activate",
DELETE = "delete",
CANCEL_INVITE = "cancelInvite",
EDIT_GROUPS = "editGroups",
RESET_PASSWORD = "resetPassword",
}
type ModalType =
| "deactivate"
| "activate"
| "delete"
| "cancelInvite"
| "editGroups"
| null;
interface UserRowActionsProps {
user: UserRow;
@@ -56,10 +50,9 @@ export default function UserRowActions({
user,
onMutate,
}: UserRowActionsProps) {
const [modal, setModal] = useState<Modal | null>(null);
const [modal, setModal] = useState<ModalType>(null);
const [popoverOpen, setPopoverOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [newPassword, setNewPassword] = useState<string | null>(null);
async function handleAction(
action: () => Promise<void>,
@@ -78,40 +71,13 @@ export default function UserRowActions({
}
}
const openModal = (type: Modal) => {
const openModal = (type: ModalType) => {
setPopoverOpen(false);
setModal(type);
};
// Status-aware action menus
const actionButtons = (() => {
// SCIM-managed users get limited actions — most changes would be
// overwritten on the next IdP sync.
if (user.is_scim_synced) {
return (
<>
{user.id && (
<Button
prominence="tertiary"
icon={SvgUsers}
onClick={() => openModal(Modal.EDIT_GROUPS)}
>
Groups &amp; Roles
</Button>
)}
<Disabled disabled>
<Button prominence="tertiary" variant="danger" icon={SvgUserX}>
Deactivate User
</Button>
</Disabled>
<Separator paddingXRem={0.5} />
<Text as="p" secondaryBody text03 className="px-3 py-1">
This is a synced SCIM user managed by your identity provider.
</Text>
</>
);
}
switch (user.status) {
case UserStatus.INVITED:
return (
@@ -119,7 +85,7 @@ export default function UserRowActions({
prominence="tertiary"
variant="danger"
icon={SvgXCircle}
onClick={() => openModal(Modal.CANCEL_INVITE)}
onClick={() => openModal("cancelInvite")}
>
Cancel Invite
</Button>
@@ -129,7 +95,7 @@ export default function UserRowActions({
return (
<Button
prominence="tertiary"
icon={SvgUserCheck}
icon={SvgCheck}
onClick={() => {
setPopoverOpen(false);
handleAction(
@@ -149,24 +115,15 @@ export default function UserRowActions({
<Button
prominence="tertiary"
icon={SvgUsers}
onClick={() => openModal(Modal.EDIT_GROUPS)}
onClick={() => openModal("editGroups")}
>
Groups &amp; Roles
Groups
</Button>
)}
<Button
prominence="tertiary"
icon={SvgKey}
onClick={() => openModal(Modal.RESET_PASSWORD)}
>
Reset Password
</Button>
<Separator paddingXRem={0.5} />
<Button
prominence="tertiary"
variant="danger"
icon={SvgUserX}
onClick={() => openModal(Modal.DEACTIVATE)}
icon={SvgXCircle}
onClick={() => openModal("deactivate")}
>
Deactivate User
</Button>
@@ -176,19 +133,27 @@ export default function UserRowActions({
case UserStatus.INACTIVE:
return (
<>
{user.id && (
<Button
prominence="tertiary"
icon={SvgUsers}
onClick={() => openModal("editGroups")}
>
Groups
</Button>
)}
<Button
prominence="tertiary"
icon={SvgUserPlus}
onClick={() => openModal(Modal.ACTIVATE)}
icon={SvgCheck}
onClick={() => openModal("activate")}
>
Activate User
</Button>
<Separator paddingXRem={0.5} />
<Button
prominence="tertiary"
variant="danger"
icon={SvgUserX}
onClick={() => openModal(Modal.DELETE)}
icon={SvgTrash}
onClick={() => openModal("delete")}
>
Delete User
</Button>
@@ -202,39 +167,36 @@ export default function UserRowActions({
}
})();
// SCIM-managed users cannot be modified from the UI — changes would be
// overwritten on the next IdP sync.
if (user.is_scim_synced) {
return null;
}
return (
<>
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<Popover.Trigger asChild>
<Button prominence="tertiary" icon={SvgMoreHorizontal} />
</Popover.Trigger>
<Popover.Content align="end" width="sm">
<Section
gap={0.5}
height="auto"
alignItems="stretch"
justifyContent="start"
>
{actionButtons}
</Section>
<Popover.Content align="end">
<div className="flex flex-col gap-0.5 p-1">{actionButtons}</div>
</Popover.Content>
</Popover>
{modal === Modal.EDIT_GROUPS && user.id && (
<EditUserModal
{modal === "editGroups" && user.id && (
<EditGroupsModal
user={user as UserRow & { id: string }}
onClose={() => setModal(null)}
onMutate={onMutate}
/>
)}
{modal === Modal.CANCEL_INVITE && (
{modal === "cancelInvite" && (
<ConfirmationModalLayout
icon={(props) => (
<SvgUserX {...props} className="text-action-danger-05" />
)}
icon={SvgXCircle}
title="Cancel Invite"
onClose={isSubmitting ? undefined : () => setModal(null)}
onClose={() => setModal(null)}
submit={
<Disabled disabled={isSubmitting}>
<Button
@@ -246,7 +208,7 @@ export default function UserRowActions({
);
}}
>
Cancel Invite
Cancel
</Button>
</Disabled>
}
@@ -260,11 +222,9 @@ export default function UserRowActions({
</ConfirmationModalLayout>
)}
{modal === Modal.DEACTIVATE && (
{modal === "deactivate" && (
<ConfirmationModalLayout
icon={(props) => (
<SvgUserX {...props} className="text-action-danger-05" />
)}
icon={SvgXCircle}
title="Deactivate User"
onClose={isSubmitting ? undefined : () => setModal(null)}
submit={
@@ -294,9 +254,9 @@ export default function UserRowActions({
</ConfirmationModalLayout>
)}
{modal === Modal.ACTIVATE && (
{modal === "activate" && (
<ConfirmationModalLayout
icon={SvgUserPlus}
icon={SvgCheck}
title="Activate User"
onClose={isSubmitting ? undefined : () => setModal(null)}
submit={
@@ -323,11 +283,9 @@ export default function UserRowActions({
</ConfirmationModalLayout>
)}
{modal === Modal.DELETE && (
{modal === "delete" && (
<ConfirmationModalLayout
icon={(props) => (
<SvgUserX {...props} className="text-action-danger-05" />
)}
icon={SvgTrash}
title="Delete User"
onClose={isSubmitting ? undefined : () => setModal(null)}
submit={
@@ -355,80 +313,6 @@ export default function UserRowActions({
</Text>
</ConfirmationModalLayout>
)}
{modal === Modal.RESET_PASSWORD && (
<ConfirmationModalLayout
icon={SvgKey}
title={newPassword ? "Password Reset" : "Reset Password"}
onClose={
isSubmitting
? undefined
: () => {
setModal(null);
setNewPassword(null);
}
}
submit={
newPassword ? (
<Button
onClick={() => {
setModal(null);
setNewPassword(null);
}}
>
Done
</Button>
) : (
<Disabled disabled={isSubmitting}>
<Button
variant="danger"
onClick={async () => {
setIsSubmitting(true);
try {
const result = await resetPassword(user.email);
setNewPassword(result.new_password);
} catch (err) {
toast.error(
err instanceof Error
? err.message
: "Failed to reset password"
);
} finally {
setIsSubmitting(false);
}
}}
>
Reset Password
</Button>
</Disabled>
)
}
>
{newPassword ? (
<div className="flex flex-col gap-2">
<Text as="p" text03>
The password for{" "}
<Text as="span" text05>
{user.email}
</Text>{" "}
has been reset. Copy the new password below it will not be
shown again.
</Text>
<code className="rounded-sm bg-background-neutral-02 px-3 py-2 text-sm select-all">
{newPassword}
</code>
</div>
) : (
<Text as="p" text03>
This will generate a new random password for{" "}
<Text as="span" text05>
{user.email}
</Text>
. Their current password will stop working immediately.
</Text>
)}
</ConfirmationModalLayout>
)}
</>
);
}

View File

@@ -6,7 +6,7 @@ import Card from "@/refresh-components/cards/Card";
import IconButton from "@/refresh-components/buttons/IconButton";
import Text from "@/refresh-components/texts/Text";
import Link from "next/link";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { ADMIN_PATHS } from "@/lib/admin-routes";
// ---------------------------------------------------------------------------
// Stats cell — number + label + hover filter icon
@@ -67,7 +67,7 @@ function ScimCard() {
variant="section"
paddingVariant="fit"
rightChildren={
<Link href={ADMIN_ROUTES.SCIM.path}>
<Link href={ADMIN_PATHS.SCIM}>
<Button prominence="tertiary" rightIcon={SvgArrowUpRight} size="sm">
Manage
</Button>

View File

@@ -237,7 +237,6 @@ export default function UsersTable({
prominence="tertiary"
size="sm"
tooltip="Download CSV"
aria-label="Download CSV"
onClick={() => {
downloadUsersCsv().catch((err) => {
toast.error(

View File

@@ -127,20 +127,6 @@ export async function inviteUsers(emails: string[]): Promise<void> {
}
}
export async function resetPassword(
email: string
): Promise<{ user_id: string; new_password: string }> {
const res = await fetch("/api/password/reset_password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_email: email }),
});
if (!res.ok) {
throw new Error(await parseErrorDetail(res, "Failed to reset password"));
}
return res.json();
}
export async function downloadUsersCsv(): Promise<void> {
const res = await fetch("/api/manage/users/download");
if (!res.ok) {

View File

@@ -161,6 +161,7 @@ export default function PreviewModal({
<Modal.Content
width={variant.width}
height={variant.height}
deferScrollingToChildren
preventAccidentalClose={false}
onOpenAutoFocus={(e) => e.preventDefault()}
>
@@ -170,10 +171,7 @@ export default function PreviewModal({
onClose={onClose}
/>
{/* Body — uses flex-1/min-h-0/overflow-hidden (not Modal.Body)
so that child ScrollIndicatorDivs become the actual scroll
container instead of the body stealing it via overflow-y-auto. */}
<div className="flex flex-col flex-1 min-h-0 overflow-hidden w-full bg-background-tint-01">
<Modal.Body padding={0} gap={0}>
{isLoading ? (
<Section>
<SimpleLoader className="h-8 w-8" />
@@ -187,7 +185,7 @@ export default function PreviewModal({
) : (
variant.renderContent(ctx)
)}
</div>
</Modal.Body>
{/* Floating footer */}
{!isLoading && !loadError && (

View File

@@ -1,55 +1,90 @@
"use client";
import { useCallback } from "react";
import { usePathname } from "next/navigation";
import { useSettingsContext } from "@/providers/SettingsProvider";
import { CgArrowsExpandUpLeft } from "react-icons/cg";
import Text from "@/refresh-components/texts/Text";
import SidebarSection from "@/sections/sidebar/SidebarSection";
import SidebarWrapper from "@/sections/sidebar/SidebarWrapper";
import { useIsKGExposed } from "@/app/admin/kg/utils";
import { useCustomAnalyticsEnabled } from "@/lib/hooks/useCustomAnalyticsEnabled";
import { useUser } from "@/providers/UserProvider";
import { UserRole } from "@/lib/types";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { CombinedSettings } from "@/interfaces/settings";
import SidebarTab from "@/refresh-components/buttons/SidebarTab";
import SidebarBody from "@/sections/sidebar/SidebarBody";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import { Disabled } from "@opal/core";
import { SvgArrowUpCircle, SvgUserManage, SvgX } from "@opal/icons";
import {
useBillingInformation,
useLicense,
hasActiveSubscription,
} from "@/lib/billing";
import { Content } from "@opal/layouts";
import { ADMIN_ROUTES, sidebarItem } from "@/lib/admin-routes";
import useFilter from "@/hooks/useFilter";
import { IconFunctionComponent } from "@opal/types";
import { Section } from "@/layouts/general-layouts";
import Text from "@/refresh-components/texts/Text";
import { getUserDisplayName } from "@/lib/user";
import { APP_SLOGAN } from "@/lib/constants";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { CombinedSettings } from "@/interfaces/settings";
import SidebarTab from "@/refresh-components/buttons/SidebarTab";
import SidebarBody from "@/sections/sidebar/SidebarBody";
import {
SvgActions,
SvgActivity,
SvgArrowUpCircle,
SvgBarChart,
SvgCpu,
SvgFileText,
SvgFolder,
SvgGlobe,
SvgArrowExchange,
SvgImage,
SvgKey,
SvgOnyxLogo,
SvgOnyxOctagon,
SvgSearch,
SvgServer,
SvgSettings,
SvgShield,
SvgThumbsUp,
SvgUploadCloud,
SvgUser,
SvgUsers,
SvgZoomIn,
SvgPaintBrush,
SvgDiscordMono,
SvgWallet,
SvgAudio,
} from "@opal/icons";
import SvgMcp from "@opal/icons/mcp";
import { ADMIN_PATHS, sidebarItem } from "@/lib/admin-routes";
import UserAvatarPopover from "@/sections/sidebar/UserAvatarPopover";
const SECTIONS = {
UNLABELED: "",
AGENTS_AND_ACTIONS: "Agents & Actions",
DOCUMENTS_AND_KNOWLEDGE: "Documents & Knowledge",
INTEGRATIONS: "Integrations",
PERMISSIONS: "Permissions",
ORGANIZATION: "Organization",
USAGE: "Usage",
} as const;
const connectors_items = () => [
sidebarItem(ADMIN_PATHS.INDEXING_STATUS),
sidebarItem(ADMIN_PATHS.ADD_CONNECTOR),
];
interface SidebarItemEntry {
section: string;
name: string;
icon: IconFunctionComponent;
link: string;
error?: boolean;
disabled?: boolean;
}
const document_management_items = () => [
sidebarItem(ADMIN_PATHS.DOCUMENT_SETS),
sidebarItem(ADMIN_PATHS.DOCUMENT_EXPLORER),
sidebarItem(ADMIN_PATHS.DOCUMENT_FEEDBACK),
];
function buildItems(
const custom_agents_items = (isCurator: boolean, enableEnterprise: boolean) => {
const items = [sidebarItem(ADMIN_PATHS.AGENTS)];
if (!isCurator) {
items.push(
sidebarItem(ADMIN_PATHS.SLACK_BOTS),
sidebarItem(ADMIN_PATHS.DISCORD_BOTS)
);
}
items.push(
sidebarItem(ADMIN_PATHS.MCP_ACTIONS),
sidebarItem(ADMIN_PATHS.OPENAPI_ACTIONS)
);
if (enableEnterprise) {
items.push(sidebarItem(ADMIN_PATHS.STANDARD_ANSWERS));
}
return items;
};
const collections = (
isCurator: boolean,
enableCloud: boolean,
enableEnterprise: boolean,
@@ -57,260 +92,204 @@ function buildItems(
kgExposed: boolean,
customAnalyticsEnabled: boolean,
hasSubscription: boolean
): SidebarItemEntry[] {
) => {
const vectorDbEnabled = settings?.settings.vector_db_enabled !== false;
const items: SidebarItemEntry[] = [];
const add = (section: string, route: Parameters<typeof sidebarItem>[0]) => {
items.push({ ...sidebarItem(route), section });
};
const addDisabled = (
section: string,
route: Parameters<typeof sidebarItem>[0],
isDisabled: boolean
) => {
items.push({ ...sidebarItem(route), section, disabled: isDisabled });
};
// 1. No header — core configuration (admin only)
if (!isCurator) {
add(SECTIONS.UNLABELED, ADMIN_ROUTES.LLM_MODELS);
add(SECTIONS.UNLABELED, ADMIN_ROUTES.WEB_SEARCH);
add(SECTIONS.UNLABELED, ADMIN_ROUTES.IMAGE_GENERATION);
add(SECTIONS.UNLABELED, ADMIN_ROUTES.VOICE);
add(SECTIONS.UNLABELED, ADMIN_ROUTES.CODE_INTERPRETER);
add(SECTIONS.UNLABELED, ADMIN_ROUTES.CHAT_PREFERENCES);
if (vectorDbEnabled && kgExposed) {
add(SECTIONS.UNLABELED, ADMIN_ROUTES.KNOWLEDGE_GRAPH);
}
if (!enableCloud && customAnalyticsEnabled) {
addDisabled(
SECTIONS.UNLABELED,
ADMIN_ROUTES.CUSTOM_ANALYTICS,
!enableEnterprise
);
}
}
// 2. Agents & Actions
add(SECTIONS.AGENTS_AND_ACTIONS, ADMIN_ROUTES.AGENTS);
add(SECTIONS.AGENTS_AND_ACTIONS, ADMIN_ROUTES.MCP_ACTIONS);
add(SECTIONS.AGENTS_AND_ACTIONS, ADMIN_ROUTES.OPENAPI_ACTIONS);
// 3. Documents & Knowledge
if (vectorDbEnabled) {
add(SECTIONS.DOCUMENTS_AND_KNOWLEDGE, ADMIN_ROUTES.INDEXING_STATUS);
add(SECTIONS.DOCUMENTS_AND_KNOWLEDGE, ADMIN_ROUTES.ADD_CONNECTOR);
add(SECTIONS.DOCUMENTS_AND_KNOWLEDGE, ADMIN_ROUTES.DOCUMENT_SETS);
if (!isCurator && !enableCloud) {
items.push({
...sidebarItem(ADMIN_ROUTES.INDEX_SETTINGS),
section: SECTIONS.DOCUMENTS_AND_KNOWLEDGE,
error: settings?.settings.needs_reindexing,
});
}
if (!isCurator && settings?.settings.opensearch_indexing_enabled) {
add(SECTIONS.DOCUMENTS_AND_KNOWLEDGE, ADMIN_ROUTES.INDEX_MIGRATION);
}
}
// 4. Integrations (admin only)
if (!isCurator) {
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.API_KEYS);
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.SLACK_BOTS);
add(SECTIONS.INTEGRATIONS, ADMIN_ROUTES.DISCORD_BOTS);
}
// 5. Permissions
if (!isCurator) {
add(SECTIONS.PERMISSIONS, ADMIN_ROUTES.USERS);
addDisabled(SECTIONS.PERMISSIONS, ADMIN_ROUTES.GROUPS, !enableEnterprise);
addDisabled(SECTIONS.PERMISSIONS, ADMIN_ROUTES.SCIM, !enableEnterprise);
} else if (enableEnterprise) {
add(SECTIONS.PERMISSIONS, ADMIN_ROUTES.GROUPS);
}
// 6. Organization (admin only)
if (!isCurator) {
if (hasSubscription) {
add(SECTIONS.ORGANIZATION, ADMIN_ROUTES.BILLING);
} else {
items.push({
section: SECTIONS.ORGANIZATION,
name: "Upgrade Plan",
icon: SvgArrowUpCircle,
link: ADMIN_ROUTES.BILLING.path,
});
}
add(SECTIONS.ORGANIZATION, ADMIN_ROUTES.TOKEN_RATE_LIMITS);
addDisabled(SECTIONS.ORGANIZATION, ADMIN_ROUTES.THEME, !enableEnterprise);
}
// 7. Usage (admin only)
if (!isCurator) {
addDisabled(SECTIONS.USAGE, ADMIN_ROUTES.USAGE, !enableEnterprise);
if (settings?.settings.query_history_type !== "disabled") {
addDisabled(
SECTIONS.USAGE,
ADMIN_ROUTES.QUERY_HISTORY,
!enableEnterprise
);
}
}
return items;
}
/** Preserve section ordering while grouping consecutive items by section. */
function groupBySection(items: SidebarItemEntry[]) {
const groups: { section: string; items: SidebarItemEntry[] }[] = [];
for (const item of items) {
const last = groups[groups.length - 1];
if (last && last.section === item.section) {
last.items.push(item);
} else {
groups.push({ section: item.section, items: [item] });
}
}
return groups;
}
return [
...(vectorDbEnabled
? [
{
name: "Connectors",
items: connectors_items(),
},
]
: []),
...(vectorDbEnabled
? [
{
name: "Document Management",
items: document_management_items(),
},
]
: []),
{
name: "Custom Agents",
items: custom_agents_items(isCurator, enableEnterprise),
},
...(isCurator && enableEnterprise
? [
{
name: "User Management",
items: [sidebarItem(ADMIN_PATHS.GROUPS)],
},
]
: []),
...(!isCurator
? [
{
name: "Configuration",
items: [
sidebarItem(ADMIN_PATHS.CHAT_PREFERENCES),
sidebarItem(ADMIN_PATHS.LLM_MODELS),
sidebarItem(ADMIN_PATHS.WEB_SEARCH),
sidebarItem(ADMIN_PATHS.IMAGE_GENERATION),
{
name: "Voice",
icon: SvgAudio,
link: "/admin/configuration/voice",
},
sidebarItem(ADMIN_PATHS.CODE_INTERPRETER),
...(!enableCloud && vectorDbEnabled
? [
{
...sidebarItem(ADMIN_PATHS.SEARCH_SETTINGS),
error: settings?.settings.needs_reindexing,
},
]
: []),
sidebarItem(ADMIN_PATHS.DOCUMENT_PROCESSING),
...(kgExposed ? [sidebarItem(ADMIN_PATHS.KNOWLEDGE_GRAPH)] : []),
],
},
{
name: "User Management",
items: [
...(enableEnterprise ? [sidebarItem(ADMIN_PATHS.GROUPS)] : []),
sidebarItem(ADMIN_PATHS.API_KEYS),
sidebarItem(ADMIN_PATHS.TOKEN_RATE_LIMITS),
],
},
{
name: "Permissions",
items: [
sidebarItem(ADMIN_PATHS.USERS),
...(enableEnterprise ? [sidebarItem(ADMIN_PATHS.SCIM)] : []),
],
},
...(enableEnterprise
? [
{
name: "Performance",
items: [
sidebarItem(ADMIN_PATHS.USAGE),
...(settings?.settings.query_history_type !== "disabled"
? [sidebarItem(ADMIN_PATHS.QUERY_HISTORY)]
: []),
...(!enableCloud && customAnalyticsEnabled
? [sidebarItem(ADMIN_PATHS.CUSTOM_ANALYTICS)]
: []),
],
},
]
: []),
{
name: "Settings",
items: [
...(enableEnterprise ? [sidebarItem(ADMIN_PATHS.THEME)] : []),
// Always show billing/upgrade - community users need access to upgrade
{
...sidebarItem(ADMIN_PATHS.BILLING),
...(hasSubscription
? {}
: { name: "Upgrade Plan", icon: SvgArrowUpCircle }),
},
...(settings?.settings.opensearch_indexing_enabled
? [sidebarItem(ADMIN_PATHS.INDEX_MIGRATION)]
: []),
],
},
]
: []),
];
};
interface AdminSidebarProps {
// Cloud flag is passed from server component (Layout.tsx) since it's a build-time constant
enableCloudSS: boolean;
// Enterprise flag is also passed but we override it with runtime license check below
enableEnterpriseSS: boolean;
}
export default function AdminSidebar({ enableCloudSS }: AdminSidebarProps) {
export default function AdminSidebar({
enableCloudSS,
enableEnterpriseSS,
}: AdminSidebarProps) {
const { kgExposed } = useIsKGExposed();
const pathname = usePathname();
const { customAnalyticsEnabled } = useCustomAnalyticsEnabled();
const { user } = useUser();
const settings = useSettingsContext();
const { data: billingData } = useBillingInformation();
const { data: licenseData } = useLicense();
// Use runtime license check for enterprise features
// This checks settings.ee_features_enabled (set by backend based on license status)
// Falls back to build-time check if LICENSE_ENFORCEMENT_ENABLED=false
const enableEnterprise = usePaidEnterpriseFeaturesEnabled();
const { data: billingData, isLoading: billingLoading } =
useBillingInformation();
const { data: licenseData, isLoading: licenseLoading } = useLicense();
const isCurator =
user?.role === UserRole.CURATOR || user?.role === UserRole.GLOBAL_CURATOR;
// Default to true while loading to avoid flashing "Upgrade Plan"
const hasSubscriptionOrLicense =
billingLoading || licenseLoading
? true
: Boolean(
(billingData && hasActiveSubscription(billingData)) ||
licenseData?.has_license
);
const allItems = buildItems(
// Check if user has an active subscription or license for billing link text
// Show "Plans & Billing" if they have either (even if Stripe connection fails)
const hasSubscription = Boolean(
(billingData && hasActiveSubscription(billingData)) ||
licenseData?.has_license
);
const items = collections(
isCurator,
enableCloudSS,
enableEnterprise,
settings,
kgExposed,
customAnalyticsEnabled,
hasSubscriptionOrLicense
hasSubscription
);
const itemExtractor = useCallback((item: SidebarItemEntry) => item.name, []);
const { query, setQuery, filtered } = useFilter(allItems, itemExtractor);
const groups = groupBySection(filtered);
return (
<SidebarWrapper>
<SidebarBody
scrollKey="admin-sidebar"
actionButtons={
<div className="flex flex-col w-full">
<SidebarTab
icon={({ className }) => <SvgX className={className} size={16} />}
href="/app"
lowlight
>
Exit Admin Panel
</SidebarTab>
<InputTypeIn
variant="internal"
leftSearchIcon
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<SidebarTab
icon={({ className }) => (
<CgArrowsExpandUpLeft className={className} size={16} />
)}
href="/app"
>
Exit Admin
</SidebarTab>
}
footer={
<Section gap={0} height="fit" alignItems="start">
<div className="p-[0.38rem] w-full">
<Content
icon={SvgUserManage}
title={getUserDisplayName(user)}
sizePreset="main-ui"
variant="body"
prominence="muted"
widthVariant="full"
/>
</div>
<div className="flex flex-row gap-1 p-[0.38rem] w-full">
<Text text03 secondaryAction>
<a
className="underline"
href="https://onyx.app"
target="_blank"
>
Onyx
</a>
<div className="flex flex-col gap-2">
{settings.webVersion && (
<Text as="p" text02 secondaryBody className="px-2">
{`Onyx version: ${settings.webVersion}`}
</Text>
<Text text03 secondaryBody>
|
</Text>
{settings.webVersion ? (
<Text text03 secondaryBody>
{settings.webVersion}
</Text>
) : (
<Text text03 secondaryBody>
{APP_SLOGAN}
</Text>
)}
</div>
</Section>
)}
<UserAvatarPopover />
</div>
}
>
{groups.map((group, groupIndex) => {
const tabs = group.items.map(({ link, icon, name, disabled }) => (
<Disabled key={link} disabled={disabled}>
{/*
# NOTE (@raunakab)
We intentionally add a `div` intermediary here.
Without it, the disabled styling that is default provided by the `Disabled` component (which we want here) would be overridden by the custom disabled styling provided by the `SidebarTab`.
Therefore, in order to avoid that overriding, we add a layer of indirection.
*/}
<div>
{items.map((collection, index) => (
<SidebarSection key={index} title={collection.name}>
<div className="flex flex-col w-full">
{collection.items.map(({ link, icon: Icon, name }, index) => (
<SidebarTab
lowlight={disabled}
icon={icon}
href={disabled ? undefined : link}
selected={!disabled && pathname.startsWith(link)}
key={index}
href={link}
selected={pathname.startsWith(link)}
icon={({ className }) => (
<Icon className={className} size={16} />
)}
>
{name}
</SidebarTab>
</div>
</Disabled>
));
if (!group.section) {
return <div key={groupIndex}>{tabs}</div>;
}
return (
<SidebarSection key={groupIndex} title={group.section}>
{tabs}
</SidebarSection>
);
})}
))}
</div>
</SidebarSection>
))}
</SidebarBody>
</SidebarWrapper>
);

View File

@@ -9,7 +9,7 @@ export default function EmbeddingSidebar() {
return (
<StepSidebar
buttonName="Index Settings"
buttonName="Search Settings"
buttonIcon={SvgSettings}
buttonHref="/admin/configuration/search"
>

View File

@@ -1,11 +1,11 @@
"use client";
import { useState } from "react";
import { LOGOUT_DISABLED } from "@/lib/constants";
import { ANONYMOUS_USER_NAME, LOGOUT_DISABLED } from "@/lib/constants";
import { Notification } from "@/interfaces/settings";
import useSWR, { preload } from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { checkUserIsNoAuthUser, getUserDisplayName, logout } from "@/lib/user";
import { checkUserIsNoAuthUser, logout } from "@/lib/user";
import { useUser } from "@/providers/UserProvider";
import InputAvatar from "@/refresh-components/inputs/InputAvatar";
import Text from "@/refresh-components/texts/Text";
@@ -27,6 +27,20 @@ import { toast } from "@/hooks/useToast";
import useAppFocus from "@/hooks/useAppFocus";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
function getDisplayName(email?: string, personalName?: string): string {
// Prioritize custom personal name if set
if (personalName && personalName.trim()) {
return personalName.trim();
}
// Fallback to email-derived username
if (!email) return ANONYMOUS_USER_NAME;
const atIndex = email.indexOf("@");
if (atIndex <= 0) return ANONYMOUS_USER_NAME;
return email.substring(0, atIndex);
}
interface SettingsPopoverProps {
onUserSettingsClick: () => void;
onOpenNotifications: () => void;
@@ -161,7 +175,7 @@ export default function UserAvatarPopover({
errorHandlingFetcher
);
const userDisplayName = getUserDisplayName(user);
const displayName = getDisplayName(user?.email, user?.personalization?.name);
const undismissedCount =
notifications?.filter((n) => !n.dismissed).length ?? 0;
const hasNotifications = undismissedCount > 0;
@@ -195,7 +209,7 @@ export default function UserAvatarPopover({
)}
>
<Text as="p" inverted secondaryBody>
{userDisplayName[0]?.toUpperCase()}
{displayName[0]?.toUpperCase()}
</Text>
</InputAvatar>
)}
@@ -217,7 +231,7 @@ export default function UserAvatarPopover({
// Specifying a dummy `onClick` handler solves that.
onClick={() => undefined}
>
{userDisplayName}
{displayName}
</SidebarTab>
</div>
</Popover.Trigger>

View File

@@ -52,9 +52,9 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
},
},
{
name: "Integrations - Slack Integration",
name: "Custom Agents - Slack Bots",
path: "bots",
pageTitle: "Slack Integration",
pageTitle: "Slack Bots",
options: {
paragraphText:
"Setup Slack bots that connect to Onyx. Once setup, you will be able to ask questions to Onyx directly from Slack. Additionally, you can:",
@@ -96,9 +96,9 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
pageTitle: "Appearance & Theming",
},
{
name: "Documents & Knowledge - Index Settings",
name: "Configuration - Search Settings",
path: "configuration/search",
pageTitle: "Index Settings",
pageTitle: "Search Settings",
},
{
name: "Custom Agents - MCP Actions",
@@ -111,9 +111,9 @@ const ADMIN_PAGES: AdminPageSnapshot[] = [
pageTitle: "OpenAPI Actions",
},
{
name: "Organization - Spending Limits",
name: "User Management - Token Rate Limits",
path: "token-rate-limits",
pageTitle: "Spending Limits",
pageTitle: "Token Rate Limits",
options: {
paragraphText:
"Token rate limits enable you control how many tokens can be spent in a given time period. With token rate limits, you can:",

View File

@@ -28,7 +28,7 @@ test.describe("Admin Workflow E2E Flows", () => {
await expect(
adminPage
.locator('[aria-label="admin-page-title"]')
.getByText("Discord Integration")
.getByText("Discord Bots")
).toBeVisible();
await expect(
adminPage.locator("text=Server Configurations").first()

View File

@@ -26,7 +26,7 @@ test.describe("Bot Configuration Page", () => {
await expect(
adminPage
.locator('[aria-label="admin-page-title"]')
.getByText("Discord Integration")
.getByText("Discord Bots")
).toBeVisible();
});

View File

@@ -304,9 +304,7 @@ export async function gotoDiscordBotPage(adminPage: Page): Promise<void> {
await adminPage.goto("/admin/discord-bot");
await adminPage.waitForLoadState("networkidle");
// Wait for the page title
await adminPage.waitForSelector("text=Discord Integration", {
timeout: 15000,
});
await adminPage.waitForSelector("text=Discord Bots", { timeout: 15000 });
}
export async function gotoGuildDetailPage(

Some files were not shown because too many files have changed in this diff Show More