mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-18 07:56:48 +00:00
Compare commits
10 Commits
v3.2.0-clo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7d36dfb04 | ||
|
|
55b8e4096e | ||
|
|
75ef2b014d | ||
|
|
37c56feabf | ||
|
|
123d02151d | ||
|
|
146d8522df | ||
|
|
ac5bae3631 | ||
|
|
b5434b2391 | ||
|
|
28e13b503b | ||
|
|
99a90ec196 |
@@ -45,7 +45,7 @@ if [ "$ACTIVE_HOME" != "$MOUNT_HOME" ]; then
|
||||
[ -d "$MOUNT_HOME/$item" ] || continue
|
||||
if [ -e "$ACTIVE_HOME/$item" ] && [ ! -L "$ACTIVE_HOME/$item" ]; then
|
||||
echo "warning: replacing $ACTIVE_HOME/$item with symlink to $MOUNT_HOME/$item" >&2
|
||||
rm -rf "$ACTIVE_HOME/$item"
|
||||
rm -rf "${ACTIVE_HOME:?}/$item"
|
||||
fi
|
||||
ln -sfn "$MOUNT_HOME/$item" "$ACTIVE_HOME/$item"
|
||||
done
|
||||
|
||||
@@ -86,6 +86,17 @@ repos:
|
||||
hooks:
|
||||
- id: actionlint
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
exclude: >-
|
||||
(?x)^(
|
||||
backend/scripts/setup_craft_templates\.sh|
|
||||
deployment/docker_compose/init-letsencrypt\.sh|
|
||||
deployment/docker_compose/install\.sh
|
||||
)$
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 8a737e727ac5ab2f1d4cf5876720ed276dc8dc4b # frozen: 25.1.0
|
||||
hooks:
|
||||
|
||||
@@ -212,7 +212,7 @@ def check_for_doc_permissions_sync(self: Task, *, tenant_id: str) -> bool | None
|
||||
# Tenant-work-gating hook: refresh this tenant's active-set membership
|
||||
# whenever doc-permission sync has any due cc_pairs to dispatch.
|
||||
if cc_pair_ids_to_sync:
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="doc_permission_sync")
|
||||
|
||||
lock_beat.reacquire()
|
||||
for cc_pair_id in cc_pair_ids_to_sync:
|
||||
|
||||
@@ -206,7 +206,7 @@ def check_for_external_group_sync(self: Task, *, tenant_id: str) -> bool | None:
|
||||
# Tenant-work-gating hook: refresh this tenant's active-set membership
|
||||
# whenever external-group sync has any due cc_pairs to dispatch.
|
||||
if cc_pair_ids_to_sync:
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="external_group_sync")
|
||||
|
||||
lock_beat.reacquire()
|
||||
for cc_pair_id in cc_pair_ids_to_sync:
|
||||
|
||||
@@ -181,7 +181,7 @@ def check_for_connector_deletion_task(self: Task, *, tenant_id: str) -> bool | N
|
||||
# nearly every tenant in the active set since most have cc_pairs
|
||||
# but almost none are actively being deleted on any given cycle.
|
||||
if has_deleting_cc_pair:
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="connector_deletion")
|
||||
|
||||
# try running cleanup on the cc_pair_ids
|
||||
for cc_pair_id in cc_pair_ids:
|
||||
|
||||
@@ -1020,7 +1020,7 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
|
||||
# `tasks_created > 0` here gives us a "real work was done" signal
|
||||
# rather than just "tenant has a cc_pair somewhere."
|
||||
if tasks_created > 0:
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="check_for_indexing")
|
||||
|
||||
# 2/3: VALIDATE
|
||||
# Check for inconsistent index attempts - active attempts without task IDs
|
||||
|
||||
@@ -263,7 +263,7 @@ def check_for_pruning(self: Task, *, tenant_id: str) -> bool | None:
|
||||
# since most tenants have cc_pairs but almost none are due on
|
||||
# any given cycle.
|
||||
if prune_dispatched:
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="check_for_pruning")
|
||||
r.set(OnyxRedisSignals.BLOCK_PRUNING, 1, ex=_get_pruning_block_expiration())
|
||||
|
||||
# we want to run this less frequently than the overall task
|
||||
|
||||
@@ -153,7 +153,7 @@ def try_generate_stale_document_sync_tasks(
|
||||
|
||||
# Tenant-work-gating hook: refresh this tenant's active-set membership
|
||||
# whenever vespa sync actually has stale docs to dispatch.
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="vespa_sync")
|
||||
|
||||
logger.info(
|
||||
f"Stale documents found (at least {stale_doc_count}). Generating sync tasks in one batch."
|
||||
|
||||
@@ -282,6 +282,7 @@ OPENSEARCH_ADMIN_USERNAME = os.environ.get("OPENSEARCH_ADMIN_USERNAME", "admin")
|
||||
OPENSEARCH_ADMIN_PASSWORD = os.environ.get(
|
||||
"OPENSEARCH_ADMIN_PASSWORD", "StrongPassword123!"
|
||||
)
|
||||
OPENSEARCH_USE_SSL = os.environ.get("OPENSEARCH_USE_SSL", "true").lower() == "true"
|
||||
USING_AWS_MANAGED_OPENSEARCH = (
|
||||
os.environ.get("USING_AWS_MANAGED_OPENSEARCH", "").lower() == "true"
|
||||
)
|
||||
|
||||
@@ -62,17 +62,19 @@ def best_effort_get_field_from_issue(jira_issue: Issue, field: str) -> Any:
|
||||
def extract_text_from_adf(adf: dict | None) -> str:
|
||||
"""Extracts plain text from Atlassian Document Format:
|
||||
https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
|
||||
|
||||
WARNING: This function is incomplete and will e.g. skip lists!
|
||||
"""
|
||||
# TODO: complete this function
|
||||
texts = []
|
||||
if adf is not None and "content" in adf:
|
||||
for block in adf["content"]:
|
||||
if "content" in block:
|
||||
for item in block["content"]:
|
||||
if item["type"] == "text":
|
||||
texts.append(item["text"])
|
||||
texts: list[str] = []
|
||||
|
||||
def _extract(node: dict) -> None:
|
||||
if node.get("type") == "text":
|
||||
text = node.get("text", "")
|
||||
if text:
|
||||
texts.append(text)
|
||||
for child in node.get("content", []):
|
||||
_extract(child)
|
||||
|
||||
if adf is not None:
|
||||
_extract(adf)
|
||||
return " ".join(texts)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from onyx.configs.app_configs import OPENSEARCH_ADMIN_PASSWORD
|
||||
from onyx.configs.app_configs import OPENSEARCH_ADMIN_USERNAME
|
||||
from onyx.configs.app_configs import OPENSEARCH_HOST
|
||||
from onyx.configs.app_configs import OPENSEARCH_REST_API_PORT
|
||||
from onyx.configs.app_configs import OPENSEARCH_USE_SSL
|
||||
from onyx.document_index.interfaces_new import TenantState
|
||||
from onyx.document_index.opensearch.constants import OpenSearchSearchType
|
||||
from onyx.document_index.opensearch.schema import DocumentChunk
|
||||
@@ -132,7 +133,7 @@ class OpenSearchClient(AbstractContextManager):
|
||||
host: str = OPENSEARCH_HOST,
|
||||
port: int = OPENSEARCH_REST_API_PORT,
|
||||
auth: tuple[str, str] = (OPENSEARCH_ADMIN_USERNAME, OPENSEARCH_ADMIN_PASSWORD),
|
||||
use_ssl: bool = True,
|
||||
use_ssl: bool = OPENSEARCH_USE_SSL,
|
||||
verify_certs: bool = False,
|
||||
ssl_show_warn: bool = False,
|
||||
timeout: int = DEFAULT_OPENSEARCH_CLIENT_TIMEOUT_S,
|
||||
@@ -302,7 +303,7 @@ class OpenSearchIndexClient(OpenSearchClient):
|
||||
host: str = OPENSEARCH_HOST,
|
||||
port: int = OPENSEARCH_REST_API_PORT,
|
||||
auth: tuple[str, str] = (OPENSEARCH_ADMIN_USERNAME, OPENSEARCH_ADMIN_PASSWORD),
|
||||
use_ssl: bool = True,
|
||||
use_ssl: bool = OPENSEARCH_USE_SSL,
|
||||
verify_certs: bool = False,
|
||||
ssl_show_warn: bool = False,
|
||||
timeout: int = DEFAULT_OPENSEARCH_CLIENT_TIMEOUT_S,
|
||||
@@ -507,8 +508,55 @@ class OpenSearchIndexClient(OpenSearchClient):
|
||||
Raises:
|
||||
Exception: There was an error updating the settings of the index.
|
||||
"""
|
||||
# TODO(andrei): Implement this.
|
||||
raise NotImplementedError
|
||||
logger.debug(f"Updating settings of index {self._index_name} with {settings}.")
|
||||
response = self._client.indices.put_settings(
|
||||
index=self._index_name, body=settings
|
||||
)
|
||||
if not response.get("acknowledged", False):
|
||||
raise RuntimeError(
|
||||
f"Failed to update settings of index {self._index_name}."
|
||||
)
|
||||
logger.debug(f"Settings of index {self._index_name} updated successfully.")
|
||||
|
||||
@log_function_time(print_only=True, debug_only=True)
|
||||
def get_settings(self) -> dict[str, Any]:
|
||||
"""Gets the settings of the index.
|
||||
|
||||
Returns:
|
||||
The settings of the index.
|
||||
|
||||
Raises:
|
||||
Exception: There was an error getting the settings of the index.
|
||||
"""
|
||||
logger.debug(f"Getting settings of index {self._index_name}.")
|
||||
response = self._client.indices.get_settings(index=self._index_name)
|
||||
return response[self._index_name]["settings"]
|
||||
|
||||
@log_function_time(print_only=True, debug_only=True)
|
||||
def open_index(self) -> None:
|
||||
"""Opens the index.
|
||||
|
||||
Raises:
|
||||
Exception: There was an error opening the index.
|
||||
"""
|
||||
logger.debug(f"Opening index {self._index_name}.")
|
||||
response = self._client.indices.open(index=self._index_name)
|
||||
if not response.get("acknowledged", False):
|
||||
raise RuntimeError(f"Failed to open index {self._index_name}.")
|
||||
logger.debug(f"Index {self._index_name} opened successfully.")
|
||||
|
||||
@log_function_time(print_only=True, debug_only=True)
|
||||
def close_index(self) -> None:
|
||||
"""Closes the index.
|
||||
|
||||
Raises:
|
||||
Exception: There was an error closing the index.
|
||||
"""
|
||||
logger.debug(f"Closing index {self._index_name}.")
|
||||
response = self._client.indices.close(index=self._index_name)
|
||||
if not response.get("acknowledged", False):
|
||||
raise RuntimeError(f"Failed to close index {self._index_name}.")
|
||||
logger.debug(f"Index {self._index_name} closed successfully.")
|
||||
|
||||
@log_function_time(
|
||||
print_only=True,
|
||||
|
||||
@@ -5,6 +5,7 @@ import uvicorn
|
||||
from onyx.configs.app_configs import MCP_SERVER_ENABLED
|
||||
from onyx.configs.app_configs import MCP_SERVER_HOST
|
||||
from onyx.configs.app_configs import MCP_SERVER_PORT
|
||||
from onyx.tracing.setup import setup_tracing
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.variable_functionality import set_is_ee_based_on_env_variable
|
||||
|
||||
@@ -18,6 +19,7 @@ def main() -> None:
|
||||
return
|
||||
|
||||
set_is_ee_based_on_env_variable()
|
||||
setup_tracing()
|
||||
logger.info(f"Starting MCP server on {MCP_SERVER_HOST}:{MCP_SERVER_PORT}")
|
||||
|
||||
from onyx.mcp_server.api import mcp_app
|
||||
|
||||
@@ -584,7 +584,7 @@ def associate_credential_to_connector(
|
||||
|
||||
# Tenant-work-gating lifecycle hook: keep new-tenant latency to
|
||||
# seconds instead of one full-fanout interval.
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="cc_pair_lifecycle")
|
||||
|
||||
# trigger indexing immediately
|
||||
client_app.send_task(
|
||||
|
||||
@@ -1643,7 +1643,7 @@ def create_connector_with_mock_credential(
|
||||
|
||||
# Tenant-work-gating lifecycle hook: keep new-tenant latency to
|
||||
# seconds instead of one full-fanout interval.
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="cc_pair_lifecycle")
|
||||
|
||||
# trigger indexing immediately
|
||||
client_app.send_task(
|
||||
|
||||
@@ -113,7 +113,7 @@ def cleanup_idle_sandboxes_task(self: Task, *, tenant_id: str) -> None: # noqa:
|
||||
|
||||
# Tenant-work-gating hook: refresh this tenant's active-set
|
||||
# membership whenever sandbox cleanup has work to do.
|
||||
maybe_mark_tenant_active(tenant_id)
|
||||
maybe_mark_tenant_active(tenant_id, caller="sandbox_cleanup")
|
||||
|
||||
task_logger.info(
|
||||
f"Found {len(idle_sandboxes)} idle sandboxes to put to sleep"
|
||||
|
||||
151
backend/onyx/server/metrics/embedding.py
Normal file
151
backend/onyx/server/metrics/embedding.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Prometheus metrics for embedding generation latency and throughput.
|
||||
|
||||
Tracks client-side round-trip latency (as seen by callers of
|
||||
``EmbeddingModel.encode``) and server-side execution time (as measured inside
|
||||
the model server for the local-model path). Both API-provider and local-model
|
||||
paths flow through the client-side metric; only the local path populates the
|
||||
server-side metric.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
|
||||
from prometheus_client import Counter
|
||||
from prometheus_client import Gauge
|
||||
from prometheus_client import Histogram
|
||||
|
||||
from shared_configs.enums import EmbeddingProvider
|
||||
from shared_configs.enums import EmbedTextType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LOCAL_PROVIDER_LABEL = "local"
|
||||
|
||||
_EMBEDDING_LATENCY_BUCKETS = (
|
||||
0.005,
|
||||
0.01,
|
||||
0.025,
|
||||
0.05,
|
||||
0.1,
|
||||
0.25,
|
||||
0.5,
|
||||
1.0,
|
||||
2.5,
|
||||
5.0,
|
||||
10.0,
|
||||
25.0,
|
||||
)
|
||||
|
||||
PROVIDER_LABEL_NAME = "provider"
|
||||
TEXT_TYPE_LABEL_NAME = "text_type"
|
||||
STATUS_LABEL_NAME = "status"
|
||||
|
||||
_client_duration = Histogram(
|
||||
"onyx_embedding_client_duration_seconds",
|
||||
"Client-side end-to-end latency of an embedding batch as seen by the caller.",
|
||||
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
|
||||
buckets=_EMBEDDING_LATENCY_BUCKETS,
|
||||
)
|
||||
|
||||
_embedding_requests_total = Counter(
|
||||
"onyx_embedding_requests_total",
|
||||
"Total embedding batch requests, labeled by outcome.",
|
||||
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME, STATUS_LABEL_NAME],
|
||||
)
|
||||
|
||||
_embedding_texts_total = Counter(
|
||||
"onyx_embedding_texts_total",
|
||||
"Total number of individual texts submitted for embedding.",
|
||||
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
|
||||
)
|
||||
|
||||
_embedding_input_chars_total = Counter(
|
||||
"onyx_embedding_input_chars_total",
|
||||
"Total number of input characters submitted for embedding.",
|
||||
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
|
||||
)
|
||||
|
||||
_embeddings_in_progress = Gauge(
|
||||
"onyx_embeddings_in_progress",
|
||||
"Number of embedding batches currently in-flight.",
|
||||
[PROVIDER_LABEL_NAME, TEXT_TYPE_LABEL_NAME],
|
||||
)
|
||||
|
||||
|
||||
def provider_label(provider: EmbeddingProvider | None) -> str:
|
||||
if provider is None:
|
||||
return LOCAL_PROVIDER_LABEL
|
||||
return provider.value
|
||||
|
||||
|
||||
def observe_embedding_client(
|
||||
provider: EmbeddingProvider | None,
|
||||
text_type: EmbedTextType,
|
||||
duration_s: float,
|
||||
num_texts: int,
|
||||
num_chars: int,
|
||||
success: bool,
|
||||
) -> None:
|
||||
"""Records a completed embedding batch.
|
||||
|
||||
Args:
|
||||
provider: The embedding provider, or ``None`` for the local model path.
|
||||
text_type: Whether this was a query- or passage-style embedding.
|
||||
duration_s: Wall-clock duration measured on the client side, in seconds.
|
||||
num_texts: Number of texts in the batch.
|
||||
num_chars: Total number of input characters in the batch.
|
||||
success: Whether the embedding call succeeded.
|
||||
"""
|
||||
try:
|
||||
provider_lbl = provider_label(provider)
|
||||
text_type_lbl = text_type.value
|
||||
status_lbl = "success" if success else "failure"
|
||||
|
||||
_embedding_requests_total.labels(
|
||||
provider=provider_lbl, text_type=text_type_lbl, status=status_lbl
|
||||
).inc()
|
||||
_client_duration.labels(provider=provider_lbl, text_type=text_type_lbl).observe(
|
||||
duration_s
|
||||
)
|
||||
if success:
|
||||
_embedding_texts_total.labels(
|
||||
provider=provider_lbl, text_type=text_type_lbl
|
||||
).inc(num_texts)
|
||||
_embedding_input_chars_total.labels(
|
||||
provider=provider_lbl, text_type=text_type_lbl
|
||||
).inc(num_chars)
|
||||
except Exception:
|
||||
logger.warning("Failed to record embedding client metrics.", exc_info=True)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def track_embedding_in_progress(
|
||||
provider: EmbeddingProvider | None,
|
||||
text_type: EmbedTextType,
|
||||
) -> Generator[None, None, None]:
|
||||
"""Context manager that tracks in-flight embedding batches via a Gauge."""
|
||||
incremented = False
|
||||
provider_lbl = provider_label(provider)
|
||||
text_type_lbl = text_type.value
|
||||
try:
|
||||
_embeddings_in_progress.labels(
|
||||
provider=provider_lbl, text_type=text_type_lbl
|
||||
).inc()
|
||||
incremented = True
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to increment in-progress embedding gauge.", exc_info=True
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if incremented:
|
||||
try:
|
||||
_embeddings_in_progress.labels(
|
||||
provider=provider_lbl, text_type=text_type_lbl
|
||||
).dec()
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to decrement in-progress embedding gauge.", exc_info=True
|
||||
)
|
||||
@@ -46,7 +46,7 @@ stop_and_remove_containers
|
||||
# Start the PostgreSQL container with optional volume
|
||||
echo "Starting PostgreSQL container..."
|
||||
if [[ -n "$POSTGRES_VOLUME" ]]; then
|
||||
docker run -p 5432:5432 --name onyx_postgres -e POSTGRES_PASSWORD=password -d -v $POSTGRES_VOLUME:/var/lib/postgresql/data postgres -c max_connections=250
|
||||
docker run -p 5432:5432 --name onyx_postgres -e POSTGRES_PASSWORD=password -d -v "$POSTGRES_VOLUME":/var/lib/postgresql/data postgres -c max_connections=250
|
||||
else
|
||||
docker run -p 5432:5432 --name onyx_postgres -e POSTGRES_PASSWORD=password -d postgres -c max_connections=250
|
||||
fi
|
||||
@@ -54,7 +54,7 @@ fi
|
||||
# Start the Vespa container with optional volume
|
||||
echo "Starting Vespa container..."
|
||||
if [[ -n "$VESPA_VOLUME" ]]; then
|
||||
docker run --detach --name onyx_vespa --hostname vespa-container --publish 8081:8081 --publish 19071:19071 -v $VESPA_VOLUME:/opt/vespa/var vespaengine/vespa:8
|
||||
docker run --detach --name onyx_vespa --hostname vespa-container --publish 8081:8081 --publish 19071:19071 -v "$VESPA_VOLUME":/opt/vespa/var vespaengine/vespa:8
|
||||
else
|
||||
docker run --detach --name onyx_vespa --hostname vespa-container --publish 8081:8081 --publish 19071:19071 vespaengine/vespa:8
|
||||
fi
|
||||
@@ -85,7 +85,7 @@ docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_DEV_FILE" --profile opensearch-en
|
||||
# Start the Redis container with optional volume
|
||||
echo "Starting Redis container..."
|
||||
if [[ -n "$REDIS_VOLUME" ]]; then
|
||||
docker run --detach --name onyx_redis --publish 6379:6379 -v $REDIS_VOLUME:/data redis
|
||||
docker run --detach --name onyx_redis --publish 6379:6379 -v "$REDIS_VOLUME":/data redis
|
||||
else
|
||||
docker run --detach --name onyx_redis --publish 6379:6379 redis
|
||||
fi
|
||||
@@ -93,7 +93,7 @@ fi
|
||||
# Start the MinIO container with optional volume
|
||||
echo "Starting MinIO container..."
|
||||
if [[ -n "$MINIO_VOLUME" ]]; then
|
||||
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin -v $MINIO_VOLUME:/data minio/minio server /data --console-address ":9001"
|
||||
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin -v "$MINIO_VOLUME":/data minio/minio server /data --console-address ":9001"
|
||||
else
|
||||
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data --console-address ":9001"
|
||||
fi
|
||||
@@ -111,6 +111,7 @@ sleep 1
|
||||
|
||||
# Alembic should be configured in the virtualenv for this repo
|
||||
if [[ -f "../.venv/bin/activate" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source ../.venv/bin/activate
|
||||
else
|
||||
echo "Warning: Python virtual environment not found at .venv/bin/activate; alembic may not work."
|
||||
|
||||
@@ -446,10 +446,107 @@ class TestOpenSearchClient:
|
||||
test_client.create_index(mappings=mappings, settings=settings)
|
||||
|
||||
def test_update_settings(self, test_client: OpenSearchIndexClient) -> None:
|
||||
"""Tests that update_settings raises NotImplementedError."""
|
||||
"""Tests updating index settings on an existing index."""
|
||||
# Precondition.
|
||||
mappings = DocumentSchema.get_document_schema(
|
||||
vector_dimension=128, multitenant=True
|
||||
)
|
||||
settings = DocumentSchema.get_index_settings_based_on_environment()
|
||||
test_client.create_index(mappings=mappings, settings=settings)
|
||||
# Assert that the current number of replicas is not the desired test
|
||||
# number we are updating to.
|
||||
test_num_replicas = 0
|
||||
current_settings = test_client.get_settings()
|
||||
assert current_settings["index"]["number_of_replicas"] != f"{test_num_replicas}"
|
||||
|
||||
# Under test.
|
||||
# Should not raise. number_of_replicas is a dynamic setting that can be
|
||||
# changed without closing the index.
|
||||
test_client.update_settings(
|
||||
settings={"index": {"number_of_replicas": test_num_replicas}}
|
||||
)
|
||||
|
||||
# Postcondition.
|
||||
current_settings = test_client.get_settings()
|
||||
assert current_settings["index"]["number_of_replicas"] == f"{test_num_replicas}"
|
||||
|
||||
def test_update_settings_on_nonexistent_index(
|
||||
self, test_client: OpenSearchIndexClient
|
||||
) -> None:
|
||||
"""Tests updating settings on a nonexistent index raises an error."""
|
||||
# Under test and postcondition.
|
||||
with pytest.raises(NotImplementedError):
|
||||
test_client.update_settings(settings={})
|
||||
with pytest.raises(Exception, match="index_not_found_exception|404"):
|
||||
test_client.update_settings(settings={"index": {"number_of_replicas": 0}})
|
||||
|
||||
def test_get_settings(self, test_client: OpenSearchIndexClient) -> None:
|
||||
"""Tests getting index settings."""
|
||||
# Precondition.
|
||||
mappings = DocumentSchema.get_document_schema(
|
||||
vector_dimension=128, multitenant=True
|
||||
)
|
||||
settings = DocumentSchema.get_index_settings_based_on_environment()
|
||||
test_client.create_index(mappings=mappings, settings=settings)
|
||||
|
||||
# Under test.
|
||||
current_settings = test_client.get_settings()
|
||||
|
||||
# Postcondition.
|
||||
assert "index" in current_settings
|
||||
# These are always present for any index.
|
||||
assert "number_of_shards" in current_settings["index"]
|
||||
assert "number_of_replicas" in current_settings["index"]
|
||||
assert current_settings["index"]["provided_name"] == test_client._index_name
|
||||
|
||||
def test_get_settings_on_nonexistent_index(
|
||||
self, test_client: OpenSearchIndexClient
|
||||
) -> None:
|
||||
"""Tests getting settings on a nonexistent index raises an error."""
|
||||
# Under test and postcondition.
|
||||
with pytest.raises(Exception, match="index_not_found_exception|404"):
|
||||
test_client.get_settings()
|
||||
|
||||
def test_close_and_open_index(self, test_client: OpenSearchIndexClient) -> None:
|
||||
"""Tests closing and reopening an index."""
|
||||
# Precondition.
|
||||
mappings = DocumentSchema.get_document_schema(
|
||||
vector_dimension=128, multitenant=True
|
||||
)
|
||||
settings = DocumentSchema.get_index_settings_based_on_environment()
|
||||
test_client.create_index(mappings=mappings, settings=settings)
|
||||
|
||||
# Under test.
|
||||
# Closing should not raise.
|
||||
test_client.close_index()
|
||||
|
||||
# Postcondition.
|
||||
# Searches on a closed index should fail.
|
||||
with pytest.raises(Exception, match="index_closed_exception|closed"):
|
||||
test_client.search_for_document_ids(
|
||||
body={"_source": False, "query": {"match_all": {}}}
|
||||
)
|
||||
|
||||
# Under test.
|
||||
# Reopening should not raise.
|
||||
test_client.open_index()
|
||||
|
||||
# Postcondition.
|
||||
# Searches should work again after reopening.
|
||||
result = test_client.search_for_document_ids(
|
||||
body={"_source": False, "query": {"match_all": {}}}
|
||||
)
|
||||
assert result == []
|
||||
|
||||
def test_close_nonexistent_index(self, test_client: OpenSearchIndexClient) -> None:
|
||||
"""Tests closing a nonexistent index raises an error."""
|
||||
# Under test and postcondition.
|
||||
with pytest.raises(Exception, match="index_not_found_exception|404"):
|
||||
test_client.close_index()
|
||||
|
||||
def test_open_nonexistent_index(self, test_client: OpenSearchIndexClient) -> None:
|
||||
"""Tests opening a nonexistent index raises an error."""
|
||||
# Under test and postcondition.
|
||||
with pytest.raises(Exception, match="index_not_found_exception|404"):
|
||||
test_client.open_index()
|
||||
|
||||
def test_create_and_delete_search_pipeline(
|
||||
self, test_client: OpenSearchIndexClient
|
||||
|
||||
257
backend/tests/unit/server/metrics/test_embedding_metrics.py
Normal file
257
backend/tests/unit/server/metrics/test_embedding_metrics.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""Tests for embedding Prometheus metrics."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from onyx.server.metrics.embedding import _client_duration
|
||||
from onyx.server.metrics.embedding import _embedding_input_chars_total
|
||||
from onyx.server.metrics.embedding import _embedding_requests_total
|
||||
from onyx.server.metrics.embedding import _embedding_texts_total
|
||||
from onyx.server.metrics.embedding import _embeddings_in_progress
|
||||
from onyx.server.metrics.embedding import LOCAL_PROVIDER_LABEL
|
||||
from onyx.server.metrics.embedding import observe_embedding_client
|
||||
from onyx.server.metrics.embedding import provider_label
|
||||
from onyx.server.metrics.embedding import PROVIDER_LABEL_NAME
|
||||
from onyx.server.metrics.embedding import TEXT_TYPE_LABEL_NAME
|
||||
from onyx.server.metrics.embedding import track_embedding_in_progress
|
||||
from shared_configs.enums import EmbeddingProvider
|
||||
from shared_configs.enums import EmbedTextType
|
||||
|
||||
|
||||
class TestProviderLabel:
|
||||
def test_none_maps_to_local(self) -> None:
|
||||
assert provider_label(None) == LOCAL_PROVIDER_LABEL
|
||||
|
||||
def test_enum_maps_to_value(self) -> None:
|
||||
assert provider_label(EmbeddingProvider.OPENAI) == "openai"
|
||||
assert provider_label(EmbeddingProvider.COHERE) == "cohere"
|
||||
|
||||
|
||||
class TestObserveEmbeddingClient:
|
||||
def test_success_records_all_counters(self) -> None:
|
||||
# Precondition.
|
||||
provider = EmbeddingProvider.OPENAI
|
||||
text_type = EmbedTextType.QUERY
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: provider.value,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
|
||||
before_requests = _embedding_requests_total.labels(
|
||||
**labels, status="success"
|
||||
)._value.get()
|
||||
before_texts = _embedding_texts_total.labels(**labels)._value.get()
|
||||
before_chars = _embedding_input_chars_total.labels(**labels)._value.get()
|
||||
before_duration_sum = _client_duration.labels(**labels)._sum.get()
|
||||
|
||||
test_duration_s = 0.123
|
||||
test_num_texts = 4
|
||||
test_num_chars = 200
|
||||
|
||||
# Under test.
|
||||
observe_embedding_client(
|
||||
provider=provider,
|
||||
text_type=text_type,
|
||||
duration_s=test_duration_s,
|
||||
num_texts=test_num_texts,
|
||||
num_chars=test_num_chars,
|
||||
success=True,
|
||||
)
|
||||
|
||||
# Postcondition.
|
||||
assert (
|
||||
_embedding_requests_total.labels(**labels, status="success")._value.get()
|
||||
== before_requests + 1
|
||||
)
|
||||
assert (
|
||||
_embedding_texts_total.labels(**labels)._value.get()
|
||||
== before_texts + test_num_texts
|
||||
)
|
||||
assert (
|
||||
_embedding_input_chars_total.labels(**labels)._value.get()
|
||||
== before_chars + test_num_chars
|
||||
)
|
||||
assert (
|
||||
_client_duration.labels(**labels)._sum.get()
|
||||
== before_duration_sum + test_duration_s
|
||||
)
|
||||
|
||||
def test_failure_records_duration_and_failure_counter_only(self) -> None:
|
||||
# Precondition.
|
||||
provider = EmbeddingProvider.COHERE
|
||||
text_type = EmbedTextType.PASSAGE
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: provider.value,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
|
||||
before_failure = _embedding_requests_total.labels(
|
||||
**labels, status="failure"
|
||||
)._value.get()
|
||||
before_texts = _embedding_texts_total.labels(**labels)._value.get()
|
||||
before_chars = _embedding_input_chars_total.labels(**labels)._value.get()
|
||||
before_duration_sum = _client_duration.labels(**labels)._sum.get()
|
||||
|
||||
test_duration_s = 0.5
|
||||
test_num_texts = 3
|
||||
test_num_chars = 150
|
||||
|
||||
# Under test.
|
||||
observe_embedding_client(
|
||||
provider=provider,
|
||||
text_type=text_type,
|
||||
duration_s=test_duration_s,
|
||||
num_texts=test_num_texts,
|
||||
num_chars=test_num_chars,
|
||||
success=False,
|
||||
)
|
||||
|
||||
# Postcondition.
|
||||
# Failure counter incremented.
|
||||
assert (
|
||||
_embedding_requests_total.labels(**labels, status="failure")._value.get()
|
||||
== before_failure + 1
|
||||
)
|
||||
# Duration still recorded.
|
||||
assert (
|
||||
_client_duration.labels(**labels)._sum.get()
|
||||
== before_duration_sum + test_duration_s
|
||||
)
|
||||
# Throughput counters NOT bumped on failure.
|
||||
assert _embedding_texts_total.labels(**labels)._value.get() == before_texts
|
||||
assert (
|
||||
_embedding_input_chars_total.labels(**labels)._value.get() == before_chars
|
||||
)
|
||||
|
||||
def test_local_provider_uses_local_label(self) -> None:
|
||||
# Precondition.
|
||||
text_type = EmbedTextType.QUERY
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: LOCAL_PROVIDER_LABEL,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
before = _embedding_requests_total.labels(
|
||||
**labels, status="success"
|
||||
)._value.get()
|
||||
|
||||
test_duration_s = 0.05
|
||||
test_num_texts = 1
|
||||
test_num_chars = 10
|
||||
|
||||
# Under test.
|
||||
observe_embedding_client(
|
||||
provider=None,
|
||||
text_type=text_type,
|
||||
duration_s=test_duration_s,
|
||||
num_texts=test_num_texts,
|
||||
num_chars=test_num_chars,
|
||||
success=True,
|
||||
)
|
||||
|
||||
# Postcondition.
|
||||
assert (
|
||||
_embedding_requests_total.labels(**labels, status="success")._value.get()
|
||||
== before + 1
|
||||
)
|
||||
|
||||
def test_exceptions_do_not_propagate(self) -> None:
|
||||
with patch.object(
|
||||
_embedding_requests_total,
|
||||
"labels",
|
||||
side_effect=RuntimeError("boom"),
|
||||
):
|
||||
# Must not raise.
|
||||
observe_embedding_client(
|
||||
provider=EmbeddingProvider.OPENAI,
|
||||
text_type=EmbedTextType.QUERY,
|
||||
duration_s=0.1,
|
||||
num_texts=1,
|
||||
num_chars=10,
|
||||
success=True,
|
||||
)
|
||||
|
||||
|
||||
class TestTrackEmbeddingInProgress:
|
||||
def test_gauge_increments_and_decrements(self) -> None:
|
||||
# Precondition.
|
||||
provider = EmbeddingProvider.OPENAI
|
||||
text_type = EmbedTextType.QUERY
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: provider.value,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
before = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
|
||||
# Under test.
|
||||
with track_embedding_in_progress(provider, text_type):
|
||||
during = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert during == before + 1
|
||||
|
||||
# Postcondition.
|
||||
after = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert after == before
|
||||
|
||||
def test_gauge_decrements_on_exception(self) -> None:
|
||||
# Precondition.
|
||||
provider = EmbeddingProvider.COHERE
|
||||
text_type = EmbedTextType.PASSAGE
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: provider.value,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
before = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
|
||||
# Under test.
|
||||
raised = False
|
||||
try:
|
||||
with track_embedding_in_progress(provider, text_type):
|
||||
raise ValueError("simulated embedding failure")
|
||||
except ValueError:
|
||||
raised = True
|
||||
assert raised
|
||||
|
||||
# Postcondition.
|
||||
after = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert after == before
|
||||
|
||||
def test_local_provider_uses_local_label(self) -> None:
|
||||
# Precondition.
|
||||
text_type = EmbedTextType.QUERY
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: LOCAL_PROVIDER_LABEL,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
before = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
|
||||
# Under test.
|
||||
with track_embedding_in_progress(None, text_type):
|
||||
during = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert during == before + 1
|
||||
|
||||
# Postcondition.
|
||||
after = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert after == before
|
||||
|
||||
def test_inc_exception_does_not_break_call(self) -> None:
|
||||
# Precondition.
|
||||
provider = EmbeddingProvider.VOYAGE
|
||||
text_type = EmbedTextType.QUERY
|
||||
labels = {
|
||||
PROVIDER_LABEL_NAME: provider.value,
|
||||
TEXT_TYPE_LABEL_NAME: text_type.value,
|
||||
}
|
||||
before = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
|
||||
# Under test.
|
||||
with patch.object(
|
||||
_embeddings_in_progress.labels(**labels),
|
||||
"inc",
|
||||
side_effect=RuntimeError("boom"),
|
||||
):
|
||||
# Context manager should still yield without decrementing.
|
||||
with track_embedding_in_progress(provider, text_type):
|
||||
during = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert during == before
|
||||
|
||||
# Postcondition.
|
||||
after = _embeddings_in_progress.labels(**labels)._value.get()
|
||||
assert after == before
|
||||
@@ -58,8 +58,7 @@ SERVICE_ORDER=(
|
||||
validate_template() {
|
||||
local template_file=$1
|
||||
echo "Validating template: $template_file..."
|
||||
aws cloudformation validate-template --template-body file://"$template_file" --region "$AWS_REGION" > /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
if ! aws cloudformation validate-template --template-body file://"$template_file" --region "$AWS_REGION" > /dev/null; then
|
||||
echo "Error: Validation failed for $template_file. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
@@ -108,13 +107,15 @@ deploy_stack() {
|
||||
fi
|
||||
|
||||
# Create temporary parameters file for this template
|
||||
local temp_params_file=$(create_parameters_from_json "$template_file")
|
||||
local temp_params_file
|
||||
temp_params_file=$(create_parameters_from_json "$template_file")
|
||||
|
||||
# Special handling for SubnetIDs parameter if needed
|
||||
if grep -q "SubnetIDs" "$template_file"; then
|
||||
echo "Template uses SubnetIDs parameter, ensuring it's properly formatted..."
|
||||
# Make sure we're passing SubnetIDs as a comma-separated list
|
||||
local subnet_ids=$(remove_comments "$CONFIG_FILE" | jq -r '.SubnetIDs // empty')
|
||||
local subnet_ids
|
||||
subnet_ids=$(remove_comments "$CONFIG_FILE" | jq -r '.SubnetIDs // empty')
|
||||
if [ -n "$subnet_ids" ]; then
|
||||
echo "Using SubnetIDs from config: $subnet_ids"
|
||||
else
|
||||
@@ -123,15 +124,13 @@ deploy_stack() {
|
||||
fi
|
||||
|
||||
echo "Deploying stack: $stack_name with template: $template_file and generated config from: $CONFIG_FILE..."
|
||||
aws cloudformation deploy \
|
||||
if ! aws cloudformation deploy \
|
||||
--stack-name "$stack_name" \
|
||||
--template-file "$template_file" \
|
||||
--parameter-overrides file://"$temp_params_file" \
|
||||
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
|
||||
--region "$AWS_REGION" \
|
||||
--no-cli-auto-prompt > /dev/null
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
--no-cli-auto-prompt > /dev/null; then
|
||||
echo "Error: Deployment failed for $stack_name. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -52,11 +52,9 @@ delete_stack() {
|
||||
--region "$AWS_REGION"
|
||||
|
||||
echo "Waiting for stack $stack_name to be deleted..."
|
||||
aws cloudformation wait stack-delete-complete \
|
||||
if aws cloudformation wait stack-delete-complete \
|
||||
--stack-name "$stack_name" \
|
||||
--region "$AWS_REGION"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
--region "$AWS_REGION"; then
|
||||
echo "Stack $stack_name deleted successfully."
|
||||
sleep 10
|
||||
else
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/sh
|
||||
# fill in the template
|
||||
export ONYX_BACKEND_API_HOST="${ONYX_BACKEND_API_HOST:-api_server}"
|
||||
export ONYX_WEB_SERVER_HOST="${ONYX_WEB_SERVER_HOST:-web_server}"
|
||||
@@ -16,12 +17,15 @@ echo "Using web server host: $ONYX_WEB_SERVER_HOST"
|
||||
echo "Using MCP server host: $ONYX_MCP_SERVER_HOST"
|
||||
echo "Using nginx proxy timeouts - connect: ${NGINX_PROXY_CONNECT_TIMEOUT}s, send: ${NGINX_PROXY_SEND_TIMEOUT}s, read: ${NGINX_PROXY_READ_TIMEOUT}s"
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
envsubst '$DOMAIN $SSL_CERT_FILE_NAME $SSL_CERT_KEY_FILE_NAME $ONYX_BACKEND_API_HOST $ONYX_WEB_SERVER_HOST $ONYX_MCP_SERVER_HOST $NGINX_PROXY_CONNECT_TIMEOUT $NGINX_PROXY_SEND_TIMEOUT $NGINX_PROXY_READ_TIMEOUT' < "/etc/nginx/conf.d/$1" > /etc/nginx/conf.d/app.conf
|
||||
|
||||
# Conditionally create MCP server configuration
|
||||
if [ "${MCP_SERVER_ENABLED}" = "True" ] || [ "${MCP_SERVER_ENABLED}" = "true" ]; then
|
||||
echo "MCP server is enabled, creating MCP configuration..."
|
||||
# shellcheck disable=SC2016
|
||||
envsubst '$ONYX_MCP_SERVER_HOST' < "/etc/nginx/conf.d/mcp_upstream.conf.inc.template" > /etc/nginx/conf.d/mcp_upstream.conf.inc
|
||||
# shellcheck disable=SC2016
|
||||
envsubst '$ONYX_MCP_SERVER_HOST' < "/etc/nginx/conf.d/mcp.conf.inc.template" > /etc/nginx/conf.d/mcp.conf.inc
|
||||
else
|
||||
echo "MCP server is disabled, removing MCP configuration..."
|
||||
|
||||
@@ -48,6 +48,19 @@ func runWebScript(args []string) {
|
||||
log.Fatalf("Failed to find web directory: %v", err)
|
||||
}
|
||||
|
||||
nodeModules := filepath.Join(webDir, "node_modules")
|
||||
if _, err := os.Stat(nodeModules); os.IsNotExist(err) {
|
||||
log.Info("node_modules not found, running npm install --no-save...")
|
||||
installCmd := exec.Command("npm", "install", "--no-save")
|
||||
installCmd.Dir = webDir
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
installCmd.Stdin = os.Stdin
|
||||
if err := installCmd.Run(); err != nil {
|
||||
log.Fatalf("Failed to run npm install: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
scriptName := args[0]
|
||||
scriptArgs := args[1:]
|
||||
if len(scriptArgs) > 0 && scriptArgs[0] == "--" {
|
||||
|
||||
@@ -55,7 +55,7 @@ A two-axis layout component that automatically routes to the correct internal la
|
||||
|
||||
Wraps `Content` and adds a `rightChildren` slot. Accepts all `Content` props plus:
|
||||
- `rightChildren`: `ReactNode` — actions rendered on the right
|
||||
- `paddingVariant`: `SizeVariant` — controls outer padding
|
||||
- `padding`: `SizeVariant` — controls outer padding
|
||||
|
||||
```typescript
|
||||
<ContentAction
|
||||
@@ -544,7 +544,7 @@ function UserCard({
|
||||
## 4. Spacing Guidelines
|
||||
|
||||
**Prefer padding over margins for spacing. When a library component exposes a padding prop
|
||||
(e.g., `paddingVariant`), use that prop instead of wrapping it in a `<div>` with padding classes.
|
||||
(e.g., `padding`), use that prop instead of wrapping it in a `<div>` with padding classes.
|
||||
If a library component does not expose a padding override and you find yourself adding a wrapper
|
||||
div for spacing, consider updating the library component to accept one.**
|
||||
|
||||
@@ -553,7 +553,7 @@ divs that exist solely for spacing.
|
||||
|
||||
```typescript
|
||||
// ✅ Good — use the component's padding prop
|
||||
<ContentAction paddingVariant="md" ... />
|
||||
<ContentAction padding="md" ... />
|
||||
|
||||
// ✅ Good — padding utilities when no component prop exists
|
||||
<div className="p-4 space-y-2">
|
||||
|
||||
@@ -68,9 +68,7 @@ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# Run the conversion into a temp file so a failed run doesn't destroy an existing .tsx
|
||||
TMPFILE="${BASE_NAME}.tsx.tmp"
|
||||
bunx @svgr/cli "$SVG_FILE" --typescript --svgo-config "$SVGO_CONFIG" --template "${SCRIPT_DIR}/icon-template.js" > "$TMPFILE"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
if bunx @svgr/cli "$SVG_FILE" --typescript --svgo-config "$SVGO_CONFIG" --template "${SCRIPT_DIR}/icon-template.js" > "$TMPFILE"; then
|
||||
# Verify the temp file has content before replacing the destination
|
||||
if [ ! -s "$TMPFILE" ]; then
|
||||
rm -f "$TMPFILE"
|
||||
@@ -84,16 +82,14 @@ if [ $? -eq 0 ]; then
|
||||
# Using perl for cross-platform compatibility (works on macOS, Linux, Windows with WSL)
|
||||
# Note: perl -i returns 0 even on some failures, so we validate the output
|
||||
|
||||
perl -i -pe 's/<svg/<svg width={size} height={size}/g' "${BASE_NAME}.tsx"
|
||||
if [ $? -ne 0 ]; then
|
||||
if ! perl -i -pe 's/<svg/<svg width={size} height={size}/g' "${BASE_NAME}.tsx"; then
|
||||
echo "Error: Failed to add width/height attributes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Icons additionally get stroke="currentColor"
|
||||
if [ "$MODE" = "icon" ]; then
|
||||
perl -i -pe 's/\{\.\.\.props\}/stroke="currentColor" {...props}/g' "${BASE_NAME}.tsx"
|
||||
if [ $? -ne 0 ]; then
|
||||
if ! perl -i -pe 's/\{\.\.\.props\}/stroke="currentColor" {...props}/g' "${BASE_NAME}.tsx"; then
|
||||
echo "Error: Failed to add stroke attribute" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -95,9 +95,9 @@ function Button({
|
||||
<Interactive.Container
|
||||
type={type}
|
||||
border={interactiveProps.prominence === "secondary"}
|
||||
heightVariant={size}
|
||||
widthVariant={width}
|
||||
roundingVariant={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
|
||||
size={size}
|
||||
width={width}
|
||||
rounding={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{iconWrapper(Icon, size, !!children)}
|
||||
|
||||
@@ -8,13 +8,13 @@ A composite component that wraps `Interactive.Stateful > Interactive.Container >
|
||||
|
||||
```
|
||||
Interactive.Stateful <- selectVariant, state, interaction, onClick, href, ref
|
||||
└─ Interactive.Container <- type, width, roundingVariant
|
||||
└─ ContentAction <- withInteractive, paddingVariant="lg"
|
||||
└─ Interactive.Container <- type, width, rounding
|
||||
└─ ContentAction <- withInteractive, padding="lg"
|
||||
├─ Content <- icon, title, description, sizePreset, variant, ...
|
||||
└─ rightChildren
|
||||
```
|
||||
|
||||
`paddingVariant` is hardcoded to `"lg"` and `withInteractive` is always `true`. These are not exposed as props.
|
||||
`padding` is hardcoded to `"lg"` and `withInteractive` is always `true`. These are not exposed as props.
|
||||
|
||||
## Props
|
||||
|
||||
@@ -35,7 +35,7 @@ Interactive.Stateful <- selectVariant, state, interaction, onClick, href
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `roundingVariant` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
|
||||
| `rounding` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
|
||||
| `width` | `WidthVariant` | `"full"` | Container width |
|
||||
| `type` | `"submit" \| "button" \| "reset"` | `"button"` | HTML button type |
|
||||
| `tooltip` | `string` | — | Tooltip text shown on hover |
|
||||
@@ -63,7 +63,7 @@ import { LineItemButton } from "@opal/components";
|
||||
<LineItemButton
|
||||
selectVariant="select-heavy"
|
||||
state={isSelected ? "selected" : "empty"}
|
||||
roundingVariant="sm"
|
||||
rounding="sm"
|
||||
onClick={handleClick}
|
||||
title="gpt-4o"
|
||||
sizePreset="main-ui"
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
} from "@opal/core";
|
||||
import type { ExtremaSizeVariants, DistributiveOmit } from "@opal/types";
|
||||
import { Tooltip, type TooltipSide } from "@opal/components";
|
||||
import type { ContentActionProps } from "@opal/layouts/content-action/components";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { type ContentActionProps, ContentAction } from "@opal/layouts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -14,7 +13,7 @@ import { ContentAction } from "@opal/layouts";
|
||||
|
||||
type ContentPassthroughProps = DistributiveOmit<
|
||||
ContentActionProps,
|
||||
"paddingVariant" | "widthVariant" | "ref"
|
||||
"padding" | "width" | "ref"
|
||||
>;
|
||||
|
||||
type LineItemButtonOwnProps = Pick<
|
||||
@@ -32,7 +31,7 @@ type LineItemButtonOwnProps = Pick<
|
||||
selectVariant?: "select-light" | "select-heavy";
|
||||
|
||||
/** Corner rounding preset (height is always content-driven). @default "md" */
|
||||
roundingVariant?: InteractiveContainerRoundingVariant;
|
||||
rounding?: InteractiveContainerRoundingVariant;
|
||||
|
||||
/** Container width. @default "full" */
|
||||
width?: ExtremaSizeVariants;
|
||||
@@ -63,7 +62,7 @@ function LineItemButton({
|
||||
type = "button",
|
||||
|
||||
// Sizing
|
||||
roundingVariant = "md",
|
||||
rounding = "md",
|
||||
width = "full",
|
||||
tooltip,
|
||||
tooltipSide = "top",
|
||||
@@ -84,14 +83,16 @@ function LineItemButton({
|
||||
>
|
||||
<Interactive.Container
|
||||
type={type}
|
||||
widthVariant={width}
|
||||
heightVariant="lg"
|
||||
roundingVariant={roundingVariant}
|
||||
width={width}
|
||||
size="fit"
|
||||
rounding={rounding}
|
||||
>
|
||||
<ContentAction
|
||||
{...(contentActionProps as ContentActionProps)}
|
||||
paddingVariant="fit"
|
||||
/>
|
||||
<div className="w-full p-2">
|
||||
<ContentAction
|
||||
{...(contentActionProps as ContentActionProps)}
|
||||
padding="fit"
|
||||
/>
|
||||
</div>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ type OpenButtonProps = Omit<InteractiveStatefulProps, "variant"> & {
|
||||
tooltipSide?: TooltipSide;
|
||||
|
||||
/** Override the default rounding derived from `size`. */
|
||||
roundingVariant?: InteractiveContainerRoundingVariant;
|
||||
rounding?: InteractiveContainerRoundingVariant;
|
||||
|
||||
/** Applies disabled styling and suppresses clicks. */
|
||||
disabled?: boolean;
|
||||
@@ -89,7 +89,7 @@ function OpenButton({
|
||||
justifyContent,
|
||||
tooltip,
|
||||
tooltipSide = "top",
|
||||
roundingVariant: roundingVariantOverride,
|
||||
rounding: roundingOverride,
|
||||
interaction,
|
||||
variant = "select-heavy",
|
||||
disabled,
|
||||
@@ -123,11 +123,10 @@ function OpenButton({
|
||||
>
|
||||
<Interactive.Container
|
||||
type="button"
|
||||
heightVariant={size}
|
||||
widthVariant={width}
|
||||
roundingVariant={
|
||||
roundingVariantOverride ??
|
||||
(isLarge ? "md" : size === "2xs" ? "xs" : "sm")
|
||||
size={size}
|
||||
width={width}
|
||||
rounding={
|
||||
roundingOverride ?? (isLarge ? "md" : size === "2xs" ? "xs" : "sm")
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -96,9 +96,9 @@ function SelectButton({
|
||||
<Interactive.Stateful disabled={disabled} {...statefulProps}>
|
||||
<Interactive.Container
|
||||
type={type}
|
||||
heightVariant={size}
|
||||
widthVariant={width}
|
||||
roundingVariant={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
|
||||
size={size}
|
||||
width={width}
|
||||
rounding={isLarge ? "md" : size === "2xs" ? "xs" : "sm"}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -93,12 +93,7 @@ function SidebarTab({
|
||||
type="button"
|
||||
group="group/SidebarTab"
|
||||
>
|
||||
<Interactive.Container
|
||||
roundingVariant="sm"
|
||||
heightVariant="lg"
|
||||
widthVariant="full"
|
||||
type={type}
|
||||
>
|
||||
<Interactive.Container rounding="sm" size="lg" width="full" type={type}>
|
||||
{href && !disabled && (
|
||||
<Link
|
||||
href={href as Route}
|
||||
@@ -120,8 +115,8 @@ function SidebarTab({
|
||||
title={folded ? "" : children}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
widthVariant="full"
|
||||
paddingVariant="fit"
|
||||
width="full"
|
||||
padding="fit"
|
||||
rightChildren={truncationSpacer}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -55,7 +55,7 @@ export const PaddingVariants: Story = {
|
||||
<div className="flex flex-col gap-4 w-96">
|
||||
{PADDING_VARIANTS.map((padding) => (
|
||||
<Card key={padding} padding={padding} border="solid">
|
||||
<p>paddingVariant: {padding}</p>
|
||||
<p>padding: {padding}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ export const RoundingVariants: Story = {
|
||||
<div className="flex flex-col gap-4 w-96">
|
||||
{ROUNDING_VARIANTS.map((rounding) => (
|
||||
<Card key={rounding} rounding={rounding} border="solid">
|
||||
<p>roundingVariant: {rounding}</p>
|
||||
<p>rounding: {rounding}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ export const AllCombinations: Story = {
|
||||
<div className="flex flex-col gap-8">
|
||||
{PADDING_VARIANTS.map((padding) => (
|
||||
<div key={padding}>
|
||||
<p className="font-bold pb-2">paddingVariant: {padding}</p>
|
||||
<p className="font-bold pb-2">padding: {padding}</p>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{BACKGROUND_VARIANTS.map((bg) =>
|
||||
BORDER_VARIANTS.map((border) => (
|
||||
|
||||
@@ -156,7 +156,7 @@ function MessageCard({
|
||||
description={description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="md"
|
||||
padding="md"
|
||||
rightChildren={right}
|
||||
/>
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ export const PaddingVariants: Story = {
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
icon={SvgGlobe}
|
||||
title={`paddingVariant: ${padding}`}
|
||||
title={`padding: ${padding}`}
|
||||
description="Shows padding differences."
|
||||
/>
|
||||
</SelectCard>
|
||||
@@ -227,7 +227,7 @@ export const RoundingVariants: Story = {
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
icon={SvgGlobe}
|
||||
title={`roundingVariant: ${rounding}`}
|
||||
title={`rounding: ${rounding}`}
|
||||
description="Shows rounding differences."
|
||||
/>
|
||||
</SelectCard>
|
||||
|
||||
@@ -167,11 +167,7 @@ function FoldableDivider({
|
||||
interaction={isOpen ? "hover" : "rest"}
|
||||
onClick={toggle}
|
||||
>
|
||||
<Interactive.Container
|
||||
roundingVariant="sm"
|
||||
heightVariant="fit"
|
||||
widthVariant="full"
|
||||
>
|
||||
<Interactive.Container rounding="sm" size="fit" width="full">
|
||||
<div className="opal-divider">
|
||||
<div className="opal-divider-row">
|
||||
<div className="opal-divider-title">
|
||||
|
||||
@@ -22,7 +22,7 @@ interface TableProps
|
||||
/** Row selection behavior. @default "no-select" */
|
||||
selectionBehavior?: SelectionBehavior;
|
||||
/** Height behavior. `"fit"` = shrink to content, `"full"` = fill available space. */
|
||||
heightVariant?: ExtremaSizeVariants;
|
||||
size?: ExtremaSizeVariants;
|
||||
/** Explicit pixel width for the table (e.g. from `table.getTotalSize()`).
|
||||
* When provided the table uses exactly this width instead of stretching
|
||||
* to fill its container, which prevents `table-layout: fixed` from
|
||||
@@ -38,7 +38,7 @@ function Table({
|
||||
ref,
|
||||
variant = "cards",
|
||||
selectionBehavior = "no-select",
|
||||
heightVariant,
|
||||
size: heightVariant,
|
||||
width,
|
||||
...props
|
||||
}: TableProps) {
|
||||
|
||||
@@ -17,7 +17,7 @@ interface HoverableRootProps
|
||||
children: React.ReactNode;
|
||||
group: string;
|
||||
/** Width preset. @default "auto" */
|
||||
widthVariant?: ExtremaSizeVariants;
|
||||
width?: ExtremaSizeVariants;
|
||||
/**
|
||||
* JS-controllable interaction state override.
|
||||
*
|
||||
@@ -70,7 +70,7 @@ interface HoverableItemProps
|
||||
function HoverableRoot({
|
||||
group,
|
||||
children,
|
||||
widthVariant = "full",
|
||||
width = "full",
|
||||
interaction = "rest",
|
||||
ref,
|
||||
...props
|
||||
@@ -79,7 +79,7 @@ function HoverableRoot({
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(widthVariants[widthVariant])}
|
||||
className={cn(widthVariants[width])}
|
||||
data-hover-group={group}
|
||||
data-interaction={interaction !== "rest" ? interaction : undefined}
|
||||
>
|
||||
|
||||
@@ -135,7 +135,7 @@ export const VariantMatrix: StoryObj = {
|
||||
),
|
||||
};
|
||||
|
||||
/** All heightVariant sizes (lg, md, sm, xs, 2xs, fit). */
|
||||
/** All size presets (lg, md, sm, xs, 2xs, fit). */
|
||||
export const Sizes: StoryObj = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
||||
@@ -146,7 +146,7 @@ export const Sizes: StoryObj = {
|
||||
prominence="secondary"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border heightVariant={size}>
|
||||
<Interactive.Container border size={size}>
|
||||
<span>{size}</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
@@ -155,7 +155,7 @@ export const Sizes: StoryObj = {
|
||||
),
|
||||
};
|
||||
|
||||
/** Container with widthVariant="full" stretching to fill its parent. */
|
||||
/** Container with width="full" stretching to fill its parent. */
|
||||
export const WidthFull: StoryObj = {
|
||||
render: () => (
|
||||
<div style={{ width: 400 }}>
|
||||
@@ -164,7 +164,7 @@ export const WidthFull: StoryObj = {
|
||||
prominence="secondary"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border widthVariant="full">
|
||||
<Interactive.Container border width="full">
|
||||
<span>Full width container</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
@@ -183,7 +183,7 @@ export const Rounding: StoryObj = {
|
||||
prominence="secondary"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border roundingVariant={rounding}>
|
||||
<Interactive.Container border rounding={rounding}>
|
||||
<span>{rounding}</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -8,9 +8,9 @@ Structural container shared by both `Interactive.Stateless` and `Interactive.Sta
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heightVariant` | `SizeVariant` | `"lg"` | Height preset (`2xs`–`lg`, `fit`) |
|
||||
| `roundingVariant` | `"md" \| "sm" \| "xs"` | `"md"` | Border-radius preset |
|
||||
| `widthVariant` | `WidthVariant` | — | Width preset (`"auto"`, `"fit"`, `"full"`) |
|
||||
| `size` | `SizeVariant` | `"lg"` | Height preset (`2xs`–`lg`, `fit`) |
|
||||
| `rounding` | `"md" \| "sm" \| "xs"` | `"md"` | Border-radius preset |
|
||||
| `width` | `WidthVariant` | — | Width preset (`"auto"`, `"fit"`, `"full"`) |
|
||||
| `border` | `boolean` | `false` | Renders a 1px border |
|
||||
| `type` | `"submit" \| "button" \| "reset"` | — | When set, renders a `<button>` element |
|
||||
|
||||
@@ -18,7 +18,7 @@ Structural container shared by both `Interactive.Stateless` and `Interactive.Sta
|
||||
|
||||
```tsx
|
||||
<Interactive.Stateless variant="default" prominence="primary">
|
||||
<Interactive.Container heightVariant="sm" roundingVariant="sm" border>
|
||||
<Interactive.Container size="sm" rounding="sm" border>
|
||||
<span>Content</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -63,21 +63,21 @@ interface InteractiveContainerProps
|
||||
*
|
||||
* @default "default"
|
||||
*/
|
||||
roundingVariant?: InteractiveContainerRoundingVariant;
|
||||
rounding?: InteractiveContainerRoundingVariant;
|
||||
|
||||
/**
|
||||
* Size preset controlling the container's height, min-width, and padding.
|
||||
*
|
||||
* @default "lg"
|
||||
*/
|
||||
heightVariant?: ContainerSizeVariants;
|
||||
size?: ContainerSizeVariants;
|
||||
|
||||
/**
|
||||
* Width preset controlling the container's horizontal size.
|
||||
*
|
||||
* @default "fit"
|
||||
*/
|
||||
widthVariant?: ExtremaSizeVariants;
|
||||
width?: ExtremaSizeVariants;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -96,9 +96,9 @@ function InteractiveContainer({
|
||||
ref,
|
||||
type,
|
||||
border,
|
||||
roundingVariant = "md",
|
||||
heightVariant = "lg",
|
||||
widthVariant = "fit",
|
||||
rounding = "md",
|
||||
size = "lg",
|
||||
width = "fit",
|
||||
...props
|
||||
}: InteractiveContainerProps) {
|
||||
const {
|
||||
@@ -115,16 +115,16 @@ function InteractiveContainer({
|
||||
target?: string;
|
||||
rel?: string;
|
||||
};
|
||||
const { height, minWidth, padding } = containerSizeVariants[heightVariant];
|
||||
const { height, minWidth, padding } = containerSizeVariants[size];
|
||||
const sharedProps = {
|
||||
...rest,
|
||||
className: cn(
|
||||
"interactive-container",
|
||||
interactiveContainerRoundingVariants[roundingVariant],
|
||||
interactiveContainerRoundingVariants[rounding],
|
||||
height,
|
||||
minWidth,
|
||||
padding,
|
||||
widthVariants[widthVariant],
|
||||
widthVariants[width],
|
||||
slotClassName
|
||||
),
|
||||
"data-border": border ? ("true" as const) : undefined,
|
||||
|
||||
@@ -46,7 +46,7 @@ import SvgNoResult from "@opal/illustrations/no-result";
|
||||
description="Some description"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
paddingVariant="lg"
|
||||
padding="lg"
|
||||
rightChildren={
|
||||
<Button icon={SvgSettings} prominence="tertiary" />
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const NoPadding: Story = {
|
||||
variant: "section",
|
||||
title: "Compact Row",
|
||||
description: "No padding around content area.",
|
||||
paddingVariant: "fit",
|
||||
padding: "fit",
|
||||
rightChildren: <Button prominence="tertiary">Action</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,9 +15,9 @@ Inherits **all** props from [`Content`](../content/README.md) (same discriminate
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `rightChildren` | `ReactNode` | `undefined` | Content rendered on the right side. Wrapper stretches to the full height of the row. |
|
||||
| `paddingVariant` | `SizeVariant` | `"lg"` | Padding preset applied around the `Content` area. Uses the shared size scale from `@opal/shared`. |
|
||||
| `padding` | `SizeVariant` | `"lg"` | Padding preset applied around the `Content` area. Uses the shared size scale from `@opal/shared`. |
|
||||
|
||||
### `paddingVariant` reference
|
||||
### `padding` reference
|
||||
|
||||
| Value | Padding class | Effective padding |
|
||||
|---|---|---|
|
||||
@@ -37,7 +37,7 @@ These values are identical to the padding applied by `Interactive.Container` at
|
||||
```
|
||||
|
||||
- The outer wrapper is `flex flex-row items-stretch w-full`.
|
||||
- `Content` sits inside a `flex-1 min-w-0` div with padding from `paddingVariant`.
|
||||
- `Content` sits inside a `flex-1 min-w-0` div with padding from `padding`.
|
||||
- `rightChildren` is wrapped in `flex items-stretch shrink-0` so it stretches vertically.
|
||||
|
||||
## Usage Examples
|
||||
@@ -56,7 +56,7 @@ import SvgSettings from "@opal/icons/settings";
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
tag={{ title: "Default", color: "blue" }}
|
||||
paddingVariant="lg"
|
||||
padding="lg"
|
||||
rightChildren={
|
||||
<Button icon={SvgSettings} prominence="tertiary" onClick={handleEdit} />
|
||||
}
|
||||
@@ -76,7 +76,7 @@ import { SvgArrowExchange, SvgCloud } from "@opal/icons";
|
||||
description="Gemini"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
paddingVariant="md"
|
||||
padding="md"
|
||||
rightChildren={
|
||||
<Button rightIcon={SvgArrowExchange} prominence="tertiary">
|
||||
Connect
|
||||
@@ -92,7 +92,7 @@ import { SvgArrowExchange, SvgCloud } from "@opal/icons";
|
||||
title="Section Header"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
paddingVariant="lg"
|
||||
padding="lg"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ type ContentActionProps = ContentProps & {
|
||||
* @default "lg"
|
||||
* @see {@link ContainerSizeVariants} for the full list of presets.
|
||||
*/
|
||||
paddingVariant?: ContainerSizeVariants;
|
||||
padding?: ContainerSizeVariants;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -31,7 +31,7 @@ type ContentActionProps = ContentProps & {
|
||||
* A row layout that pairs a {@link Content} block with optional right-side
|
||||
* action children (e.g. buttons, badges).
|
||||
*
|
||||
* The `Content` area receives padding controlled by `paddingVariant`, using
|
||||
* The `Content` area receives padding controlled by `padding`, using
|
||||
* the same size scale as `Interactive.Container` and `Button`. The
|
||||
* `rightChildren` wrapper stretches to the full height of the row.
|
||||
*
|
||||
@@ -47,21 +47,21 @@ type ContentActionProps = ContentProps & {
|
||||
* description="GPT"
|
||||
* sizePreset="main-content"
|
||||
* variant="section"
|
||||
* paddingVariant="lg"
|
||||
* padding="lg"
|
||||
* rightChildren={<Button icon={SvgSettings} prominence="tertiary" />}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
function ContentAction({
|
||||
rightChildren,
|
||||
paddingVariant = "lg",
|
||||
padding = "lg",
|
||||
...contentProps
|
||||
}: ContentActionProps) {
|
||||
const { padding } = containerSizeVariants[paddingVariant];
|
||||
const { padding: paddingClass } = containerSizeVariants[padding];
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-stretch w-full">
|
||||
<div className={cn("flex-1 min-w-0 self-center", padding)}>
|
||||
<div className={cn("flex-1 min-w-0 self-center", paddingClass)}>
|
||||
<Content {...contentProps} />
|
||||
</div>
|
||||
{rightChildren && (
|
||||
|
||||
@@ -184,7 +184,7 @@ export const SmMuted: Story = {
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// widthVariant: full
|
||||
// width: full
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const WidthFull: Story = {
|
||||
@@ -192,7 +192,7 @@ export const WidthFull: Story = {
|
||||
sizePreset: "main-content",
|
||||
variant: "section",
|
||||
title: "Full Width Content",
|
||||
widthVariant: "full",
|
||||
width: "full",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -58,9 +58,20 @@ interface ContentBaseProps {
|
||||
* - `"fit"` — Shrink-wraps to content width
|
||||
* - `"full"` — Stretches to fill the parent's width
|
||||
*
|
||||
* @default "fit"
|
||||
* @default "full"
|
||||
*/
|
||||
widthVariant?: ExtremaSizeVariants;
|
||||
width?: ExtremaSizeVariants;
|
||||
|
||||
/**
|
||||
* Opt out of the automatic interactive color override.
|
||||
*
|
||||
* When `Content` is nested inside an `.interactive` element, its title and
|
||||
* icon colors are normally overridden by the interactive foreground palette.
|
||||
* Set this to `true` to keep Content's own colors regardless of context.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
nonInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>` of the resolved layout. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
@@ -126,7 +137,8 @@ function Content(props: ContentProps) {
|
||||
const {
|
||||
sizePreset = "headline",
|
||||
variant = "heading",
|
||||
widthVariant = "full",
|
||||
width = "full",
|
||||
nonInteractive,
|
||||
ref,
|
||||
...rest
|
||||
} = props;
|
||||
@@ -186,7 +198,14 @@ function Content(props: ContentProps) {
|
||||
`Content: no layout matched for sizePreset="${sizePreset}" variant="${variant}"`
|
||||
);
|
||||
|
||||
return <div className={widthVariants[widthVariant]}>{layout}</div>;
|
||||
return (
|
||||
<div
|
||||
className={widthVariants[width]}
|
||||
data-opal-non-interactive={nonInteractive || undefined}
|
||||
>
|
||||
{layout}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -352,36 +352,52 @@
|
||||
the title inherits color from the Interactive's `--interactive-foreground`
|
||||
and icons switch to `--interactive-foreground-icon`. This is automatic —
|
||||
no opt-in prop is required.
|
||||
|
||||
Opt-out: set `nonInteractive` on <Content> to add
|
||||
`data-opal-non-interactive` to the wrapper div, which excludes
|
||||
the element from these overrides via the `:not(…) >` selector.
|
||||
=========================================================================== */
|
||||
|
||||
.interactive .opal-content-xl {
|
||||
.interactive :not([data-opal-non-interactive]) > .opal-content-xl {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.interactive .opal-content-xl .opal-content-xl-icon {
|
||||
.interactive
|
||||
:not([data-opal-non-interactive])
|
||||
> .opal-content-xl
|
||||
.opal-content-xl-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
.interactive .opal-content-lg {
|
||||
.interactive :not([data-opal-non-interactive]) > .opal-content-lg {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.interactive .opal-content-lg .opal-content-lg-icon {
|
||||
.interactive
|
||||
:not([data-opal-non-interactive])
|
||||
> .opal-content-lg
|
||||
.opal-content-lg-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
.interactive .opal-content-md {
|
||||
.interactive :not([data-opal-non-interactive]) > .opal-content-md {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.interactive .opal-content-md .opal-content-md-icon {
|
||||
.interactive
|
||||
:not([data-opal-non-interactive])
|
||||
> .opal-content-md
|
||||
.opal-content-md-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
.interactive .opal-content-sm {
|
||||
.interactive :not([data-opal-non-interactive]) > .opal-content-sm {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.interactive .opal-content-sm .opal-content-sm-icon {
|
||||
.interactive
|
||||
:not([data-opal-non-interactive])
|
||||
> .opal-content-sm
|
||||
.opal-content-sm-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ function Horizontal({
|
||||
tag={tag}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">{children}</div>
|
||||
|
||||
@@ -57,9 +57,9 @@ const containerSizeVariants: Record<
|
||||
// A named scale of width/height presets that map to Tailwind width/height utility classes.
|
||||
//
|
||||
// Consumers (for width):
|
||||
// - Interactive.Container (widthVariant)
|
||||
// - Interactive.Container (width)
|
||||
// - Button (width)
|
||||
// - Content (widthVariant)
|
||||
// - Content (width)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -96,8 +96,8 @@ const heightVariants: Record<ExtremaSizeVariants, string> = {
|
||||
// Shared padding and rounding scales for card components (Card, SelectCard).
|
||||
//
|
||||
// Consumers:
|
||||
// - Card (paddingVariant, roundingVariant)
|
||||
// - SelectCard (paddingVariant, roundingVariant)
|
||||
// - Card (padding, rounding)
|
||||
// - SelectCard (padding, rounding)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const paddingVariants: Record<PaddingVariants, string> = {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function ScimSyncCard({
|
||||
description="Connect your identity provider to import and sync users and groups."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
hasToken ? (
|
||||
<Button
|
||||
|
||||
@@ -77,7 +77,7 @@ export const InMessageImage = memo(function InMessageImage({
|
||||
onOpenChange={(open) => setFullImageShowing(open)}
|
||||
/>
|
||||
|
||||
<Hoverable.Root group="messageImage" widthVariant="fit">
|
||||
<Hoverable.Root group="messageImage" width="fit">
|
||||
<div className={cn("relative", shapeContainerClasses)}>
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 bg-background-tint-02 animate-pulse rounded-lg" />
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function ProjectContextPanel({
|
||||
<div className="flex flex-col gap-6 w-full max-w-[var(--app-page-main-content-width)] mx-auto p-4 pt-14 pb-6">
|
||||
<div className="flex flex-col gap-1 text-text-04">
|
||||
<SvgFolderOpen className="h-8 w-8 text-text-04" />
|
||||
<Hoverable.Root group="projectName" widthVariant="fit">
|
||||
<Hoverable.Root group="projectName" width="fit">
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditingName ? (
|
||||
<ButtonRenaming
|
||||
|
||||
@@ -198,7 +198,7 @@ const HumanMessage = React.memo(function HumanMessage({
|
||||
);
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="humanMessage" widthVariant="full">
|
||||
<Hoverable.Root group="humanMessage" width="full">
|
||||
<div
|
||||
id="onyx-human-message"
|
||||
className="flex flex-col justify-end w-full relative"
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function MultiModelPanel({
|
||||
<ContentAction
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
paddingVariant="lg"
|
||||
padding="lg"
|
||||
icon={ModelIcon}
|
||||
title={isHidden ? markdown(`~~${displayName}~~`) : displayName}
|
||||
rightChildren={
|
||||
|
||||
@@ -71,7 +71,7 @@ function MemoryTagWithTooltip({
|
||||
icon={SvgAddLines}
|
||||
title={operationLabel}
|
||||
sizePreset="secondary"
|
||||
paddingVariant="sm"
|
||||
padding="sm"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
rightChildren={
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function ShareButton({
|
||||
description={opt.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
padding="sm"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -272,7 +272,7 @@ export default function HookFormModal({
|
||||
<ContentAction
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
title={hookPointDisplayName}
|
||||
description={hookPointDescription}
|
||||
rightChildren={
|
||||
@@ -283,7 +283,7 @@ export default function HookFormModal({
|
||||
icon={SvgShareWebhook}
|
||||
title="Hook Point"
|
||||
prominence="muted"
|
||||
widthVariant="fit"
|
||||
width="fit"
|
||||
/>
|
||||
{docsUrl && (
|
||||
<LinkButton href={docsUrl} target="_blank">
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function SearchCard({
|
||||
|
||||
return (
|
||||
<Interactive.Stateless onClick={handleClick} prominence="secondary">
|
||||
<Interactive.Container heightVariant="fit" widthVariant="full">
|
||||
<Interactive.Container size="fit" width="full">
|
||||
<Section alignItems="start" gap={0} padding={0.25}>
|
||||
{/* Title Row */}
|
||||
<Section
|
||||
|
||||
@@ -202,7 +202,7 @@ function AttachmentItemLayout({
|
||||
description={description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
</div>
|
||||
{middleText && (
|
||||
|
||||
@@ -173,7 +173,7 @@ export default function InputImage({
|
||||
const dropzoneProps = onDrop ? getRootProps() : {};
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="inputImage" widthVariant="fit">
|
||||
<Hoverable.Root group="inputImage" width="fit">
|
||||
<div
|
||||
className={cn("relative", className)}
|
||||
style={{ width: size, height: size }}
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function FileTile({
|
||||
const isMuted = state === "processing" || state === "disabled";
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="fileTile" widthVariant="fit">
|
||||
<Hoverable.Root group="fileTile" width="fit">
|
||||
<div
|
||||
onClick={onOpen && state !== "disabled" ? () => onOpen() : undefined}
|
||||
className={cn(
|
||||
|
||||
@@ -211,7 +211,7 @@ function AgentIconEditor({ existingAgent }: AgentIconEditorProps) {
|
||||
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<Hoverable.Root group="inputAvatar" widthVariant="fit">
|
||||
<Hoverable.Root group="inputAvatar" width="fit">
|
||||
<InputAvatar className="relative flex flex-col items-center justify-center h-[7.5rem] w-[7.5rem]">
|
||||
{/* We take the `InputAvatar`'s height/width (in REM) and multiply it by 16 (the REM -> px conversion factor). */}
|
||||
<CustomAgentAvatar
|
||||
@@ -288,7 +288,7 @@ function OpenApiToolCard({ tool }: OpenApiToolCardProps) {
|
||||
description={tool.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
/>
|
||||
}
|
||||
topRightChildren={<SwitchField name={toolFieldName} />}
|
||||
@@ -353,7 +353,7 @@ function MCPServerCard({
|
||||
description={tool.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
/>
|
||||
}
|
||||
topRightChildren={
|
||||
@@ -388,7 +388,7 @@ function MCPServerCard({
|
||||
description={server.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<GeneralLayouts.Section
|
||||
flexDirection="row"
|
||||
|
||||
@@ -269,7 +269,7 @@ function GeneralSettings() {
|
||||
title="Profile"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -332,7 +332,7 @@ function GeneralSettings() {
|
||||
title="Appearance"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -443,7 +443,7 @@ function GeneralSettings() {
|
||||
title="Danger Zone"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -831,7 +831,7 @@ function ChatPreferencesSettings() {
|
||||
title="Chats"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -920,7 +920,7 @@ function ChatPreferencesSettings() {
|
||||
title="Memory"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -968,7 +968,7 @@ function ChatPreferencesSettings() {
|
||||
title="Prompt Shortcuts"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -993,7 +993,7 @@ function ChatPreferencesSettings() {
|
||||
title="Voice"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -1365,7 +1365,7 @@ function AccountsAccessSettings() {
|
||||
title="Accounts"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
<Card>
|
||||
<InputHorizontal
|
||||
@@ -1401,7 +1401,7 @@ function AccountsAccessSettings() {
|
||||
title="Access Tokens"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
{canCreateTokens ? (
|
||||
<Card padding={0.25}>
|
||||
@@ -1463,8 +1463,8 @@ function AccountsAccessSettings() {
|
||||
return (
|
||||
<Interactive.Container
|
||||
key={pat.id}
|
||||
heightVariant="fit"
|
||||
widthVariant="full"
|
||||
size="fit"
|
||||
width="full"
|
||||
>
|
||||
<div className="w-full bg-background-tint-01">
|
||||
<AttachmentItemLayout
|
||||
@@ -1605,7 +1605,7 @@ function FederatedConnectorCard({
|
||||
}
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
padding="sm"
|
||||
rightChildren={
|
||||
connector.has_oauth_token ? (
|
||||
<Button
|
||||
@@ -1678,7 +1678,7 @@ function ConnectorsSettings() {
|
||||
title="Connectors"
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
{hasConnectors ? (
|
||||
<>
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
SvgExpand,
|
||||
SvgFold,
|
||||
SvgExternalLink,
|
||||
SvgAlertCircle,
|
||||
SvgRefreshCw,
|
||||
} from "@opal/icons";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
@@ -49,7 +48,7 @@ import {
|
||||
PYTHON_TOOL_ID,
|
||||
OPEN_URL_TOOL_ID,
|
||||
} from "@/app/app/components/tools/constants";
|
||||
import { Button, Divider, Text, Card } from "@opal/components";
|
||||
import { Button, Divider, Text, Card, MessageCard } from "@opal/components";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import Switch from "@/refresh-components/inputs/Switch";
|
||||
import useMcpServersForAgentEditor from "@/hooks/useMcpServersForAgentEditor";
|
||||
@@ -157,7 +156,7 @@ function MCPServerCard({
|
||||
description={server.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<Tooltip tooltip={authTooltip} side="top">
|
||||
<Switch
|
||||
@@ -287,7 +286,7 @@ function NumericLimitField({
|
||||
};
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="numericLimit" widthVariant="full">
|
||||
<Hoverable.Root group="numericLimit" width="full">
|
||||
<InputTypeIn
|
||||
inputMode="numeric"
|
||||
showClearButton={false}
|
||||
@@ -1091,14 +1090,11 @@ export default function ChatPreferencesPage() {
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
<Card background="none" border="solid" padding="sm">
|
||||
<Content
|
||||
sizePreset="main-ui"
|
||||
icon={SvgAlertCircle}
|
||||
title="Modify with caution."
|
||||
description="System prompt affects all chats, agents, and projects. Significant changes may degrade response quality."
|
||||
/>
|
||||
</Card>
|
||||
<MessageCard
|
||||
title="Modify with caution."
|
||||
description="System prompt affects all chats, agents, and projects. Significant changes may degrade response quality."
|
||||
padding="xs"
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
|
||||
@@ -294,7 +294,7 @@ export default function EditUserModal({
|
||||
description="This controls their general permissions."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<InputSelect
|
||||
value={selectedRole}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function UserRoleCell({ user, onMutate }: UserRoleCellProps) {
|
||||
variant="select-tinted"
|
||||
width="full"
|
||||
justifyContent="between"
|
||||
roundingVariant="sm"
|
||||
rounding="sm"
|
||||
>
|
||||
{USER_ROLE_LABELS[user.role]}
|
||||
</OpenButton>
|
||||
|
||||
@@ -23,7 +23,7 @@ function StatCell({ value, label, onFilter }: StatCellProps) {
|
||||
const display = value === null ? "\u2014" : value.toLocaleString();
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="stat" widthVariant="full">
|
||||
<Hoverable.Root group="stat" width="full">
|
||||
<div
|
||||
className={`relative flex flex-col items-start gap-0.5 w-full p-2 rounded-08 transition-colors ${
|
||||
onFilter ? "cursor-pointer hover:bg-background-tint-02" : ""
|
||||
@@ -70,7 +70,7 @@ function ScimCard() {
|
||||
description="Users are synced from your identity provider."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<Link href={ADMIN_ROUTES.SCIM.path}>
|
||||
<Button prominence="tertiary" rightIcon={SvgArrowUpRight} size="sm">
|
||||
|
||||
@@ -36,16 +36,12 @@ export default function Suggestions({ onSubmit }: SuggestionsProps) {
|
||||
prominence="tertiary"
|
||||
onClick={() => handleSuggestionClick(message)}
|
||||
>
|
||||
<Interactive.Container
|
||||
widthVariant="full"
|
||||
roundingVariant="sm"
|
||||
heightVariant="lg"
|
||||
>
|
||||
<Interactive.Container width="full" rounding="sm" size="lg">
|
||||
<Content
|
||||
title={message}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
prominence="muted"
|
||||
/>
|
||||
</Interactive.Container>
|
||||
|
||||
@@ -241,7 +241,7 @@ function FormContent({
|
||||
`Specify an OpenAPI schema that defines the APIs you want to make available as part of this action. Learn more about [OpenAPI actions](${DOCS_ADMINS_PATH}/actions/openapi).`
|
||||
)}
|
||||
>
|
||||
<Hoverable.Root group="definitionField" widthVariant="full">
|
||||
<Hoverable.Root group="definitionField" width="full">
|
||||
<div className="relative w-full">
|
||||
{values.definition.trim() && (
|
||||
<div className="absolute z-[100000] top-2 right-2 bg-background-tint-00">
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function AdminListHeader({
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
widthVariant="fit"
|
||||
width="fit"
|
||||
/>
|
||||
{actionButton}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function DocumentSetCard({
|
||||
<Interactive.Container
|
||||
data-testid={`document-set-card-${documentSet.id}`}
|
||||
border
|
||||
heightVariant="fit"
|
||||
size="fit"
|
||||
>
|
||||
<AttachmentItemLayout
|
||||
icon={SvgFiles}
|
||||
|
||||
@@ -21,7 +21,7 @@ function Removable({ onRemove, children }: RemovableProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="fileCard" widthVariant="fit">
|
||||
<Hoverable.Root group="fileCard" width="fit">
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
@@ -184,7 +184,7 @@ export function FileCard({
|
||||
}
|
||||
>
|
||||
<div className="min-w-0 max-w-[12rem]">
|
||||
<Interactive.Container border heightVariant="fit" widthVariant="full">
|
||||
<Interactive.Container border size="fit" width="full">
|
||||
<AttachmentItemLayout
|
||||
icon={isProcessing ? SimpleLoader : SvgFileText}
|
||||
title={file.name}
|
||||
|
||||
@@ -88,7 +88,7 @@ function ViewerMCPServerCard({ server, tools }: ViewerMCPServerCardProps) {
|
||||
description={server.description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
/>
|
||||
}
|
||||
topRightChildren={
|
||||
@@ -267,7 +267,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
title="Featured"
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
widthVariant="fit"
|
||||
width="fit"
|
||||
/>
|
||||
)}
|
||||
<Content
|
||||
@@ -276,7 +276,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
widthVariant="fit"
|
||||
width="fit"
|
||||
/>
|
||||
{agent.is_public && (
|
||||
<Content
|
||||
@@ -285,7 +285,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
widthVariant="fit"
|
||||
width="fit"
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
@@ -425,7 +425,7 @@ export default function AgentViewerModal({ agent }: AgentViewerModalProps) {
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -28,15 +28,9 @@ import {
|
||||
ModalWrapper,
|
||||
} from "@/sections/modals/llmConfig/shared";
|
||||
import { fetchBedrockModels } from "@/lib/llmConfig/svc";
|
||||
import { Card } from "@opal/components";
|
||||
import { Card, MessageCard } from "@opal/components";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { SvgAlertCircle } from "@opal/icons";
|
||||
import {
|
||||
Content,
|
||||
InputDivider,
|
||||
InputPadder,
|
||||
InputVertical,
|
||||
} from "@opal/layouts";
|
||||
import { InputDivider, InputPadder, InputVertical } from "@opal/layouts";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { refreshLlmProviderCaches } from "@/lib/llmConfig/cache";
|
||||
|
||||
@@ -218,14 +212,10 @@ function BedrockModalInternals({
|
||||
|
||||
{authMethod === AUTH_METHOD_IAM && (
|
||||
<InputPadder>
|
||||
<Card background="none" border="solid" padding="sm">
|
||||
<Content
|
||||
icon={SvgAlertCircle}
|
||||
title="Onyx will use the IAM role attached to the environment it’s running in to authenticate."
|
||||
variant="body"
|
||||
sizePreset="main-ui"
|
||||
/>
|
||||
</Card>
|
||||
<MessageCard
|
||||
variant="info"
|
||||
title="Onyx will use the IAM role attached to the environment it’s running in to authenticate."
|
||||
/>
|
||||
</InputPadder>
|
||||
)}
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ export default function CustomModal({
|
||||
description={markdown(
|
||||
"Add extra properties as needed by the model provider. These are passed to LiteLLM's `completion()` call as [environment variables](https://docs.litellm.ai/docs/set_keys#environment-variables). See [documentation](https://docs.onyx.app/admins/ai_models/custom_inference_provider) for more instructions."
|
||||
)}
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
variant="section"
|
||||
sizePreset="main-content"
|
||||
/>
|
||||
@@ -433,7 +433,7 @@ export default function CustomModal({
|
||||
description="List LLM models you wish to use and their configurations for this provider. See full list of models at LiteLLM."
|
||||
variant="section"
|
||||
sizePreset="main-content"
|
||||
widthVariant="full"
|
||||
width="full"
|
||||
/>
|
||||
</InputPadder>
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ export function ModelAccessField() {
|
||||
Always shared
|
||||
</Text>
|
||||
}
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
/>
|
||||
</Card>
|
||||
{selectedGroupIds.length > 0 && (
|
||||
@@ -304,7 +304,7 @@ export function ModelAccessField() {
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -341,7 +341,7 @@ export function ModelAccessField() {
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -548,7 +548,7 @@ export function ModelSelectionField({
|
||||
prominence="tertiary"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<Interactive.Container type="button" widthVariant="full">
|
||||
<Interactive.Container type="button" width="full">
|
||||
<Content
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function NonAdminStep() {
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
@@ -99,7 +99,7 @@ export default function NonAdminStep() {
|
||||
description="We will display this name in the app."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<InputTypeIn
|
||||
@@ -125,7 +125,7 @@ export default function NonAdminStep() {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Hoverable.Root group="nonAdminName" widthVariant="full">
|
||||
<Hoverable.Root group="nonAdminName" width="full">
|
||||
<div
|
||||
className={containerClasses}
|
||||
aria-label="Edit display name"
|
||||
|
||||
@@ -48,7 +48,7 @@ const OnboardingHeader = React.memo(
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
paddingVariant="sm"
|
||||
padding="sm"
|
||||
rightChildren={
|
||||
stepButtonText ? (
|
||||
<Section flexDirection="row">
|
||||
|
||||
@@ -30,7 +30,7 @@ const FinalStepItem = React.memo(
|
||||
description={description}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
padding="sm"
|
||||
rightChildren={
|
||||
<Link href={buttonHref as Route} {...linkProps}>
|
||||
<Button prominence="tertiary" rightIcon={SvgExternalLink}>
|
||||
|
||||
@@ -156,7 +156,7 @@ const LLMStep = memo(
|
||||
description="Onyx supports both self-hosted models and popular providers."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="lg"
|
||||
padding="lg"
|
||||
rightChildren={
|
||||
<Button
|
||||
disabled={disabled}
|
||||
|
||||
@@ -52,7 +52,7 @@ const NameStep = React.memo(
|
||||
description="We will display this name in the app."
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="fit"
|
||||
padding="fit"
|
||||
rightChildren={
|
||||
<InputTypeIn
|
||||
ref={inputRef}
|
||||
@@ -66,7 +66,7 @@ const NameStep = React.memo(
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Hoverable.Root group="nameStep" widthVariant="full">
|
||||
<Hoverable.Root group="nameStep" width="full">
|
||||
<div
|
||||
className={containerClasses}
|
||||
onClick={() => {
|
||||
|
||||
Reference in New Issue
Block a user