From 34c2aa0860d51fdf7a414b86cfa07302662dbec0 Mon Sep 17 00:00:00 2001 From: Chris Weaver <25087905+Weves@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:22:20 -0700 Subject: [PATCH] Support svg navigation items (#2542) * Support SVG nav items * Handle specifying custom SVGs for navbar * Add comment * More comment * More comment --- backend/danswer/db/llm.py | 7 --- backend/danswer/server/manage/llm/api.py | 12 ++++- .../server/enterprise_settings/models.py | 15 ++++++- backend/ee/danswer/server/seeding.py | 45 +++++++++++++++++-- web/src/app/admin/settings/interfaces.ts | 3 +- web/src/components/UserDropdown.tsx | 36 ++++++++++++--- 6 files changed, 100 insertions(+), 18 deletions(-) diff --git a/backend/danswer/db/llm.py b/backend/danswer/db/llm.py index 5e3a7abb8e..af2ded9562 100644 --- a/backend/danswer/db/llm.py +++ b/backend/danswer/db/llm.py @@ -64,19 +64,12 @@ def upsert_cloud_embedding_provider( def upsert_llm_provider( llm_provider: LLMProviderUpsertRequest, db_session: Session, - is_creation: bool = True, ) -> FullLLMProvider: existing_llm_provider = db_session.scalar( select(LLMProviderModel).where(LLMProviderModel.name == llm_provider.name) ) - if existing_llm_provider and is_creation: - raise ValueError(f"LLM Provider with name {llm_provider.name} already exists") if not existing_llm_provider: - if not is_creation: - raise ValueError( - f"LLM Provider with name {llm_provider.name} does not exist" - ) existing_llm_provider = LLMProviderModel(name=llm_provider.name) db_session.add(existing_llm_provider) diff --git a/backend/danswer/server/manage/llm/api.py b/backend/danswer/server/manage/llm/api.py index 2fe44f07f6..e0d80433d0 100644 --- a/backend/danswer/server/manage/llm/api.py +++ b/backend/danswer/server/manage/llm/api.py @@ -10,6 +10,7 @@ from danswer.auth.users import current_admin_user from danswer.auth.users import current_user from danswer.db.engine import get_session from danswer.db.llm import fetch_existing_llm_providers +from danswer.db.llm import fetch_provider from danswer.db.llm import remove_llm_provider from danswer.db.llm import update_default_provider from danswer.db.llm import upsert_llm_provider @@ -130,11 +131,20 @@ def put_llm_provider( _: User | None = Depends(current_admin_user), db_session: Session = Depends(get_session), ) -> FullLLMProvider: + # validate request (e.g. if we're intending to create but the name already exists we should throw an error) + # NOTE: may involve duplicate fetching to Postgres, but we're assuming SQLAlchemy is smart enough to cache + # the result + existing_provider = fetch_provider(db_session, llm_provider.name) + if existing_provider and is_creation: + raise HTTPException( + status_code=400, + detail=f"LLM Provider with name {llm_provider.name} already exists", + ) + try: return upsert_llm_provider( llm_provider=llm_provider, db_session=db_session, - is_creation=is_creation, ) except ValueError as e: logger.exception("Failed to upsert LLM Provider") diff --git a/backend/ee/danswer/server/enterprise_settings/models.py b/backend/ee/danswer/server/enterprise_settings/models.py index c770fbd73e..df8f022a40 100644 --- a/backend/ee/danswer/server/enterprise_settings/models.py +++ b/backend/ee/danswer/server/enterprise_settings/models.py @@ -1,3 +1,4 @@ +from typing import Any from typing import List from pydantic import BaseModel @@ -6,8 +7,20 @@ from pydantic import Field class NavigationItem(BaseModel): link: str - icon: str title: str + # Right now must be one of the FA icons + icon: str | None = None + # NOTE: SVG must not have a width / height specified + # This is the actual SVG as a string. Done this way to reduce + # complexity / having to store additional "logos" in Postgres + svg_logo: str | None = None + + @classmethod + def model_validate(cls, *args: Any, **kwargs: Any) -> "NavigationItem": + instance = super().model_validate(*args, **kwargs) + if bool(instance.icon) == bool(instance.svg_logo): + raise ValueError("Exactly one of fa_icon or svg_logo must be specified") + return instance class EnterpriseSettings(BaseModel): diff --git a/backend/ee/danswer/server/seeding.py b/backend/ee/danswer/server/seeding.py index 007aa352ca..feb10cc19c 100644 --- a/backend/ee/danswer/server/seeding.py +++ b/backend/ee/danswer/server/seeding.py @@ -1,5 +1,6 @@ import json import os +from copy import deepcopy from typing import List from typing import Optional @@ -22,6 +23,7 @@ from ee.danswer.db.standard_answer import ( ) from ee.danswer.server.enterprise_settings.models import AnalyticsScriptUpload from ee.danswer.server.enterprise_settings.models import EnterpriseSettings +from ee.danswer.server.enterprise_settings.models import NavigationItem from ee.danswer.server.enterprise_settings.store import store_analytics_script from ee.danswer.server.enterprise_settings.store import ( store_settings as store_ee_settings, @@ -44,6 +46,13 @@ logger = setup_logger() _SEED_CONFIG_ENV_VAR_NAME = "ENV_SEED_CONFIGURATION" +class NavigationItemSeed(BaseModel): + link: str + title: str + # NOTE: SVG at this path must not have a width / height specified + svg_path: str + + class SeedConfiguration(BaseModel): llms: list[LLMProviderUpsertRequest] | None = None admin_user_emails: list[str] | None = None @@ -51,6 +60,10 @@ class SeedConfiguration(BaseModel): personas: list[CreatePersonaRequest] | None = None settings: Settings | None = None enterprise_settings: EnterpriseSettings | None = None + + # allows for specifying custom navigation items that have your own custom SVG logos + nav_item_overrides: list[NavigationItemSeed] | None = None + # Use existing `CUSTOM_ANALYTICS_SECRET_KEY` for reference analytics_script_path: str | None = None custom_tools: List[CustomToolSeed] | None = None @@ -60,7 +73,7 @@ def _parse_env() -> SeedConfiguration | None: seed_config_str = os.getenv(_SEED_CONFIG_ENV_VAR_NAME) if not seed_config_str: return None - seed_config = SeedConfiguration.parse_raw(seed_config_str) + seed_config = SeedConfiguration.model_validate_json(seed_config_str) return seed_config @@ -152,9 +165,35 @@ def _seed_settings(settings: Settings) -> None: def _seed_enterprise_settings(seed_config: SeedConfiguration) -> None: - if seed_config.enterprise_settings is not None: + if ( + seed_config.enterprise_settings is not None + or seed_config.nav_item_overrides is not None + ): + final_enterprise_settings = ( + deepcopy(seed_config.enterprise_settings) + if seed_config.enterprise_settings + else EnterpriseSettings() + ) + + final_nav_items = final_enterprise_settings.custom_nav_items + if seed_config.nav_item_overrides is not None: + final_nav_items = [] + for item in seed_config.nav_item_overrides: + with open(item.svg_path, "r") as file: + svg_content = file.read().strip() + + final_nav_items.append( + NavigationItem( + link=item.link, + title=item.title, + svg_logo=svg_content, + ) + ) + + final_enterprise_settings.custom_nav_items = final_nav_items + logger.notice("Seeding enterprise settings") - store_ee_settings(seed_config.enterprise_settings) + store_ee_settings(final_enterprise_settings) def _seed_logo(db_session: Session, logo_path: str | None) -> None: diff --git a/web/src/app/admin/settings/interfaces.ts b/web/src/app/admin/settings/interfaces.ts index 576b7479ba..8327d69d44 100644 --- a/web/src/app/admin/settings/interfaces.ts +++ b/web/src/app/admin/settings/interfaces.ts @@ -18,7 +18,8 @@ export interface Notification { export interface NavigationItem { link: string; - icon: string; + icon?: string; + svg_logo?: string; title: string; } diff --git a/web/src/components/UserDropdown.tsx b/web/src/components/UserDropdown.tsx index 3bc18b5241..cf12b72d82 100644 --- a/web/src/components/UserDropdown.tsx +++ b/web/src/components/UserDropdown.tsx @@ -61,7 +61,9 @@ export function UserDropdown({ combinedSettings?.enterpriseSettings?.custom_nav_items || []; useEffect(() => { - const iconNames = customNavItems.map((item) => item.icon); + const iconNames = customNavItems + .map((item) => item.icon) + .filter((icon) => icon) as string[]; preloadIcons(iconNames); }, [customNavItems]); @@ -141,10 +143,34 @@ export function UserDropdown({ key={i} href={item.link} icon={ - + item.svg_logo ? ( +
+ +
+ ) : ( + + ) } label={item.title} />