From ee1a15447d9c5b8bd4edaebe03bd652b5860557f Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Fri, 10 Jul 2026 16:56:46 +0500 Subject: [PATCH 1/7] feat(admin-ui): implement mobile header, dashboard, and health screens (#2920) Signed-off-by: faisalsiddique4400 --- .../GluuDatePicker/GluuDatePicker.style.ts | 40 +++ .../GluuSearchToolbar/GluuRefreshButton.tsx | 7 +- .../app/components/GluuSearchToolbar/types.ts | 2 + admin-ui/app/components/SVG/Refresh.tsx | 29 ++ admin-ui/app/components/SVG/index.tsx | 2 + admin-ui/app/locales/en/translation.json | 7 +- admin-ui/app/locales/es/translation.json | 5 +- admin-ui/app/locales/fr/translation.json | 5 +- admin-ui/app/locales/pt/translation.json | 5 +- admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx | 82 +++-- .../routes/Apps/Gluu/hooks/useNavbarTheme.ts | 3 +- .../Apps/Gluu/styles/GluuNavBar.style.ts | 59 ++++ .../routes/Dashboards/DashboardPage.style.ts | 158 ++++++++++ .../app/routes/Dashboards/DashboardPage.tsx | 243 ++++++++------- .../Dropdowns/MobileProfileDropdown.tsx | 285 ++++++++++++++++++ .../styles/MobileProfileDropdown.style.ts | 174 +++++++++++ .../components/LogoThemed/LogoThemed.tsx | 27 +- .../app/styles/miltonbo/scss/_layout.scss | 4 +- .../components/Health/HealthPage.style.ts | 88 +++++- .../admin/components/Health/HealthPage.tsx | 93 +++--- .../components/ServiceStatusCard.style.ts | 139 +++++---- .../Health/components/ServiceStatusCard.tsx | 1 + 22 files changed, 1212 insertions(+), 246 deletions(-) create mode 100644 admin-ui/app/components/SVG/Refresh.tsx create mode 100644 admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx create mode 100644 admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts diff --git a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts index aac4045e98..b7b1c6b935 100644 --- a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts +++ b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts @@ -123,6 +123,46 @@ const buildTextFieldSx = ( const POPUP_BOX_SHADOW = '0 8px 24px rgba(0, 0, 0, 0.18)' const buildPopperSx = (tc: PickerThemeColors): SxProps => ({ + // Keep the calendar popup within the viewport on small screens instead of + // letting it render at its full desktop width and overflow the card/screen. + '@media (max-width:767px)': { + '& .MuiPaper-root': { + width: 'min(256px, calc(100vw - 32px))', + maxWidth: 'calc(100vw - 32px)', + boxSizing: 'border-box', + }, + '& .MuiDateCalendar-root': { + width: '100%', + maxWidth: '100%', + height: 'auto', + maxHeight: 'none', + }, + // The month grid has a fixed min-height on desktop that leaves large empty + // space on mobile; let it size to its rows and compact the day cells. + '& .MuiDayCalendar-slideTransition': { + minHeight: '180px', + }, + '& .MuiDayCalendar-header, & .MuiDayCalendar-weekContainer': { + justifyContent: 'space-around', + }, + '& .MuiPickersDay-root, & .MuiPickerDay-root': { + width: '28px', + height: '28px', + margin: '1px', + fontSize: '12px', + }, + '& .MuiDayCalendar-weekDayLabel': { + width: '28px', + margin: '1px', + fontSize: '12px', + }, + // Tighten the calendar header (month label + arrows) to the narrower width. + '& .MuiPickersCalendarHeader-root': { + paddingLeft: '12px', + paddingRight: '8px', + marginTop: '8px', + }, + }, '& .MuiPaper-root': { 'backgroundColor': tc.popupBg, 'color': tc.inputTextColor, diff --git a/admin-ui/app/components/GluuSearchToolbar/GluuRefreshButton.tsx b/admin-ui/app/components/GluuSearchToolbar/GluuRefreshButton.tsx index 99a8602ed3..0f3b032097 100644 --- a/admin-ui/app/components/GluuSearchToolbar/GluuRefreshButton.tsx +++ b/admin-ui/app/components/GluuSearchToolbar/GluuRefreshButton.tsx @@ -13,6 +13,7 @@ const GluuRefreshButton: React.FC = ({ label, loading = false, className, + icon, variant = 'outlined', minHeight, size = 'md', @@ -52,7 +53,11 @@ const GluuRefreshButton: React.FC = ({ useOpacityOnHover={useOpacityOnHover} minHeight={minHeight} > - + {icon ? ( + {icon} + ) : ( + + )} {displayLabel} ) diff --git a/admin-ui/app/components/GluuSearchToolbar/types.ts b/admin-ui/app/components/GluuSearchToolbar/types.ts index ebd4c82daa..e09836ffef 100644 --- a/admin-ui/app/components/GluuSearchToolbar/types.ts +++ b/admin-ui/app/components/GluuSearchToolbar/types.ts @@ -87,6 +87,8 @@ export type GluuRefreshButtonProps = { label?: string loading?: boolean className?: string + // Optional custom icon rendered in place of the default MUI refresh icon. + icon?: ReactNode variant?: 'primary' | 'outlined' minHeight?: number size?: 'sm' | 'md' | 'lg' diff --git a/admin-ui/app/components/SVG/Refresh.tsx b/admin-ui/app/components/SVG/Refresh.tsx new file mode 100644 index 0000000000..eb2f5dc0a3 --- /dev/null +++ b/admin-ui/app/components/SVG/Refresh.tsx @@ -0,0 +1,29 @@ +import { memo } from 'react' + +type RefreshIconProps = { + width?: number | string + height?: number | string + className?: string +} + +// Refresh icon matching the Figma design (node "refresh 1"): a circular +// double-arrow drawn as a single filled path, sized on a 22x22 viewBox. +export const RefreshIcon = memo(({ width = 22, height = 22, className }) => ( + +)) + +RefreshIcon.displayName = 'RefreshIcon' diff --git a/admin-ui/app/components/SVG/index.tsx b/admin-ui/app/components/SVG/index.tsx index ec13370c63..edf4601574 100644 --- a/admin-ui/app/components/SVG/index.tsx +++ b/admin-ui/app/components/SVG/index.tsx @@ -12,6 +12,7 @@ import SmtpZoneIcon from './menu/Smtp' import ScriptsIcon from './menu/Scripts' import LockIcon from './menu/Lock' import { ChevronIcon } from './Chevron' +import { RefreshIcon } from './Refresh' export { JansKcLinkIcon, @@ -28,4 +29,5 @@ export { ScriptsIcon, LockIcon, ChevronIcon, + RefreshIcon, } diff --git a/admin-ui/app/locales/en/translation.json b/admin-ui/app/locales/en/translation.json index 626c04d8d7..a839a95a35 100644 --- a/admin-ui/app/locales/en/translation.json +++ b/admin-ui/app/locales/en/translation.json @@ -759,6 +759,7 @@ "search_filter": "Search Filter" }, "languages": { + "language": "Language", "french": "French", "english": "English", "portuguese": "Portuguese", @@ -773,6 +774,7 @@ "no_notifications": "No notifications" }, "menus": { + "hello": "Hello {{name}}", "assets": "Assets", "scim": "SCIM", "saml": "SAML", @@ -1187,7 +1189,7 @@ "service_status_down": "Service is currently unavailable.", "service_status_degraded": "Service is experiencing issues.", "service_status_unknown": "Service status could not be determined.", - "services_healthy_count": "{{healthyCount}} of {{totalCount}} services healthy", + "services_healthy_count": "{{healthyCount}} of {{totalCount}} Services Healthy", "error_fetching_health_status": "Error fetching health status. Please try again.", "no_services_found": "No services found.", "fido2_metrics_health_status": "FIDO2 Metrics", @@ -1581,7 +1583,8 @@ "view_identity_provider": "View Identity Provider", "delete_identity_provider": "Delete Identity Provider", "search_filter": "Search Filter", - "uma_resource_detail": "UMA Resource Detail" + "uma_resource_detail": "UMA Resource Detail", + "services_health_heading": "Services Health" }, "options": { "admin": "ADMIN", diff --git a/admin-ui/app/locales/es/translation.json b/admin-ui/app/locales/es/translation.json index 2044f6a250..6b5cc800b1 100644 --- a/admin-ui/app/locales/es/translation.json +++ b/admin-ui/app/locales/es/translation.json @@ -759,6 +759,7 @@ "total_users_label": "Total de Usuarios" }, "languages": { + "language": "Idioma", "french": "Frances", "english": "Ingles", "portuguese": "Portugues", @@ -773,6 +774,7 @@ "no_notifications": "Sin notificaciones" }, "menus": { + "hello": "Hola {{name}}", "assets": "Recursos", "scim": "SCIM", "saml": "SAML", @@ -1584,7 +1586,8 @@ "onboarding_time_graph": "Gráfico de Tiempo de Incorporación", "passkey_adoption_rate": "Tasa de Adopción de Passkey", "passkey_authentication": "Autenticación con Passkey", - "passkey_metrics_dashboard": "Panel de Métricas de Passkey" + "passkey_metrics_dashboard": "Panel de Métricas de Passkey", + "services_health_heading": "Estado de Servicios" }, "options": { "admin": "ADMIN", diff --git a/admin-ui/app/locales/fr/translation.json b/admin-ui/app/locales/fr/translation.json index 0feb7ca1dd..a9ed0e91ac 100644 --- a/admin-ui/app/locales/fr/translation.json +++ b/admin-ui/app/locales/fr/translation.json @@ -16,6 +16,7 @@ "hide": "Masquer le mot de passe" }, "languages": { + "language": "Langue", "french": "Français", "english": "English", "portuguese": "Português", @@ -70,6 +71,7 @@ "mau_clients": "Janvier verrouiller les clients" }, "menus": { + "hello": "Bonjour {{name}}", "authentication": "Authentification", "adminui": "Administratrice", "assets": "Ressources", @@ -1589,7 +1591,8 @@ "add_alias": "Add New Mapping", "edit_alias": "Edit Mapping", "creation_date": "Creation Date", - "uma_resource_detail": "Détail de la ressource UMA" + "uma_resource_detail": "Détail de la ressource UMA", + "services_health_heading": "État des Services" }, "options": { "admin": "ADMINISTRER", diff --git a/admin-ui/app/locales/pt/translation.json b/admin-ui/app/locales/pt/translation.json index 607f45da3f..fb4e422ec7 100644 --- a/admin-ui/app/locales/pt/translation.json +++ b/admin-ui/app/locales/pt/translation.json @@ -14,6 +14,7 @@ "hide": "Ocultar senha" }, "languages": { + "language": "Idioma", "french": "Français", "english": "English", "portuguese": "Português", @@ -68,6 +69,7 @@ "mau_clients": "Janeiro bloquear clientes" }, "menus": { + "hello": "Olá {{name}}", "authentication": "Autenticação", "adminui": "Admin", "assets": "Recursos", @@ -1585,7 +1587,8 @@ "onboarding_time_graph": "Gráfico de Tempo de Integração", "passkey_adoption_rate": "Taxa de Adoção de Passkey", "passkey_authentication": "Autenticação com Passkey", - "passkey_metrics_dashboard": "Painel de Métricas de Passkey" + "passkey_metrics_dashboard": "Painel de Métricas de Passkey", + "services_health_heading": "Saúde dos Serviços" }, "options": { "admin": "ADMIN", diff --git a/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx b/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx index 26e5d49427..6b67338a20 100755 --- a/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx @@ -1,13 +1,19 @@ import { useEffect, useRef, useMemo, useCallback, memo } from 'react' +import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' +import useMediaQuery from '@mui/material/useMediaQuery' +import { Link } from 'react-router-dom' import { Notifications, ChevronIcon } from 'Components' import GluuText from 'Routes/Apps/Gluu/GluuText' +import { LogoThemed } from 'Routes/components/LogoThemed/LogoThemed' import { DropdownProfile } from 'Routes/components/Dropdowns/DropdownProfile' +import { MobileProfileDropdown } from 'Routes/components/Dropdowns/MobileProfileDropdown' +import { ROUTES } from '@/helpers/navigation' import type { UserInfo } from 'Redux/features/types/authTypes' import { LanguageMenu } from './LanguageMenu' import { ThemeDropdownComponent } from './ThemeDropdown' import { UserIcon } from './components/UserIcon' -import { useStyles } from './styles/GluuNavBar.style' +import { useStyles, MOBILE_MEDIA_QUERY } from './styles/GluuNavBar.style' import { useNavbarTheme } from './hooks/useNavbarTheme' import { usePageTitle } from './hooks/usePageTitle' import { useAppSelector } from '@/redux/hooks' @@ -17,10 +23,12 @@ const selectUserInfo = (state: { authReducer: { userinfo: UserInfo | null } }) = const GluuNavBar = () => { const userInfo = useAppSelector(selectUserInfo) + const { t } = useTranslation() const { navbarColors } = useNavbarTheme() const { classes } = useStyles({ navbarColors }) const pageTitle = usePageTitle() + const isMobile = useMediaQuery(MOBILE_MEDIA_QUERY) const navbarRef = useRef(null) const applyNavbarColors = useCallback((element: HTMLElement, colors: typeof navbarColors) => { @@ -71,6 +79,9 @@ const GluuNavBar = () => { + + + { - - - - {userInfo && ( + {!isMobile && ( <> - - - - - + + + {userInfo && ( + <> + + + + + + + + )} )} - ( - - - - {displayName} - - - + {isMobile ? ( + ( + + + + {t('menus.hello', { name: displayName })} + + + + - - )} - position="bottom" - /> + )} + /> + ) : ( + ( + + + + {displayName} + + + + + + )} + position="bottom" + /> + )} diff --git a/admin-ui/app/routes/Apps/Gluu/hooks/useNavbarTheme.ts b/admin-ui/app/routes/Apps/Gluu/hooks/useNavbarTheme.ts index e9dd60c5bc..9a9173c456 100644 --- a/admin-ui/app/routes/Apps/Gluu/hooks/useNavbarTheme.ts +++ b/admin-ui/app/routes/Apps/Gluu/hooks/useNavbarTheme.ts @@ -22,6 +22,7 @@ export const useNavbarTheme = () => { useEffect(() => { document.documentElement.style.setProperty('--theme-navbar-text', navbarColors.text) + document.documentElement.style.setProperty('--theme-navbar-border', navbarColors.border) const styleId = 'navbar-theme-colors' let styleElement = document.getElementById(styleId) as HTMLStyleElement | null @@ -70,7 +71,7 @@ export const useNavbarTheme = () => { } } } - }, [navbarColors.text]) + }, [navbarColors.text, navbarColors.border]) return { currentTheme, diff --git a/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts b/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts index 81457254bf..369e659a34 100644 --- a/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts +++ b/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts @@ -1,6 +1,8 @@ import { makeStyles } from 'tss-react/mui' import { fontFamily, fontWeights, fontSizes, letterSpacing } from '@/styles/fonts' +export const MOBILE_MEDIA_QUERY = '(max-width:767px)' + interface NavbarColors { background: string border: string @@ -50,18 +52,33 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC [theme.breakpoints.down('sm')]: { fontSize: fontSizes.md, }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + display: 'none', + }, + }, + mobileLogo: { + display: 'none', + alignItems: 'center', + height: '100%', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + display: 'flex', + }, }, navbarContainer: { flex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center', + // Allow flex children to shrink below their intrinsic content width so the + // greeting can truncate instead of the row overflowing. + minWidth: 0, }, leftSection: { display: 'flex', alignItems: 'center', gap: '20px', height: '100%', + flexShrink: 0, }, rightSection: { display: 'flex', @@ -69,6 +86,7 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC justifyContent: 'flex-end', gap: '19px', height: '100%', + minWidth: 0, [theme.breakpoints.down('md')]: { gap: '12px', }, @@ -146,6 +164,47 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC display: 'none', }, }, + mobileProfileTrigger: { + 'display': 'flex', + 'alignItems': 'center', + 'gap': '7px', + 'cursor': 'pointer', + 'height': '100%', + 'flexShrink': 0, + 'minWidth': 0, + // Below 400px allow the trigger to shrink so the greeting can truncate + // within the available space instead of overflowing off-screen. + '@media (max-width:399px)': { + flexShrink: 1, + }, + }, + mobileGreeting: { + fontFamily, + 'fontSize': fontSizes.sm, + 'fontWeight': fontWeights.semiBold, + 'color': navbarColors.text, + 'letterSpacing': '0.24px', + 'lineHeight': 'normal', + // display:block is required for maxWidth + text-overflow:ellipsis to take + // effect (they are ignored on the default inline span). + 'display': 'block', + 'whiteSpace': 'nowrap', + 'overflow': 'hidden', + 'textOverflow': 'ellipsis', + 'minWidth': 0, + 'margin': 0, + 'padding': 0, + // Hard cap on mobile so a long username always truncates with an ellipsis + // (display:block makes maxWidth + text-overflow:ellipsis take effect). + // Uses the shared MOBILE_MEDIA_QUERY that is known to emit correctly here. + [`@media ${MOBILE_MEDIA_QUERY}`]: { + maxWidth: '120px', + }, + // Tighter cap on very small screens. + '@media (max-width:399px)': { + maxWidth: '100px', + }, + }, userIcon: { flexShrink: 0, }, diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.style.ts b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts index 17156c9bc6..38cb955d6e 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.style.ts +++ b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts @@ -29,6 +29,34 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea }) return { + mobileContentPad: { + [`@media ${MOBILE_MEDIA_QUERY}`]: { + paddingLeft: '30px', + paddingRight: '30px', + boxSizing: 'border-box', + }, + }, + mobilePageTitle: { + fontFamily, + fontSize: '28px', + fontStyle: 'normal', + fontWeight: fontWeights.bold, + lineHeight: 'normal', + color: isDark ? customColors.white : themeColors.text, + margin: 0, + marginBottom: '26px', + }, + summarySectionTitle: { + fontFamily, + fontSize: fontSizes.content, + fontStyle: 'normal', + fontWeight: fontWeights.medium, + lineHeight: '22px', + color: isDark ? customColors.white : themeColors.text, + margin: 0, + marginTop: '32px', + marginBottom: '20px', + }, flex: { flexGrow: 1, display: 'flex', @@ -46,6 +74,13 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea padding: '38.5px', backgroundColor: themeColors.cardBg, position: 'relative', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + height: 'auto', + minHeight: 136, + justifyContent: 'flex-start', + gap: '16px', + padding: '28px 29px', + }, }, summaryText: { fontFamily, @@ -57,6 +92,13 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea position: 'absolute', top: '44px', left: '40px', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + position: 'static', + top: 'auto', + left: 'auto', + fontSize: fontSizes.lg, + lineHeight: '22px', + }, }, summaryIcon: { height: '30px', @@ -75,6 +117,14 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea top: '110px', left: '40px', transform: 'translateY(-50%)', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + position: 'static', + top: 'auto', + left: 'auto', + transform: 'none', + fontSize: fontSizes['3xl'], + lineHeight: '1', + }, }, dashboardCard: { background: 'transparent', @@ -155,6 +205,10 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea padding: '16px', height: 'auto', }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + padding: '28px', + height: 'auto', + }, }, userInfoTitle: { fontFamily, @@ -167,6 +221,13 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea [theme.breakpoints.down('sm')]: { fontSize: fontSizes.lg, }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + fontSize: fontSizes.lg, + fontWeight: fontWeights.medium, + lineHeight: '22px', + marginBottom: '20px', + color: isDark ? customColors.white : themeColors.text, + }, }, userInfoContent: { 'display': 'grid', @@ -185,11 +246,19 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea gridTemplateColumns: '1fr', rowGap: '20px', }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + gridTemplateColumns: '1fr 1fr', + columnGap: '24px', + rowGap: '15px', + }, }, userInfoItem: { display: 'flex', flexDirection: 'column', gap: '8px', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + gap: 0, + }, }, userInfoText: { fontFamily, @@ -198,6 +267,11 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea marginBottom: 0, color: themeColors.text, fontWeight: fontWeights.medium, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + fontSize: fontSizes.base, + lineHeight: '26px', + color: isDark ? customColors.white : themeColors.text, + }, }, userInfoValue: { fontFamily, @@ -205,6 +279,11 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea lineHeight: lineHeights.loose, color: themeColors.text, fontWeight: fontWeights.bold, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + fontSize: fontSizes.md, + lineHeight: '26px', + color: isDark ? customColors.white : themeColors.text, + }, }, chartContainerTable: { width: 780, @@ -265,6 +344,11 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea [theme.breakpoints.down('sm')]: { height: '70px', }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + height: 'auto', + padding: 0, + alignItems: 'flex-start', + }, }, topGridNoMargin: { marginBottom: 0, @@ -276,14 +360,40 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea alignItems: 'center', justifyContent: 'space-between', gap: '40px', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + 'display': 'flex', + 'flexWrap': 'wrap', + 'alignItems': 'center', + 'justifyContent': 'flex-start', + 'columnGap': '25px', + 'rowGap': '4px', + '& > :first-child': { + flexBasis: '100%', + marginBottom: '8px', + }, + }, }, statusTitle: { fontFamily, fontSize: fontSizes.md, + fontStyle: 'normal', fontWeight: fontWeights.semiBold, lineHeight: lineHeights.relaxed, color: themeColors.text, whiteSpace: 'nowrap', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + fontSize: fontSizes.base, + }, + }, + statusHeading: { + [`@media ${MOBILE_MEDIA_QUERY}`]: { + fontFamily, + fontSize: fontSizes.content, + fontStyle: 'normal', + fontWeight: fontWeights.medium, + lineHeight: '22px', + color: isDark ? customColors.white : themeColors.text, + }, }, statusItems: { display: 'flex', @@ -305,12 +415,19 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea flex: '0 0 auto', whiteSpace: 'nowrap', flexShrink: 0, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + gap: '6px', + }, }, statusDot: { width: 16, height: 16, borderRadius: BORDER_RADIUS.CIRCLE, flexShrink: 0, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + width: 10, + height: 10, + }, }, statusDotActive: { backgroundColor: themeColors.statusActive, @@ -361,6 +478,14 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea display: 'flex', flexDirection: 'column', boxSizing: 'border-box', + [theme.breakpoints.down('md')]: { + padding: '20px', + height: 'auto', + }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + padding: '28px', + height: 'auto', + }, }, chartTitle: { fontFamily, @@ -370,6 +495,13 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea color: themeColors.text, marginBottom: '24px', marginTop: 0, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + fontSize: fontSizes.lg, + fontWeight: fontWeights.medium, + lineHeight: '22px', + marginBottom: '20px', + color: isDark ? customColors.white : themeColors.text, + }, }, chartDatePickers: { display: 'flex', @@ -421,6 +553,16 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea width: '100%', marginTop: '8px', padding: '0 8px', + boxSizing: 'border-box', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + flexDirection: 'column', + flexWrap: 'nowrap', + alignItems: 'flex-start', + gap: '10px', + padding: 0, + overflow: 'hidden', + maxWidth: '100%', + }, }, userInfoChartRow: { display: 'flex', @@ -448,18 +590,34 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea fontSize: fontSizes.sm, color: themeColors.text, flex: '0 0 auto', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + alignItems: 'flex-start', + width: '100%', + maxWidth: '100%', + minWidth: 0, + }, }, legendColor: { width: '20px', height: '3px', flexShrink: 0, borderRadius: BORDER_RADIUS.THIN, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + marginTop: '8px', + }, }, legendLabel: { fontFamily, fontSize: fontSizes.sm, color: themeColors.text, whiteSpace: 'nowrap', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + flex: 1, + minWidth: 0, + whiteSpace: 'normal', + overflowWrap: 'anywhere', + wordBreak: 'break-word', + }, }, } }) diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.tsx b/admin-ui/app/routes/Dashboards/DashboardPage.tsx index 2587bc93ce..6782dc9532 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.tsx +++ b/admin-ui/app/routes/Dashboards/DashboardPage.tsx @@ -31,6 +31,7 @@ import { useStyles, MOBILE_MEDIA_QUERY } from './DashboardPage.style' import { GluuPageContent } from '@/components' import { StatusIndicator, SummaryCard, UserInfoItem } from './components' import GluuText from 'Routes/Apps/Gluu/GluuText' +import { usePageTitle } from 'Routes/Apps/Gluu/hooks/usePageTitle' import { isAfterDate, isBeforeDate, @@ -45,6 +46,7 @@ const DASHBOARD_RESOURCE_ID = ADMIN_UI_RESOURCES.Dashboard const DashboardPage = () => { const { t } = useTranslation() const isMobile = useMediaQuery(MOBILE_MEDIA_QUERY) + const pageTitle = usePageTitle() const themeContext = use(ThemeContext) const currentTheme = useMemo( @@ -303,127 +305,146 @@ const DashboardPage = () => { - - -
-
- - {t('dashboard.system_status')}: - - {visibleStatusDetails.map(({ label, key }) => ( - - ))} +
+ {isMobile && ( + + {pageTitle} + + )} + + +
+
+ + {t('dashboard.system_status')}: + + {visibleStatusDetails.map(({ label, key }) => ( + + ))} +
-
- - - - - - - {summaryData.slice(0, 3).map((data) => ( - - - - ))} - -
-
- - - -
- - {t('dashboard.user_info')} - -
- {userInfo.map((item) => ( - - ))} -
-
-
+ {isMobile && ( + + {t('dashboard.summary_title')}: + + )} + + + + + {summaryData.slice(0, 3).map((data) => ( + + -
-
-
- -
- - {t('dashboard.access_tokens_graph')} - -
- -
-
-
- -
-
- {CHART_LEGEND_CONFIG.map((config) => ( -
-
- - {t(config.translationKey)} + ))} + + + + +
+
+ + + +
+ + {t('dashboard.user_info')} + {isMobile ? ':' : ''} +
+ {userInfo.map((item) => ( + + ))} +
- ))} +
+
+
+
+
+ +
+ + {t('dashboard.access_tokens_graph')} + {isMobile ? ':' : ''} + +
+ +
+
+
+ +
+
+ {CHART_LEGEND_CONFIG.map((config) => ( +
+
+ + {t(config.translationKey)} + +
+ ))} +
-
- + +
-
+
- +
diff --git a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx new file mode 100644 index 0000000000..a634d11625 --- /dev/null +++ b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx @@ -0,0 +1,285 @@ +import { useEffect, useMemo, useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' +import { GluuDropdown, type GluuDropdownOption, ChevronIcon } from 'Components' +import GluuText from 'Routes/Apps/Gluu/GluuText' +import { useAppDispatch, useAppSelector } from '@/redux/hooks' +import { useAppNavigation, ROUTES } from '@/helpers/navigation' +import { auditLogoutLogs } from 'Redux/features/sessionSlice' +import { useTheme } from '@/context/theme/themeContext' +import { THEME_LIGHT, THEME_DARK, isValidTheme, type ThemeValue } from '@/context/theme/constants' +import { ensureLocaleLoaded } from '@/i18n' +import { logger } from '@/utils/logger' +import { storage } from '@/utils/storage' +import { STORAGE_KEYS, LANG_CODES, DEFAULT_LANG } from '@/constants' +import type { UserInfo } from 'Redux/features/types/authTypes' +import { useStyles } from './styles/MobileProfileDropdown.style' + +type UserConfig = { + lang?: Record + theme?: Record +} + +type MobileProfileDropdownProps = { + userInfo: UserInfo | null + renderTrigger: (isOpen: boolean) => React.ReactNode +} + +const safeParseUserConfig = (): UserConfig => + storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} + +const getInitialLang = (inum?: string): string => { + const initLang = storage.get(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG + const config = safeParseUserConfig() + return config?.lang?.[inum || ''] || initLang +} + +const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdownProps) => { + const { t, i18n } = useTranslation() + const dispatch = useAppDispatch() + const { navigateToRoute } = useAppNavigation() + const { logoutAuditSucceeded } = useAppSelector((state) => state.logoutAuditReducer) + + const { state: themeState, dispatch: themeDispatch } = useTheme() + const currentTheme = themeState.theme + const isDark = currentTheme === THEME_DARK + const { classes } = useStyles({ isDark }) + + const inum = userInfo?.inum + + const [isOpen, setIsOpen] = useState(false) + const [lang, setLang] = useState(() => getInitialLang(inum)) + const containerRef = useRef(null) + + // Close the menu when a logout audit completes and we navigate away. + useEffect(() => { + if (logoutAuditSucceeded === true) { + navigateToRoute(ROUTES.LOGOUT) + } + }, [logoutAuditSucceeded, navigateToRoute]) + + useEffect(() => { + if (!isOpen) return + + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node + if (containerRef.current && !containerRef.current.contains(target)) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [isOpen]) + + const onChangeTheme = useCallback( + (value: string) => { + if (!isValidTheme(value)) { + logger.warn('Invalid theme value:', value) + return + } + + const themeValue: ThemeValue = value + + if (!inum) { + themeDispatch({ type: themeValue }) + return + } + + try { + const existingConfig = safeParseUserConfig() + const newConfig = { + ...existingConfig, + lang: existingConfig.lang || {}, + theme: { ...(existingConfig.theme || {}), [inum]: themeValue }, + } + storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) + } catch (e) { + logger.error('Failed to parse userConfig:', e instanceof Error ? e : String(e)) + storage.setJSON(STORAGE_KEYS.USER_CONFIG, { lang: {}, theme: { [inum]: themeValue } }) + } + + themeDispatch({ type: themeValue }) + }, + [inum, themeDispatch], + ) + + const changeLanguage = useCallback( + (code: string) => { + setLang(code) + void ensureLocaleLoaded(code).then(() => i18n.changeLanguage(code)) + + const config = safeParseUserConfig() + const langConfig = { ...(config?.lang || {}) } + if (inum) { + langConfig[inum] = code + } + storage.setJSON(STORAGE_KEYS.USER_CONFIG, { ...config, lang: langConfig }) + storage.set(STORAGE_KEYS.INIT_LANG, code) + }, + [i18n, inum], + ) + + const handleProfile = useCallback(() => { + setIsOpen(false) + navigateToRoute(ROUTES.PROFILE) + }, [navigateToRoute]) + + const handleLogout = useCallback(() => { + setIsOpen(false) + dispatch(auditLogoutLogs({ message: 'User logged out manually' })) + }, [dispatch]) + + const themeOptions: GluuDropdownOption[] = useMemo( + () => [ + { value: THEME_LIGHT, label: t('themes.light') }, + { value: THEME_DARK, label: t('themes.dark') }, + ], + [t], + ) + + const languageOptions: GluuDropdownOption[] = useMemo( + () => [ + { value: LANG_CODES.EN, label: t('languages.english') }, + { value: LANG_CODES.FR, label: t('languages.french') }, + { value: LANG_CODES.PT, label: t('languages.portuguese') }, + { value: LANG_CODES.ES, label: t('languages.spanish') }, + ], + [t], + ) + + const themeLabel = currentTheme === THEME_DARK ? t('themes.dark') : t('themes.light') + // Match the desktop navbar language control: show the uppercased code (e.g. "FR"). + const langLabel = (lang || DEFAULT_LANG).split('-')[0].toUpperCase() + + return ( + + setIsOpen((prev) => !prev)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setIsOpen((prev) => !prev) + } + }} + sx={{ cursor: 'pointer' }} + > + {renderTrigger(isOpen)} + + + {isOpen && ( + +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleProfile() + } + }} + > + + {t('menus.my_profile')} + + +
+ +
+ +
+ + {t('themes.theme')} + + onChangeTheme(value)} + position="bottom" + minWidth={96} + maxWidth={140} + showArrow={false} + closeOnSelect + dropdownClassName={classes.compactMenu} + renderTrigger={() => ( + + {themeLabel} + + + + + )} + /> +
+ +
+ +
+ + {t('languages.language')} + + changeLanguage(value)} + position="bottom" + minWidth={96} + maxWidth={140} + showArrow={false} + closeOnSelect + dropdownClassName={classes.compactMenu} + renderTrigger={() => ( + + {langLabel} + + + + + )} + /> +
+ + +
+ )} +
+ ) +} + +MobileProfileDropdown.displayName = 'MobileProfileDropdown' + +export { MobileProfileDropdown } diff --git a/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts new file mode 100644 index 0000000000..609ca06db7 --- /dev/null +++ b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts @@ -0,0 +1,174 @@ +import { makeStyles } from 'tss-react/mui' +import { fontFamily, fontWeights } from '@/styles/fonts' +import customColors from '@/customColors' + +type StyleParams = { + isDark: boolean +} + +// Colors taken directly from the Figma dropdown spec (nodes 3755:1350 light / +// 3755:1671 dark). Kept local to this component because they are dropdown-only +// tokens that do not exist in the shared theme config. +const LIGHT = { + bg: customColors.white, + border: '#cfcfcf', + divider: '#c7c7c7', + text: '#0a2540', + controlBorder: '#ebebeb', + controlText: '#425466', + signOutBg: '#f5f6f8', + arrowBg: '#f5f6f8', +} as const + +const DARK = { + bg: '#0a2540', + border: '#3a628c', + divider: '#4a72a0', + text: customColors.white, + controlBorder: '#193f66', + controlText: customColors.white, + signOutBg: '#16395d', + arrowBg: '#16395d', +} as const + +export const useStyles = makeStyles()((_theme, { isDark }) => { + const c = isDark ? DARK : LIGHT + + return { + menu: { + width: 172, + boxSizing: 'border-box', + backgroundColor: c.bg, + border: `1.5px solid ${c.border}`, + borderRadius: 10, + boxShadow: '0px 4px 5.5px rgba(0, 0, 0, 0.05)', + padding: '12px', + display: 'flex', + flexDirection: 'column', + gap: 0, + }, + row: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + minHeight: 27, + }, + rowLabel: { + fontFamily, + fontSize: '13px', + fontWeight: fontWeights.semiBold, + lineHeight: 'normal', + letterSpacing: '0.22px', + color: c.text, + margin: 0, + whiteSpace: 'nowrap', + }, + profileRow: { + cursor: 'pointer', + }, + arrowButton: { + width: 20, + height: 21, + borderRadius: '50%', + backgroundColor: c.arrowBg, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + color: c.text, + }, + arrowIcon: { + width: 10, + height: 10, + display: 'block', + }, + divider: { + height: 0, + border: 'none', + borderTop: `1px solid ${c.divider}`, + margin: '12px 0', + width: '100%', + }, + control: { + minWidth: 74, + height: 27, + boxSizing: 'border-box', + border: `1px solid ${c.controlBorder}`, + borderRadius: 3, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 6px', + cursor: 'pointer', + gap: 4, + }, + controlText: { + fontFamily, + fontSize: '12px', + fontWeight: fontWeights.medium, + lineHeight: '21.6px', + color: c.controlText, + whiteSpace: 'nowrap', + }, + controlIcon: { + 'width': 8, + 'height': 8, + 'flexShrink': 0, + 'display': 'flex', + 'alignItems': 'center', + 'justifyContent': 'center', + 'color': c.controlText, + '& svg': { + width: '100%', + height: '100%', + }, + }, + signOut: { + marginTop: 12, + height: 32, + borderRadius: 3, + backgroundColor: c.signOutBg, + border: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + width: '100%', + padding: 0, + }, + signOutText: { + fontFamily, + fontSize: '13px', + fontWeight: fontWeights.semiBold, + lineHeight: '22.273px', + color: c.text, + whiteSpace: 'nowrap', + }, + // Shrinks the nested GluuDropdown option panel (theme/language selectors) + // so its list matches the compact 50x27 control instead of the default + // full-size dropdown rows. Applied via `dropdownClassName`. + compactMenu: { + 'minWidth': '96px !important', + 'width': 'auto', + // Attach the list directly below the control, aligned to its right edge + // (the control sits at the right of its row). Override GluuDropdown's + // default centered + 13px-gap positioning. + 'left': 'auto !important', + 'right': '0 !important', + 'transform': 'none !important', + 'marginTop': '4px !important', + '& [role="option"]': { + minHeight: 'auto', + marginBottom: '2px', + padding: '6px 9px', + fontSize: '12px', + lineHeight: '16px', + borderRadius: 4, + }, + // Tighten the menu content padding (default 16px) to match the compact rows. + '& > div': { + padding: '6px', + }, + }, + } +}) diff --git a/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx b/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx index 1367c2cf49..2179492278 100755 --- a/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx +++ b/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx @@ -4,7 +4,13 @@ import { ThemeContext } from '@/context/theme/themeContext' import { THEME_DARK, THEME_LIGHT, DEFAULT_THEME } from '@/context/theme/constants' import logoImage from '../../../images/logos/logo192.png' -type LogoThemedProps = Omit, 'src' | 'alt' | 'style'> +// When `variant` is set the logo ignores the active theme and always renders +// that color (e.g. the mobile header always shows the green logo in both themes). +type LogoVariant = 'green' | 'white' + +type LogoThemedProps = Omit, 'src' | 'alt' | 'style'> & { + variant?: LogoVariant +} const LOGO_FILTERS = { [THEME_DARK]: 'brightness(0) invert(1)', @@ -12,7 +18,18 @@ const LOGO_FILTERS = { 'brightness(0) saturate(100%) invert(48%) sepia(79%) saturate(2476%) hue-rotate(130deg) brightness(95%) contrast(101%)', } as const -const LogoThemed: React.FC = ({ className, width, height, ...otherProps }) => { +const VARIANT_FILTERS: Record = { + white: LOGO_FILTERS[THEME_DARK], + green: LOGO_FILTERS[THEME_LIGHT], +} + +const LogoThemed: React.FC = ({ + className, + width, + height, + variant, + ...otherProps +}) => { const themeContext = use(ThemeContext) const currentTheme = themeContext?.state.theme || DEFAULT_THEME @@ -23,9 +40,11 @@ const LogoThemed: React.FC = ({ className, width, height, ...ot return { width: widthValue, height: heightValue, - filter: LOGO_FILTERS[currentTheme] || LOGO_FILTERS[DEFAULT_THEME], + filter: variant + ? VARIANT_FILTERS[variant] + : LOGO_FILTERS[currentTheme] || LOGO_FILTERS[DEFAULT_THEME], } - }, [currentTheme, width, height]) + }, [currentTheme, width, height, variant]) return (
diff --git a/admin-ui/app/styles/miltonbo/scss/_layout.scss b/admin-ui/app/styles/miltonbo/scss/_layout.scss index e36887dac6..51951ecdb1 100644 --- a/admin-ui/app/styles/miltonbo/scss/_layout.scss +++ b/admin-ui/app/styles/miltonbo/scss/_layout.scss @@ -17,7 +17,7 @@ @media (max-width: breakpoint-max("md", $grid-breakpoints)) { left: 0; background: var(--theme-navbar-background, $navbar-background-color); - border-bottom: 1px solid $navbar-border-color; + border-bottom: 1px solid var(--theme-navbar-border, #{$navbar-border-color}); } /* noscript fallback */ @@ -93,6 +93,6 @@ &--only-navbar .layout__navbar { background: $navbar-background-color; - border-bottom: 1px solid $navbar-border-color; + border-bottom: 1px solid var(--theme-navbar-border, #{$navbar-border-color}); } } diff --git a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts index dbf6daa338..fa6931f202 100644 --- a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts +++ b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts @@ -4,6 +4,10 @@ import { SPACING, BORDER_RADIUS } from '@/constants' import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' import { getCardBorderStyle } from '@/styles/cardBorderStyles' +const MOBILE_MEDIA_QUERY = '(max-width:767px)' +// Below this width the 16px header title runs into the refresh icon. +const NARROW_MEDIA_QUERY = '(max-width:350px)' + interface HealthPageThemeColors { cardBg: string navbarBorder: string @@ -23,6 +27,31 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole }) return { + // Screen-edge inset for the page on mobile. Figma insets the card 30px from + // each side (matching the Dashboard mobile layout); apply the same padding + // so the Health page lines up with the rest of the mobile design system. + mobileContentPad: { + [`@media ${MOBILE_MEDIA_QUERY}`]: { + paddingLeft: '30px', + paddingRight: '30px', + boxSizing: 'border-box', + }, + }, + mobilePageTitle: { + display: 'none', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + display: 'block', + fontFamily, + fontSize: '28px', + fontStyle: 'normal', + fontWeight: fontWeights.bold, + lineHeight: 'normal', + color: themeColors.text, + margin: 0, + // Figma gap from the heading baseline to the card top is ~34px. + marginBottom: '34px', + }, + }, healthCard: { backgroundColor: themeColors.cardBg, ...cardBorderStyle, @@ -35,6 +64,9 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole display: 'flex', flexDirection: 'column', boxSizing: 'border-box', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + minHeight: 'auto', + }, }, header: { paddingTop: `${SPACING.CONTENT_PADDING}px`, @@ -46,14 +78,30 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole alignItems: 'flex-start', position: 'relative', height: '84.5px', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + height: '58.5px', + alignItems: 'flex-start', + paddingTop: '20px', + paddingLeft: '28px', + paddingRight: '28px', + }, }, headerTitle: { fontFamily, fontWeight: fontWeights.medium, - fontSize: fontSizes.lg, + fontSize: fontSizes.md, lineHeight: lineHeights.tight, color: themeColors.text, margin: 0, + // Without min-width:0 a flex item refuses to shrink below its content + // width, so the title would run under the refresh button. + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + [`@media ${NARROW_MEDIA_QUERY}`]: { + fontSize: fontSizes.base, + }, }, headerDivider: { position: 'absolute', @@ -69,6 +117,17 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole top: '50%', transform: 'translateY(-50%)', zIndex: 10, + // In flow on mobile so it reserves its width and the title cannot overlap. + [`@media ${MOBILE_MEDIA_QUERY}`]: { + position: 'static', + transform: 'none', + flexShrink: 0, + // The title is conditional; keeps the button right-aligned when it is + // the header's only in-flow child. + marginLeft: 'auto', + // Centres the 22px icon inside the button's 32px mobile box. + marginTop: `${-(32 - 22) / 2}px`, + }, }, refreshButton: { borderRadius: 4, @@ -84,6 +143,25 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole fontSize: fontSizes.base, fontWeight: fontWeights.medium, lineHeight: lineHeights.relaxed, + // Icon-only on mobile per Figma: strip the button chrome and collapse the + // text label to zero size. GluuButton applies these via INLINE styles, so + // every override here must use !important to win over the inline values. + [`@media ${MOBILE_MEDIA_QUERY}`]: { + 'minWidth': 'auto !important', + 'width': '32px !important', + 'height': '32px !important', + 'minHeight': '32px !important', + 'padding': '0 !important', + 'gap': '0 !important', + 'fontSize': '0 !important', + 'border': 'none !important', + 'borderColor': 'transparent !important', + 'backgroundColor': 'transparent !important', + // Keep the refresh icon itself visible at a fixed size. + '& svg, & .MuiSvgIcon-root': { + fontSize: '22px !important', + }, + }, }, messageBlock: { padding: `${SPACING.CONTENT_PADDING}px`, @@ -121,6 +199,14 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole [theme.breakpoints.down('sm')]: { gridTemplateColumns: '1fr', }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + gridTemplateColumns: '1fr', + gap: 12, + paddingTop: '20.5px', + paddingLeft: '28.5px', + paddingRight: '28.5px', + paddingBottom: '26px', + }, }, serviceCardWrapper: { minWidth: 0, diff --git a/admin-ui/plugins/admin/components/Health/HealthPage.tsx b/admin-ui/plugins/admin/components/Health/HealthPage.tsx index 4a4843973e..d2b46060e3 100644 --- a/admin-ui/plugins/admin/components/Health/HealthPage.tsx +++ b/admin-ui/plugins/admin/components/Health/HealthPage.tsx @@ -10,6 +10,7 @@ import { GluuPageContent } from '@/components' import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' import GluuText from 'Routes/Apps/Gluu/GluuText' import { GluuRefreshButton } from '@/components/GluuSearchToolbar' +import { RefreshIcon } from '@/components/SVG' import { useHealthStatus, useFido2HealthStatus } from './hooks' import ServiceStatusCard from './components/ServiceStatusCard' import { useStyles } from './HealthPage.style' @@ -62,55 +63,61 @@ const HealthPage: React.FC = () => { return ( - -
- {!isLoading && !isError && totalCount > 0 && ( - - {t('messages.services_healthy_count', { - healthyCount: healthyCount + (fido2HealthCard.status === 'up' ? 1 : 0), - totalCount: totalCount + 1, - })} - - )} -
- +
+ + {t('titles.services_health_heading')} + + +
+ {!isLoading && !isError && totalCount > 0 && ( + + {t('messages.services_healthy_count', { + healthyCount: healthyCount + (fido2HealthCard.status === 'up' ? 1 : 0), + totalCount: totalCount + 1, + })} + + )} +
+ } + /> +
+
-
-
- {isError && ( -
- - {t('messages.error_fetching_health_status')} -
- )} + {isError && ( +
+ + {t('messages.error_fetching_health_status')} +
+ )} - {!isLoading && !isError && services.length === 0 && ( -
- - - {t('messages.no_services_found')} - -
- )} + {!isLoading && !isError && services.length === 0 && ( +
+ + + {t('messages.no_services_found')} + +
+ )} - {!isLoading && !isError && (services.length > 0 || !fido2Health.isLoading) && ( -
- {services.map((service) => ( -
- + {!isLoading && !isError && (services.length > 0 || !fido2Health.isLoading) && ( +
+ {services.map((service) => ( +
+ +
+ ))} +
+
- ))} -
-
-
- )} - + )} + +
) diff --git a/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts b/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts index 9c27a4286e..438dc0b6b0 100644 --- a/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts +++ b/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts @@ -1,64 +1,93 @@ import { makeStyles } from 'tss-react/mui' import customColors from '@/customColors' -import { SPACING } from '@/constants' -import { fontFamily, fontWeights, fontSizes } from '@/styles/fonts' +import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' -const useStyles = makeStyles<{ isDark: boolean }>()((_, { isDark }) => ({ - card: { - backgroundColor: isDark ? customColors.darkInputBg : customColors.white, - borderRadius: 16, - width: '100%', - minHeight: 135, - padding: 0, - position: 'relative', - display: 'flex', - flexDirection: 'column', - boxSizing: 'border-box', - border: `1px solid ${isDark ? customColors.darkBorder : customColors.lightBorder}`, - }, - content: { - padding: `${SPACING.CARD_PADDING}px`, - display: 'flex', - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'space-between', - gap: `${SPACING.CARD_CONTENT_GAP}px`, - minHeight: 0, - boxSizing: 'border-box', - }, - textContainer: { - flex: 1, - minWidth: 0, - display: 'flex', - flexDirection: 'column', - gap: `${SPACING.CARD_CONTENT_GAP}px`, - }, - serviceName: { - fontFamily, - fontWeight: fontWeights.bold, - fontSize: fontSizes.lg, - lineHeight: '32px', - color: isDark ? customColors.white : customColors.primaryDark, - margin: 0, - padding: 0, - wordWrap: 'break-word', - overflowWrap: 'break-word', - }, - serviceMessage: { +// Intentionally not the shared `darkBorder` token. +const DARK_CARD_BORDER = '#284b6e' + +const CARD_MIN_HEIGHT = 79 +const CARD_RADIUS = 8 +const CARD_PADDING_Y = 16.5 +const CARD_PADDING_X = 19.5 +// Figma tops the pill at y=18.89 against a content box starting at y=16.5. +const BADGE_TOP_OFFSET = 2.39 +// `space-between` positions the pill; this is only a guard so a long service +// name never collides with it. +const TEXT_BADGE_MIN_GAP = 8 +// Invisible on the dark card, which reads its edge from the border instead. +const CARD_SHADOW = '0px 1.985px 2.73px 0px rgba(0, 0, 0, 0.05)' + +const useStyles = makeStyles<{ isDark: boolean }>()((_, { isDark }) => { + const cardText = { fontFamily, - fontWeight: fontWeights.medium, - fontSize: fontSizes.md, - lineHeight: '32px', + lineHeight: lineHeights.tight, color: isDark ? customColors.white : customColors.primaryDark, margin: 0, padding: 0, - wordWrap: 'break-word', - overflowWrap: 'break-word', - }, - statusBadge: { - flexShrink: 0, - alignSelf: 'flex-start', - }, -})) + wordWrap: 'break-word' as const, + overflowWrap: 'break-word' as const, + } + + return { + card: { + backgroundColor: isDark ? customColors.darkInputBg : customColors.white, + borderRadius: CARD_RADIUS, + width: '100%', + minHeight: CARD_MIN_HEIGHT, + padding: 0, + position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + boxSizing: 'border-box', + border: `1px solid ${isDark ? DARK_CARD_BORDER : customColors.lightBorder}`, + boxShadow: isDark ? 'none' : CARD_SHADOW, + }, + content: { + padding: `${CARD_PADDING_Y}px ${CARD_PADDING_X}px`, + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + gap: `${TEXT_BADGE_MIN_GAP}px`, + minHeight: 0, + boxSizing: 'border-box', + }, + textContainer: { + flex: 1, + minWidth: 0, + display: 'flex', + flexDirection: 'column', + // The two 22px line boxes butt up against each other by design. + gap: 0, + }, + serviceName: { + ...cardText, + fontWeight: fontWeights.bold, + fontSize: fontSizes.md, + }, + serviceMessage: { + ...cardText, + fontWeight: fontWeights.medium, + // No 13px design token exists. + fontSize: '13px', + }, + statusBadge: { + flexShrink: 0, + alignSelf: 'flex-start', + marginTop: `${BADGE_TOP_OFFSET}px`, + }, + // GluuBadge writes its sizing to an inline `style` attribute, so these + // declarations need !important to win over it. + badge: { + fontSize: '10px !important', + lineHeight: '18.133px !important', + letterSpacing: '0.1511px !important', + padding: '6.044px !important', + borderRadius: '3.778px !important', + borderWidth: '0 !important', + }, + } +}) export { useStyles } diff --git a/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.tsx b/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.tsx index edac3afbef..fdab52282b 100644 --- a/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.tsx +++ b/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.tsx @@ -84,6 +84,7 @@ const ServiceStatusCard: React.FC = memo(({ service, isD
Date: Fri, 10 Jul 2026 17:34:45 +0500 Subject: [PATCH 2/7] feat(admin-ui): resolve code rabbit issues Signed-off-by: faisalsiddique4400 --- admin-ui/app/hooks/useThemePersistence.ts | 45 +++++++++++++ admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx | 2 +- .../app/routes/Apps/Gluu/ThemeDropdown.tsx | 63 ++----------------- .../Dropdowns/MobileProfileDropdown.tsx | 48 +++++--------- 4 files changed, 65 insertions(+), 93 deletions(-) create mode 100644 admin-ui/app/hooks/useThemePersistence.ts diff --git a/admin-ui/app/hooks/useThemePersistence.ts b/admin-ui/app/hooks/useThemePersistence.ts new file mode 100644 index 0000000000..aba97de810 --- /dev/null +++ b/admin-ui/app/hooks/useThemePersistence.ts @@ -0,0 +1,45 @@ +import { useCallback } from 'react' +import { useTheme } from '@/context/theme/themeContext' +import { isValidTheme, type ThemeValue } from '@/context/theme/constants' +import { logger } from '@/utils/logger' +import { storage } from '@/utils/storage' +import { STORAGE_KEYS } from '@/constants' +import type { UserInfo } from 'Redux/features/types/authTypes' + +type UserConfig = { + lang?: Record + theme?: Record +} + +/** + * Applies a theme change and, when the user is identified, persists it to + * `USER_CONFIG` keyed by `inum`. Anonymous users get the dispatch without a write. + */ +export const useThemePersistence = (userInfo: UserInfo | null | undefined) => { + const { dispatch } = useTheme() + const inum = userInfo?.inum + + return useCallback( + (value: string) => { + if (!isValidTheme(value)) { + logger.warn('Invalid theme value:', value) + return + } + + const themeValue: ThemeValue = value + + if (inum) { + // storage.getJSON already logs and returns null on a parse failure. + const existingConfig = storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} + storage.setJSON(STORAGE_KEYS.USER_CONFIG, { + ...existingConfig, + lang: existingConfig.lang || {}, + theme: { ...(existingConfig.theme || {}), [inum]: themeValue }, + }) + } + + dispatch({ type: themeValue }) + }, + [inum, dispatch], + ) +} diff --git a/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx b/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx index 6b67338a20..38ac5aa65d 100755 --- a/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx @@ -79,7 +79,7 @@ const GluuNavBar = () => { - + (({ userInfo }) => { const { t } = useTranslation() - const { state, dispatch } = useTheme() + const { state } = useTheme() const currentTheme = state.theme const isDark = currentTheme === THEME_DARK const { classes } = useStyles({ isDark }) - const onChangeTheme = useCallback( - (value: string) => { - if (!isValidTheme(value)) { - logger.warn('Invalid theme value:', value) - return - } - - const themeValue: ThemeValue = value - - if (!userInfo) { - dispatch({ type: themeValue }) - return - } - - const { inum } = userInfo - if (!inum) { - dispatch({ type: themeValue }) - return - } - - try { - const existingConfig = - storage.getJSON<{ - theme?: Record - lang?: Record - }>(STORAGE_KEYS.USER_CONFIG) ?? {} - - const updatedTheme = { - ...(existingConfig.theme || {}), - [inum]: themeValue, - } - - const newConfig = { - ...existingConfig, - lang: existingConfig.lang || {}, - theme: updatedTheme, - } - - storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) - } catch (e) { - logger.error('Failed to parse userConfig:', e instanceof Error ? e : String(e)) - const newConfig = { - lang: {}, - theme: { [inum]: themeValue }, - } - storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) - } - - dispatch({ type: themeValue }) - }, - [userInfo, dispatch], - ) + const onChangeTheme = useThemePersistence(userInfo) const options: GluuDropdownOption[] = useMemo( () => [ diff --git a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx index a634d11625..eecd819f29 100644 --- a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx +++ b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx @@ -7,8 +7,9 @@ import { useAppDispatch, useAppSelector } from '@/redux/hooks' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { auditLogoutLogs } from 'Redux/features/sessionSlice' import { useTheme } from '@/context/theme/themeContext' -import { THEME_LIGHT, THEME_DARK, isValidTheme, type ThemeValue } from '@/context/theme/constants' +import { THEME_LIGHT, THEME_DARK } from '@/context/theme/constants' import { ensureLocaleLoaded } from '@/i18n' +import { useThemePersistence } from '@/hooks/useThemePersistence' import { logger } from '@/utils/logger' import { storage } from '@/utils/storage' import { STORAGE_KEYS, LANG_CODES, DEFAULT_LANG } from '@/constants' @@ -40,7 +41,7 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow const { navigateToRoute } = useAppNavigation() const { logoutAuditSucceeded } = useAppSelector((state) => state.logoutAuditReducer) - const { state: themeState, dispatch: themeDispatch } = useTheme() + const { state: themeState } = useTheme() const currentTheme = themeState.theme const isDark = currentTheme === THEME_DARK const { classes } = useStyles({ isDark }) @@ -74,42 +75,21 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow } }, [isOpen]) - const onChangeTheme = useCallback( - (value: string) => { - if (!isValidTheme(value)) { - logger.warn('Invalid theme value:', value) - return - } - - const themeValue: ThemeValue = value - - if (!inum) { - themeDispatch({ type: themeValue }) - return - } - - try { - const existingConfig = safeParseUserConfig() - const newConfig = { - ...existingConfig, - lang: existingConfig.lang || {}, - theme: { ...(existingConfig.theme || {}), [inum]: themeValue }, - } - storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) - } catch (e) { - logger.error('Failed to parse userConfig:', e instanceof Error ? e : String(e)) - storage.setJSON(STORAGE_KEYS.USER_CONFIG, { lang: {}, theme: { [inum]: themeValue } }) - } - - themeDispatch({ type: themeValue }) - }, - [inum, themeDispatch], - ) + const onChangeTheme = useThemePersistence(userInfo) const changeLanguage = useCallback( (code: string) => { setLang(code) - void ensureLocaleLoaded(code).then(() => i18n.changeLanguage(code)) + // i18n falls back to DEFAULT_LANG when a bundle is missing, so a failure + // here is not worth reverting the stored preference for. + void ensureLocaleLoaded(code) + .then(() => i18n.changeLanguage(code)) + .catch((error) => { + logger.error( + `Failed to switch language to "${code}":`, + error instanceof Error ? error : String(error), + ) + }) const config = safeParseUserConfig() const langConfig = { ...(config?.lang || {}) } From c65bd1ccf8ab34da692e25ebe53f32af5a7fdc06 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Mon, 13 Jul 2026 11:19:46 +0500 Subject: [PATCH 3/7] minor fixes Signed-off-by: faisalsiddique4400 --- .../GluuDatePicker/GluuDatePicker.style.ts | 5 - .../components/GluuDropdown/GluuDropdown.tsx | 4 +- .../app/components/GluuSearchToolbar/types.ts | 1 - .../MobileBottomNav/MobileNavSheet.style.ts | 28 +++- .../MobileBottomNav/MobileNavSheet.tsx | 100 ++++++++++----- .../MobileBottomNav/sheetConstants.ts | 2 + admin-ui/app/components/SVG/ArrowRight.tsx | 32 +++++ admin-ui/app/components/SVG/Refresh.tsx | 2 - admin-ui/app/components/SVG/index.tsx | 2 + admin-ui/app/components/index.tsx | 3 +- admin-ui/app/customColors.ts | 5 + admin-ui/app/hooks/useLangPersistence.ts | 69 ++++++++++ admin-ui/app/hooks/useThemePersistence.ts | 13 +- admin-ui/app/locales/en/translation.json | 2 +- .../app/routes/Apps/Gluu/LanguageMenu.tsx | 61 +-------- .../Apps/Gluu/styles/GluuNavBar.style.ts | 10 -- admin-ui/app/routes/Apps/Gluu/types/common.ts | 4 +- .../Dropdowns/MobileProfileDropdown.tsx | 121 +++++++----------- .../styles/MobileProfileDropdown.style.ts | 86 ++++++------- .../app/routes/components/Dropdowns/types.ts | 6 + .../components/LogoThemed/LogoThemed.tsx | 2 - .../app/utils/types/TokenControllerTypes.ts | 4 +- admin-ui/app/utils/userConfig.ts | 13 ++ .../components/Health/HealthPage.style.ts | 15 --- .../components/ServiceStatusCard.style.ts | 14 +- admin-ui/plugins/admin/helper/assets.ts | 3 +- admin-ui/plugins/admin/helper/webhook.ts | 30 ++--- .../AuthServerProperties/types/index.ts | 8 +- .../ConfigApiProperties/utils/valueUtils.ts | 4 +- .../components/Sessions/types/sessionTypes.ts | 6 +- .../WebsiteSsoServiceProviderForm.tsx | 10 +- admin-ui/plugins/saml/helper/utils.ts | 3 +- admin-ui/plugins/saml/types/formValues.ts | 9 +- admin-ui/plugins/scim/components/ScimPage.tsx | 3 +- .../types/UserClaimsListPage.types.ts | 8 +- .../components/UserDetailViewPage.tsx | 3 +- admin-ui/vite.config.ts | 10 +- 37 files changed, 355 insertions(+), 346 deletions(-) create mode 100644 admin-ui/app/components/SVG/ArrowRight.tsx create mode 100644 admin-ui/app/hooks/useLangPersistence.ts create mode 100644 admin-ui/app/utils/userConfig.ts diff --git a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts index b7b1c6b935..ed6218e4e6 100644 --- a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts +++ b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts @@ -123,8 +123,6 @@ const buildTextFieldSx = ( const POPUP_BOX_SHADOW = '0 8px 24px rgba(0, 0, 0, 0.18)' const buildPopperSx = (tc: PickerThemeColors): SxProps => ({ - // Keep the calendar popup within the viewport on small screens instead of - // letting it render at its full desktop width and overflow the card/screen. '@media (max-width:767px)': { '& .MuiPaper-root': { width: 'min(256px, calc(100vw - 32px))', @@ -137,8 +135,6 @@ const buildPopperSx = (tc: PickerThemeColors): SxProps => ({ height: 'auto', maxHeight: 'none', }, - // The month grid has a fixed min-height on desktop that leaves large empty - // space on mobile; let it size to its rows and compact the day cells. '& .MuiDayCalendar-slideTransition': { minHeight: '180px', }, @@ -156,7 +152,6 @@ const buildPopperSx = (tc: PickerThemeColors): SxProps => ({ margin: '1px', fontSize: '12px', }, - // Tighten the calendar header (month label + arrows) to the narrower width. '& .MuiPickersCalendarHeader-root': { paddingLeft: '12px', paddingRight: '8px', diff --git a/admin-ui/app/components/GluuDropdown/GluuDropdown.tsx b/admin-ui/app/components/GluuDropdown/GluuDropdown.tsx index a298a67c2a..0278cf0b6f 100644 --- a/admin-ui/app/components/GluuDropdown/GluuDropdown.tsx +++ b/admin-ui/app/components/GluuDropdown/GluuDropdown.tsx @@ -110,9 +110,7 @@ export const GluuDropdown = ({ ) const getSelectedOption = useCallback((): - | GluuDropdownOption - | GluuDropdownOption[] - | undefined => { + GluuDropdownOption | GluuDropdownOption[] | undefined => { if (selectedValue === undefined) return undefined if (multiple && Array.isArray(selectedValue)) { return options.filter((opt) => selectedValue.includes(opt.value)) diff --git a/admin-ui/app/components/GluuSearchToolbar/types.ts b/admin-ui/app/components/GluuSearchToolbar/types.ts index e09836ffef..b842d8d5fa 100644 --- a/admin-ui/app/components/GluuSearchToolbar/types.ts +++ b/admin-ui/app/components/GluuSearchToolbar/types.ts @@ -87,7 +87,6 @@ export type GluuRefreshButtonProps = { label?: string loading?: boolean className?: string - // Optional custom icon rendered in place of the default MUI refresh icon. icon?: ReactNode variant?: 'primary' | 'outlined' minHeight?: number diff --git a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.style.ts b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.style.ts index 519b93065b..373cfdd5eb 100644 --- a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.style.ts +++ b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.style.ts @@ -15,6 +15,11 @@ const useStyles = makeStyles<{ colors: MobileNavSheetThemeColors }>()((_theme, { padding: 0, cursor: 'pointer', backgroundColor: `rgba(${hexToRgb(customColors.black)}, ${SHEET.SCRIM_OPACITY})`, + opacity: 0, + transition: `opacity ${SHEET.TRANSITION_MS}ms ease`, + }, + scrimOpen: { + opacity: 1, }, sheet: { position: 'fixed', @@ -30,6 +35,12 @@ const useStyles = makeStyles<{ colors: MobileNavSheetThemeColors }>()((_theme, { borderRadius: `${BORDER_RADIUS.MOBILE_SHEET}px ${BORDER_RADIUS.MOBILE_SHEET}px 0 0`, boxShadow: `0px ${SHEET.SHADOW_OFFSET_Y}px ${SHEET.SHADOW_BLUR}px 0px rgba(${hexToRgb(customColors.black)}, ${SHEET.SHADOW_OPACITY})`, paddingBottom: `calc(${BOTTOM_NAV.HEIGHT}px + env(safe-area-inset-bottom))`, + transform: 'translateY(100%)', + transition: `transform ${SHEET.TRANSITION_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`, + willChange: 'transform', + }, + sheetOpen: { + transform: 'translateY(0)', }, header: { position: 'relative', @@ -184,7 +195,7 @@ const useStyles = makeStyles<{ colors: MobileNavSheetThemeColors }>()((_theme, { display: 'flex', flexDirection: 'column', alignItems: 'flex-start', - gap: SHEET.LIST_ROW_GAP, + gap: 0, width: '100%', }, chevron: { @@ -192,6 +203,20 @@ const useStyles = makeStyles<{ colors: MobileNavSheetThemeColors }>()((_theme, { color: colors.listText, transition: 'transform 0.15s ease', }, + subListWrap: { + display: 'grid', + gridTemplateRows: '0fr', + width: '100%', + transition: `grid-template-rows ${SHEET.SUBLIST_TRANSITION_MS}ms ease`, + }, + subListWrapOpen: { + gridTemplateRows: '1fr', + }, + subListInner: { + overflow: 'hidden', + minHeight: 0, + width: '100%', + }, subList: { display: 'flex', flexDirection: 'column', @@ -199,6 +224,7 @@ const useStyles = makeStyles<{ colors: MobileNavSheetThemeColors }>()((_theme, { gap: SHEET.LIST_ROW_GAP, width: '100%', paddingLeft: SHEET.SUBLIST_INDENT, + paddingTop: SHEET.LIST_ROW_GAP, }, subListItem: { padding: 0, diff --git a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx index c256510f83..9b7dff5bbd 100644 --- a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx +++ b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from 'react' +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type JSX, + type TransitionEvent as ReactTransitionEvent, +} from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' @@ -38,6 +46,8 @@ const MobileNavSheet = ({ const isLight = state.theme === THEME_LIGHT const [expandedKeys, setExpandedKeys] = useState([]) const [drillTile, setDrillTile] = useState(null) + const [renderKey, setRenderKey] = useState(openKey) + const [entered, setEntered] = useState(false) const closeButtonRef = useRef(null) const toggleExpanded = useCallback((key: string): void => { @@ -61,18 +71,38 @@ const MobileNavSheet = ({ const { classes, cx } = useStyles({ colors: themeColors }) useEffect(() => { - if (!openKey) return undefined + if (!openKey) { + setEntered(false) + return undefined + } + setRenderKey(openKey) + let inner = 0 + const outer = requestAnimationFrame(() => { + inner = requestAnimationFrame(() => setEntered(true)) + }) + return () => { + cancelAnimationFrame(outer) + cancelAnimationFrame(inner) + } + }, [openKey]) + + useEffect(() => { + if (!renderKey) return undefined + closeButtonRef.current?.focus() const onKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) - }, [openKey, onClose]) + }, [renderKey, onClose]) - useEffect(() => { - if (!openKey) return - closeButtonRef.current?.focus() - }, [openKey]) + const handleSheetTransitionEnd = useCallback( + (e: ReactTransitionEvent): void => { + if (e.target !== e.currentTarget || e.propertyName !== 'transform') return + if (!openKey) setRenderKey(null) + }, + [openKey], + ) const isItemActive = useCallback( (item: SheetItem): boolean => @@ -103,10 +133,10 @@ const MobileNavSheet = ({ setExpandedKeys(activeGroups) }, [openKey, isItemActive]) - if (!openKey || typeof document === 'undefined') return null + if (!renderKey || typeof document === 'undefined') return null - const isMore = openKey === SHEET_KEYS.MORE - const section = isSectionKey(openKey) ? SECTION_MENUS[openKey] : null + const isMore = renderKey === SHEET_KEYS.MORE + const section = isSectionKey(renderKey) ? SECTION_MENUS[renderKey] : null const isDrilled = isMore && drillTile !== null const headerTitleKey = isDrilled @@ -133,12 +163,13 @@ const MobileNavSheet = ({ <> - {expanded ? ( -
- {item.children.map((child) => { - const childActive = isItemActive(child) - return ( - - ) - })} +
+
+
+ {item.children.map((child) => { + const childActive = isItemActive(child) + return ( + + ) + })} +
- ) : null} +
) } diff --git a/admin-ui/app/components/MobileBottomNav/sheetConstants.ts b/admin-ui/app/components/MobileBottomNav/sheetConstants.ts index 33d8cdd583..9f29fd6e7f 100644 --- a/admin-ui/app/components/MobileBottomNav/sheetConstants.ts +++ b/admin-ui/app/components/MobileBottomNav/sheetConstants.ts @@ -167,6 +167,8 @@ export const SHEET = { SHADOW_BLUR: 25, SHADOW_OPACITY: 0.1, SCRIM_OPACITY: 0.6, + TRANSITION_MS: 800, + SUBLIST_TRANSITION_MS: 500, Z_SCRIM: 10001, Z_SHEET: 10002, Z_BAR_ELEVATED: 10003, diff --git a/admin-ui/app/components/SVG/ArrowRight.tsx b/admin-ui/app/components/SVG/ArrowRight.tsx new file mode 100644 index 0000000000..973b9143c5 --- /dev/null +++ b/admin-ui/app/components/SVG/ArrowRight.tsx @@ -0,0 +1,32 @@ +import { memo } from 'react' + +type ArrowRightIconProps = { + width?: number | string + height?: number | string + className?: string +} + +export const ArrowRightIcon = memo( + ({ width = 10, height = 10, className }) => ( + + ), +) + +ArrowRightIcon.displayName = 'ArrowRightIcon' diff --git a/admin-ui/app/components/SVG/Refresh.tsx b/admin-ui/app/components/SVG/Refresh.tsx index eb2f5dc0a3..80b110b1f6 100644 --- a/admin-ui/app/components/SVG/Refresh.tsx +++ b/admin-ui/app/components/SVG/Refresh.tsx @@ -6,8 +6,6 @@ type RefreshIconProps = { className?: string } -// Refresh icon matching the Figma design (node "refresh 1"): a circular -// double-arrow drawn as a single filled path, sized on a 22x22 viewBox. export const RefreshIcon = memo(({ width = 22, height = 22, className }) => ( { + const initLang = storage.get(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG + const config = safeParseUserConfig() + return config.lang?.[inum || ''] || initLang +} + +const applyLanguage = (code: string, i18nInstance: I18n) => { + void ensureLocaleLoaded(code) + .then(() => i18nInstance.changeLanguage(code)) + .catch((error) => { + logger.error( + `Failed to switch language to "${code}":`, + error instanceof Error ? error : String(error), + ) + }) +} + +const useLangPersistence = (inum?: string) => { + const { i18n } = useTranslation() + const [lang, setLang] = useState(() => getInitialLang(inum)) + const hasInitializedRef = useRef(false) + const prevInumRef = useRef(inum) + + useEffect(() => { + if (prevInumRef.current !== inum) { + hasInitializedRef.current = false + prevInumRef.current = inum + } + + if (hasInitializedRef.current) return + + const userLang = getInitialLang(inum) + if (userLang !== i18n.language) { + setLang(userLang) + applyLanguage(userLang, i18n) + } + + hasInitializedRef.current = true + }, [i18n, inum]) + + const changeLanguage = useCallback( + (code: string) => { + setLang(code) + applyLanguage(code, i18n) + + const config = safeParseUserConfig() + const langConfig = { ...(config.lang || {}) } + if (inum) { + langConfig[inum] = code + } + storage.setJSON(STORAGE_KEYS.USER_CONFIG, { ...config, lang: langConfig }) + storage.set(STORAGE_KEYS.INIT_LANG, code) + }, + [i18n, inum], + ) + + return { lang, changeLanguage } +} + +export { useLangPersistence, getInitialLang } diff --git a/admin-ui/app/hooks/useThemePersistence.ts b/admin-ui/app/hooks/useThemePersistence.ts index aba97de810..a9f7eb534b 100644 --- a/admin-ui/app/hooks/useThemePersistence.ts +++ b/admin-ui/app/hooks/useThemePersistence.ts @@ -4,17 +4,9 @@ import { isValidTheme, type ThemeValue } from '@/context/theme/constants' import { logger } from '@/utils/logger' import { storage } from '@/utils/storage' import { STORAGE_KEYS } from '@/constants' +import { safeParseUserConfig } from '@/utils/userConfig' import type { UserInfo } from 'Redux/features/types/authTypes' -type UserConfig = { - lang?: Record - theme?: Record -} - -/** - * Applies a theme change and, when the user is identified, persists it to - * `USER_CONFIG` keyed by `inum`. Anonymous users get the dispatch without a write. - */ export const useThemePersistence = (userInfo: UserInfo | null | undefined) => { const { dispatch } = useTheme() const inum = userInfo?.inum @@ -29,8 +21,7 @@ export const useThemePersistence = (userInfo: UserInfo | null | undefined) => { const themeValue: ThemeValue = value if (inum) { - // storage.getJSON already logs and returns null on a parse failure. - const existingConfig = storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} + const existingConfig = safeParseUserConfig() storage.setJSON(STORAGE_KEYS.USER_CONFIG, { ...existingConfig, lang: existingConfig.lang || {}, diff --git a/admin-ui/app/locales/en/translation.json b/admin-ui/app/locales/en/translation.json index a839a95a35..8dd6c079f7 100644 --- a/admin-ui/app/locales/en/translation.json +++ b/admin-ui/app/locales/en/translation.json @@ -1189,7 +1189,7 @@ "service_status_down": "Service is currently unavailable.", "service_status_degraded": "Service is experiencing issues.", "service_status_unknown": "Service status could not be determined.", - "services_healthy_count": "{{healthyCount}} of {{totalCount}} Services Healthy", + "services_healthy_count": "{{healthyCount}} of {{totalCount}} services healthy", "error_fetching_health_status": "Error fetching health status. Please try again.", "no_services_found": "No services found.", "fido2_metrics_health_status": "FIDO2 Metrics", diff --git a/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx b/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx index 4f3dd1f1b4..83a2e5e95f 100644 --- a/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx +++ b/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx @@ -1,75 +1,24 @@ -import { useState, useEffect, use, useMemo, useCallback, memo, useRef } from 'react' +import { use, useMemo, memo } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' import { GluuDropdown, type GluuDropdownOption, ChevronIcon } from 'Components' import GluuText from 'Routes/Apps/Gluu/GluuText' import { ThemeContext } from 'Context/theme/themeContext' import { THEME_DARK, DEFAULT_THEME } from '@/context/theme/constants' -import { storage } from '@/utils/storage' -import { ensureLocaleLoaded } from '@/i18n' -import { STORAGE_KEYS, LANG_CODES, DEFAULT_LANG } from '@/constants' +import { useLangPersistence } from '@/hooks/useLangPersistence' +import { LANG_CODES } from '@/constants' import { useStyles } from './styles/LanguageMenu.style' import type { LanguageMenuProps } from './types' -interface UserConfig { - lang?: Record - theme?: Record -} - -const safeParseUserConfig = (): UserConfig => - storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} - -const getInitialLang = (inum?: string): string => { - const initLang = storage.get(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG - const config = safeParseUserConfig() - return config?.lang?.[inum || ''] || initLang -} - const LanguageMenu = memo(({ userInfo }) => { - const { t, i18n } = useTranslation() + const { t } = useTranslation() const { inum } = userInfo const themeContext = use(ThemeContext) const currentTheme = themeContext?.state?.theme || DEFAULT_THEME const isDark = currentTheme === THEME_DARK const { classes } = useStyles({ isDark }) - const hasInitializedRef = useRef(false) - const prevInumRef = useRef(inum) - const [lang, setLang] = useState(() => getInitialLang(inum)) - - useEffect(() => { - if (prevInumRef.current !== inum) { - hasInitializedRef.current = false - prevInumRef.current = inum - } - - if (hasInitializedRef.current) return - - const userLang = getInitialLang(inum) - if (userLang !== i18n.language) { - setLang(userLang) - void ensureLocaleLoaded(userLang).then(() => i18n.changeLanguage(userLang)) - } - - hasInitializedRef.current = true - }, [i18n, inum]) - - const changeLanguage = useCallback( - (code: string) => { - setLang(code) - void ensureLocaleLoaded(code).then(() => i18n.changeLanguage(code)) - - const config = safeParseUserConfig() - const langConfig = { ...(config?.lang || {}) } - if (inum) { - langConfig[inum] = code - } - const newConfig = { ...config, lang: langConfig } - storage.setJSON(STORAGE_KEYS.USER_CONFIG, newConfig) - storage.set(STORAGE_KEYS.INIT_LANG, code) - }, - [i18n, inum], - ) + const { lang, changeLanguage } = useLangPersistence(inum) const options: GluuDropdownOption[] = useMemo( () => [ diff --git a/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts b/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts index 369e659a34..de4ac38ece 100644 --- a/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts +++ b/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts @@ -69,8 +69,6 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC display: 'flex', justifyContent: 'space-between', alignItems: 'center', - // Allow flex children to shrink below their intrinsic content width so the - // greeting can truncate instead of the row overflowing. minWidth: 0, }, leftSection: { @@ -172,8 +170,6 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC 'height': '100%', 'flexShrink': 0, 'minWidth': 0, - // Below 400px allow the trigger to shrink so the greeting can truncate - // within the available space instead of overflowing off-screen. '@media (max-width:399px)': { flexShrink: 1, }, @@ -185,8 +181,6 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC 'color': navbarColors.text, 'letterSpacing': '0.24px', 'lineHeight': 'normal', - // display:block is required for maxWidth + text-overflow:ellipsis to take - // effect (they are ignored on the default inline span). 'display': 'block', 'whiteSpace': 'nowrap', 'overflow': 'hidden', @@ -194,13 +188,9 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC 'minWidth': 0, 'margin': 0, 'padding': 0, - // Hard cap on mobile so a long username always truncates with an ellipsis - // (display:block makes maxWidth + text-overflow:ellipsis take effect). - // Uses the shared MOBILE_MEDIA_QUERY that is known to emit correctly here. [`@media ${MOBILE_MEDIA_QUERY}`]: { maxWidth: '120px', }, - // Tighter cap on very small screens. '@media (max-width:399px)': { maxWidth: '100px', }, diff --git a/admin-ui/app/routes/Apps/Gluu/types/common.ts b/admin-ui/app/routes/Apps/Gluu/types/common.ts index fa795b076e..adb8b81565 100644 --- a/admin-ui/app/routes/Apps/Gluu/types/common.ts +++ b/admin-ui/app/routes/Apps/Gluu/types/common.ts @@ -1,8 +1,6 @@ type JsonPrimitive = string | number | boolean | null export type JsonValue = - | JsonPrimitive - | { [key: string]: JsonPrimitive | object | undefined } - | (JsonPrimitive | object)[] + JsonPrimitive | { [key: string]: JsonPrimitive | object | undefined } | (JsonPrimitive | object)[] export type JsonObject = { [key: string]: JsonValue } diff --git a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx index eecd819f29..926abb3e8e 100644 --- a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx +++ b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx @@ -1,58 +1,68 @@ -import { useEffect, useMemo, useCallback, useRef, useState } from 'react' +import { + useEffect, + useMemo, + useCallback, + useRef, + useState, + type TransitionEvent as ReactTransitionEvent, +} from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' -import { GluuDropdown, type GluuDropdownOption, ChevronIcon } from 'Components' +import { GluuDropdown, type GluuDropdownOption, ChevronIcon, ArrowRightIcon } from 'Components' import GluuText from 'Routes/Apps/Gluu/GluuText' import { useAppDispatch, useAppSelector } from '@/redux/hooks' import { useAppNavigation, ROUTES } from '@/helpers/navigation' import { auditLogoutLogs } from 'Redux/features/sessionSlice' import { useTheme } from '@/context/theme/themeContext' import { THEME_LIGHT, THEME_DARK } from '@/context/theme/constants' -import { ensureLocaleLoaded } from '@/i18n' import { useThemePersistence } from '@/hooks/useThemePersistence' -import { logger } from '@/utils/logger' -import { storage } from '@/utils/storage' -import { STORAGE_KEYS, LANG_CODES, DEFAULT_LANG } from '@/constants' -import type { UserInfo } from 'Redux/features/types/authTypes' +import { useLangPersistence } from '@/hooks/useLangPersistence' +import { LANG_CODES, DEFAULT_LANG } from '@/constants' import { useStyles } from './styles/MobileProfileDropdown.style' - -type UserConfig = { - lang?: Record - theme?: Record -} - -type MobileProfileDropdownProps = { - userInfo: UserInfo | null - renderTrigger: (isOpen: boolean) => React.ReactNode -} - -const safeParseUserConfig = (): UserConfig => - storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} - -const getInitialLang = (inum?: string): string => { - const initLang = storage.get(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG - const config = safeParseUserConfig() - return config?.lang?.[inum || ''] || initLang -} +import type { MobileProfileDropdownProps } from './types' const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdownProps) => { - const { t, i18n } = useTranslation() + const { t } = useTranslation() const dispatch = useAppDispatch() const { navigateToRoute } = useAppNavigation() const { logoutAuditSucceeded } = useAppSelector((state) => state.logoutAuditReducer) const { state: themeState } = useTheme() const currentTheme = themeState.theme - const isDark = currentTheme === THEME_DARK - const { classes } = useStyles({ isDark }) + const { classes } = useStyles({ theme: currentTheme }) const inum = userInfo?.inum const [isOpen, setIsOpen] = useState(false) - const [lang, setLang] = useState(() => getInitialLang(inum)) + const [rendered, setRendered] = useState(false) + const [entered, setEntered] = useState(false) + const { lang, changeLanguage } = useLangPersistence(inum) const containerRef = useRef(null) - // Close the menu when a logout audit completes and we navigate away. + useEffect(() => { + if (!isOpen) { + setEntered(false) + return undefined + } + setRendered(true) + let inner = 0 + const outer = requestAnimationFrame(() => { + inner = requestAnimationFrame(() => setEntered(true)) + }) + return () => { + cancelAnimationFrame(outer) + cancelAnimationFrame(inner) + } + }, [isOpen]) + + const handleMenuTransitionEnd = useCallback( + (e: ReactTransitionEvent): void => { + if (e.target !== e.currentTarget || e.propertyName !== 'transform') return + if (!isOpen) setRendered(false) + }, + [isOpen], + ) + useEffect(() => { if (logoutAuditSucceeded === true) { navigateToRoute(ROUTES.LOGOUT) @@ -77,31 +87,6 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow const onChangeTheme = useThemePersistence(userInfo) - const changeLanguage = useCallback( - (code: string) => { - setLang(code) - // i18n falls back to DEFAULT_LANG when a bundle is missing, so a failure - // here is not worth reverting the stored preference for. - void ensureLocaleLoaded(code) - .then(() => i18n.changeLanguage(code)) - .catch((error) => { - logger.error( - `Failed to switch language to "${code}":`, - error instanceof Error ? error : String(error), - ) - }) - - const config = safeParseUserConfig() - const langConfig = { ...(config?.lang || {}) } - if (inum) { - langConfig[inum] = code - } - storage.setJSON(STORAGE_KEYS.USER_CONFIG, { ...config, lang: langConfig }) - storage.set(STORAGE_KEYS.INIT_LANG, code) - }, - [i18n, inum], - ) - const handleProfile = useCallback(() => { setIsOpen(false) navigateToRoute(ROUTES.PROFILE) @@ -131,7 +116,6 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow ) const themeLabel = currentTheme === THEME_DARK ? t('themes.dark') : t('themes.light') - // Match the desktop navbar language control: show the uppercased code (e.g. "FR"). const langLabel = (lang || DEFAULT_LANG).split('-')[0].toUpperCase() return ( @@ -153,9 +137,10 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow {renderTrigger(isOpen)}
- {isOpen && ( + {rendered && ( @@ -175,23 +160,7 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow {t('menus.my_profile')}
diff --git a/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts index 609ca06db7..3265942c0a 100644 --- a/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts +++ b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts @@ -1,51 +1,48 @@ import { makeStyles } from 'tss-react/mui' import { fontFamily, fontWeights } from '@/styles/fonts' -import customColors from '@/customColors' +import customColors, { hexToRgb } from '@/customColors' +import getThemeColor from '@/context/theme/config' +import { THEME_DARK, type ThemeValue } from '@/context/theme/constants' type StyleParams = { - isDark: boolean + theme: ThemeValue } -// Colors taken directly from the Figma dropdown spec (nodes 3755:1350 light / -// 3755:1671 dark). Kept local to this component because they are dropdown-only -// tokens that do not exist in the shared theme config. -const LIGHT = { - bg: customColors.white, - border: '#cfcfcf', - divider: '#c7c7c7', - text: '#0a2540', - controlBorder: '#ebebeb', - controlText: '#425466', - signOutBg: '#f5f6f8', - arrowBg: '#f5f6f8', -} as const +const MENU_SHADOW_OPACITY = 0.05 +const MENU_SHADOW = `0px 4px 5.5px rgba(${hexToRgb(customColors.black)}, ${MENU_SHADOW_OPACITY})` +const MENU_TRANSITION_MS = 300 -const DARK = { - bg: '#0a2540', - border: '#3a628c', - divider: '#4a72a0', - text: customColors.white, - controlBorder: '#193f66', - controlText: customColors.white, - signOutBg: '#16395d', - arrowBg: '#16395d', -} as const - -export const useStyles = makeStyles()((_theme, { isDark }) => { - const c = isDark ? DARK : LIGHT +export const useStyles = makeStyles()((_theme, { theme }) => { + const tc = getThemeColor(theme) + const isDark = theme === THEME_DARK + const controlTextColor = isDark ? customColors.white : customColors.textSecondary + const chipBg = isDark + ? customColors.mobileSheetTileChipDark + : customColors.mobileSheetTileChipLight return { menu: { width: 172, boxSizing: 'border-box', - backgroundColor: c.bg, - border: `1.5px solid ${c.border}`, + backgroundColor: tc.menu.background, + border: `1.5px solid ${ + isDark ? customColors.mobileDropdownBorderDark : customColors.mobileDropdownBorderLight + }`, borderRadius: 10, - boxShadow: '0px 4px 5.5px rgba(0, 0, 0, 0.05)', + boxShadow: MENU_SHADOW, padding: '12px', display: 'flex', flexDirection: 'column', gap: 0, + transformOrigin: 'top right', + opacity: 0, + transform: 'translateY(-8px) scale(0.96)', + transition: `opacity ${MENU_TRANSITION_MS}ms ease, transform ${MENU_TRANSITION_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`, + willChange: 'opacity, transform', + }, + menuOpen: { + opacity: 1, + transform: 'translateY(0) scale(1)', }, row: { display: 'flex', @@ -59,7 +56,7 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { fontWeight: fontWeights.semiBold, lineHeight: 'normal', letterSpacing: '0.22px', - color: c.text, + color: tc.menu.color, margin: 0, whiteSpace: 'nowrap', }, @@ -70,12 +67,12 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { width: 20, height: 21, borderRadius: '50%', - backgroundColor: c.arrowBg, + backgroundColor: chipBg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, - color: c.text, + color: tc.menu.color, }, arrowIcon: { width: 10, @@ -85,7 +82,9 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { divider: { height: 0, border: 'none', - borderTop: `1px solid ${c.divider}`, + borderTop: `1px solid ${ + isDark ? customColors.mobileDropdownDividerDark : customColors.mobileDropdownDividerLight + }`, margin: '12px 0', width: '100%', }, @@ -93,7 +92,7 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { minWidth: 74, height: 27, boxSizing: 'border-box', - border: `1px solid ${c.controlBorder}`, + border: `1px solid ${tc.settings.inputBorder}`, borderRadius: 3, display: 'flex', alignItems: 'center', @@ -107,7 +106,7 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { fontSize: '12px', fontWeight: fontWeights.medium, lineHeight: '21.6px', - color: c.controlText, + color: controlTextColor, whiteSpace: 'nowrap', }, controlIcon: { @@ -117,7 +116,7 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { 'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center', - 'color': c.controlText, + 'color': controlTextColor, '& svg': { width: '100%', height: '100%', @@ -127,7 +126,7 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { marginTop: 12, height: 32, borderRadius: 3, - backgroundColor: c.signOutBg, + backgroundColor: chipBg, border: 'none', display: 'flex', alignItems: 'center', @@ -141,18 +140,12 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { fontSize: '13px', fontWeight: fontWeights.semiBold, lineHeight: '22.273px', - color: c.text, + color: tc.menu.color, whiteSpace: 'nowrap', }, - // Shrinks the nested GluuDropdown option panel (theme/language selectors) - // so its list matches the compact 50x27 control instead of the default - // full-size dropdown rows. Applied via `dropdownClassName`. compactMenu: { 'minWidth': '96px !important', 'width': 'auto', - // Attach the list directly below the control, aligned to its right edge - // (the control sits at the right of its row). Override GluuDropdown's - // default centered + 13px-gap positioning. 'left': 'auto !important', 'right': '0 !important', 'transform': 'none !important', @@ -165,7 +158,6 @@ export const useStyles = makeStyles()((_theme, { isDark }) => { lineHeight: '16px', borderRadius: 4, }, - // Tighten the menu content padding (default 16px) to match the compact rows. '& > div': { padding: '6px', }, diff --git a/admin-ui/app/routes/components/Dropdowns/types.ts b/admin-ui/app/routes/components/Dropdowns/types.ts index d9a7032be4..714dfe45d4 100644 --- a/admin-ui/app/routes/components/Dropdowns/types.ts +++ b/admin-ui/app/routes/components/Dropdowns/types.ts @@ -1,5 +1,6 @@ import type React from 'react' import type { DropdownPosition, GluuDropdownOption } from 'Components' +import type { UserInfo } from 'Redux/features/types/authTypes' export type DropdownProfileProps = { trigger?: React.ReactNode @@ -9,3 +10,8 @@ export type DropdownProfileProps = { ) => React.ReactNode position?: DropdownPosition } + +export type MobileProfileDropdownProps = { + userInfo: UserInfo | null + renderTrigger: (isOpen: boolean) => React.ReactNode +} diff --git a/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx b/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx index 2179492278..11b5a983ce 100755 --- a/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx +++ b/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx @@ -4,8 +4,6 @@ import { ThemeContext } from '@/context/theme/themeContext' import { THEME_DARK, THEME_LIGHT, DEFAULT_THEME } from '@/context/theme/constants' import logoImage from '../../../images/logos/logo192.png' -// When `variant` is set the logo ignores the active theme and always renders -// that color (e.g. the mobile header always shows the green logo in both themes). type LogoVariant = 'green' | 'white' type LogoThemedProps = Omit, 'src' | 'alt' | 'style'> & { diff --git a/admin-ui/app/utils/types/TokenControllerTypes.ts b/admin-ui/app/utils/types/TokenControllerTypes.ts index 462c3d3ad1..3f92d10e9c 100644 --- a/admin-ui/app/utils/types/TokenControllerTypes.ts +++ b/admin-ui/app/utils/types/TokenControllerTypes.ts @@ -20,9 +20,7 @@ export type AdditionalPayload = { tableData?: JsonValue omitPayload?: boolean [key: string]: - | JsonValue - | { action_message?: string; action_data?: AdditionalActionData } - | undefined + JsonValue | { action_message?: string; action_data?: AdditionalActionData } | undefined } export type AxiosErrorLike = { diff --git a/admin-ui/app/utils/userConfig.ts b/admin-ui/app/utils/userConfig.ts new file mode 100644 index 0000000000..595c4cd5a9 --- /dev/null +++ b/admin-ui/app/utils/userConfig.ts @@ -0,0 +1,13 @@ +import { storage } from '@/utils/storage' +import { STORAGE_KEYS } from '@/constants' + +type UserConfig = { + lang?: Record + theme?: Record +} + +const safeParseUserConfig = (): UserConfig => + storage.getJSON(STORAGE_KEYS.USER_CONFIG) ?? {} + +export { safeParseUserConfig } +export type { UserConfig } diff --git a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts index fa6931f202..c21f0e45d0 100644 --- a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts +++ b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts @@ -5,7 +5,6 @@ import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' import { getCardBorderStyle } from '@/styles/cardBorderStyles' const MOBILE_MEDIA_QUERY = '(max-width:767px)' -// Below this width the 16px header title runs into the refresh icon. const NARROW_MEDIA_QUERY = '(max-width:350px)' interface HealthPageThemeColors { @@ -27,9 +26,6 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole }) return { - // Screen-edge inset for the page on mobile. Figma insets the card 30px from - // each side (matching the Dashboard mobile layout); apply the same padding - // so the Health page lines up with the rest of the mobile design system. mobileContentPad: { [`@media ${MOBILE_MEDIA_QUERY}`]: { paddingLeft: '30px', @@ -48,7 +44,6 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole lineHeight: 'normal', color: themeColors.text, margin: 0, - // Figma gap from the heading baseline to the card top is ~34px. marginBottom: '34px', }, }, @@ -93,8 +88,6 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole lineHeight: lineHeights.tight, color: themeColors.text, margin: 0, - // Without min-width:0 a flex item refuses to shrink below its content - // width, so the title would run under the refresh button. minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', @@ -117,15 +110,11 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole top: '50%', transform: 'translateY(-50%)', zIndex: 10, - // In flow on mobile so it reserves its width and the title cannot overlap. [`@media ${MOBILE_MEDIA_QUERY}`]: { position: 'static', transform: 'none', flexShrink: 0, - // The title is conditional; keeps the button right-aligned when it is - // the header's only in-flow child. marginLeft: 'auto', - // Centres the 22px icon inside the button's 32px mobile box. marginTop: `${-(32 - 22) / 2}px`, }, }, @@ -143,9 +132,6 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole fontSize: fontSizes.base, fontWeight: fontWeights.medium, lineHeight: lineHeights.relaxed, - // Icon-only on mobile per Figma: strip the button chrome and collapse the - // text label to zero size. GluuButton applies these via INLINE styles, so - // every override here must use !important to win over the inline values. [`@media ${MOBILE_MEDIA_QUERY}`]: { 'minWidth': 'auto !important', 'width': '32px !important', @@ -157,7 +143,6 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole 'border': 'none !important', 'borderColor': 'transparent !important', 'backgroundColor': 'transparent !important', - // Keep the refresh icon itself visible at a fixed size. '& svg, & .MuiSvgIcon-root': { fontSize: '22px !important', }, diff --git a/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts b/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts index 438dc0b6b0..4f9818c01f 100644 --- a/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts +++ b/admin-ui/plugins/admin/components/Health/components/ServiceStatusCard.style.ts @@ -1,21 +1,17 @@ import { makeStyles } from 'tss-react/mui' -import customColors from '@/customColors' +import customColors, { hexToRgb } from '@/customColors' import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' -// Intentionally not the shared `darkBorder` token. const DARK_CARD_BORDER = '#284b6e' const CARD_MIN_HEIGHT = 79 const CARD_RADIUS = 8 const CARD_PADDING_Y = 16.5 const CARD_PADDING_X = 19.5 -// Figma tops the pill at y=18.89 against a content box starting at y=16.5. const BADGE_TOP_OFFSET = 2.39 -// `space-between` positions the pill; this is only a guard so a long service -// name never collides with it. const TEXT_BADGE_MIN_GAP = 8 -// Invisible on the dark card, which reads its edge from the border instead. -const CARD_SHADOW = '0px 1.985px 2.73px 0px rgba(0, 0, 0, 0.05)' +const CARD_SHADOW_OPACITY = 0.05 +const CARD_SHADOW = `0px 1.985px 2.73px 0px rgba(${hexToRgb(customColors.black)}, ${CARD_SHADOW_OPACITY})` const useStyles = makeStyles<{ isDark: boolean }>()((_, { isDark }) => { const cardText = { @@ -58,7 +54,6 @@ const useStyles = makeStyles<{ isDark: boolean }>()((_, { isDark }) => { minWidth: 0, display: 'flex', flexDirection: 'column', - // The two 22px line boxes butt up against each other by design. gap: 0, }, serviceName: { @@ -69,7 +64,6 @@ const useStyles = makeStyles<{ isDark: boolean }>()((_, { isDark }) => { serviceMessage: { ...cardText, fontWeight: fontWeights.medium, - // No 13px design token exists. fontSize: '13px', }, statusBadge: { @@ -77,8 +71,6 @@ const useStyles = makeStyles<{ isDark: boolean }>()((_, { isDark }) => { alignSelf: 'flex-start', marginTop: `${BADGE_TOP_OFFSET}px`, }, - // GluuBadge writes its sizing to an inline `style` attribute, so these - // declarations need !important to win over it. badge: { fontSize: '10px !important', lineHeight: '18.133px !important', diff --git a/admin-ui/plugins/admin/helper/assets.ts b/admin-ui/plugins/admin/helper/assets.ts index cac7cfbec5..409f5cb229 100644 --- a/admin-ui/plugins/admin/helper/assets.ts +++ b/admin-ui/plugins/admin/helper/assets.ts @@ -35,8 +35,7 @@ export const buildAssetInitialValues = ( ): AssetFormValues => { const service = getServiceFromAsset(asset) const rec = asset as - | Record - | undefined + Record | undefined const doc = asset as Document | undefined const fileName = toStringValue(doc?.fileName ?? rec?.displayName ?? rec?.fileName, '') return { diff --git a/admin-ui/plugins/admin/helper/webhook.ts b/admin-ui/plugins/admin/helper/webhook.ts index 29f09c793b..b3e8d8e51e 100644 --- a/admin-ui/plugins/admin/helper/webhook.ts +++ b/admin-ui/plugins/admin/helper/webhook.ts @@ -13,22 +13,20 @@ const fromEntry = ( } export const toWebhookEntries = (entries: PagedResultEntriesItem[] | undefined): WebhookEntry[] => - (entries ?? []).map( - (entry): WebhookEntry => ({ - dn: fromEntry(entry, 'dn', undefined), - inum: fromEntry(entry, 'inum', undefined), - displayName: fromEntry(entry, 'displayName', ''), - description: fromEntry(entry, 'description', undefined), - url: fromEntry(entry, 'url', ''), - httpRequestBodyString: fromEntry(entry, 'httpRequestBodyString', undefined), - httpMethod: fromEntry(entry, 'httpMethod', undefined), - jansEnabled: fromEntry(entry, 'jansEnabled', undefined), - httpHeaders: fromEntry(entry, 'httpHeaders', undefined), - auiFeatureIds: fromEntry(entry, 'auiFeatureIds', undefined), - httpRequestBody: fromEntry(entry, 'httpRequestBody', undefined), - baseDn: fromEntry(entry, 'baseDn', undefined), - }), - ) + (entries ?? []).map((entry): WebhookEntry => ({ + dn: fromEntry(entry, 'dn', undefined), + inum: fromEntry(entry, 'inum', undefined), + displayName: fromEntry(entry, 'displayName', ''), + description: fromEntry(entry, 'description', undefined), + url: fromEntry(entry, 'url', ''), + httpRequestBodyString: fromEntry(entry, 'httpRequestBodyString', undefined), + httpMethod: fromEntry(entry, 'httpMethod', undefined), + jansEnabled: fromEntry(entry, 'jansEnabled', undefined), + httpHeaders: fromEntry(entry, 'httpHeaders', undefined), + auiFeatureIds: fromEntry(entry, 'auiFeatureIds', undefined), + httpRequestBody: fromEntry(entry, 'httpRequestBody', undefined), + baseDn: fromEntry(entry, 'baseDn', undefined), + })) export const HTTP_METHODS = [ { value: 'GET', label: 'GET' }, diff --git a/admin-ui/plugins/auth-server/components/AuthServerProperties/types/index.ts b/admin-ui/plugins/auth-server/components/AuthServerProperties/types/index.ts index 00fcaf73cd..8f0c31496e 100644 --- a/admin-ui/plugins/auth-server/components/AuthServerProperties/types/index.ts +++ b/admin-ui/plugins/auth-server/components/AuthServerProperties/types/index.ts @@ -40,13 +40,7 @@ export type ArrayItemSelectProps = { } export type PropertyValue = - | string - | number - | boolean - | string[] - | AppConfiguration - | null - | undefined + string | number | boolean | string[] | AppConfiguration | null | undefined export type JsonPropertyBuilderProps = { propKey: string diff --git a/admin-ui/plugins/auth-server/components/ConfigApiProperties/utils/valueUtils.ts b/admin-ui/plugins/auth-server/components/ConfigApiProperties/utils/valueUtils.ts index 1e3cbba30e..d7a4b14473 100644 --- a/admin-ui/plugins/auth-server/components/ConfigApiProperties/utils/valueUtils.ts +++ b/admin-ui/plugins/auth-server/components/ConfigApiProperties/utils/valueUtils.ts @@ -103,9 +103,7 @@ const getNestedValue = (obj: ApiAppConfiguration, path: string[]): TraversableVa const key = String(part) if (key in current) { current = (current as Record)[key] as - | PropertyValue - | PropertyValue[] - | ApiAppConfiguration + PropertyValue | PropertyValue[] | ApiAppConfiguration } else { return null } diff --git a/admin-ui/plugins/auth-server/components/Sessions/types/sessionTypes.ts b/admin-ui/plugins/auth-server/components/Sessions/types/sessionTypes.ts index c8781f5358..d6d6dcc1e3 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/types/sessionTypes.ts +++ b/admin-ui/plugins/auth-server/components/Sessions/types/sessionTypes.ts @@ -28,11 +28,7 @@ export type SessionDetailPageProps = { export type SessionState = (typeof SESSION_STATES)[number] export type SearchFilterType = - | 'client_id' - | 'auth_user' - | 'expirationDate' - | 'authenticationTime' - | null + 'client_id' | 'auth_user' | 'expirationDate' | 'authenticationTime' | null export type MutationCallbacks = { onSuccess?: () => void diff --git a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx index 862fb111a1..4f82900236 100644 --- a/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx +++ b/admin-ui/plugins/saml/components/WebsiteSsoServiceProviderForm.tsx @@ -118,12 +118,10 @@ const WebsiteSsoServiceProviderForm = ({ const attributesList = useMemo( () => attributesData?.entries - ? attributesData.entries.map( - (item): ScopeOption => ({ - dn: String(item?.dn || ''), - name: String(item?.displayName || ''), - }), - ) + ? attributesData.entries.map((item): ScopeOption => ({ + dn: String(item?.dn || ''), + name: String(item?.displayName || ''), + })) : [], [attributesData], ) diff --git a/admin-ui/plugins/saml/helper/utils.ts b/admin-ui/plugins/saml/helper/utils.ts index 8dd659bfa2..97212401c9 100644 --- a/admin-ui/plugins/saml/helper/utils.ts +++ b/admin-ui/plugins/saml/helper/utils.ts @@ -136,8 +136,7 @@ export const transformToWebsiteSsoServiceProviderFormValues = ( spMetaDataURL: configs?.spMetaDataURL, redirectUris: configs?.redirectUris, profileConfigurations: configs?.profileConfigurations as - | Record - | undefined, + Record | undefined, dn: configs?.dn, validationLog: configs?.validationLog, validationStatus: configs?.validationStatus, diff --git a/admin-ui/plugins/saml/types/formValues.ts b/admin-ui/plugins/saml/types/formValues.ts index 9e2d878a9d..d65c97277c 100644 --- a/admin-ui/plugins/saml/types/formValues.ts +++ b/admin-ui/plugins/saml/types/formValues.ts @@ -18,14 +18,7 @@ export type RootFields = Record type NestedRecord = Record export type CleanableValue = - | string - | number - | boolean - | File - | null - | undefined - | string[] - | NestedRecord + string | number | boolean | File | null | undefined | string[] | NestedRecord // Form value types export type WebsiteSsoIdentityProviderFormValues = { diff --git a/admin-ui/plugins/scim/components/ScimPage.tsx b/admin-ui/plugins/scim/components/ScimPage.tsx index bf1e016cdc..15e793fbb3 100644 --- a/admin-ui/plugins/scim/components/ScimPage.tsx +++ b/admin-ui/plugins/scim/components/ScimPage.tsx @@ -54,8 +54,7 @@ const ScimPage: React.FC = () => { onMutate: async (variables: { data?: JsonPatch[] }) => { await queryClient.cancelQueries({ queryKey: getGetScimConfigQueryKey() }) const previousConfig = queryClient.getQueryData(getGetScimConfigQueryKey()) as - | AppConfiguration3 - | undefined + AppConfiguration3 | undefined if (previousConfig) { queryClient.setQueryData(getGetScimConfigQueryKey(), () => { return (variables.data ?? []).reduce( diff --git a/admin-ui/plugins/user-claims/components/types/UserClaimsListPage.types.ts b/admin-ui/plugins/user-claims/components/types/UserClaimsListPage.types.ts index 8dbe685cb4..2997f5be19 100644 --- a/admin-ui/plugins/user-claims/components/types/UserClaimsListPage.types.ts +++ b/admin-ui/plugins/user-claims/components/types/UserClaimsListPage.types.ts @@ -1,11 +1,5 @@ type ModifiedFieldValue = - | string - | number - | boolean - | string[] - | number[] - | AttributeValidation - | null + string | number | boolean | string[] | number[] | AttributeValidation | null export type ModifiedFields = { [key: string]: ModifiedFieldValue diff --git a/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx b/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx index 349a61af69..5a739446dd 100644 --- a/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx +++ b/admin-ui/plugins/user-management/components/UserDetailViewPage.tsx @@ -19,8 +19,7 @@ const UserDetailViewPage = ({ row }: RowProps) => { const attrs = (rowData as { customAttributes?: CustomAttrWithValues[] })?.customAttributes if (!Array.isArray(attrs)) return '' const roleAttr = attrs.find((a) => a?.name === JANS_ADMIN_UI_ROLE_ATTR) as - | CustomAttrWithValues - | undefined + CustomAttrWithValues | undefined if (!roleAttr) return '' if (Array.isArray(roleAttr.values)) { diff --git a/admin-ui/vite.config.ts b/admin-ui/vite.config.ts index 9151d374c2..0c686f99b7 100644 --- a/admin-ui/vite.config.ts +++ b/admin-ui/vite.config.ts @@ -203,12 +203,10 @@ type VendorChunkGroup = { const VENDOR_CHUNK_GROUPS: VendorChunkGroup[] = [ { name: 'vendor-react', test: isVendorPackageIn(REACT_RUNTIME_PACKAGES) }, - ...FEATURE_GROUPS.map( - ([name, packages]): VendorChunkGroup => ({ - name, - test: isVendorPackageIn(new Set(packages)), - }), - ), + ...FEATURE_GROUPS.map(([name, packages]): VendorChunkGroup => ({ + name, + test: isVendorPackageIn(new Set(packages)), + })), { name: 'vendor-orval-jans', test: (id: string) => id.replace(REGEX_BACKSLASH, '/').includes('/jans_config_api_orval/src/'), From 3e6692ba86dad1d1891646246e9a35bdaa2e32d9 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Mon, 13 Jul 2026 11:42:49 +0500 Subject: [PATCH 4/7] new hardcoded colors fixed Signed-off-by: faisalsiddique4400 --- admin-ui/app/customColors.ts | 5 ----- .../Dropdowns/styles/MobileProfileDropdown.style.ts | 8 ++------ 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/admin-ui/app/customColors.ts b/admin-ui/app/customColors.ts index 1380db8ddd..73dd818b1f 100644 --- a/admin-ui/app/customColors.ts +++ b/admin-ui/app/customColors.ts @@ -109,11 +109,6 @@ const customColors = { mobileSheetTitleDark: '#e1e5ea', - mobileDropdownBorderLight: '#cfcfcf', - mobileDropdownDividerLight: '#c7c7c7', - mobileDropdownBorderDark: '#3a628c', - mobileDropdownDividerDark: '#4a72a0', - // Other darkBorderGradientBase: '#00d5e6', ribbonShadowColor: '#1a237e', diff --git a/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts index 3265942c0a..500d69c2f9 100644 --- a/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts +++ b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts @@ -25,9 +25,7 @@ export const useStyles = makeStyles()((_theme, { theme }) => { width: 172, boxSizing: 'border-box', backgroundColor: tc.menu.background, - border: `1.5px solid ${ - isDark ? customColors.mobileDropdownBorderDark : customColors.mobileDropdownBorderLight - }`, + border: `1.5px solid ${tc.borderColor}`, borderRadius: 10, boxShadow: MENU_SHADOW, padding: '12px', @@ -82,9 +80,7 @@ export const useStyles = makeStyles()((_theme, { theme }) => { divider: { height: 0, border: 'none', - borderTop: `1px solid ${ - isDark ? customColors.mobileDropdownDividerDark : customColors.mobileDropdownDividerLight - }`, + borderTop: `1px solid ${tc.borderColor}`, margin: '12px 0', width: '100%', }, From 7fbd4860951d68ffa22fc191c40c7580e214f366 Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Mon, 13 Jul 2026 12:09:54 +0500 Subject: [PATCH 5/7] mathing the padding with nav Signed-off-by: faisalsiddique4400 --- admin-ui/app/constants/ui.ts | 5 +++++ .../app/routes/Apps/Gluu/styles/GluuNavBar.style.ts | 5 +++-- admin-ui/app/routes/Dashboards/DashboardPage.style.ts | 10 +++++++--- .../admin/components/Health/HealthPage.style.ts | 10 +++++++--- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts index 966f920e85..b2796c2197 100644 --- a/admin-ui/app/constants/ui.ts +++ b/admin-ui/app/constants/ui.ts @@ -52,6 +52,11 @@ export const SCROLLBAR = { export const MOBILE_BOTTOM_NAV_HEIGHT = 64 +export const MOBILE_PAGE_PADDING_X = { + MD: 20, + SM: 15, +} as const + export const getScrollbarStyles = (themeColors: ThemeConfig) => ({ '&::-webkit-scrollbar': { width: SCROLLBAR.WIDTH, diff --git a/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts b/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts index de4ac38ece..e0fb019d07 100644 --- a/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts +++ b/admin-ui/app/routes/Apps/Gluu/styles/GluuNavBar.style.ts @@ -1,5 +1,6 @@ import { makeStyles } from 'tss-react/mui' import { fontFamily, fontWeights, fontSizes, letterSpacing } from '@/styles/fonts' +import { MOBILE_PAGE_PADDING_X } from '@/constants' export const MOBILE_MEDIA_QUERY = '(max-width:767px)' @@ -23,11 +24,11 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC marginTop: '-1px', borderBottom: `1px solid ${navbarColors.border}`, [theme.breakpoints.down('md')]: { - padding: '0px 20px', + padding: `0px ${MOBILE_PAGE_PADDING_X.MD}px`, height: '80px', }, [theme.breakpoints.down('sm')]: { - padding: '0px 15px', + padding: `0px ${MOBILE_PAGE_PADDING_X.SM}px`, height: '70px', }, }, diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.style.ts b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts index 38cb955d6e..e2c4f5b3f5 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.style.ts +++ b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts @@ -1,5 +1,5 @@ import customColors from '@/customColors' -import { OPACITY } from '@/constants' +import { OPACITY, MOBILE_PAGE_PADDING_X } from '@/constants' import { makeStyles } from 'tss-react/mui' import type { Theme } from '@mui/material/styles' import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' @@ -31,10 +31,14 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea return { mobileContentPad: { [`@media ${MOBILE_MEDIA_QUERY}`]: { - paddingLeft: '30px', - paddingRight: '30px', + paddingLeft: `${MOBILE_PAGE_PADDING_X.MD}px`, + paddingRight: `${MOBILE_PAGE_PADDING_X.MD}px`, boxSizing: 'border-box', }, + [theme.breakpoints.down('sm')]: { + paddingLeft: `${MOBILE_PAGE_PADDING_X.SM}px`, + paddingRight: `${MOBILE_PAGE_PADDING_X.SM}px`, + }, }, mobilePageTitle: { fontFamily, diff --git a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts index c21f0e45d0..031789e156 100644 --- a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts +++ b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts @@ -1,6 +1,6 @@ import { makeStyles } from 'tss-react/mui' import type { Theme } from '@mui/material/styles' -import { SPACING, BORDER_RADIUS } from '@/constants' +import { SPACING, BORDER_RADIUS, MOBILE_PAGE_PADDING_X } from '@/constants' import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' import { getCardBorderStyle } from '@/styles/cardBorderStyles' @@ -28,10 +28,14 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole return { mobileContentPad: { [`@media ${MOBILE_MEDIA_QUERY}`]: { - paddingLeft: '30px', - paddingRight: '30px', + paddingLeft: `${MOBILE_PAGE_PADDING_X.MD}px`, + paddingRight: `${MOBILE_PAGE_PADDING_X.MD}px`, boxSizing: 'border-box', }, + [theme.breakpoints.down('sm')]: { + paddingLeft: `${MOBILE_PAGE_PADDING_X.SM}px`, + paddingRight: `${MOBILE_PAGE_PADDING_X.SM}px`, + }, }, mobilePageTitle: { display: 'none', From cd371eec19d967e078d4c44cae56bf4acdee3cbe Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Mon, 13 Jul 2026 12:18:22 +0500 Subject: [PATCH 6/7] spaces fixed Signed-off-by: faisalsiddique4400 --- admin-ui/app/routes/Dashboards/DashboardPage.style.ts | 5 +++-- admin-ui/plugins/admin/components/Health/HealthPage.style.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/admin-ui/app/routes/Dashboards/DashboardPage.style.ts b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts index e2c4f5b3f5..4e0c3daa02 100644 --- a/admin-ui/app/routes/Dashboards/DashboardPage.style.ts +++ b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts @@ -1,5 +1,5 @@ import customColors from '@/customColors' -import { OPACITY, MOBILE_PAGE_PADDING_X } from '@/constants' +import { OPACITY, MOBILE_PAGE_PADDING_X, SPACING } from '@/constants' import { makeStyles } from 'tss-react/mui' import type { Theme } from '@mui/material/styles' import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' @@ -33,6 +33,7 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea [`@media ${MOBILE_MEDIA_QUERY}`]: { paddingLeft: `${MOBILE_PAGE_PADDING_X.MD}px`, paddingRight: `${MOBILE_PAGE_PADDING_X.MD}px`, + marginTop: `-${SPACING.PAGE / 2}px`, boxSizing: 'border-box', }, [theme.breakpoints.down('sm')]: { @@ -48,7 +49,7 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea lineHeight: 'normal', color: isDark ? customColors.white : themeColors.text, margin: 0, - marginBottom: '26px', + marginBottom: SPACING.PAGE, }, summarySectionTitle: { fontFamily, diff --git a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts index 031789e156..0da8483f0a 100644 --- a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts +++ b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts @@ -30,6 +30,7 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole [`@media ${MOBILE_MEDIA_QUERY}`]: { paddingLeft: `${MOBILE_PAGE_PADDING_X.MD}px`, paddingRight: `${MOBILE_PAGE_PADDING_X.MD}px`, + marginTop: `-${SPACING.PAGE / 2}px`, boxSizing: 'border-box', }, [theme.breakpoints.down('sm')]: { @@ -48,7 +49,7 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole lineHeight: 'normal', color: themeColors.text, margin: 0, - marginBottom: '34px', + marginBottom: SPACING.PAGE, }, }, healthCard: { From d45e7369015400384d94ddcbf557ca9e247cbdaf Mon Sep 17 00:00:00 2001 From: faisalsiddique4400 Date: Mon, 13 Jul 2026 12:24:09 +0500 Subject: [PATCH 7/7] coderabbit fixes Signed-off-by: faisalsiddique4400 --- .../components/MobileBottomNav/MobileNavSheet.tsx | 1 + admin-ui/app/hooks/useLangPersistence.ts | 14 ++++++++++---- .../components/Dropdowns/MobileProfileDropdown.tsx | 8 ++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx index 9b7dff5bbd..66a327600c 100644 --- a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx +++ b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.tsx @@ -282,6 +282,7 @@ const MobileNavSheet = ({ childActive && classes.listItemActive, )} aria-current={childActive ? 'page' : undefined} + tabIndex={expanded ? 0 : -1} onClick={() => onSelect(child)} > {t(child.titleKey)} diff --git a/admin-ui/app/hooks/useLangPersistence.ts b/admin-ui/app/hooks/useLangPersistence.ts index c605e64904..45900bad88 100644 --- a/admin-ui/app/hooks/useLangPersistence.ts +++ b/admin-ui/app/hooks/useLangPersistence.ts @@ -13,9 +13,12 @@ const getInitialLang = (inum?: string): string => { return config.lang?.[inum || ''] || initLang } -const applyLanguage = (code: string, i18nInstance: I18n) => { +const applyLanguage = (code: string, i18nInstance: I18n, latestRef: { current: string }) => { void ensureLocaleLoaded(code) - .then(() => i18nInstance.changeLanguage(code)) + .then(() => { + if (latestRef.current !== code) return undefined + return i18nInstance.changeLanguage(code) + }) .catch((error) => { logger.error( `Failed to switch language to "${code}":`, @@ -29,6 +32,7 @@ const useLangPersistence = (inum?: string) => { const [lang, setLang] = useState(() => getInitialLang(inum)) const hasInitializedRef = useRef(false) const prevInumRef = useRef(inum) + const latestLangRef = useRef(getInitialLang(inum)) useEffect(() => { if (prevInumRef.current !== inum) { @@ -40,8 +44,9 @@ const useLangPersistence = (inum?: string) => { const userLang = getInitialLang(inum) if (userLang !== i18n.language) { + latestLangRef.current = userLang setLang(userLang) - applyLanguage(userLang, i18n) + applyLanguage(userLang, i18n, latestLangRef) } hasInitializedRef.current = true @@ -49,8 +54,9 @@ const useLangPersistence = (inum?: string) => { const changeLanguage = useCallback( (code: string) => { + latestLangRef.current = code setLang(code) - applyLanguage(code, i18n) + applyLanguage(code, i18n, latestLangRef) const config = safeParseUserConfig() const langConfig = { ...(config.lang || {}) } diff --git a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx index 926abb3e8e..4d4e6c9f9d 100644 --- a/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx +++ b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx @@ -79,9 +79,17 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow } } + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('mousedown', handleClickOutside) + document.removeEventListener('keydown', handleKeyDown) } }, [isOpen])