diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index 04ecbad05b..cfc243ca83 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -20,6 +20,7 @@ from rest_framework.views import APIView import InvenTree.version import users.models +from InvenTree import helpers from InvenTree.mixins import ListCreateAPI from InvenTree.templatetags.inventree_extras import plugins_info from part.models import Part @@ -198,6 +199,14 @@ class VersionTextView(ListAPI): class InfoApiSerializer(serializers.Serializer): """InvenTree server information - some information might be blanked if called without elevated credentials.""" + class CustomizeSerializer(serializers.Serializer): + """Serializer for customize field.""" + + logo = serializers.CharField() + splash = serializers.CharField() + login_message = serializers.CharField() + navbar_message = serializers + server = serializers.CharField(read_only=True) version = serializers.CharField(read_only=True) instance = serializers.CharField(read_only=True) @@ -214,6 +223,7 @@ class InfoApiSerializer(serializers.Serializer): default_locale = serializers.ChoiceField( choices=settings.LOCALE_CODES, read_only=True ) + customize = CustomizeSerializer(read_only=True) system_health = serializers.BooleanField(read_only=True) database = serializers.CharField(read_only=True) platform = serializers.CharField(read_only=True) @@ -263,6 +273,12 @@ class InfoView(APIView): 'debug_mode': settings.DEBUG, 'docker_mode': settings.DOCKER, 'default_locale': settings.LANGUAGE_CODE, + 'customize': { + 'logo': helpers.getLogoImage(), + 'splash': helpers.getSplashScreen(), + 'login_message': helpers.getCustomOption('login_message'), + 'navbar_message': helpers.getCustomOption('navbar_message'), + }, # Following fields are only available to staff users 'system_health': check_system_health() if is_staff else None, 'database': InvenTree.version.inventreeDatabase() if is_staff else None, diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index d84fd35737..9ca039c83f 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 306 +INVENTREE_API_VERSION = 307 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v307 - 2025-01-29 : https://github.com/inventree/InvenTree/pull/8969 + - Extend Info Endpoint to include customizations + v306 - 2025-01-28 : https://github.com/inventree/InvenTree/pull/8966 - Adds "start_date" to PurchasesOrder API - Adds "start_date" to SalesOrder API diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index b0f9b6aa84..1565aa4d50 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -193,6 +193,15 @@ def getSplashScreen(custom=True): return static_storage.url('img/inventree_splash.jpg') +def getCustomOption(reference: str): + """Return the value of a custom option from settings.CUSTOMIZE. + + Args: + reference: Reference key for the custom option + """ + return settings.CUSTOMIZE.get(reference, None) + + def TestIfImageURL(url): """Test if an image URL (or filename) looks like a valid image format. diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index a97f3aa605..3a5ec0ad20 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -1,4 +1,11 @@ -import { ActionIcon, Container, Group, Indicator, Tabs } from '@mantine/core'; +import { + ActionIcon, + Container, + Group, + Indicator, + Tabs, + Text +} from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconBell, IconSearch } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; @@ -10,7 +17,7 @@ import { navTabs as mainNavTabs } from '../../defaults/links'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { navigateToLink } from '../../functions/navigation'; import * as classes from '../../main.css'; -import { apiUrl } from '../../states/ApiState'; +import { apiUrl, useServerApiState } from '../../states/ApiState'; import { useLocalState } from '../../states/LocalState'; import { useGlobalSettingsState } from '../../states/SettingsState'; import { useUserState } from '../../states/UserState'; @@ -27,6 +34,7 @@ export function Header() { state.setNavigationOpen, state.navigationOpen ]); + const [server] = useServerApiState((state) => [state.server]); const [navDrawerOpened, { open: openNavDrawer, close: closeNavDrawer }] = useDisclosure(navigationOpen); const [ @@ -40,11 +48,13 @@ export function Header() { ] = useDisclosure(false); const { isLoggedIn } = useUserState(); - const [notificationCount, setNotificationCount] = useState(0); - const globalSettings = useGlobalSettingsState(); + const navbar_message = useMemo(() => { + return server.customize?.navbar_message; + }, [server.customize]); + // Fetch number of notifications for the current user const notifications = useQuery({ queryKey: ['notification-count'], @@ -105,6 +115,12 @@ export function Header() { + {navbar_message && ( + + {/* biome-ignore lint/security/noDangerouslySetInnerHtml: */} +
+ + )} { + const val = server.customize?.login_message; + if (val) { + return ( + <> + + + + dangerouslySetInnerHTML={{ __html: val }} + /> + + + ); + } + return null; + }, [server.customize]); + + const SplashComponent = useMemo(() => { + const temp = server.customize?.splash; + if (temp) { + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); + } + return ({ children }: { children: React.ReactNode }) => <>{children}; + }, [server.customize]); + // Data manipulation functions function ChangeHost(newHost: string | null): void { if (newHost === null) return; @@ -75,31 +110,45 @@ export default function Login() { // Main rendering block return ( -
- - {hostEdit ? ( - - ) : ( - <> - - - {loginMode ? ( - Welcome, log in below - ) : ( - Register below - )} - - {loginMode ? : } - - - - - )} - -
+ +
+
+ + {hostEdit ? ( + + ) : ( + <> + + + {loginMode ? ( + Welcome, log in below + ) : ( + Register below + )} + + {loginMode ? : } + + {LoginMessage} + + + + )} + +
+
+
); } diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx index e0ca580930..e381428528 100644 --- a/src/frontend/src/states/states.tsx +++ b/src/frontend/src/states/states.tsx @@ -48,6 +48,12 @@ export interface ServerAPIProps { target: null | string; default_locale: null | string; django_admin: null | string; + customize: null | { + logo: string; + splash: string; + login_message: string; + navbar_message: string; + }; } export interface AuthProps {