From 782f36cd48801c194726555e3d20f5f195ed909c Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 31 Oct 2023 00:02:28 +0100 Subject: [PATCH] [PUI] Added AboutInventreeModal (#5813) * updated typing to allow either link or action * fixed typing * made it possible to use an action instead of a link * added ServerInfo Modal skeleton * fixed anchor * added content to ServerInfo * Factored database lookup out * Extended status API to CUI level * extended ServerInfo to CUI level * Made modal larger * fixed default settings * Refactored urls into seperate functions * Refactored python version into seperate function * Added endpoint and modal for PUI version modal * switched to indirect imports to reduce imports * Added copy button * Added full copy button * added default * cleaned unused vars * cleaned unused vars * Refactored auth check for InfoView * implemented suggested changes * fixed check logic --- InvenTree/InvenTree/api.py | 73 +++++++- InvenTree/InvenTree/api_version.py | 5 +- InvenTree/InvenTree/urls.py | 7 +- InvenTree/InvenTree/version.py | 26 +++ .../part/templatetags/inventree_extras.py | 15 +- InvenTree/templates/about.html | 2 +- .../src/components/items/ButtonMenu.tsx | 1 - .../src/components/items/CopyButton.tsx | 28 +++ .../components/modals/AboutInvenTreeModal.tsx | 171 ++++++++++++++++++ src/frontend/src/contexts/ThemeContext.tsx | 7 +- src/frontend/src/defaults/defaults.tsx | 5 +- src/frontend/src/defaults/links.tsx | 12 +- src/frontend/src/pages/Notifications.tsx | 1 - src/frontend/src/states/ApiState.tsx | 3 + src/frontend/src/states/states.tsx | 3 + 15 files changed, 335 insertions(+), 24 deletions(-) create mode 100644 src/frontend/src/components/items/CopyButton.tsx create mode 100644 src/frontend/src/components/modals/AboutInvenTreeModal.tsx diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index d2524f9fc4..ab239e23f3 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -11,21 +11,52 @@ from rest_framework.response import Response from rest_framework.serializers import ValidationError from rest_framework.views import APIView +import InvenTree.version import users.models from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.mixins import ListCreateAPI from InvenTree.permissions import RolePermission from part.templatetags.inventree_extras import plugins_info from plugin.serializers import MetadataSerializer +from users.models import ApiToken from .email import is_email_configured from .mixins import RetrieveUpdateAPI from .status import check_system_health, is_worker_running -from .version import (inventreeApiVersion, inventreeDatabase, - inventreeInstanceName, inventreeVersion) from .views import AjaxView +class VersionView(APIView): + """Simple JSON endpoint for InvenTree version information.""" + + permission_classes = [ + permissions.IsAdminUser, + ] + + def get(self, request, *args, **kwargs): + """Return information about the InvenTree server.""" + return JsonResponse({ + 'dev': InvenTree.version.isInvenTreeDevelopmentVersion(), + 'up_to_date': InvenTree.version.isInvenTreeUpToDate(), + 'version': { + 'server': InvenTree.version.inventreeVersion(), + 'api': InvenTree.version.inventreeApiVersion(), + 'commit_hash': InvenTree.version.inventreeCommitHash(), + 'commit_date': InvenTree.version.inventreeCommitDate(), + 'commit_branch': InvenTree.version.inventreeBranch(), + 'python': InvenTree.version.inventreePythonVersion(), + 'django': InvenTree.version.inventreeDjangoVersion() + }, + 'links': { + 'doc': InvenTree.version.inventreeDocUrl(), + 'code': InvenTree.version.inventreeGithubUrl(), + 'credit': InvenTree.version.inventreeCreditsUrl(), + 'app': InvenTree.version.inventreeAppUrl(), + 'bug': f'{InvenTree.version.inventreeGithubUrl()}/issues' + } + }) + + class InfoView(AjaxView): """Simple JSON endpoint for InvenTree information. @@ -40,11 +71,16 @@ class InfoView(AjaxView): def get(self, request, *args, **kwargs): """Serve current server information.""" + is_staff = request.user.is_staff + if not is_staff and request.user.is_anonymous: + # Might be Token auth - check if so + is_staff = self.check_auth_header(request) + data = { 'server': 'InvenTree', - 'version': inventreeVersion(), - 'instance': inventreeInstanceName(), - 'apiVersion': inventreeApiVersion(), + 'version': InvenTree.version.inventreeVersion(), + 'instance': InvenTree.version.inventreeInstanceName(), + 'apiVersion': InvenTree.version.inventreeApiVersion(), 'worker_running': is_worker_running(), 'worker_pending_tasks': self.worker_pending_tasks(), 'plugins_enabled': settings.PLUGINS_ENABLED, @@ -52,12 +88,35 @@ class InfoView(AjaxView): 'email_configured': is_email_configured(), 'debug_mode': settings.DEBUG, 'docker_mode': settings.DOCKER, - 'system_health': check_system_health() if request.user.is_staff else None, - 'database': inventreeDatabase()if request.user.is_staff else None + 'system_health': check_system_health() if is_staff else None, + 'database': InvenTree.version.inventreeDatabase()if is_staff else None, + 'platform': InvenTree.version.inventreePlatform() if is_staff else None, + 'installer': InvenTree.version.inventreeInstaller() if is_staff else None, + 'target': InvenTree.version.inventreeTarget()if is_staff else None, } return JsonResponse(data) + def check_auth_header(self, request): + """Check if user is authenticated via a token in the header.""" + # TODO @matmair: remove after refacgtor of Token check is done + headers = request.headers.get('Authorization', request.headers.get('authorization')) + if not headers: + return False + + auth = headers.strip() + if not (auth.lower().startswith('token') and len(auth.split()) == 2): + return False + + token_key = auth.split()[1] + try: + token = ApiToken.objects.get(key=token_key) + if token.active and token.user and token.user.is_staff: + return True + except ApiToken.DoesNotExist: + pass + return False + class NotFoundView(AjaxView): """Simple JSON view when accessing an invalid API view.""" diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 3697898dea..04e1dbf7f9 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 143 +INVENTREE_API_VERSION = 144 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v144 -> 2023-10-23: https://github.com/inventree/InvenTree/pull/5811 + - Adds version information API endpoint + v143 -> 2023-10-29: https://github.com/inventree/InvenTree/pull/5810 - Extends the status endpoint to include information about system status and health diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 82a21fd331..5c62db8c75 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -36,7 +36,7 @@ from plugin.urls import get_plugin_urls from stock.urls import stock_urls from web.urls import urlpatterns as platform_urls -from .api import APISearchView, InfoView, NotFoundView +from .api import APISearchView, InfoView, NotFoundView, VersionView from .magic_login import GetSimpleLoginView from .social_auth_urls import SocialProviderListView, social_auth_urlpatterns from .views import (AboutView, AppearanceSelectView, CustomConnectionsView, @@ -76,8 +76,9 @@ apipatterns = [ # OpenAPI Schema re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'), - # InvenTree information endpoint - path('', InfoView.as_view(), name='api-inventree-info'), + # InvenTree information endpoints + path('version/', VersionView.as_view(), name='api-version'), # version info + path('', InfoView.as_view(), name='api-inventree-info'), # server info # Auth API endpoints path('auth/', include([ diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 72b6455ca6..cb64def136 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -97,6 +97,27 @@ def inventreeDocsVersion(): return INVENTREE_SW_VERSION # pragma: no cover +def inventreeDocUrl(): + """Return URL for InvenTree documentation site.""" + tag = inventreeDocsVersion() + return f"https://docs.inventree.org/en/{tag}" + + +def inventreeAppUrl(): + """Return URL for InvenTree app site.""" + return f'{inventreeDocUrl()}/app/app', + + +def inventreeCreditsUrl(): + """Return URL for InvenTree credits site.""" + return "https://docs.inventree.org/en/latest/credits/" + + +def inventreeGithubUrl(): + """Return URL for InvenTree github site.""" + return "https://github.com/InvenTree/InvenTree/" + + def isInvenTreeUpToDate(): """Test if the InvenTree instance is "up to date" with the latest version. @@ -126,6 +147,11 @@ def inventreeDjangoVersion(): return django.get_version() +def inventreePythonVersion(): + """Returns the version of python""" + return sys.version.split(' ')[0] + + def inventreeCommitHash(): """Returns the git commit hash for the running codebase.""" # First look in the environment variables, i.e. if running in docker diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 8e7e488bad..9abd6748bf 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -2,7 +2,6 @@ import logging import os -import sys from datetime import date, datetime from django import template @@ -222,7 +221,7 @@ def inventree_base_url(*args, **kwargs): @register.simple_tag() def python_version(*args, **kwargs): """Return the current python version.""" - return sys.version.split(' ')[0] + return version.inventreePythonVersion() @register.simple_tag() @@ -302,21 +301,25 @@ def inventree_platform(*args, **kwargs): @register.simple_tag() def inventree_github_url(*args, **kwargs): """Return URL for InvenTree github site.""" - return "https://github.com/InvenTree/InvenTree/" + return version.inventreeGithubUrl() @register.simple_tag() def inventree_docs_url(*args, **kwargs): """Return URL for InvenTree documentation site.""" - tag = version.inventreeDocsVersion() + return version.inventreeDocUrl() - return f"https://docs.inventree.org/en/{tag}" + +@register.simple_tag() +def inventree_app_url(*args, **kwargs): + """Return URL for InvenTree app site.""" + return version.inventreeAppUrl() @register.simple_tag() def inventree_credits_url(*args, **kwargs): """Return URL for InvenTree credits site.""" - return "https://docs.inventree.org/en/latest/credits/" + return version.inventreeCreditsUrl() @register.simple_tag() diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 0c02ade0e1..7a71c10483 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -77,7 +77,7 @@ {% trans "Mobile App" %} - {% inventree_docs_url %}/app/app + {% inventree_app_url %} diff --git a/src/frontend/src/components/items/ButtonMenu.tsx b/src/frontend/src/components/items/ButtonMenu.tsx index 234bce7a62..c30b146633 100644 --- a/src/frontend/src/components/items/ButtonMenu.tsx +++ b/src/frontend/src/components/items/ButtonMenu.tsx @@ -1,5 +1,4 @@ import { ActionIcon, Menu, Tooltip } from '@mantine/core'; -import { Component } from 'react'; /** * A ButtonMenu is a button that opens a menu when clicked. diff --git a/src/frontend/src/components/items/CopyButton.tsx b/src/frontend/src/components/items/CopyButton.tsx new file mode 100644 index 0000000000..02bba03d6b --- /dev/null +++ b/src/frontend/src/components/items/CopyButton.tsx @@ -0,0 +1,28 @@ +import { t } from '@lingui/macro'; +import { Button, CopyButton as MantineCopyButton } from '@mantine/core'; +import { IconCopy } from '@tabler/icons-react'; + +export function CopyButton({ + value, + label +}: { + value: any; + label?: JSX.Element; +}) { + return ( + + {({ copied, copy }) => ( + + )} + + ); +} diff --git a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx new file mode 100644 index 0000000000..9a7771fb00 --- /dev/null +++ b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx @@ -0,0 +1,171 @@ +import { Trans } from '@lingui/macro'; +import { Anchor, Badge, Group, Stack, Table, Text, Title } from '@mantine/core'; +import { ContextModalProps } from '@mantine/modals'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '../../App'; +import { ApiPaths, apiUrl, useServerApiState } from '../../states/ApiState'; +import { useLocalState } from '../../states/LocalState'; +import { useUserState } from '../../states/UserState'; +import { CopyButton } from '../items/CopyButton'; + +type AboutLookupRef = { + ref: string; + title: JSX.Element; + link?: string; + copy?: boolean; +}; + +export function AboutInvenTreeModal({}: ContextModalProps<{ + modalBody: string; +}>) { + const [user] = useUserState((state) => [state.user]); + const { host } = useLocalState.getState(); + const [server] = useServerApiState((state) => [state.server]); + + if (user?.is_staff != true) + return ( + + This information is only available for staff users + + ); + + const { isLoading, data } = useQuery({ + queryKey: ['version'], + queryFn: () => api.get(apiUrl(ApiPaths.version)).then((res) => res.data) + }); + + function fillTable( + lookup: AboutLookupRef[], + data: any, + alwaysLink: boolean = false + ) { + return lookup.map((map: AboutLookupRef, idx) => ( + + {map.title} + + + {alwaysLink ? ( + + {data[map.ref]} + + ) : map.link ? ( + + {data[map.ref]} + + ) : ( + data[map.ref] + )} + {map.copy && } + + + + )); + } + /* renderer */ + if (isLoading) return Loading; + + const copyval = `InvenTree-Version: ${data.version.server}\nDjango Version: ${ + data.version.django + }\n${ + data.version.commit_hash && + `Commit Hash: ${data.version.commit_hash}\nCommit Date: ${data.version.commit_date}\nCommit Branch: ${data.version.commit_branch}\n` + }Database: ${server.database}\nDebug-Mode: ${ + server.debug_mode ? 'True' : 'False' + }\nDeployed using Docker: ${ + server.docker_mode ? 'True' : 'False' + }\nPlatform: ${server.platform}\nInstaller: ${server.installer}\n${ + server.target && `Target: ${server.target}\n` + }Active plugins: ${JSON.stringify(server.active_plugins)}`; + return ( + + + + Your InvenTree version status is + + {data.dev ? ( + + Development Version + + ) : data.up_to_date ? ( + + Up to Date + + ) : ( + + Update Available + + )} + + + <Trans>Version Information</Trans> + + + + {fillTable( + [ + { + ref: 'server', + title: InvenTree Version, + link: 'https://github.com/inventree/InvenTree/releases', + copy: true + }, + { + ref: 'commit_hash', + title: Commit Hash, + copy: true + }, + { + ref: 'commit_date', + title: Commit Date, + copy: true + }, + { + ref: 'commit_branch', + title: Commit Branch, + copy: true + }, + { + ref: 'api', + title: API Version, + link: `${host}api-doc/` + }, + { ref: 'python', title: Python Version }, + { + ref: 'django', + title: Django Version, + link: 'https://www.djangoproject.com/', + copy: true + } + ], + data.version + )} + +
+ + <Trans>Links</Trans> + + + + {fillTable( + [ + { ref: 'doc', title: InvenTree Documentation }, + { ref: 'code', title: View Code on GitHub }, + { ref: 'credit', title: Credits }, + { ref: 'app', title: Mobile App }, + { ref: 'bug', title: Submit Bug Report } + ], + data.links, + true + )} + +
+ + Copy version information} + /> + +
+ ); +} diff --git a/src/frontend/src/contexts/ThemeContext.tsx b/src/frontend/src/contexts/ThemeContext.tsx index 92ed79f454..b9ee5160b1 100644 --- a/src/frontend/src/contexts/ThemeContext.tsx +++ b/src/frontend/src/contexts/ThemeContext.tsx @@ -9,6 +9,7 @@ import { useColorScheme, useLocalStorage } from '@mantine/hooks'; import { ModalsProvider } from '@mantine/modals'; import { Notifications } from '@mantine/notifications'; +import { AboutInvenTreeModal } from '../components/modals/AboutInvenTreeModal'; import { QrCodeModal } from '../components/modals/QrCodeModal'; import { ServerInfoModal } from '../components/modals/ServerInfoModal'; import { useLocalState } from '../states/LocalState'; @@ -61,7 +62,11 @@ export function ThemeContext({ children }: { children: JSX.Element }) { {children} diff --git a/src/frontend/src/defaults/defaults.tsx b/src/frontend/src/defaults/defaults.tsx index f9f161c081..48e1796f94 100644 --- a/src/frontend/src/defaults/defaults.tsx +++ b/src/frontend/src/defaults/defaults.tsx @@ -13,7 +13,10 @@ export const emptyServerAPI = { debug_mode: null, docker_mode: null, database: null, - system_health: null + system_health: null, + platform: null, + installer: null, + target: null }; export interface SiteMarkProps { diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index 08032b19db..0e4c1d8222 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -79,6 +79,15 @@ function serverInfo() { }); } +function aboutInvenTree() { + return openContextModal({ + modal: 'about', + title: About InvenTree, + size: 'xl', + innerProps: {} + }); +} + // TODO @matmair: Add the following pages and adjust the links export const aboutLinks: DocumentationLinkItem[] = [ { @@ -91,8 +100,7 @@ export const aboutLinks: DocumentationLinkItem[] = [ id: 'about', title: About InvenTree, description: About the InvenTree org, - link: '/about', - placeholder: true + action: aboutInvenTree }, { id: 'licenses', diff --git a/src/frontend/src/pages/Notifications.tsx b/src/frontend/src/pages/Notifications.tsx index 45f73eb878..0c2b1c4f78 100644 --- a/src/frontend/src/pages/Notifications.tsx +++ b/src/frontend/src/pages/Notifications.tsx @@ -4,7 +4,6 @@ import { IconBellCheck, IconBellExclamation } from '@tabler/icons-react'; import { useMemo } from 'react'; import { api } from '../App'; -import { StylishText } from '../components/items/StylishText'; import { PageDetail } from '../components/nav/PageDetail'; import { PanelGroup } from '../components/nav/PanelGroup'; import { NotificationTable } from '../components/tables/notifications/NotificationsTable'; diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 91fb908410..eb77dbc10b 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -68,6 +68,7 @@ export enum ApiPaths { barcode = 'api-barcode', news = 'news', global_status = 'api-global-status', + version = 'api-version', // Build order URLs build_order_list = 'api-build-list', @@ -158,6 +159,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'news/'; case ApiPaths.global_status: return 'generic/status/'; + case ApiPaths.version: + return 'version/'; case ApiPaths.build_order_list: return 'build/'; case ApiPaths.build_order_attachment_list: diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx index 5e67621d6b..27c21038d0 100644 --- a/src/frontend/src/states/states.tsx +++ b/src/frontend/src/states/states.tsx @@ -33,6 +33,9 @@ export interface ServerAPIProps { docker_mode: null | boolean; database: null | string; system_health: null | boolean; + platform: null | string; + installer: null | string; + target: null | string; } // Type interface defining a single 'setting' object