Support svg navigation items (#2542)

* Support SVG nav items

* Handle specifying custom SVGs for navbar

* Add comment

* More comment

* More comment
This commit is contained in:
Chris Weaver 2024-09-23 13:22:20 -07:00 committed by GitHub
parent 45f67368a2
commit 34c2aa0860
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 100 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ export interface Notification {
export interface NavigationItem {
link: string;
icon: string;
icon?: string;
svg_logo?: string;
title: string;
}

View File

@ -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={
<DynamicFaIcon
name={item.icon}
className="h-4 w-4 my-auto mr-2"
/>
item.svg_logo ? (
<div
className="
h-4
w-4
my-auto
mr-2
overflow-hidden
flex
items-center
justify-center
"
aria-label={item.title}
>
<svg
viewBox="0 0 24 24"
width="100%"
height="100%"
preserveAspectRatio="xMidYMid meet"
dangerouslySetInnerHTML={{ __html: item.svg_logo }}
/>
</div>
) : (
<DynamicFaIcon
name={item.icon!}
className="h-4 w-4 my-auto mr-2"
/>
)
}
label={item.title}
/>