Compare commits

..

3 Commits

Author SHA1 Message Date
Jamison Lahman
0f46e1e084 nit 2026-03-14 19:38:43 -07:00
Jamison Lahman
f4d379ceed fix(voice): plumb fatal errors to the frontend 2026-03-14 19:26:41 -07:00
Jamison Lahman
8f1076e69d chore(voice): support non-default FE ports for IS_DEV 2026-03-14 19:02:35 -07:00
122 changed files with 855 additions and 2259 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

@@ -455,7 +455,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -529,7 +529,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -607,7 +607,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -668,7 +668,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -750,7 +750,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -836,7 +836,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -894,7 +894,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -967,7 +967,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -1044,7 +1044,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -1105,7 +1105,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |
@@ -1178,7 +1178,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |
@@ -1256,7 +1256,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |
@@ -1317,7 +1317,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -1397,7 +1397,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |
@@ -1480,7 +1480,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ needs.determine-builds.outputs.is-test-run == 'true' && env.RUNS_ON_ECR_CACHE || env.REGISTRY_IMAGE }}
flavor: |

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

@@ -105,7 +105,7 @@ jobs:
- name: Upload build artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: desktop-build-${{ matrix.platform }}-${{ github.run_id }}
path: |

View File

@@ -174,7 +174,7 @@ jobs:
- name: Upload Docker logs
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-logs-${{ matrix.test-dir }}
path: docker-logs/

View File

@@ -25,7 +25,7 @@ jobs:
outputs:
modules: ${{ steps.set-modules.outputs.modules }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
persist-credentials: false
- id: set-modules
@@ -39,7 +39,7 @@ jobs:
matrix:
modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # zizmor: ignore[cache-poisoning]

View File

@@ -466,7 +466,7 @@ jobs:
- name: Upload logs
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-all-logs-${{ matrix.edition }}-${{ matrix.test-dir.name }}
path: ${{ github.workspace }}/docker-compose.log
@@ -587,7 +587,7 @@ jobs:
- name: Upload logs (onyx-lite)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-all-logs-onyx-lite
path: ${{ github.workspace }}/docker-compose-onyx-lite.log
@@ -725,7 +725,7 @@ jobs:
- name: Upload logs (multi-tenant)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-all-logs-multitenant
path: ${{ github.workspace }}/docker-compose-multitenant.log

View File

@@ -44,7 +44,7 @@ jobs:
- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: jest-coverage-${{ github.run_id }}
path: ./web/coverage

View File

@@ -445,7 +445,7 @@ jobs:
run: |
npx playwright test --project ${PROJECT}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always()
with:
# Includes test results and trace.zip files
@@ -454,7 +454,7 @@ jobs:
retention-days: 30
- name: Upload screenshots
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always()
with:
name: playwright-screenshots-${{ matrix.project }}-${{ github.run_id }}
@@ -534,7 +534,7 @@ jobs:
"s3://${PLAYWRIGHT_S3_BUCKET}/reports/pr-${PR_NUMBER}/${RUN_ID}/${PROJECT}/"
- name: Upload visual diff summary
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always()
with:
name: screenshot-diff-summary-${{ matrix.project }}
@@ -543,7 +543,7 @@ jobs:
retention-days: 5
- name: Upload visual diff report artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always()
with:
name: screenshot-diff-report-${{ matrix.project }}-${{ github.run_id }}
@@ -590,7 +590,7 @@ jobs:
- name: Upload logs
if: success() || failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-logs-${{ matrix.project }}-${{ github.run_id }}
path: ${{ github.workspace }}/docker-compose.log
@@ -674,7 +674,7 @@ jobs:
working-directory: ./web
run: npx playwright test --project lite
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always()
with:
name: playwright-test-results-lite-${{ github.run_id }}
@@ -692,7 +692,7 @@ jobs:
- name: Upload logs
if: success() || failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-logs-lite-${{ github.run_id }}
path: ${{ github.workspace }}/docker-compose.log

View File

@@ -122,7 +122,7 @@ jobs:
- name: Upload logs
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-all-logs
path: ${{ github.workspace }}/docker-compose.log

View File

@@ -319,7 +319,7 @@ jobs:
- name: Upload logs
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-all-logs-nightly-${{ matrix.provider }}-llm-provider
path: |

View File

@@ -125,7 +125,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |
@@ -195,7 +195,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |
@@ -268,7 +268,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # ratchet:docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |

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

@@ -29,6 +29,7 @@ from onyx.configs.app_configs import OPENAI_DEFAULT_API_KEY
from onyx.configs.app_configs import OPENROUTER_DEFAULT_API_KEY
from onyx.configs.app_configs import VERTEXAI_DEFAULT_CREDENTIALS
from onyx.configs.app_configs import VERTEXAI_DEFAULT_LOCATION
from onyx.configs.constants import MilestoneRecordType
from onyx.db.engine.sql_engine import get_session_with_shared_schema
from onyx.db.engine.sql_engine import get_session_with_tenant
from onyx.db.image_generation import create_default_image_gen_config_from_api_key
@@ -58,6 +59,7 @@ from onyx.server.manage.llm.models import LLMProviderUpsertRequest
from onyx.server.manage.llm.models import ModelConfigurationUpsertRequest
from onyx.setup import setup_onyx
from onyx.utils.logger import setup_logger
from onyx.utils.telemetry import mt_cloud_telemetry
from shared_configs.configs import MULTI_TENANT
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
from shared_configs.configs import TENANT_ID_PREFIX
@@ -69,9 +71,7 @@ logger = setup_logger()
async def get_or_provision_tenant(
email: str,
referral_source: str | None = None,
request: Request | None = None,
email: str, referral_source: str | None = None, request: Request | None = None
) -> str:
"""
Get existing tenant ID for an email or create a new tenant if none exists.
@@ -693,6 +693,12 @@ async def assign_tenant_to_user(
try:
add_users_to_tenant([email], tenant_id)
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=email,
event=MilestoneRecordType.TENANT_CREATED,
)
except Exception:
logger.exception(f"Failed to assign tenant {tenant_id} to user {email}")
raise Exception("Failed to assign tenant to user")

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

@@ -812,17 +812,10 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
distinct_id=user.email,
event=MilestoneRecordType.USER_SIGNED_UP,
)
if user_count == 1:
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
event=MilestoneRecordType.TENANT_CREATED,
)
finally:
CURRENT_TENANT_ID_CONTEXTVAR.reset(token)

View File

@@ -490,13 +490,13 @@ def handle_stream_message_objects(
# Milestone tracking, most devs using the API don't need to understand this
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id) if not user.is_anonymous else tenant_id,
distinct_id=user.email if not user.is_anonymous else tenant_id,
event=MilestoneRecordType.MULTIPLE_ASSISTANTS,
)
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id) if not user.is_anonymous else tenant_id,
distinct_id=user.email if not user.is_anonymous else tenant_id,
event=MilestoneRecordType.USER_MESSAGE_SENT,
properties={
"origin": new_msg_req.origin.value,

View File

@@ -1046,8 +1046,6 @@ POD_NAMESPACE = os.environ.get("POD_NAMESPACE")
DEV_MODE = os.environ.get("DEV_MODE", "").lower() == "true"
HOOK_ENABLED = os.environ.get("HOOK_ENABLED", "").lower() == "true"
INTEGRATION_TESTS_MODE = os.environ.get("INTEGRATION_TESTS_MODE", "").lower() == "true"
#####

View File

@@ -1,192 +0,0 @@
from __future__ import annotations
import logging
import re
from typing import Any
from urllib.parse import urlparse
from onyx.connectors.cross_connector_utils.rate_limit_wrapper import (
rl_requests,
)
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
logger = logging.getLogger(__name__)
# Requests timeout in seconds.
_CANVAS_CALL_TIMEOUT: int = 30
_CANVAS_API_VERSION: str = "/api/v1"
# Matches the "next" URL in a Canvas Link header, e.g.:
# <https://canvas.example.com/api/v1/courses?page=2>; rel="next"
# Captures the URL inside the angle brackets.
_NEXT_LINK_PATTERN: re.Pattern[str] = re.compile(r'<([^>]+)>;\s*rel="next"')
_STATUS_TO_ERROR_CODE: dict[int, OnyxErrorCode] = {
401: OnyxErrorCode.CREDENTIAL_EXPIRED,
403: OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
404: OnyxErrorCode.BAD_GATEWAY,
429: OnyxErrorCode.RATE_LIMITED,
}
def _error_code_for_status(status_code: int) -> OnyxErrorCode:
"""Map an HTTP status code to the appropriate OnyxErrorCode.
Expects a >= 400 status code. Known codes (401, 403, 404, 429) are
mapped to specific error codes; all other codes (unrecognised 4xx
and 5xx) map to BAD_GATEWAY as unexpected upstream errors.
"""
if status_code in _STATUS_TO_ERROR_CODE:
return _STATUS_TO_ERROR_CODE[status_code]
return OnyxErrorCode.BAD_GATEWAY
class CanvasApiClient:
def __init__(
self,
bearer_token: str,
canvas_base_url: str,
) -> None:
parsed_base = urlparse(canvas_base_url)
if not parsed_base.hostname:
raise ValueError("canvas_base_url must include a valid host")
if parsed_base.scheme != "https":
raise ValueError("canvas_base_url must use https")
self._bearer_token = bearer_token
self.base_url = (
canvas_base_url.rstrip("/").removesuffix(_CANVAS_API_VERSION)
+ _CANVAS_API_VERSION
)
# Hostname is already validated above; reuse parsed_base instead
# of re-parsing. Used by _parse_next_link to validate pagination URLs.
self._expected_host: str = parsed_base.hostname
def get(
self,
endpoint: str = "",
params: dict[str, Any] | None = None,
full_url: str | None = None,
) -> tuple[Any, str | None]:
"""Make a GET request to the Canvas API.
Returns a tuple of (json_body, next_url).
next_url is parsed from the Link header and is None if there are no more pages.
If full_url is provided, it is used directly (for following pagination links).
Security note: full_url must only be set to values returned by
``_parse_next_link``, which validates the host against the configured
Canvas base URL. Passing an arbitrary URL would leak the bearer token.
"""
# full_url is used when following pagination (Canvas returns the
# next-page URL in the Link header). For the first request we build
# the URL from the endpoint name instead.
url = full_url if full_url else self._build_url(endpoint)
headers = self._build_headers()
response = rl_requests.get(
url,
headers=headers,
params=params if not full_url else None,
timeout=_CANVAS_CALL_TIMEOUT,
)
try:
response_json = response.json()
except ValueError as e:
if response.status_code < 300:
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
detail=f"Invalid JSON in Canvas response: {e}",
)
logger.warning(
"Failed to parse JSON from Canvas error response (status=%d): %s",
response.status_code,
e,
)
response_json = {}
if response.status_code >= 400:
# Try to extract the most specific error message from the
# Canvas response body. Canvas uses three different shapes
# depending on the endpoint and error type:
default_error: str = response.reason or f"HTTP {response.status_code}"
error = default_error
if isinstance(response_json, dict):
# Shape 1: {"error": {"message": "Not authorized"}}
error_field = response_json.get("error")
if isinstance(error_field, dict):
response_error = error_field.get("message", "")
if response_error:
error = response_error
# Shape 2: {"error": "Invalid access token"}
elif isinstance(error_field, str):
error = error_field
# Shape 3: {"errors": [{"message": "..."}]}
# Used for validation errors. Only use as fallback if
# we didn't already find a more specific message above.
if error == default_error:
errors_list = response_json.get("errors")
if isinstance(errors_list, list) and errors_list:
first_error = errors_list[0]
if isinstance(first_error, dict):
msg = first_error.get("message", "")
if msg:
error = msg
raise OnyxError(
_error_code_for_status(response.status_code),
detail=error,
status_code_override=response.status_code,
)
next_url = self._parse_next_link(response.headers.get("Link", ""))
return response_json, next_url
def _parse_next_link(self, link_header: str) -> str | None:
"""Extract the 'next' URL from a Canvas Link header.
Only returns URLs whose host matches the configured Canvas base URL
to prevent leaking the bearer token to arbitrary hosts.
"""
expected_host = self._expected_host
for match in _NEXT_LINK_PATTERN.finditer(link_header):
url = match.group(1)
parsed_url = urlparse(url)
if parsed_url.hostname != expected_host:
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
detail=(
"Canvas pagination returned an unexpected host "
f"({parsed_url.hostname}); expected {expected_host}"
),
)
if parsed_url.scheme != "https":
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
detail=(
"Canvas pagination link must use https, "
f"got {parsed_url.scheme!r}"
),
)
return url
return None
def _build_headers(self) -> dict[str, str]:
"""Return the Authorization header with the bearer token."""
return {"Authorization": f"Bearer {self._bearer_token}"}
def _build_url(self, endpoint: str) -> str:
"""Build a full Canvas API URL from an endpoint path.
Assumes endpoint is non-empty (e.g. ``"courses"``, ``"announcements"``).
Only called on a first request, endpoint must be set for first request.
Verify endpoint exists in case of future changes where endpoint might be optional.
Leading slashes are stripped to avoid double-slash in the result.
self.base_url is already normalized with no trailing slash.
"""
final_url = self.base_url
clean_endpoint = endpoint.lstrip("/")
if clean_endpoint:
final_url += "/" + clean_endpoint
return final_url

View File

@@ -1,74 +0,0 @@
from typing import Literal
from typing import TypeAlias
from pydantic import BaseModel
from onyx.connectors.models import ConnectorCheckpoint
class CanvasCourse(BaseModel):
id: int
name: str
course_code: str
created_at: str
workflow_state: str
class CanvasPage(BaseModel):
page_id: int
url: str
title: str
body: str | None = None
created_at: str
updated_at: str
course_id: int
class CanvasAssignment(BaseModel):
id: int
name: str
description: str | None = None
html_url: str
course_id: int
created_at: str
updated_at: str
due_at: str | None = None
class CanvasAnnouncement(BaseModel):
id: int
title: str
message: str | None = None
html_url: str
posted_at: str | None = None
course_id: int
CanvasStage: TypeAlias = Literal["pages", "assignments", "announcements"]
class CanvasConnectorCheckpoint(ConnectorCheckpoint):
"""Checkpoint state for resumable Canvas indexing.
Fields:
course_ids: Materialized list of course IDs to process.
current_course_index: Index into course_ids for current course.
stage: Which item type we're processing for the current course.
next_url: Pagination cursor within the current stage. None means
start from the first page; a URL means resume from that page.
Invariant:
If current_course_index is incremented, stage must be reset to
"pages" and next_url must be reset to None.
"""
course_ids: list[int] = []
current_course_index: int = 0
stage: CanvasStage = "pages"
next_url: str | None = None
def advance_course(self) -> None:
"""Move to the next course and reset within-course state."""
self.current_course_index += 1
self.stage = "pages"
self.next_url = None

View File

@@ -35,8 +35,6 @@ class OnyxErrorCode(Enum):
INSUFFICIENT_PERMISSIONS = ("INSUFFICIENT_PERMISSIONS", 403)
ADMIN_ONLY = ("ADMIN_ONLY", 403)
EE_REQUIRED = ("EE_REQUIRED", 403)
SINGLE_TENANT_ONLY = ("SINGLE_TENANT_ONLY", 403)
ENV_VAR_GATED = ("ENV_VAR_GATED", 403)
# ------------------------------------------------------------------
# Validation / Bad Request (400)

View File

@@ -1,26 +0,0 @@
from onyx.configs.app_configs import HOOK_ENABLED
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from shared_configs.configs import MULTI_TENANT
def require_hook_enabled() -> None:
"""FastAPI dependency that gates all hook management endpoints.
Hooks are only available in single-tenant / self-hosted deployments with
HOOK_ENABLED=true explicitly set. Two layers of protection:
1. MULTI_TENANT check — rejects even if HOOK_ENABLED is accidentally set true
2. HOOK_ENABLED flag — explicit opt-in by the operator
Use as: Depends(require_hook_enabled)
"""
if MULTI_TENANT:
raise OnyxError(
OnyxErrorCode.SINGLE_TENANT_ONLY,
"Hooks are not available in multi-tenant deployments",
)
if not HOOK_ENABLED:
raise OnyxError(
OnyxErrorCode.ENV_VAR_GATED,
"Hooks are not enabled. Set HOOK_ENABLED=true to enable.",
)

View File

@@ -1319,7 +1319,7 @@ def get_connector_indexing_status(
# Track admin page visit for analytics
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
distinct_id=user.email,
event=MilestoneRecordType.VISITED_ADMIN_PAGE,
)
@@ -1533,7 +1533,7 @@ def create_connector_from_model(
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
distinct_id=user.email,
event=MilestoneRecordType.CREATED_CONNECTOR,
)
@@ -1611,7 +1611,7 @@ def create_connector_with_mock_credential(
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
distinct_id=user.email,
event=MilestoneRecordType.CREATED_CONNECTOR,
)
return response
@@ -1915,7 +1915,9 @@ def submit_connector_request(
if not connector_name:
raise HTTPException(status_code=400, detail="Connector name cannot be empty")
# Get user identifier for telemetry
user_email = user.email
distinct_id = user_email or tenant_id
# Track connector request via PostHog telemetry (Cloud only)
from shared_configs.configs import MULTI_TENANT
@@ -1923,11 +1925,11 @@ def submit_connector_request(
if MULTI_TENANT:
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
distinct_id=distinct_id,
event=MilestoneRecordType.REQUESTED_CONNECTOR,
properties={
"connector_name": connector_name,
"user_email": user.email,
"user_email": user_email,
},
)

View File

@@ -314,7 +314,7 @@ def create_persona(
)
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=str(user.id),
distinct_id=user.email,
event=MilestoneRecordType.CREATED_ASSISTANT,
)

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

@@ -118,6 +118,12 @@ async def handle_streaming_transcription(
if result is None: # End of stream
logger.info("Streaming transcription: transcript stream ended")
break
if result.error:
logger.warning(
f"Streaming transcription: provider error: {result.error}"
)
await websocket.send_json({"type": "error", "message": result.error})
continue
# Send if text changed OR if VAD detected end of speech (for auto-send trigger)
if result.text and (result.text != last_transcript or result.is_vad_end):
last_transcript = result.text

View File

@@ -561,7 +561,7 @@ def handle_send_chat_message(
tenant_id = get_current_tenant_id()
mt_cloud_telemetry(
tenant_id=tenant_id,
distinct_id=tenant_id if user.is_anonymous else str(user.id),
distinct_id=tenant_id if user.is_anonymous else user.email,
event=MilestoneRecordType.RAN_QUERY,
)

View File

@@ -15,6 +15,9 @@ class TranscriptResult(BaseModel):
is_vad_end: bool = False
"""True if VAD detected end of speech (silence). Use for auto-send."""
error: str | None = None
"""Provider error message to forward to the client, if any."""
class StreamingTranscriberProtocol(Protocol):
"""Protocol for streaming transcription sessions."""

View File

@@ -56,6 +56,17 @@ def _http_to_ws_url(http_url: str) -> str:
return http_url
_USER_FACING_ERROR_MESSAGES: dict[str, str] = {
"input_audio_buffer_commit_empty": (
"No audio was recorded. Please check your microphone and try again."
),
"invalid_api_key": "Voice service authentication failed. Please contact support.",
"rate_limit_exceeded": "Voice service is temporarily busy. Please try again shortly.",
}
_DEFAULT_USER_ERROR = "A voice transcription error occurred. Please try again."
class OpenAIStreamingTranscriber(StreamingTranscriberProtocol):
"""Streaming transcription using OpenAI Realtime API."""
@@ -142,6 +153,17 @@ class OpenAIStreamingTranscriber(StreamingTranscriberProtocol):
if msg_type == OpenAIRealtimeMessageType.ERROR:
error = data.get("error", {})
self._logger.error(f"OpenAI error: {error}")
error_code = error.get("code", "")
user_message = _USER_FACING_ERROR_MESSAGES.get(
error_code, _DEFAULT_USER_ERROR
)
await self._transcript_queue.put(
TranscriptResult(
text="",
is_vad_end=False,
error=user_message,
)
)
continue
# Handle VAD events

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

@@ -45,21 +45,6 @@ npx playwright test <TEST_NAME>
Shared fixtures live in `backend/tests/conftest.py`. Test subdirectories can define
their own `conftest.py` for directory-scoped fixtures.
## Running Tests Repeatedly (`pytest-repeat`)
Use `pytest-repeat` to catch flaky tests by running them multiple times:
```bash
# Run a specific test 50 times
pytest --count=50 backend/tests/unit/path/to/test.py::test_name
# Stop on first failure with -x
pytest --count=50 -x backend/tests/unit/path/to/test.py::test_name
# Repeat an entire test file
pytest --count=10 backend/tests/unit/path/to/test_file.py
```
## Best Practices
### Use `enable_ee` fixture instead of inlining

View File

@@ -1,381 +0,0 @@
"""Tests for Canvas connector — client (PR1)."""
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import patch
import pytest
from onyx.connectors.canvas.client import CanvasApiClient
from onyx.error_handling.exceptions import OnyxError
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
FAKE_BASE_URL = "https://myschool.instructure.com"
FAKE_TOKEN = "fake-canvas-token"
def _mock_response(
status_code: int = 200,
json_data: Any = None,
link_header: str = "",
) -> MagicMock:
"""Create a mock HTTP response with status, json, and Link header."""
resp = MagicMock()
resp.status_code = status_code
resp.reason = "OK" if status_code < 300 else "Error"
resp.json.return_value = json_data if json_data is not None else []
resp.headers = {"Link": link_header}
return resp
# ---------------------------------------------------------------------------
# CanvasApiClient.__init__ tests
# ---------------------------------------------------------------------------
class TestCanvasApiClientInit:
def test_success(self) -> None:
client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url=FAKE_BASE_URL,
)
expected_base_url = f"{FAKE_BASE_URL}/api/v1"
expected_host = "myschool.instructure.com"
assert client.base_url == expected_base_url
assert client._expected_host == expected_host
def test_normalizes_trailing_slash(self) -> None:
client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url=f"{FAKE_BASE_URL}/",
)
expected_base_url = f"{FAKE_BASE_URL}/api/v1"
assert client.base_url == expected_base_url
def test_normalizes_existing_api_v1(self) -> None:
client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url=f"{FAKE_BASE_URL}/api/v1",
)
expected_base_url = f"{FAKE_BASE_URL}/api/v1"
assert client.base_url == expected_base_url
def test_rejects_non_https_scheme(self) -> None:
with pytest.raises(ValueError, match="must use https"):
CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url="ftp://myschool.instructure.com",
)
def test_rejects_http(self) -> None:
with pytest.raises(ValueError, match="must use https"):
CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url="http://myschool.instructure.com",
)
def test_rejects_missing_host(self) -> None:
with pytest.raises(ValueError, match="must include a valid host"):
CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url="https://",
)
# ---------------------------------------------------------------------------
# CanvasApiClient._build_url tests
# ---------------------------------------------------------------------------
class TestBuildUrl:
def setup_method(self) -> None:
self.client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url=FAKE_BASE_URL,
)
def test_appends_endpoint(self) -> None:
result = self.client._build_url("courses")
expected = f"{FAKE_BASE_URL}/api/v1/courses"
assert result == expected
def test_strips_leading_slash_from_endpoint(self) -> None:
result = self.client._build_url("/courses")
expected = f"{FAKE_BASE_URL}/api/v1/courses"
assert result == expected
# ---------------------------------------------------------------------------
# CanvasApiClient._build_headers tests
# ---------------------------------------------------------------------------
class TestBuildHeaders:
def setup_method(self) -> None:
self.client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url=FAKE_BASE_URL,
)
def test_returns_bearer_auth(self) -> None:
result = self.client._build_headers()
expected = {"Authorization": f"Bearer {FAKE_TOKEN}"}
assert result == expected
# ---------------------------------------------------------------------------
# CanvasApiClient.get tests
# ---------------------------------------------------------------------------
class TestGet:
def setup_method(self) -> None:
self.client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url=FAKE_BASE_URL,
)
@patch("onyx.connectors.canvas.client.rl_requests")
def test_success_returns_json_and_next_url(self, mock_requests: MagicMock) -> None:
next_link = f"<{FAKE_BASE_URL}/api/v1/courses?page=2>; " 'rel="next"'
mock_requests.get.return_value = _mock_response(
json_data=[{"id": 1}], link_header=next_link
)
data, next_url = self.client.get("courses")
expected_data = [{"id": 1}]
expected_next = f"{FAKE_BASE_URL}/api/v1/courses?page=2"
assert data == expected_data
assert next_url == expected_next
@patch("onyx.connectors.canvas.client.rl_requests")
def test_success_no_next_page(self, mock_requests: MagicMock) -> None:
mock_requests.get.return_value = _mock_response(json_data=[{"id": 1}])
data, next_url = self.client.get("courses")
assert data == [{"id": 1}]
assert next_url is None
@patch("onyx.connectors.canvas.client.rl_requests")
def test_raises_on_error_status(self, mock_requests: MagicMock) -> None:
mock_requests.get.return_value = _mock_response(403, {})
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
assert exc_info.value.status_code == 403
@patch("onyx.connectors.canvas.client.rl_requests")
def test_raises_on_404(self, mock_requests: MagicMock) -> None:
mock_requests.get.return_value = _mock_response(404, {})
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
assert exc_info.value.status_code == 404
@patch("onyx.connectors.canvas.client.rl_requests")
def test_raises_on_429(self, mock_requests: MagicMock) -> None:
mock_requests.get.return_value = _mock_response(429, {})
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
assert exc_info.value.status_code == 429
@patch("onyx.connectors.canvas.client.rl_requests")
def test_skips_params_when_using_full_url(self, mock_requests: MagicMock) -> None:
mock_requests.get.return_value = _mock_response(json_data=[])
full = f"{FAKE_BASE_URL}/api/v1/courses?page=2"
self.client.get(params={"per_page": "100"}, full_url=full)
_, kwargs = mock_requests.get.call_args
assert kwargs["params"] is None
@patch("onyx.connectors.canvas.client.rl_requests")
def test_error_extracts_message_from_error_dict(
self, mock_requests: MagicMock
) -> None:
"""Shape 1: {"error": {"message": "Not authorized"}}"""
mock_requests.get.return_value = _mock_response(
403, {"error": {"message": "Not authorized"}}
)
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
result = exc_info.value.detail
expected = "Not authorized"
assert result == expected
@patch("onyx.connectors.canvas.client.rl_requests")
def test_error_extracts_message_from_error_string(
self, mock_requests: MagicMock
) -> None:
"""Shape 2: {"error": "Invalid access token"}"""
mock_requests.get.return_value = _mock_response(
401, {"error": "Invalid access token"}
)
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
result = exc_info.value.detail
expected = "Invalid access token"
assert result == expected
@patch("onyx.connectors.canvas.client.rl_requests")
def test_error_extracts_message_from_errors_list(
self, mock_requests: MagicMock
) -> None:
"""Shape 3: {"errors": [{"message": "Invalid query"}]}"""
mock_requests.get.return_value = _mock_response(
400, {"errors": [{"message": "Invalid query"}]}
)
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
result = exc_info.value.detail
expected = "Invalid query"
assert result == expected
@patch("onyx.connectors.canvas.client.rl_requests")
def test_error_dict_takes_priority_over_errors_list(
self, mock_requests: MagicMock
) -> None:
"""When both error shapes are present, error dict wins."""
mock_requests.get.return_value = _mock_response(
403, {"error": "Specific error", "errors": [{"message": "Generic"}]}
)
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
result = exc_info.value.detail
expected = "Specific error"
assert result == expected
@patch("onyx.connectors.canvas.client.rl_requests")
def test_error_falls_back_to_reason_when_no_json_message(
self, mock_requests: MagicMock
) -> None:
"""Empty error body falls back to response.reason."""
mock_requests.get.return_value = _mock_response(500, {})
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
result = exc_info.value.detail
expected = "Error" # from _mock_response's reason for >= 300
assert result == expected
@patch("onyx.connectors.canvas.client.rl_requests")
def test_invalid_json_on_success_raises(self, mock_requests: MagicMock) -> None:
"""Invalid JSON on a 2xx response raises OnyxError."""
resp = MagicMock()
resp.status_code = 200
resp.json.side_effect = ValueError("No JSON")
resp.headers = {"Link": ""}
mock_requests.get.return_value = resp
with pytest.raises(OnyxError, match="Invalid JSON"):
self.client.get("courses")
@patch("onyx.connectors.canvas.client.rl_requests")
def test_invalid_json_on_error_falls_back_to_reason(
self, mock_requests: MagicMock
) -> None:
"""Invalid JSON on a 4xx response falls back to response.reason."""
resp = MagicMock()
resp.status_code = 500
resp.reason = "Internal Server Error"
resp.json.side_effect = ValueError("No JSON")
resp.headers = {"Link": ""}
mock_requests.get.return_value = resp
with pytest.raises(OnyxError) as exc_info:
self.client.get("courses")
result = exc_info.value.detail
expected = "Internal Server Error"
assert result == expected
# ---------------------------------------------------------------------------
# CanvasApiClient._parse_next_link tests
# ---------------------------------------------------------------------------
class TestParseNextLink:
def setup_method(self) -> None:
self.client = CanvasApiClient(
bearer_token=FAKE_TOKEN,
canvas_base_url="https://canvas.example.com",
)
def test_found(self) -> None:
header = '<https://canvas.example.com/api/v1/courses?page=2>; rel="next"'
result = self.client._parse_next_link(header)
expected = "https://canvas.example.com/api/v1/courses?page=2"
assert result == expected
def test_not_found(self) -> None:
header = '<https://canvas.example.com/api/v1/courses?page=1>; rel="current"'
result = self.client._parse_next_link(header)
assert result is None
def test_empty(self) -> None:
result = self.client._parse_next_link("")
assert result is None
def test_multiple_rels(self) -> None:
header = (
'<https://canvas.example.com/api/v1/courses?page=1>; rel="current", '
'<https://canvas.example.com/api/v1/courses?page=2>; rel="next"'
)
result = self.client._parse_next_link(header)
expected = "https://canvas.example.com/api/v1/courses?page=2"
assert result == expected
def test_rejects_host_mismatch(self) -> None:
header = '<https://evil.example.com/api/v1/courses?page=2>; rel="next"'
with pytest.raises(OnyxError, match="unexpected host"):
self.client._parse_next_link(header)
def test_rejects_non_https_link(self) -> None:
header = '<http://canvas.example.com/api/v1/courses?page=2>; rel="next"'
with pytest.raises(OnyxError, match="must use https"):
self.client._parse_next_link(header)

View File

@@ -1,40 +0,0 @@
"""Unit tests for the hooks feature gate."""
from unittest.mock import patch
import pytest
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.hooks.api_dependencies import require_hook_enabled
class TestRequireHookEnabled:
def test_raises_when_multi_tenant(self) -> None:
with (
patch("onyx.hooks.api_dependencies.MULTI_TENANT", True),
patch("onyx.hooks.api_dependencies.HOOK_ENABLED", True),
):
with pytest.raises(OnyxError) as exc_info:
require_hook_enabled()
assert exc_info.value.error_code is OnyxErrorCode.SINGLE_TENANT_ONLY
assert exc_info.value.status_code == 403
assert "multi-tenant" in exc_info.value.detail
def test_raises_when_flag_disabled(self) -> None:
with (
patch("onyx.hooks.api_dependencies.MULTI_TENANT", False),
patch("onyx.hooks.api_dependencies.HOOK_ENABLED", False),
):
with pytest.raises(OnyxError) as exc_info:
require_hook_enabled()
assert exc_info.value.error_code is OnyxErrorCode.ENV_VAR_GATED
assert exc_info.value.status_code == 403
assert "HOOK_ENABLED" in exc_info.value.detail
def test_passes_when_enabled_single_tenant(self) -> None:
with (
patch("onyx.hooks.api_dependencies.MULTI_TENANT", False),
patch("onyx.hooks.api_dependencies.HOOK_ENABLED", True),
):
require_hook_enabled() # must not raise

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

View File

@@ -17,7 +17,7 @@ def test_mt_cloud_telemetry_noop_when_not_multi_tenant(monkeypatch: Any) -> None
telemetry_utils.mt_cloud_telemetry(
tenant_id="tenant-1",
distinct_id="12345678-1234-1234-1234-123456789abc",
distinct_id="user@example.com",
event=MilestoneRecordType.USER_MESSAGE_SENT,
properties={"origin": "web"},
)
@@ -40,7 +40,7 @@ def test_mt_cloud_telemetry_calls_event_telemetry_when_multi_tenant(
telemetry_utils.mt_cloud_telemetry(
tenant_id="tenant-1",
distinct_id="12345678-1234-1234-1234-123456789abc",
distinct_id="user@example.com",
event=MilestoneRecordType.USER_MESSAGE_SENT,
properties={"origin": "web"},
)
@@ -51,7 +51,7 @@ def test_mt_cloud_telemetry_calls_event_telemetry_when_multi_tenant(
fallback=telemetry_utils.noop_fallback,
)
event_telemetry.assert_called_once_with(
"12345678-1234-1234-1234-123456789abc",
"user@example.com",
MilestoneRecordType.USER_MESSAGE_SENT,
{"origin": "web", "tenant_id": "tenant-1"},
)

View File

@@ -32,17 +32,15 @@ def test_run_with_timeout_raises_on_timeout(slow: float, timeout: float) -> None
"""Test that a function that exceeds timeout raises TimeoutError"""
def slow_function() -> None:
time.sleep(slow)
time.sleep(slow) # Sleep for 2 seconds
start = time.monotonic()
with pytest.raises(TimeoutError) as exc_info:
run_with_timeout(timeout, slow_function)
elapsed = time.monotonic() - start
start = time.time()
run_with_timeout(timeout, slow_function) # Set timeout to 0.1 seconds
end = time.time()
assert end - start >= timeout
assert end - start < (slow + timeout) / 2
assert f"timed out after {timeout} seconds" in str(exc_info.value)
assert elapsed >= timeout
# Should return around the timeout duration, not the full sleep duration
assert elapsed == pytest.approx(timeout, abs=0.8)
@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning")

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}
/>
);
@@ -176,10 +167,7 @@ function AttachmentItemLayout({
<Section flexDirection="row" gap={0.25} padding={0.25}>
<div className={cn("h-[2.25rem] aspect-square rounded-08")}>
<Section>
<div
className="attachment-button__icon-wrapper"
data-testid="attachment-item-icon-wrapper"
>
<div className="attachment-button__icon-wrapper">
<Icon className="attachment-button__icon" />
</div>
</Section>
@@ -190,7 +178,7 @@ function AttachmentItemLayout({
alignItems="center"
gap={1.5}
>
<div data-testid="attachment-item-title" className="flex-1 min-w-0">
<div className="flex-1 min-w-0">
<Content
title={title}
description={description}

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

@@ -110,6 +110,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;
@@ -132,5 +133,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

@@ -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

@@ -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={{

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