Add beta request nav toggle
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user