From 354dc12b3b015d4936f113b8acd2030b0b80c0ef Mon Sep 17 00:00:00 2001 From: Debanjum Date: Wed, 4 Dec 2024 23:53:43 -0800 Subject: [PATCH] Style the Admin Panel with a modern theme and Khoj branding (#999) Overview - The default django admin panel UI looks pretty dated and didn't have any Khoj specific branding - Used the Unfold Django admin panel theme for a modern look - Used the Khoj logo and name in Admin panel title, headings, favicons Details: All models shown on Admin panel need to inherit from unfold's ModelAdmin to get styling applied. So - Make all models on Admin panel inherit from unfold's ModelAdmin - Subclassed UserAdmin to inherit from unfold's ModelAdmin - Deregistered the unused Auth Group model from the Admin panel We can add it back when its actually used. Avoid confusion for now - Explicitly register DjangoJobExecution on admin panel and again make it inherit from the unfold.admin.ModelAdmin --- pyproject.toml | 1 + .../icons/khoj_lantern_128x128_dark.png | Bin 0 -> 7025 bytes src/khoj/app/settings.py | 21 ++++++ src/khoj/database/admin.py | 71 +++++++++++------- 4 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 src/interface/web/public/assets/icons/khoj_lantern_128x128_dark.png diff --git a/pyproject.toml b/pyproject.toml index 59adf952..af3460b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ dependencies = [ "anyio == 3.7.1", "pymupdf == 1.24.11", "django == 5.0.9", + "django-unfold == 0.42.0", "authlib == 1.2.1", "llama-cpp-python == 0.2.88", "itsdangerous == 2.1.2", diff --git a/src/interface/web/public/assets/icons/khoj_lantern_128x128_dark.png b/src/interface/web/public/assets/icons/khoj_lantern_128x128_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8fc673a5ce54471adf63c06cca583244a41466db GIT binary patch literal 7025 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 K2`g5zc=vyRiKt@$ literal 0 HcmV?d00001 diff --git a/src/khoj/app/settings.py b/src/khoj/app/settings.py index 020b2b2e..708e11d0 100644 --- a/src/khoj/app/settings.py +++ b/src/khoj/app/settings.py @@ -13,6 +13,8 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ import os from pathlib import Path +from django.templatetags.static import static + from khoj.utils.helpers import in_debug_mode, is_env_var_true # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -72,6 +74,7 @@ INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "khoj.database.apps.DatabaseConfig", + "unfold", "django.contrib.admin", "django.contrib.sessions", "django.contrib.messages", @@ -195,3 +198,21 @@ APSCHEDULER_DATETIME_FORMAT = "N j, Y, f:s a" # that supports multiple background worker processes instead (e.g. Dramatiq, Celery, Django-RQ, # etc. See: https://djangopackages.org/grids/g/workers-queues-tasks/ for popular options). APSCHEDULER_RUN_NOW_TIMEOUT = 240 # Seconds + +UNFOLD = { + "SITE_TITLE": "Khoj Admin Panel", + "SITE_HEADER": "Khoj Admin Panel", + "SITE_URL": "/", + "SITE_ICON": { + "light": lambda request: static("assets/icons/khoj_lantern_128x128.png"), + "dark": lambda request: static("assets/icons/khoj_lantern_128x128_dark.png"), + }, + "SITE_FAVICONS": [ + { + "rel": "icon", + "sizes": "32x32", + "type": "image/svg+xml", + "href": lambda request: static("assets/icons/khoj_lantern.svg"), + }, + ], +} diff --git a/src/khoj/database/admin.py b/src/khoj/database/admin.py index 906f2ffe..0c5bb3e8 100644 --- a/src/khoj/database/admin.py +++ b/src/khoj/database/admin.py @@ -4,11 +4,14 @@ from datetime import date, datetime, timedelta, timezone from apscheduler.job import Job from django.contrib import admin, messages -from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import Group from django.http import HttpResponse -from django_apscheduler.admin import DjangoJobAdmin +from django_apscheduler.admin import DjangoJobAdmin, DjangoJobExecutionAdmin from django_apscheduler.jobstores import DjangoJobStore -from django_apscheduler.models import DjangoJob +from django_apscheduler.models import DjangoJob, DjangoJobExecution +from unfold import admin as unfold_admin from khoj.database.models import ( Agent, @@ -35,10 +38,8 @@ from khoj.database.models import ( ) from khoj.utils.helpers import ImageIntentType -admin.site.unregister(DjangoJob) - -class KhojDjangoJobAdmin(DjangoJobAdmin): +class KhojDjangoJobAdmin(DjangoJobAdmin, unfold_admin.ModelAdmin): list_display = ( "id", "next_run_time", @@ -62,10 +63,25 @@ class KhojDjangoJobAdmin(DjangoJobAdmin): return queryset, use_distinct +class KhojDjangoJobExecutionAdmin(DjangoJobExecutionAdmin, unfold_admin.ModelAdmin): + pass + + +admin.site.unregister(DjangoJob) admin.site.register(DjangoJob, KhojDjangoJobAdmin) +admin.site.unregister(DjangoJobExecution) +admin.site.register(DjangoJobExecution, KhojDjangoJobExecutionAdmin) -class KhojUserAdmin(UserAdmin): +class GroupAdmin(BaseGroupAdmin, unfold_admin.ModelAdmin): + pass + + +class UserAdmin(BaseUserAdmin, unfold_admin.ModelAdmin): + pass + + +class KhojUserAdmin(UserAdmin, unfold_admin.ModelAdmin): class DateJoinedAfterFilter(admin.SimpleListFilter): title = "Joined after" parameter_name = "joined_after" @@ -137,21 +153,22 @@ class KhojUserAdmin(UserAdmin): get_email_login_url.short_description = "Get email login URL" # type: ignore +admin.site.unregister(Group) admin.site.register(KhojUser, KhojUserAdmin) -admin.site.register(ProcessLock) -admin.site.register(SpeechToTextModelOptions) -admin.site.register(ReflectiveQuestion) -admin.site.register(ClientApplication) -admin.site.register(GithubConfig) -admin.site.register(NotionConfig) -admin.site.register(UserVoiceModelConfig) -admin.site.register(VoiceModelOption) -admin.site.register(UserRequests) +admin.site.register(ProcessLock, unfold_admin.ModelAdmin) +admin.site.register(SpeechToTextModelOptions, unfold_admin.ModelAdmin) +admin.site.register(ReflectiveQuestion, unfold_admin.ModelAdmin) +admin.site.register(ClientApplication, unfold_admin.ModelAdmin) +admin.site.register(GithubConfig, unfold_admin.ModelAdmin) +admin.site.register(NotionConfig, unfold_admin.ModelAdmin) +admin.site.register(UserVoiceModelConfig, unfold_admin.ModelAdmin) +admin.site.register(VoiceModelOption, unfold_admin.ModelAdmin) +admin.site.register(UserRequests, unfold_admin.ModelAdmin) @admin.register(Agent) -class AgentAdmin(admin.ModelAdmin): +class AgentAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "name", @@ -161,7 +178,7 @@ class AgentAdmin(admin.ModelAdmin): @admin.register(Entry) -class EntryAdmin(admin.ModelAdmin): +class EntryAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "created_at", @@ -183,7 +200,7 @@ class EntryAdmin(admin.ModelAdmin): @admin.register(Subscription) -class KhojUserSubscription(admin.ModelAdmin): +class KhojUserSubscription(unfold_admin.ModelAdmin): list_display = ( "id", "user", @@ -195,7 +212,7 @@ class KhojUserSubscription(admin.ModelAdmin): @admin.register(ChatModelOptions) -class ChatModelOptionsAdmin(admin.ModelAdmin): +class ChatModelOptionsAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "chat_model", @@ -206,7 +223,7 @@ class ChatModelOptionsAdmin(admin.ModelAdmin): @admin.register(TextToImageModelConfig) -class TextToImageModelOptionsAdmin(admin.ModelAdmin): +class TextToImageModelOptionsAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "model_name", @@ -216,7 +233,7 @@ class TextToImageModelOptionsAdmin(admin.ModelAdmin): @admin.register(OpenAIProcessorConversationConfig) -class OpenAIProcessorConversationConfigAdmin(admin.ModelAdmin): +class OpenAIProcessorConversationConfigAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "name", @@ -227,7 +244,7 @@ class OpenAIProcessorConversationConfigAdmin(admin.ModelAdmin): @admin.register(SearchModelConfig) -class SearchModelConfigAdmin(admin.ModelAdmin): +class SearchModelConfigAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "name", @@ -238,7 +255,7 @@ class SearchModelConfigAdmin(admin.ModelAdmin): @admin.register(ServerChatSettings) -class ServerChatSettingsAdmin(admin.ModelAdmin): +class ServerChatSettingsAdmin(unfold_admin.ModelAdmin): list_display = ( "chat_default", "chat_advanced", @@ -247,7 +264,7 @@ class ServerChatSettingsAdmin(admin.ModelAdmin): @admin.register(WebScraper) -class WebScraperAdmin(admin.ModelAdmin): +class WebScraperAdmin(unfold_admin.ModelAdmin): list_display = ( "priority", "name", @@ -261,7 +278,7 @@ class WebScraperAdmin(admin.ModelAdmin): @admin.register(Conversation) -class ConversationAdmin(admin.ModelAdmin): +class ConversationAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "user", @@ -367,7 +384,7 @@ class ConversationAdmin(admin.ModelAdmin): @admin.register(UserConversationConfig) -class UserConversationConfigAdmin(admin.ModelAdmin): +class UserConversationConfigAdmin(unfold_admin.ModelAdmin): list_display = ( "id", "get_user_email",