Add beta request nav toggle
Magent CI/CD / verify (push) Successful in 10m47s
Magent CI/CD / deploy-prod (push) Has been skipped
Magent CI/CD / deploy-beta (push) Successful in 18s

This commit is contained in:
2026-06-21 12:32:03 +12:00
parent e163920e21
commit 0667a172d1
7 changed files with 69 additions and 7 deletions
+3
View File
@@ -110,6 +110,9 @@ class Settings(BaseSettings):
site_login_show_signup_link: bool = Field( site_login_show_signup_link: bool = Field(
default=True, validation_alias=AliasChoices("SITE_LOGIN_SHOW_SIGNUP_LINK") default=True, validation_alias=AliasChoices("SITE_LOGIN_SHOW_SIGNUP_LINK")
) )
site_nav_show_requests: bool = Field(
default=True, validation_alias=AliasChoices("SITE_NAV_SHOW_REQUESTS")
)
site_changelog: Optional[str] = Field(default=CHANGELOG) site_changelog: Optional[str] = Field(default=CHANGELOG)
magent_application_url: Optional[str] = Field( magent_application_url: Optional[str] = Field(
+1
View File
@@ -241,6 +241,7 @@ SETTING_KEYS: List[str] = [
"site_login_show_local_login", "site_login_show_local_login",
"site_login_show_forgot_password", "site_login_show_forgot_password",
"site_login_show_signup_link", "site_login_show_signup_link",
"site_nav_show_requests",
] ]
+3
View File
@@ -30,6 +30,9 @@ def _build_site_info(include_changelog: bool) -> Dict[str, Any]:
"showForgotPassword": bool(runtime.site_login_show_forgot_password), "showForgotPassword": bool(runtime.site_login_show_forgot_password),
"showSignupLink": bool(runtime.site_login_show_signup_link), "showSignupLink": bool(runtime.site_login_show_signup_link),
}, },
"navigation": {
"showRequests": bool(runtime.site_nav_show_requests),
},
} }
if include_changelog: if include_changelog:
info["changelog"] = (CHANGELOG or "").strip() info["changelog"] = (CHANGELOG or "").strip()
+1
View File
@@ -39,6 +39,7 @@ _BOOL_FIELDS = {
"site_login_show_local_login", "site_login_show_local_login",
"site_login_show_forgot_password", "site_login_show_forgot_password",
"site_login_show_signup_link", "site_login_show_signup_link",
"site_nav_show_requests",
} }
_SKIP_OVERRIDE_FIELDS = {"site_build_number", "site_changelog"} _SKIP_OVERRIDE_FIELDS = {"site_build_number", "site_changelog"}
+22
View File
@@ -1,4 +1,5 @@
import os import os
from types import SimpleNamespace
import tempfile import tempfile
import unittest import unittest
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
@@ -13,6 +14,7 @@ from backend.app.network_security import request_trusts_forwarded_headers, valid
from backend.app.routers import auth as auth_router from backend.app.routers import auth as auth_router
from backend.app.routers import portal as portal_router from backend.app.routers import portal as portal_router
from backend.app.routers import requests as requests_router from backend.app.routers import requests as requests_router
from backend.app.routers import site as site_router
from backend.app.routers import status as status_router from backend.app.routers import status as status_router
from backend.app.security import PASSWORD_POLICY_MESSAGE, validate_password_policy from backend.app.security import PASSWORD_POLICY_MESSAGE, validate_password_policy
from backend.app.services import password_reset from backend.app.services import password_reset
@@ -132,6 +134,26 @@ class ServiceStatusTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("credentials", result["message"].lower()) self.assertIn("credentials", result["message"].lower())
class SiteInfoTests(unittest.TestCase):
def test_site_public_exposes_requests_navigation_toggle(self) -> None:
runtime = SimpleNamespace(
site_build_number="test-build",
site_banner_enabled=False,
site_banner_message="",
site_banner_tone="info",
site_login_show_jellyfin_login=True,
site_login_show_local_login=True,
site_login_show_forgot_password=True,
site_login_show_signup_link=True,
site_nav_show_requests=False,
)
with patch.object(site_router, "get_runtime_settings", return_value=runtime):
info = site_router._build_site_info(False)
self.assertEqual(info["navigation"], {"showRequests": False})
class RequestCacheTests(unittest.TestCase): class RequestCacheTests(unittest.TestCase):
def tearDown(self) -> None: def tearDown(self) -> None:
requests_router._detail_cache.clear() requests_router._detail_cache.clear()
+12
View File
@@ -50,6 +50,7 @@ const BOOL_SETTINGS = new Set([
'site_login_show_local_login', 'site_login_show_local_login',
'site_login_show_forgot_password', 'site_login_show_forgot_password',
'site_login_show_signup_link', 'site_login_show_signup_link',
'site_nav_show_requests',
'magent_proxy_enabled', 'magent_proxy_enabled',
'magent_proxy_trust_forwarded_headers', 'magent_proxy_trust_forwarded_headers',
'magent_ssl_bind_enabled', 'magent_ssl_bind_enabled',
@@ -272,6 +273,12 @@ const SITE_SECTION_GROUPS: Array<{
'site_login_show_signup_link', 'site_login_show_signup_link',
], ],
}, },
{
key: 'site-navigation',
title: 'Beta Navigation',
description: 'Temporarily show or hide beta navigation entries while new request pipelines are built.',
keys: ['site_nav_show_requests'],
},
] ]
const SETTING_LABEL_OVERRIDES: Record<string, string> = { const SETTING_LABEL_OVERRIDES: Record<string, string> = {
@@ -319,6 +326,7 @@ const SETTING_LABEL_OVERRIDES: Record<string, string> = {
site_login_show_local_login: 'Login page: local Magent sign-in', site_login_show_local_login: 'Login page: local Magent sign-in',
site_login_show_forgot_password: 'Login page: forgot password', site_login_show_forgot_password: 'Login page: forgot password',
site_login_show_signup_link: 'Login page: invite signup link', site_login_show_signup_link: 'Login page: invite signup link',
site_nav_show_requests: 'Top navigation: Requests',
log_file_max_bytes: 'Log file max size (bytes)', log_file_max_bytes: 'Log file max size (bytes)',
log_file_backup_count: 'Rotated log files to keep', log_file_backup_count: 'Rotated log files to keep',
log_http_client_level: 'Service HTTP log level', log_http_client_level: 'Service HTTP log level',
@@ -349,6 +357,7 @@ const labelFromKey = (key: string) =>
.replace('site banner enabled', 'Sitewide banner enabled') .replace('site banner enabled', 'Sitewide banner enabled')
.replace('site banner message', 'Sitewide banner message') .replace('site banner message', 'Sitewide banner message')
.replace('site banner tone', 'Sitewide banner tone') .replace('site banner tone', 'Sitewide banner tone')
.replace('site nav show requests', 'Top navigation: Requests')
.replace('site changelog', 'Changelog text') .replace('site changelog', 'Changelog text')
const formatBytes = (value?: number | null) => { const formatBytes = (value?: number | null) => {
@@ -645,6 +654,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
'site_login_show_local_login', 'site_login_show_local_login',
'site_login_show_forgot_password', 'site_login_show_forgot_password',
'site_login_show_signup_link', 'site_login_show_signup_link',
'site_nav_show_requests',
] ]
const sortByOrder = (items: AdminSetting[], order: string[]) => { const sortByOrder = (items: AdminSetting[], order: string[]) => {
const position = new Map(order.map((key, index) => [key, index])) const position = new Map(order.map((key, index) => [key, index]))
@@ -853,6 +863,8 @@ export default function SettingsPage({ section }: SettingsPageProps) {
site_login_show_local_login: 'Show the local Magent login button on the login page.', site_login_show_local_login: 'Show the local Magent login button on the login page.',
site_login_show_forgot_password: 'Show the forgot-password link on the login page.', site_login_show_forgot_password: 'Show the forgot-password link on the login page.',
site_login_show_signup_link: 'Show the invite signup link on the login page.', site_login_show_signup_link: 'Show the invite signup link on the login page.',
site_nav_show_requests:
'Show the Requests item in the top navigation. Disable during beta while request routing is being reworked.',
site_changelog: 'One update per line for the public changelog.', site_changelog: 'One update per line for the public changelog.',
} }
+22 -2
View File
@@ -7,18 +7,23 @@ import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
export default function HeaderActions() { export default function HeaderActions() {
const [signedIn, setSignedIn] = useState(false) const [signedIn, setSignedIn] = useState(false)
const [role, setRole] = useState<string | null>(null) const [role, setRole] = useState<string | null>(null)
const [showRequestsNav, setShowRequestsNav] = useState(true)
const pathname = usePathname() const pathname = usePathname()
useEffect(() => { useEffect(() => {
const token = getToken() const token = getToken()
setSignedIn(Boolean(token)) setSignedIn(Boolean(token))
if (!token) { if (!token) {
setShowRequestsNav(true)
return return
} }
const load = async () => { const load = async () => {
try { try {
const baseUrl = getApiBase() const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/auth/me`) const [response, siteResponse] = await Promise.all([
authFetch(`${baseUrl}/auth/me`),
fetch(`${baseUrl}/site/public`).catch(() => null),
])
if (!response.ok) { if (!response.ok) {
clearToken() clearToken()
setSignedIn(false) setSignedIn(false)
@@ -27,8 +32,15 @@ export default function HeaderActions() {
} }
const data = await response.json() const data = await response.json()
setRole(data?.role ?? null) setRole(data?.role ?? null)
if (siteResponse?.ok) {
const siteData = await siteResponse.json()
setShowRequestsNav(siteData?.navigation?.showRequests !== false)
} else {
setShowRequestsNav(true)
}
} catch (err) { } catch (err) {
console.error(err) console.error(err)
setShowRequestsNav(true)
} }
} }
void load() void load()
@@ -62,18 +74,26 @@ export default function HeaderActions() {
}, },
] ]
const items = [ const commonItems = [
{ href: '/', label: 'Health', match: (path: string) => path === '/' }, { href: '/', label: 'Health', match: (path: string) => path === '/' },
...(showRequestsNav
? [
{ {
href: '/portal/requests', href: '/portal/requests',
label: 'Requests', label: 'Requests',
match: (path: string) => path === '/portal/requests' || path.startsWith('/requests/'), match: (path: string) => path === '/portal/requests' || path.startsWith('/requests/'),
}, },
]
: []),
{ {
href: '/portal/issues', href: '/portal/issues',
label: 'Issues', label: 'Issues',
match: (path: string) => path === '/portal/issues' || path === '/admin/issues', match: (path: string) => path === '/portal/issues' || path === '/admin/issues',
}, },
]
const items = [
...commonItems,
...roleItems, ...roleItems,
] ]