diff --git a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts index aac4045e9..ed6218e4e 100644 --- a/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts +++ b/admin-ui/app/components/GluuDatePicker/GluuDatePicker.style.ts @@ -123,6 +123,41 @@ const buildTextFieldSx = ( const POPUP_BOX_SHADOW = '0 8px 24px rgba(0, 0, 0, 0.18)' const buildPopperSx = (tc: PickerThemeColors): SxProps => ({ + '@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', + }, + '& .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', + }, + '& .MuiPickersCalendarHeader-root': { + paddingLeft: '12px', + paddingRight: '8px', + marginTop: '8px', + }, + }, '& .MuiPaper-root': { 'backgroundColor': tc.popupBg, 'color': tc.inputTextColor, diff --git a/admin-ui/app/components/GluuDropdown/GluuDropdown.tsx b/admin-ui/app/components/GluuDropdown/GluuDropdown.tsx index a298a67c2..0278cf0b6 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/GluuRefreshButton.tsx b/admin-ui/app/components/GluuSearchToolbar/GluuRefreshButton.tsx index 99a8602ed..0f3b03209 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 ebd4c82da..b842d8d5f 100644 --- a/admin-ui/app/components/GluuSearchToolbar/types.ts +++ b/admin-ui/app/components/GluuSearchToolbar/types.ts @@ -87,6 +87,7 @@ export type GluuRefreshButtonProps = { label?: string loading?: boolean className?: string + icon?: ReactNode variant?: 'primary' | 'outlined' minHeight?: number size?: 'sm' | 'md' | 'lg' diff --git a/admin-ui/app/components/MobileBottomNav/MobileNavSheet.style.ts b/admin-ui/app/components/MobileBottomNav/MobileNavSheet.style.ts index 519b93065..373cfdd5e 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 c256510f8..66a327600 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 33d8cdd58..9f29fd6e7 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 000000000..973b9143c --- /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 new file mode 100644 index 000000000..80b110b1f --- /dev/null +++ b/admin-ui/app/components/SVG/Refresh.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react' + +type RefreshIconProps = { + width?: number | string + height?: number | string + className?: string +} + +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 ec13370c6..b2fead769 100644 --- a/admin-ui/app/components/SVG/index.tsx +++ b/admin-ui/app/components/SVG/index.tsx @@ -12,6 +12,8 @@ import SmtpZoneIcon from './menu/Smtp' import ScriptsIcon from './menu/Scripts' import LockIcon from './menu/Lock' import { ChevronIcon } from './Chevron' +import { RefreshIcon } from './Refresh' +import { ArrowRightIcon } from './ArrowRight' export { JansKcLinkIcon, @@ -28,4 +30,6 @@ export { ScriptsIcon, LockIcon, ChevronIcon, + RefreshIcon, + ArrowRightIcon, } diff --git a/admin-ui/app/components/index.tsx b/admin-ui/app/components/index.tsx index 0eb21748a..b182e19e0 100755 --- a/admin-ui/app/components/index.tsx +++ b/admin-ui/app/components/index.tsx @@ -14,7 +14,7 @@ import { ThemeProvider } from './Theme' import { GluuDropdown } from './GluuDropdown' import { GluuPageContent } from './GluuPageContent' import { GluuDynamicList } from './GluuDynamicList' -import { ChevronIcon } from './SVG' +import { ChevronIcon, ArrowRightIcon } from './SVG' import Wizard from './Wizard' import WizardStep from './Wizard/WizardStep' import ApiKey from './LicenseScreens/ApiKey' @@ -55,6 +55,7 @@ export { GluuDropdown, GluuPageContent, ChevronIcon, + ArrowRightIcon, Wizard, WizardStep, ApiKey, diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts index 966f920e8..b2796c219 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/hooks/useLangPersistence.ts b/admin-ui/app/hooks/useLangPersistence.ts new file mode 100644 index 000000000..45900bad8 --- /dev/null +++ b/admin-ui/app/hooks/useLangPersistence.ts @@ -0,0 +1,75 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import type { i18n as I18n } from 'i18next' +import { ensureLocaleLoaded } from '@/i18n' +import { logger } from '@/utils/logger' +import { storage } from '@/utils/storage' +import { STORAGE_KEYS, DEFAULT_LANG } from '@/constants' +import { safeParseUserConfig } from '@/utils/userConfig' + +const getInitialLang = (inum?: string): string => { + const initLang = storage.get(STORAGE_KEYS.INIT_LANG) || DEFAULT_LANG + const config = safeParseUserConfig() + return config.lang?.[inum || ''] || initLang +} + +const applyLanguage = (code: string, i18nInstance: I18n, latestRef: { current: string }) => { + void ensureLocaleLoaded(code) + .then(() => { + if (latestRef.current !== code) return undefined + return 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) + const latestLangRef = useRef(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) { + latestLangRef.current = userLang + setLang(userLang) + applyLanguage(userLang, i18n, latestLangRef) + } + + hasInitializedRef.current = true + }, [i18n, inum]) + + const changeLanguage = useCallback( + (code: string) => { + latestLangRef.current = code + setLang(code) + applyLanguage(code, i18n, latestLangRef) + + 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 new file mode 100644 index 000000000..a9f7eb534 --- /dev/null +++ b/admin-ui/app/hooks/useThemePersistence.ts @@ -0,0 +1,36 @@ +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 { safeParseUserConfig } from '@/utils/userConfig' +import type { UserInfo } from 'Redux/features/types/authTypes' + +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) { + const existingConfig = safeParseUserConfig() + 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/locales/en/translation.json b/admin-ui/app/locales/en/translation.json index 626c04d8d..8dd6c079f 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", @@ -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 2044f6a25..6b5cc800b 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 0feb7ca1d..a9ed0e91a 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 607f45da3..fb4e422ec 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 26e5d4942..38ac5aa65 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/LanguageMenu.tsx b/admin-ui/app/routes/Apps/Gluu/LanguageMenu.tsx index 4f3dd1f1b..83a2e5e95 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/ThemeDropdown.tsx b/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx index f153c586c..1d6b346ec 100644 --- a/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx +++ b/admin-ui/app/routes/Apps/Gluu/ThemeDropdown.tsx @@ -1,75 +1,22 @@ -import { useMemo, useCallback, memo } from 'react' +import { 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 { useTheme } from '@/context/theme/themeContext' -import { THEME_LIGHT, THEME_DARK, isValidTheme, type ThemeValue } from '@/context/theme/constants' -import { logger } from '@/utils/logger' -import { storage } from '@/utils/storage' -import { STORAGE_KEYS } from '@/constants' +import { THEME_LIGHT, THEME_DARK } from '@/context/theme/constants' +import { useThemePersistence } from '@/hooks/useThemePersistence' import { useStyles } from './styles/ThemeDropdown.style' import type { ThemeDropdownComponentProps } from './types' export const ThemeDropdownComponent = memo(({ 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/Apps/Gluu/hooks/useNavbarTheme.ts b/admin-ui/app/routes/Apps/Gluu/hooks/useNavbarTheme.ts index e9dd60c5b..9a9173c45 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 81457254b..e0fb019d0 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,8 @@ 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)' interface NavbarColors { background: string @@ -21,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', }, }, @@ -50,18 +53,31 @@ 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', + minWidth: 0, }, leftSection: { display: 'flex', alignItems: 'center', gap: '20px', height: '100%', + flexShrink: 0, }, rightSection: { display: 'flex', @@ -69,6 +85,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 +163,39 @@ const useStyles = makeStyles<{ navbarColors: NavbarColors }>()((theme, { navbarC display: 'none', }, }, + mobileProfileTrigger: { + 'display': 'flex', + 'alignItems': 'center', + 'gap': '7px', + 'cursor': 'pointer', + 'height': '100%', + 'flexShrink': 0, + 'minWidth': 0, + '@media (max-width:399px)': { + flexShrink: 1, + }, + }, + mobileGreeting: { + fontFamily, + 'fontSize': fontSizes.sm, + 'fontWeight': fontWeights.semiBold, + 'color': navbarColors.text, + 'letterSpacing': '0.24px', + 'lineHeight': 'normal', + 'display': 'block', + 'whiteSpace': 'nowrap', + 'overflow': 'hidden', + 'textOverflow': 'ellipsis', + 'minWidth': 0, + 'margin': 0, + 'padding': 0, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + maxWidth: '120px', + }, + '@media (max-width:399px)': { + maxWidth: '100px', + }, + }, userIcon: { flexShrink: 0, }, diff --git a/admin-ui/app/routes/Apps/Gluu/types/common.ts b/admin-ui/app/routes/Apps/Gluu/types/common.ts index fa795b076..adb8b8156 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/Dashboards/DashboardPage.style.ts b/admin-ui/app/routes/Dashboards/DashboardPage.style.ts index 17156c9bc..4e0c3daa0 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, SPACING } from '@/constants' import { makeStyles } from 'tss-react/mui' import type { Theme } from '@mui/material/styles' import { fontFamily, fontWeights, fontSizes, lineHeights } from '@/styles/fonts' @@ -29,6 +29,39 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea }) return { + mobileContentPad: { + [`@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')]: { + paddingLeft: `${MOBILE_PAGE_PADDING_X.SM}px`, + paddingRight: `${MOBILE_PAGE_PADDING_X.SM}px`, + }, + }, + mobilePageTitle: { + fontFamily, + fontSize: '28px', + fontStyle: 'normal', + fontWeight: fontWeights.bold, + lineHeight: 'normal', + color: isDark ? customColors.white : themeColors.text, + margin: 0, + marginBottom: SPACING.PAGE, + }, + 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 +79,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 +97,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 +122,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 +210,10 @@ const useStyles = makeStyles<{ themeColors: DashboardThemeColors; isDark: boolea padding: '16px', height: 'auto', }, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + padding: '28px', + height: 'auto', + }, }, userInfoTitle: { fontFamily, @@ -167,6 +226,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 +251,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 +272,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 +284,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 +349,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 +365,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 +420,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 +483,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 +500,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 +558,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 +595,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 2587bc93c..6782dc953 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 000000000..4d4e6c9f9 --- /dev/null +++ b/admin-ui/app/routes/components/Dropdowns/MobileProfileDropdown.tsx @@ -0,0 +1,242 @@ +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, 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 { useThemePersistence } from '@/hooks/useThemePersistence' +import { useLangPersistence } from '@/hooks/useLangPersistence' +import { LANG_CODES, DEFAULT_LANG } from '@/constants' +import { useStyles } from './styles/MobileProfileDropdown.style' +import type { MobileProfileDropdownProps } from './types' + +const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdownProps) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { navigateToRoute } = useAppNavigation() + const { logoutAuditSucceeded } = useAppSelector((state) => state.logoutAuditReducer) + + const { state: themeState } = useTheme() + const currentTheme = themeState.theme + const { classes } = useStyles({ theme: currentTheme }) + + const inum = userInfo?.inum + + const [isOpen, setIsOpen] = useState(false) + const [rendered, setRendered] = useState(false) + const [entered, setEntered] = useState(false) + const { lang, changeLanguage } = useLangPersistence(inum) + const containerRef = useRef(null) + + 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) + } + }, [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) + } + } + + 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]) + + const onChangeTheme = useThemePersistence(userInfo) + + 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') + 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)} + + + {rendered && ( + +
{ + 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 000000000..500d69c2f --- /dev/null +++ b/admin-ui/app/routes/components/Dropdowns/styles/MobileProfileDropdown.style.ts @@ -0,0 +1,162 @@ +import { makeStyles } from 'tss-react/mui' +import { fontFamily, fontWeights } from '@/styles/fonts' +import customColors, { hexToRgb } from '@/customColors' +import getThemeColor from '@/context/theme/config' +import { THEME_DARK, type ThemeValue } from '@/context/theme/constants' + +type StyleParams = { + theme: ThemeValue +} + +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 + +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: tc.menu.background, + border: `1.5px solid ${tc.borderColor}`, + borderRadius: 10, + 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', + alignItems: 'center', + justifyContent: 'space-between', + minHeight: 27, + }, + rowLabel: { + fontFamily, + fontSize: '13px', + fontWeight: fontWeights.semiBold, + lineHeight: 'normal', + letterSpacing: '0.22px', + color: tc.menu.color, + margin: 0, + whiteSpace: 'nowrap', + }, + profileRow: { + cursor: 'pointer', + }, + arrowButton: { + width: 20, + height: 21, + borderRadius: '50%', + backgroundColor: chipBg, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + color: tc.menu.color, + }, + arrowIcon: { + width: 10, + height: 10, + display: 'block', + }, + divider: { + height: 0, + border: 'none', + borderTop: `1px solid ${tc.borderColor}`, + margin: '12px 0', + width: '100%', + }, + control: { + minWidth: 74, + height: 27, + boxSizing: 'border-box', + border: `1px solid ${tc.settings.inputBorder}`, + 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: controlTextColor, + whiteSpace: 'nowrap', + }, + controlIcon: { + 'width': 8, + 'height': 8, + 'flexShrink': 0, + 'display': 'flex', + 'alignItems': 'center', + 'justifyContent': 'center', + 'color': controlTextColor, + '& svg': { + width: '100%', + height: '100%', + }, + }, + signOut: { + marginTop: 12, + height: 32, + borderRadius: 3, + backgroundColor: chipBg, + 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: tc.menu.color, + whiteSpace: 'nowrap', + }, + compactMenu: { + 'minWidth': '96px !important', + 'width': 'auto', + '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, + }, + '& > div': { + padding: '6px', + }, + }, + } +}) diff --git a/admin-ui/app/routes/components/Dropdowns/types.ts b/admin-ui/app/routes/components/Dropdowns/types.ts index d9a7032be..714dfe45d 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 1367c2cf4..11b5a983c 100755 --- a/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx +++ b/admin-ui/app/routes/components/LogoThemed/LogoThemed.tsx @@ -4,7 +4,11 @@ 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'> +type LogoVariant = 'green' | 'white' + +type LogoThemedProps = Omit, 'src' | 'alt' | 'style'> & { + variant?: LogoVariant +} const LOGO_FILTERS = { [THEME_DARK]: 'brightness(0) invert(1)', @@ -12,7 +16,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 +38,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 e36887dac..51951ecdb 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/app/utils/types/TokenControllerTypes.ts b/admin-ui/app/utils/types/TokenControllerTypes.ts index 462c3d3ad..3f92d10e9 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 000000000..595c4cd5a --- /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 dbf6daa33..0da8483f0 100644 --- a/admin-ui/plugins/admin/components/Health/HealthPage.style.ts +++ b/admin-ui/plugins/admin/components/Health/HealthPage.style.ts @@ -1,9 +1,12 @@ 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' +const MOBILE_MEDIA_QUERY = '(max-width:767px)' +const NARROW_MEDIA_QUERY = '(max-width:350px)' + interface HealthPageThemeColors { cardBg: string navbarBorder: string @@ -23,6 +26,32 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole }) return { + mobileContentPad: { + [`@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')]: { + paddingLeft: `${MOBILE_PAGE_PADDING_X.SM}px`, + paddingRight: `${MOBILE_PAGE_PADDING_X.SM}px`, + }, + }, + mobilePageTitle: { + display: 'none', + [`@media ${MOBILE_MEDIA_QUERY}`]: { + display: 'block', + fontFamily, + fontSize: '28px', + fontStyle: 'normal', + fontWeight: fontWeights.bold, + lineHeight: 'normal', + color: themeColors.text, + margin: 0, + marginBottom: SPACING.PAGE, + }, + }, 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,28 @@ 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, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + [`@media ${NARROW_MEDIA_QUERY}`]: { + fontSize: fontSizes.base, + }, }, headerDivider: { position: 'absolute', @@ -69,6 +115,13 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole top: '50%', transform: 'translateY(-50%)', zIndex: 10, + [`@media ${MOBILE_MEDIA_QUERY}`]: { + position: 'static', + transform: 'none', + flexShrink: 0, + marginLeft: 'auto', + marginTop: `${-(32 - 22) / 2}px`, + }, }, refreshButton: { borderRadius: 4, @@ -84,6 +137,21 @@ const useStyles = makeStyles<{ themeColors: HealthPageThemeColors; isDark: boole fontSize: fontSizes.base, fontWeight: fontWeights.medium, lineHeight: lineHeights.relaxed, + [`@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', + '& svg, & .MuiSvgIcon-root': { + fontSize: '22px !important', + }, + }, }, messageBlock: { padding: `${SPACING.CONTENT_PADDING}px`, @@ -121,6 +189,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 4a4843973..d2b46060e 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 9c27a4286..4f9818c01 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,85 @@ import { makeStyles } from 'tss-react/mui' -import customColors from '@/customColors' -import { SPACING } from '@/constants' -import { fontFamily, fontWeights, fontSizes } from '@/styles/fonts' +import customColors, { hexToRgb } from '@/customColors' +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: { +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 +const BADGE_TOP_OFFSET = 2.39 +const TEXT_BADGE_MIN_GAP = 8 +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 = { 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', + gap: 0, + }, + serviceName: { + ...cardText, + fontWeight: fontWeights.bold, + fontSize: fontSizes.md, + }, + serviceMessage: { + ...cardText, + fontWeight: fontWeights.medium, + fontSize: '13px', + }, + statusBadge: { + flexShrink: 0, + alignSelf: 'flex-start', + marginTop: `${BADGE_TOP_OFFSET}px`, + }, + 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 edac3afbe..fdab52282 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
{ 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 29f09c793..b3e8d8e51 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 00fcaf73c..8f0c31496 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 1e3cbba30..d7a4b1447 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 c8781f535..d6d6dcc1e 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 862fb111a..4f8290023 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 8dd659bfa..97212401c 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 9e2d878a9..d65c97277 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 bf1e016cd..15e793fbb 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 8dbe685cb..2997f5be1 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 349a61af6..5a739446d 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 9151d374c..0c686f99b 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/'),