mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-17 13:42:41 +00:00
Compare commits
3 Commits
dane/index
...
jamison/vo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f46e1e084 | ||
|
|
f4d379ceed | ||
|
|
8f1076e69d |
25
.github/actions/slack-notify/action.yml
vendored
25
.github/actions/slack-notify/action.yml
vendored
@@ -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 }'
|
||||
}
|
||||
|
||||
18
.github/actions/slack-notify/user-mappings.json
vendored
18
.github/actions/slack-notify/user-mappings.json
vendored
@@ -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"
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
#####
|
||||
|
||||
@@ -5,7 +5,6 @@ accidentally reaches the vector DB layer will fail loudly instead of timing
|
||||
out against a nonexistent Vespa/OpenSearch instance.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from onyx.context.search.models import IndexFilters
|
||||
@@ -67,7 +66,7 @@ class DisabledDocumentIndex(DocumentIndex):
|
||||
# ------------------------------------------------------------------
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk], # noqa: ARG002
|
||||
chunks: list[DocMetadataAwareIndexChunk], # noqa: ARG002
|
||||
index_batch_params: IndexBatchParams, # noqa: ARG002
|
||||
) -> set[DocumentInsertionRecord]:
|
||||
raise RuntimeError(VECTOR_DB_DISABLED_ERROR)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import abc
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
@@ -207,7 +206,7 @@ class Indexable(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk],
|
||||
chunks: list[DocMetadataAwareIndexChunk],
|
||||
index_batch_params: IndexBatchParams,
|
||||
) -> set[DocumentInsertionRecord]:
|
||||
"""
|
||||
@@ -227,8 +226,8 @@ class Indexable(abc.ABC):
|
||||
it is done automatically outside of this code.
|
||||
|
||||
Parameters:
|
||||
- chunks: Document chunks with all of the information needed for
|
||||
indexing to the document index.
|
||||
- chunks: Document chunks with all of the information needed for indexing to the document
|
||||
index.
|
||||
- tenant_id: The tenant id of the user whose chunks are being indexed
|
||||
- large_chunks_enabled: Whether large chunks are enabled
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import abc
|
||||
from collections.abc import Iterable
|
||||
from typing import Self
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -210,7 +209,7 @@ class Indexable(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk],
|
||||
chunks: list[DocMetadataAwareIndexChunk],
|
||||
indexing_metadata: IndexingMetadata,
|
||||
) -> list[DocumentInsertionRecord]:
|
||||
"""Indexes a list of document chunks into the document index.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
@@ -346,7 +346,7 @@ class OpenSearchOldDocumentIndex(OldDocumentIndex):
|
||||
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk],
|
||||
chunks: list[DocMetadataAwareIndexChunk],
|
||||
index_batch_params: IndexBatchParams,
|
||||
) -> set[OldDocumentInsertionRecord]:
|
||||
"""
|
||||
@@ -642,8 +642,8 @@ class OpenSearchDocumentIndex(DocumentIndex):
|
||||
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk],
|
||||
indexing_metadata: IndexingMetadata,
|
||||
chunks: list[DocMetadataAwareIndexChunk],
|
||||
indexing_metadata: IndexingMetadata, # noqa: ARG002
|
||||
) -> list[DocumentInsertionRecord]:
|
||||
"""Indexes a list of document chunks into the document index.
|
||||
|
||||
@@ -668,32 +668,29 @@ class OpenSearchDocumentIndex(DocumentIndex):
|
||||
document is newly indexed or had already existed and was just
|
||||
updated.
|
||||
"""
|
||||
total_chunks = sum(
|
||||
cc.new_chunk_cnt
|
||||
for cc in indexing_metadata.doc_id_to_chunk_cnt_diff.values()
|
||||
# Group chunks by document ID.
|
||||
doc_id_to_chunks: dict[str, list[DocMetadataAwareIndexChunk]] = defaultdict(
|
||||
list
|
||||
)
|
||||
for chunk in chunks:
|
||||
doc_id_to_chunks[chunk.source_document.id].append(chunk)
|
||||
logger.debug(
|
||||
f"[OpenSearchDocumentIndex] Indexing {total_chunks} chunks from {len(indexing_metadata.doc_id_to_chunk_cnt_diff)} "
|
||||
f"[OpenSearchDocumentIndex] Indexing {len(chunks)} chunks from {len(doc_id_to_chunks)} "
|
||||
f"documents for index {self._index_name}."
|
||||
)
|
||||
|
||||
document_indexing_results: list[DocumentInsertionRecord] = []
|
||||
deleted_doc_ids: set[str] = set()
|
||||
# Buffer chunks per document as they arrive from the iterable.
|
||||
# When the document ID changes flush the buffered chunks.
|
||||
current_doc_id: str | None = None
|
||||
current_chunks: list[DocMetadataAwareIndexChunk] = []
|
||||
|
||||
def _flush_chunks(doc_chunks: list[DocMetadataAwareIndexChunk]) -> None:
|
||||
# Try to index per-document.
|
||||
for _, chunks in doc_id_to_chunks.items():
|
||||
# Create a batch of OpenSearch-formatted chunks for bulk insertion.
|
||||
# Since we are doing this in batches, an error occurring midway
|
||||
# can result in a state where chunks are deleted and not all the
|
||||
# new chunks have been indexed.
|
||||
# Do this before deleting existing chunks to reduce the amount of
|
||||
# time the document index has no content for a given document, and
|
||||
# to reduce the chance of entering a state where we delete chunks,
|
||||
# then some error happens, and never successfully index new chunks.
|
||||
chunk_batch: list[DocumentChunk] = [
|
||||
_convert_onyx_chunk_to_opensearch_document(chunk)
|
||||
for chunk in doc_chunks
|
||||
_convert_onyx_chunk_to_opensearch_document(chunk) for chunk in chunks
|
||||
]
|
||||
onyx_document: Document = doc_chunks[0].source_document
|
||||
onyx_document: Document = chunks[0].source_document
|
||||
# First delete the doc's chunks from the index. This is so that
|
||||
# there are no dangling chunks in the index, in the event that the
|
||||
# new document's content contains fewer chunks than the previous
|
||||
@@ -702,36 +699,22 @@ class OpenSearchDocumentIndex(DocumentIndex):
|
||||
# if the chunk count has actually decreased. This assumes that
|
||||
# overlapping chunks are perfectly overwritten. If we can't
|
||||
# guarantee that then we need the code as-is.
|
||||
if onyx_document.id not in deleted_doc_ids:
|
||||
num_chunks_deleted = self.delete(
|
||||
onyx_document.id, onyx_document.chunk_count
|
||||
)
|
||||
deleted_doc_ids.add(onyx_document.id)
|
||||
document_indexing_results.append(
|
||||
DocumentInsertionRecord(
|
||||
document_id=onyx_document.id,
|
||||
already_existed=num_chunks_deleted > 0,
|
||||
)
|
||||
)
|
||||
num_chunks_deleted = self.delete(
|
||||
onyx_document.id, onyx_document.chunk_count
|
||||
)
|
||||
# If we see that chunks were deleted we assume the doc already
|
||||
# existed.
|
||||
document_insertion_record = DocumentInsertionRecord(
|
||||
document_id=onyx_document.id,
|
||||
already_existed=num_chunks_deleted > 0,
|
||||
)
|
||||
# Now index. This will raise if a chunk of the same ID exists, which
|
||||
# we do not expect because we should have deleted all chunks.
|
||||
self._client.bulk_index_documents(
|
||||
documents=chunk_batch,
|
||||
tenant_state=self._tenant_state,
|
||||
)
|
||||
|
||||
for chunk in chunks:
|
||||
doc_id = chunk.source_document.id
|
||||
if doc_id != current_doc_id:
|
||||
if current_chunks:
|
||||
_flush_chunks(current_chunks)
|
||||
current_doc_id = doc_id
|
||||
current_chunks = [chunk]
|
||||
else:
|
||||
current_chunks.append(chunk)
|
||||
|
||||
if current_chunks:
|
||||
_flush_chunks(current_chunks)
|
||||
document_indexing_results.append(document_insertion_record)
|
||||
|
||||
return document_indexing_results
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import re
|
||||
import time
|
||||
import urllib
|
||||
import zipfile
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
@@ -462,7 +461,7 @@ class VespaIndex(DocumentIndex):
|
||||
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk],
|
||||
chunks: list[DocMetadataAwareIndexChunk],
|
||||
index_batch_params: IndexBatchParams,
|
||||
) -> set[OldDocumentInsertionRecord]:
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import random
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
@@ -320,7 +318,7 @@ class VespaDocumentIndex(DocumentIndex):
|
||||
|
||||
def index(
|
||||
self,
|
||||
chunks: Iterable[DocMetadataAwareIndexChunk],
|
||||
chunks: list[DocMetadataAwareIndexChunk],
|
||||
indexing_metadata: IndexingMetadata,
|
||||
) -> list[DocumentInsertionRecord]:
|
||||
doc_id_to_chunk_cnt_diff = indexing_metadata.doc_id_to_chunk_cnt_diff
|
||||
@@ -340,31 +338,22 @@ class VespaDocumentIndex(DocumentIndex):
|
||||
|
||||
# Vespa has restrictions on valid characters, yet document IDs come from
|
||||
# external w.r.t. this class. We need to sanitize them.
|
||||
#
|
||||
# Instead of materializing all cleaned chunks upfront, we stream them
|
||||
# through a generator that cleans IDs and builds the original-ID mapping
|
||||
# incrementally as chunks flow into Vespa.
|
||||
def _clean_and_track(
|
||||
chunks_iter: Iterable[DocMetadataAwareIndexChunk],
|
||||
id_map: dict[str, str],
|
||||
seen_ids: set[str],
|
||||
) -> Generator[DocMetadataAwareIndexChunk, None, None]:
|
||||
"""Cleans chunk IDs and builds the original-ID mapping
|
||||
incrementally as chunks flow through, avoiding a separate
|
||||
materialization pass."""
|
||||
for chunk in chunks_iter:
|
||||
original_id = chunk.source_document.id
|
||||
cleaned = clean_chunk_id_copy(chunk)
|
||||
cleaned_id = cleaned.source_document.id
|
||||
# Needed so the final DocumentInsertionRecord returned can have
|
||||
# the original document ID. cleaned_chunks might not contain IDs
|
||||
# exactly as callers supplied them.
|
||||
id_map[cleaned_id] = original_id
|
||||
seen_ids.add(cleaned_id)
|
||||
yield cleaned
|
||||
cleaned_chunks: list[DocMetadataAwareIndexChunk] = [
|
||||
clean_chunk_id_copy(chunk) for chunk in chunks
|
||||
]
|
||||
assert len(cleaned_chunks) == len(
|
||||
chunks
|
||||
), "Bug: Cleaned chunks and input chunks have different lengths."
|
||||
|
||||
new_document_id_to_original_document_id: dict[str, str] = {}
|
||||
all_cleaned_doc_ids: set[str] = set()
|
||||
# Needed so the final DocumentInsertionRecord returned can have the
|
||||
# original document ID. cleaned_chunks might not contain IDs exactly as
|
||||
# callers supplied them.
|
||||
new_document_id_to_original_document_id: dict[str, str] = dict()
|
||||
for i, cleaned_chunk in enumerate(cleaned_chunks):
|
||||
old_chunk = chunks[i]
|
||||
new_document_id_to_original_document_id[
|
||||
cleaned_chunk.source_document.id
|
||||
] = old_chunk.source_document.id
|
||||
|
||||
existing_docs: set[str] = set()
|
||||
|
||||
@@ -420,13 +409,7 @@ class VespaDocumentIndex(DocumentIndex):
|
||||
executor=executor,
|
||||
)
|
||||
|
||||
# Insert new Vespa documents, streaming through the cleaning
|
||||
# pipeline so chunks are never fully materialized.
|
||||
cleaned_chunks = _clean_and_track(
|
||||
chunks,
|
||||
new_document_id_to_original_document_id,
|
||||
all_cleaned_doc_ids,
|
||||
)
|
||||
# Insert new Vespa documents.
|
||||
for chunk_batch in batch_generator(cleaned_chunks, BATCH_SIZE):
|
||||
batch_index_vespa_chunks(
|
||||
chunks=chunk_batch,
|
||||
@@ -436,6 +419,10 @@ class VespaDocumentIndex(DocumentIndex):
|
||||
executor=executor,
|
||||
)
|
||||
|
||||
all_cleaned_doc_ids: set[str] = {
|
||||
chunk.source_document.id for chunk in cleaned_chunks
|
||||
}
|
||||
|
||||
return [
|
||||
DocumentInsertionRecord(
|
||||
document_id=new_document_id_to_original_document_id[cleaned_doc_id],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
12
uv.lock
generated
12
uv.lock
generated
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
4
web/package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -97,8 +97,16 @@ function InputChipField({
|
||||
<Chip
|
||||
key={chip.id}
|
||||
onRemove={disabled ? undefined : () => onRemoveChip(chip.id)}
|
||||
rightIcon={chip.error ? SvgAlertTriangle : undefined}
|
||||
error={chip.error}
|
||||
rightIcon={
|
||||
chip.error
|
||||
? (props) => (
|
||||
<SvgAlertTriangle
|
||||
{...props}
|
||||
className="text-status-warning-text"
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
smallLabel={layout === "stacked"}
|
||||
>
|
||||
{chip.label}
|
||||
@@ -116,7 +124,7 @@ function InputChipField({
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
placeholder={chips.length === 0 ? placeholder : undefined}
|
||||
className={cn(
|
||||
"flex-1 min-w-[80px] h-[1.5rem] bg-transparent p-0.5 focus:outline-none",
|
||||
innerClasses[variant],
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SvgFold,
|
||||
SvgExternalLink,
|
||||
} from "@opal/icons";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
|
||||
import { Content } from "@opal/layouts";
|
||||
import {
|
||||
useSettingsContext,
|
||||
@@ -58,7 +58,7 @@ import useFilter from "@/hooks/useFilter";
|
||||
import { MCPServer } from "@/lib/tools/interfaces";
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const route = ADMIN_ROUTES.CHAT_PREFERENCES;
|
||||
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.CHAT_PREFERENCES]!;
|
||||
|
||||
interface DefaultAgentConfiguration {
|
||||
tool_ids: number[];
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SvgUnplug,
|
||||
SvgXOctagon,
|
||||
} from "@opal/icons";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Button } from "@opal/components";
|
||||
import { Disabled } from "@opal/core";
|
||||
@@ -23,7 +23,7 @@ import { updateCodeInterpreter } from "@/lib/admin/code-interpreter/svc";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
|
||||
const route = ADMIN_ROUTES.CODE_INTERPRETER;
|
||||
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.CODE_INTERPRETER]!;
|
||||
|
||||
interface CodeInterpreterCardProps {
|
||||
variant?: CardProps["variant"];
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Button } from "@opal/components";
|
||||
import { Hoverable } from "@opal/core";
|
||||
import { SvgArrowExchange, SvgSettings, SvgTrash } from "@opal/icons";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
|
||||
import * as GeneralLayouts from "@/layouts/general-layouts";
|
||||
import {
|
||||
getProviderDisplayName,
|
||||
@@ -47,7 +47,7 @@ import { LMStudioForm } from "@/sections/modals/llmConfig/LMStudioForm";
|
||||
import { LiteLLMProxyModal } from "@/sections/modals/llmConfig/LiteLLMProxyModal";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
|
||||
const route = ADMIN_ROUTES.LLM_MODELS;
|
||||
const route = ADMIN_ROUTE_CONFIG[ADMIN_PATHS.LLM_MODELS]!;
|
||||
|
||||
// ============================================================================
|
||||
// Provider form mapping (keyed by provider name from the API)
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useState, useMemo, useRef, useCallback } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
import { SvgUsers, SvgUser, SvgLogOut, SvgCheck } from "@opal/icons";
|
||||
import { Disabled } from "@opal/core";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import InputSelect from "@/refresh-components/inputs/InputSelect";
|
||||
import Popover from "@/refresh-components/Popover";
|
||||
import LineItem from "@/refresh-components/buttons/LineItem";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import ShadowDiv from "@/refresh-components/ShadowDiv";
|
||||
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { UserRole, USER_ROLE_LABELS } from "@/lib/types";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import { addUserToGroup, removeUserFromGroup, setUserRole } from "./svc";
|
||||
import type { UserRow } from "./interfaces";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
@@ -36,7 +33,7 @@ const ASSIGNABLE_ROLES: UserRole[] = [
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface EditUserModalProps {
|
||||
interface EditGroupsModalProps {
|
||||
user: UserRow & { id: string };
|
||||
onClose: () => void;
|
||||
onMutate: () => void;
|
||||
@@ -46,16 +43,25 @@ interface EditUserModalProps {
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function EditUserModal({
|
||||
export default function EditGroupsModal({
|
||||
user,
|
||||
onClose,
|
||||
onMutate,
|
||||
}: EditUserModalProps) {
|
||||
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
|
||||
}: EditGroupsModalProps) {
|
||||
const { data: allGroups, isLoading: groupsLoading } = useGroups();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const closeDropdown = useCallback(() => {
|
||||
// Delay to allow click events on dropdown items to fire before closing
|
||||
setTimeout(() => {
|
||||
if (!containerRef.current?.contains(document.activeElement)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
}, []);
|
||||
const [selectedRole, setSelectedRole] = useState<UserRole | "">(
|
||||
user.role ?? ""
|
||||
);
|
||||
@@ -89,10 +95,6 @@ export default function EditUserModal({
|
||||
);
|
||||
}, [memberGroupIds, initialMemberGroupIds]);
|
||||
|
||||
const visibleRoles = isPaidEnterpriseFeaturesEnabled
|
||||
? ASSIGNABLE_ROLES
|
||||
: ASSIGNABLE_ROLES.filter((r) => r !== UserRole.GLOBAL_CURATOR);
|
||||
|
||||
const hasRoleChange =
|
||||
user.role !== null && selectedRole !== "" && selectedRole !== user.role;
|
||||
const hasChanges = hasGroupChanges || hasRoleChange;
|
||||
@@ -158,17 +160,10 @@ export default function EditUserModal({
|
||||
};
|
||||
|
||||
const displayName = user.personal_name ?? user.email;
|
||||
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
|
||||
const contentRef = useCallback((node: HTMLDivElement | null) => {
|
||||
setContentEl(node);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={(isOpen) => !isOpen && !isSubmitting && onClose()}
|
||||
>
|
||||
<Modal.Content width="sm" ref={contentRef}>
|
||||
<Modal open onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
<Modal.Content width="sm">
|
||||
<Modal.Header
|
||||
icon={SvgUsers}
|
||||
title="Edit User's Groups & Roles"
|
||||
@@ -177,119 +172,108 @@ export default function EditUserModal({
|
||||
? `${user.personal_name} (${user.email})`
|
||||
: user.email
|
||||
}
|
||||
onClose={isSubmitting ? undefined : onClose}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<Modal.Body twoTone>
|
||||
<Section padding={0} height="auto" alignItems="stretch">
|
||||
<Section
|
||||
gap={0.5}
|
||||
padding={0.25}
|
||||
height={joinedGroups.length === 0 && !popoverOpen ? "auto" : 14.5}
|
||||
alignItems="stretch"
|
||||
justifyContent="start"
|
||||
className="bg-background-tint-02 rounded-08"
|
||||
>
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
{/* asChild merges trigger props onto this div instead of rendering a <button>.
|
||||
Without it, the trigger <button> would nest around InputTypeIn's
|
||||
internal IconButton <button>, causing a hydration error. */}
|
||||
<div>
|
||||
<InputTypeIn
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search groups to join..."
|
||||
leftSearchIcon
|
||||
/>
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
width="trigger"
|
||||
align="start"
|
||||
container={contentEl}
|
||||
>
|
||||
{groupsLoading ? (
|
||||
<LineItem skeleton description="Loading groups...">
|
||||
Loading...
|
||||
</LineItem>
|
||||
) : dropdownGroups.length === 0 ? (
|
||||
<LineItem
|
||||
skeleton
|
||||
description="Try a different search term."
|
||||
>
|
||||
No groups found
|
||||
</LineItem>
|
||||
) : (
|
||||
<ShadowDiv
|
||||
shadowHeight="0.75rem"
|
||||
className={cn(
|
||||
"flex flex-col gap-1 max-h-[15rem] rounded-08"
|
||||
)}
|
||||
>
|
||||
{dropdownGroups.map((group) => {
|
||||
const isMember = memberGroupIds.has(group.id);
|
||||
return (
|
||||
<LineItem
|
||||
key={group.id}
|
||||
icon={isMember ? SvgCheck : SvgUsers}
|
||||
description={`${group.users.length} ${
|
||||
group.users.length === 1 ? "user" : "users"
|
||||
}`}
|
||||
selected={isMember}
|
||||
emphasized={isMember}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
>
|
||||
{group.name}
|
||||
</LineItem>
|
||||
);
|
||||
})}
|
||||
</ShadowDiv>
|
||||
)}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
|
||||
<ShadowDiv
|
||||
className={cn(" max-h-[11rem] flex flex-col gap-1 rounded-08")}
|
||||
shadowHeight="0.75rem"
|
||||
<Section
|
||||
gap={1}
|
||||
height="auto"
|
||||
alignItems="stretch"
|
||||
justifyContent="start"
|
||||
>
|
||||
{/* Subsection: white card behind search + groups */}
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-2 bg-background-neutral-00 rounded-12" />
|
||||
<Section
|
||||
gap={0.5}
|
||||
height="auto"
|
||||
alignItems="stretch"
|
||||
justifyContent="start"
|
||||
>
|
||||
<div ref={containerRef} className="relative">
|
||||
<InputTypeIn
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (!dropdownOpen) setDropdownOpen(true);
|
||||
}}
|
||||
onFocus={() => setDropdownOpen(true)}
|
||||
onBlur={closeDropdown}
|
||||
placeholder="Search groups to join..."
|
||||
leftSearchIcon
|
||||
/>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-background-neutral-00 border border-border-02 rounded-12 shadow-md p-1">
|
||||
{groupsLoading ? (
|
||||
<Text as="p" text03 secondaryBody className="px-3 py-2">
|
||||
Loading groups...
|
||||
</Text>
|
||||
) : dropdownGroups.length === 0 ? (
|
||||
<Text as="p" text03 secondaryBody className="px-3 py-2">
|
||||
No groups found
|
||||
</Text>
|
||||
) : (
|
||||
<ShadowDiv className="max-h-[200px] flex flex-col gap-1">
|
||||
{dropdownGroups.map((group) => {
|
||||
const isMember = memberGroupIds.has(group.id);
|
||||
return (
|
||||
<LineItem
|
||||
key={group.id}
|
||||
icon={isMember ? SvgCheck : SvgUsers}
|
||||
description={`${group.users.length} ${
|
||||
group.users.length === 1 ? "user" : "users"
|
||||
}`}
|
||||
selected={isMember}
|
||||
emphasized={isMember}
|
||||
onMouseDown={(e: React.MouseEvent) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
>
|
||||
{group.name}
|
||||
</LineItem>
|
||||
);
|
||||
})}
|
||||
</ShadowDiv>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{joinedGroups.length === 0 ? (
|
||||
<LineItem
|
||||
icon={SvgUsers}
|
||||
skeleton
|
||||
interactive={false}
|
||||
description={`${displayName} is not in any groups.`}
|
||||
muted
|
||||
>
|
||||
No groups found
|
||||
No groups joined
|
||||
</LineItem>
|
||||
) : (
|
||||
joinedGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className="bg-background-tint-01 rounded-08"
|
||||
>
|
||||
<LineItem
|
||||
<ShadowDiv className="flex flex-col gap-1 max-h-[200px]">
|
||||
{joinedGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
icon={SvgUsers}
|
||||
description={`${group.users.length} ${
|
||||
group.users.length === 1 ? "user" : "users"
|
||||
}`}
|
||||
rightChildren={
|
||||
<SimpleTooltip
|
||||
tooltip="Remove from group"
|
||||
side="left"
|
||||
>
|
||||
<SvgLogOut height={16} width={16} />
|
||||
</SimpleTooltip>
|
||||
}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
className="bg-background-tint-01 rounded-08"
|
||||
>
|
||||
{group.name}
|
||||
</LineItem>
|
||||
</div>
|
||||
))
|
||||
<LineItem
|
||||
icon={SvgUsers}
|
||||
description={`${group.users.length} ${
|
||||
group.users.length === 1 ? "user" : "users"
|
||||
}`}
|
||||
rightChildren={
|
||||
<SvgLogOut className="w-4 h-4 text-text-03" />
|
||||
}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
>
|
||||
{group.name}
|
||||
</LineItem>
|
||||
</div>
|
||||
))}
|
||||
</ShadowDiv>
|
||||
)}
|
||||
</ShadowDiv>
|
||||
</Section>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
{user.role && (
|
||||
<>
|
||||
<Separator noPadding />
|
||||
@@ -307,7 +291,7 @@ export default function EditUserModal({
|
||||
>
|
||||
<InputSelect.Trigger />
|
||||
<InputSelect.Content>
|
||||
{user.role && !visibleRoles.includes(user.role) && (
|
||||
{user.role && !ASSIGNABLE_ROLES.includes(user.role) && (
|
||||
<InputSelect.Item
|
||||
key={user.role}
|
||||
value={user.role}
|
||||
@@ -316,7 +300,7 @@ export default function EditUserModal({
|
||||
{USER_ROLE_LABELS[user.role]}
|
||||
</InputSelect.Item>
|
||||
)}
|
||||
{visibleRoles.map((role) => (
|
||||
{ASSIGNABLE_ROLES.map((role) => (
|
||||
<InputSelect.Item
|
||||
key={role}
|
||||
value={role}
|
||||
@@ -335,10 +319,7 @@ export default function EditUserModal({
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={isSubmitting ? undefined : onClose}
|
||||
>
|
||||
<Button prominence="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Disabled disabled={isSubmitting || !hasChanges}>
|
||||
@@ -12,7 +12,7 @@ import { Tag } from "@opal/components";
|
||||
import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
|
||||
import EditUserModal from "./EditUserModal";
|
||||
import EditGroupsModal from "./EditGroupsModal";
|
||||
import type { UserRow, UserGroupInfo } from "./interfaces";
|
||||
|
||||
interface GroupsCellProps {
|
||||
@@ -184,7 +184,7 @@ export default function GroupsCell({
|
||||
)}
|
||||
</div>
|
||||
{showModal && user.id != null && (
|
||||
<EditUserModal
|
||||
<EditGroupsModal
|
||||
user={{ ...user, id: user.id }}
|
||||
onClose={() => setShowModal(false)}
|
||||
onMutate={onMutate}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
import { SvgUsers, SvgAlertTriangle } from "@opal/icons";
|
||||
import { SvgUsers } from "@opal/icons";
|
||||
import { Disabled } from "@opal/core";
|
||||
import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
|
||||
import InputChipField from "@/refresh-components/inputs/InputChipField";
|
||||
import type { ChipItem } from "@/refresh-components/inputs/InputChipField";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { inviteUsers } from "./svc";
|
||||
|
||||
@@ -146,20 +145,9 @@ export default function InviteUsersModal({
|
||||
onAdd={addEmail}
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
placeholder="Add an email and press enter"
|
||||
placeholder="Add emails to invite, comma separated"
|
||||
layout="stacked"
|
||||
/>
|
||||
{chips.some((c) => c.error) && (
|
||||
<div className="flex items-center gap-1 pt-1">
|
||||
<SvgAlertTriangle
|
||||
size={14}
|
||||
className="text-status-warning-05 shrink-0"
|
||||
/>
|
||||
<Text secondaryBody text03>
|
||||
Some email addresses are invalid and will be skipped.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
|
||||
@@ -166,7 +166,6 @@ export default function UserFilters({
|
||||
<Popover>
|
||||
<Popover.Trigger asChild>
|
||||
<FilterButton
|
||||
data-testid="filter-role"
|
||||
leftIcon={SvgUsers}
|
||||
active={hasRoleFilter}
|
||||
onClear={() => onRolesChange([])}
|
||||
@@ -214,7 +213,6 @@ export default function UserFilters({
|
||||
>
|
||||
<Popover.Trigger asChild>
|
||||
<FilterButton
|
||||
data-testid="filter-group"
|
||||
leftIcon={SvgUsers}
|
||||
active={hasGroupFilter}
|
||||
onClear={() => onGroupsChange([])}
|
||||
@@ -269,7 +267,6 @@ export default function UserFilters({
|
||||
<Popover>
|
||||
<Popover.Trigger asChild>
|
||||
<FilterButton
|
||||
data-testid="filter-status"
|
||||
leftIcon={SvgUsers}
|
||||
active={hasStatusFilter}
|
||||
onClear={() => onStatusesChange([])}
|
||||
|
||||
@@ -51,6 +51,16 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (user.is_scim_synced) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Text as="span" mainUiBody text03>
|
||||
{USER_ROLE_LABELS[user.role] ?? user.role}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const applyRole = async (newRole: UserRole) => {
|
||||
if (isUpdatingRef.current) return;
|
||||
isUpdatingRef.current = true;
|
||||
@@ -79,46 +89,47 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
|
||||
|
||||
const currentIcon = ROLE_ICONS[user.role] ?? SvgUser;
|
||||
|
||||
const visibleRoles = isPaidEnterpriseFeaturesEnabled
|
||||
? SELECTABLE_ROLES
|
||||
: SELECTABLE_ROLES.filter((r) => r !== UserRole.GLOBAL_CURATOR);
|
||||
|
||||
const roleItems = visibleRoles.map((role) => {
|
||||
const isSelected = user.role === role;
|
||||
const icon = ROLE_ICONS[role] ?? SvgUser;
|
||||
return (
|
||||
<LineItem
|
||||
key={role}
|
||||
icon={isSelected ? SvgCheck : icon}
|
||||
selected={isSelected}
|
||||
emphasized={isSelected}
|
||||
onClick={() => handleSelect(role)}
|
||||
>
|
||||
{USER_ROLE_LABELS[role]}
|
||||
</LineItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Disabled disabled={isUpdating}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<OpenButton
|
||||
icon={currentIcon}
|
||||
variant="select-tinted"
|
||||
width="full"
|
||||
justifyContent="between"
|
||||
roundingVariant="compact"
|
||||
>
|
||||
{USER_ROLE_LABELS[user.role]}
|
||||
</OpenButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start">
|
||||
<div className="flex flex-col gap-1 p-1 min-w-[160px]">
|
||||
{roleItems}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
</Disabled>
|
||||
<div className="[&_button]:rounded-08">
|
||||
<Disabled disabled={isUpdating}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<OpenButton
|
||||
icon={currentIcon}
|
||||
variant="select-tinted"
|
||||
width="full"
|
||||
justifyContent="between"
|
||||
>
|
||||
{USER_ROLE_LABELS[user.role]}
|
||||
</OpenButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start">
|
||||
<div className="flex flex-col gap-1 p-1 min-w-[160px]">
|
||||
{SELECTABLE_ROLES.map((role) => {
|
||||
if (
|
||||
role === UserRole.GLOBAL_CURATOR &&
|
||||
!isPaidEnterpriseFeaturesEnabled
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const isSelected = user.role === role;
|
||||
const icon = ROLE_ICONS[role] ?? SvgUser;
|
||||
return (
|
||||
<LineItem
|
||||
key={role}
|
||||
icon={isSelected ? SvgCheck : icon}
|
||||
selected={isSelected}
|
||||
emphasized={isSelected}
|
||||
onClick={() => handleSelect(role)}
|
||||
>
|
||||
{USER_ROLE_LABELS[role]}
|
||||
</LineItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
</Disabled>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,12 @@ import {
|
||||
SvgMoreHorizontal,
|
||||
SvgUsers,
|
||||
SvgXCircle,
|
||||
SvgUserCheck,
|
||||
SvgUserPlus,
|
||||
SvgUserX,
|
||||
SvgKey,
|
||||
SvgTrash,
|
||||
SvgCheck,
|
||||
} from "@opal/icons";
|
||||
import { Disabled } from "@opal/core";
|
||||
import Popover from "@/refresh-components/Popover";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { UserStatus } from "@/lib/types";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
@@ -25,23 +21,21 @@ import {
|
||||
deleteUser,
|
||||
cancelInvite,
|
||||
approveRequest,
|
||||
resetPassword,
|
||||
} from "./svc";
|
||||
import EditUserModal from "./EditUserModal";
|
||||
import EditGroupsModal from "./EditGroupsModal";
|
||||
import type { UserRow } from "./interfaces";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum Modal {
|
||||
DEACTIVATE = "deactivate",
|
||||
ACTIVATE = "activate",
|
||||
DELETE = "delete",
|
||||
CANCEL_INVITE = "cancelInvite",
|
||||
EDIT_GROUPS = "editGroups",
|
||||
RESET_PASSWORD = "resetPassword",
|
||||
}
|
||||
type ModalType =
|
||||
| "deactivate"
|
||||
| "activate"
|
||||
| "delete"
|
||||
| "cancelInvite"
|
||||
| "editGroups"
|
||||
| null;
|
||||
|
||||
interface UserRowActionsProps {
|
||||
user: UserRow;
|
||||
@@ -56,10 +50,9 @@ export default function UserRowActions({
|
||||
user,
|
||||
onMutate,
|
||||
}: UserRowActionsProps) {
|
||||
const [modal, setModal] = useState<Modal | null>(null);
|
||||
const [modal, setModal] = useState<ModalType>(null);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [newPassword, setNewPassword] = useState<string | null>(null);
|
||||
|
||||
async function handleAction(
|
||||
action: () => Promise<void>,
|
||||
@@ -78,40 +71,13 @@ export default function UserRowActions({
|
||||
}
|
||||
}
|
||||
|
||||
const openModal = (type: Modal) => {
|
||||
const openModal = (type: ModalType) => {
|
||||
setPopoverOpen(false);
|
||||
setModal(type);
|
||||
};
|
||||
|
||||
// Status-aware action menus
|
||||
const actionButtons = (() => {
|
||||
// SCIM-managed users get limited actions — most changes would be
|
||||
// overwritten on the next IdP sync.
|
||||
if (user.is_scim_synced) {
|
||||
return (
|
||||
<>
|
||||
{user.id && (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgUsers}
|
||||
onClick={() => openModal(Modal.EDIT_GROUPS)}
|
||||
>
|
||||
Groups & Roles
|
||||
</Button>
|
||||
)}
|
||||
<Disabled disabled>
|
||||
<Button prominence="tertiary" variant="danger" icon={SvgUserX}>
|
||||
Deactivate User
|
||||
</Button>
|
||||
</Disabled>
|
||||
<Separator paddingXRem={0.5} />
|
||||
<Text as="p" secondaryBody text03 className="px-3 py-1">
|
||||
This is a synced SCIM user managed by your identity provider.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
switch (user.status) {
|
||||
case UserStatus.INVITED:
|
||||
return (
|
||||
@@ -119,7 +85,7 @@ export default function UserRowActions({
|
||||
prominence="tertiary"
|
||||
variant="danger"
|
||||
icon={SvgXCircle}
|
||||
onClick={() => openModal(Modal.CANCEL_INVITE)}
|
||||
onClick={() => openModal("cancelInvite")}
|
||||
>
|
||||
Cancel Invite
|
||||
</Button>
|
||||
@@ -129,7 +95,7 @@ export default function UserRowActions({
|
||||
return (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgUserCheck}
|
||||
icon={SvgCheck}
|
||||
onClick={() => {
|
||||
setPopoverOpen(false);
|
||||
handleAction(
|
||||
@@ -149,24 +115,15 @@ export default function UserRowActions({
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgUsers}
|
||||
onClick={() => openModal(Modal.EDIT_GROUPS)}
|
||||
onClick={() => openModal("editGroups")}
|
||||
>
|
||||
Groups & Roles
|
||||
Groups
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgKey}
|
||||
onClick={() => openModal(Modal.RESET_PASSWORD)}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
<Separator paddingXRem={0.5} />
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
variant="danger"
|
||||
icon={SvgUserX}
|
||||
onClick={() => openModal(Modal.DEACTIVATE)}
|
||||
icon={SvgXCircle}
|
||||
onClick={() => openModal("deactivate")}
|
||||
>
|
||||
Deactivate User
|
||||
</Button>
|
||||
@@ -176,19 +133,27 @@ export default function UserRowActions({
|
||||
case UserStatus.INACTIVE:
|
||||
return (
|
||||
<>
|
||||
{user.id && (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgUsers}
|
||||
onClick={() => openModal("editGroups")}
|
||||
>
|
||||
Groups
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgUserPlus}
|
||||
onClick={() => openModal(Modal.ACTIVATE)}
|
||||
icon={SvgCheck}
|
||||
onClick={() => openModal("activate")}
|
||||
>
|
||||
Activate User
|
||||
</Button>
|
||||
<Separator paddingXRem={0.5} />
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
variant="danger"
|
||||
icon={SvgUserX}
|
||||
onClick={() => openModal(Modal.DELETE)}
|
||||
icon={SvgTrash}
|
||||
onClick={() => openModal("delete")}
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
@@ -202,39 +167,36 @@ export default function UserRowActions({
|
||||
}
|
||||
})();
|
||||
|
||||
// SCIM-managed users cannot be modified from the UI — changes would be
|
||||
// overwritten on the next IdP sync.
|
||||
if (user.is_scim_synced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<Button prominence="tertiary" icon={SvgMoreHorizontal} />
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="end" width="sm">
|
||||
<Section
|
||||
gap={0.5}
|
||||
height="auto"
|
||||
alignItems="stretch"
|
||||
justifyContent="start"
|
||||
>
|
||||
{actionButtons}
|
||||
</Section>
|
||||
<Popover.Content align="end">
|
||||
<div className="flex flex-col gap-0.5 p-1">{actionButtons}</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
|
||||
{modal === Modal.EDIT_GROUPS && user.id && (
|
||||
<EditUserModal
|
||||
{modal === "editGroups" && user.id && (
|
||||
<EditGroupsModal
|
||||
user={user as UserRow & { id: string }}
|
||||
onClose={() => setModal(null)}
|
||||
onMutate={onMutate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modal === Modal.CANCEL_INVITE && (
|
||||
{modal === "cancelInvite" && (
|
||||
<ConfirmationModalLayout
|
||||
icon={(props) => (
|
||||
<SvgUserX {...props} className="text-action-danger-05" />
|
||||
)}
|
||||
icon={SvgXCircle}
|
||||
title="Cancel Invite"
|
||||
onClose={isSubmitting ? undefined : () => setModal(null)}
|
||||
onClose={() => setModal(null)}
|
||||
submit={
|
||||
<Disabled disabled={isSubmitting}>
|
||||
<Button
|
||||
@@ -246,7 +208,7 @@ export default function UserRowActions({
|
||||
);
|
||||
}}
|
||||
>
|
||||
Cancel Invite
|
||||
Cancel
|
||||
</Button>
|
||||
</Disabled>
|
||||
}
|
||||
@@ -260,11 +222,9 @@ export default function UserRowActions({
|
||||
</ConfirmationModalLayout>
|
||||
)}
|
||||
|
||||
{modal === Modal.DEACTIVATE && (
|
||||
{modal === "deactivate" && (
|
||||
<ConfirmationModalLayout
|
||||
icon={(props) => (
|
||||
<SvgUserX {...props} className="text-action-danger-05" />
|
||||
)}
|
||||
icon={SvgXCircle}
|
||||
title="Deactivate User"
|
||||
onClose={isSubmitting ? undefined : () => setModal(null)}
|
||||
submit={
|
||||
@@ -294,9 +254,9 @@ export default function UserRowActions({
|
||||
</ConfirmationModalLayout>
|
||||
)}
|
||||
|
||||
{modal === Modal.ACTIVATE && (
|
||||
{modal === "activate" && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgUserPlus}
|
||||
icon={SvgCheck}
|
||||
title="Activate User"
|
||||
onClose={isSubmitting ? undefined : () => setModal(null)}
|
||||
submit={
|
||||
@@ -323,11 +283,9 @@ export default function UserRowActions({
|
||||
</ConfirmationModalLayout>
|
||||
)}
|
||||
|
||||
{modal === Modal.DELETE && (
|
||||
{modal === "delete" && (
|
||||
<ConfirmationModalLayout
|
||||
icon={(props) => (
|
||||
<SvgUserX {...props} className="text-action-danger-05" />
|
||||
)}
|
||||
icon={SvgTrash}
|
||||
title="Delete User"
|
||||
onClose={isSubmitting ? undefined : () => setModal(null)}
|
||||
submit={
|
||||
@@ -355,80 +313,6 @@ export default function UserRowActions({
|
||||
</Text>
|
||||
</ConfirmationModalLayout>
|
||||
)}
|
||||
|
||||
{modal === Modal.RESET_PASSWORD && (
|
||||
<ConfirmationModalLayout
|
||||
icon={SvgKey}
|
||||
title={newPassword ? "Password Reset" : "Reset Password"}
|
||||
onClose={
|
||||
isSubmitting
|
||||
? undefined
|
||||
: () => {
|
||||
setModal(null);
|
||||
setNewPassword(null);
|
||||
}
|
||||
}
|
||||
submit={
|
||||
newPassword ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setModal(null);
|
||||
setNewPassword(null);
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
) : (
|
||||
<Disabled disabled={isSubmitting}>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await resetPassword(user.email);
|
||||
setNewPassword(result.new_password);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Failed to reset password"
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</Disabled>
|
||||
)
|
||||
}
|
||||
>
|
||||
{newPassword ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text as="p" text03>
|
||||
The password for{" "}
|
||||
<Text as="span" text05>
|
||||
{user.email}
|
||||
</Text>{" "}
|
||||
has been reset. Copy the new password below — it will not be
|
||||
shown again.
|
||||
</Text>
|
||||
<code className="rounded-sm bg-background-neutral-02 px-3 py-2 text-sm select-all">
|
||||
{newPassword}
|
||||
</code>
|
||||
</div>
|
||||
) : (
|
||||
<Text as="p" text03>
|
||||
This will generate a new random password for{" "}
|
||||
<Text as="span" text05>
|
||||
{user.email}
|
||||
</Text>
|
||||
. Their current password will stop working immediately.
|
||||
</Text>
|
||||
)}
|
||||
</ConfirmationModalLayout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user