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