Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions admin-ui/app/components/GluuDropdown/GluuDropdown.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const getPositionStyles = (position: DropdownPosition) => {
marginLeft: SHARED_DROPDOWN_STYLES.margin,
...baseTransform,
}
// Right-aligned to the trigger instead of centred on it, so a menu wider than
// its trigger grows inward. Centring overflows the viewport for a trigger that
// sits at the right edge, which scrolls the page horizontally.
case 'bottom-end':
return {
top: '100%',
marginTop: SHARED_DROPDOWN_STYLES.margin,
right: 0,
}
default:
return {}
}
Expand All @@ -54,6 +63,16 @@ const getArrowStyles = (position: DropdownPosition) => {
left: '50%',
transform: 'translateX(-50%)',
}
// Rendered against the wrapper rather than the menu (see GluuDropdown.tsx), so it
// centres on the trigger. 100% is the trigger's bottom edge; the menu starts 13px
// below that and the arrow sits 15px above the menu's top, hence the 2px back up.
case 'bottom-end':
return {
top: 'calc(100% - 2px)',
left: '50%',
transform: 'translateX(-50%)',
zIndex: SHARED_DROPDOWN_STYLES.menuZIndex + 1,
}
case 'left':
return {
right: '-15px',
Expand All @@ -76,7 +95,8 @@ export const useStyles = makeStyles<{
position: DropdownPosition
dropdownBg: string
centerText?: boolean
}>()((_theme, { isDark, position, dropdownBg, centerText }) => ({
optionPadding?: string
}>()((_theme, { isDark, position, dropdownBg, centerText, optionPadding }) => ({
dropdownWrapper: {
position: 'relative',
display: 'inline-block',
Expand All @@ -93,7 +113,12 @@ export const useStyles = makeStyles<{
maxHeight: SHARED_DROPDOWN_STYLES.maxHeight,
overflow: 'visible',
...getPositionStyles(position),
marginTop: position === 'bottom' ? '13px' : position === 'top' ? undefined : '4px',
marginTop:
position === 'bottom' || position === 'bottom-end'
? '13px'
: position === 'top'
? undefined
: '4px',
},
dropdownMenuContent: {
padding: SHARED_DROPDOWN_STYLES.padding,
Expand Down Expand Up @@ -140,6 +165,7 @@ export const useStyles = makeStyles<{
...createBaseOptionStyles({
isDark,
...(centerText && { optionPadding: '12px 12px' }),
...(optionPadding && { optionPadding }),
}),
'&.single-option': {
justifyContent: 'center',
Expand Down
17 changes: 14 additions & 3 deletions admin-ui/app/components/GluuDropdown/GluuDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const GluuDropdown = <T extends DropdownValue = DropdownValue>({
renderOption,
renderTrigger,
centerText = false,
optionPadding,
}: GluuDropdownProps<T>): React.ReactElement => {
const [internalState, setInternalState] = useState<DropdownState>({
isOpen: false,
Expand All @@ -66,9 +67,14 @@ export const GluuDropdown = <T extends DropdownValue = DropdownValue>({
const dropdownBg = useMemo(() => {
return isDark ? customColors.darkDropdownBg : customColors.white
}, [isDark])
const { classes } = useStyles({ isDark, position, dropdownBg, centerText })
const { classes } = useStyles({ isDark, position, dropdownBg, centerText, optionPadding })

const isOpen = controlled ? (controlledIsOpen ?? false) : internalState.isOpen
// A 'bottom-end' menu is right-aligned to the trigger, so an arrow positioned against
// the menu can't find the trigger's centre — the menu is wider than the trigger by an
// amount that varies with the options. Rendering it against the wrapper instead, which
// is inline-block around the trigger alone, centres it with plain CSS.
const arrowAnchoredToTrigger = position === 'bottom-end'
const searchQuery = internalState.searchQuery

const setIsOpen = useCallback(
Expand Down Expand Up @@ -152,7 +158,7 @@ export const GluuDropdown = <T extends DropdownValue = DropdownValue>({
option.onClick?.(option.value, option)
onSelect?.(option.value, option)

if (closeOnSelect) {
if (closeOnSelect && !option.keepOpen) {
setIsOpen(false)
setInternalState((prev) => ({ ...prev, searchQuery: '' }))
}
Expand Down Expand Up @@ -269,7 +275,7 @@ export const GluuDropdown = <T extends DropdownValue = DropdownValue>({
role="listbox"
id={listboxId}
>
{showArrow && (
{showArrow && !arrowAnchoredToTrigger && (
<div className={classes.arrow}>
<ArrowIcon />
</div>
Expand All @@ -293,6 +299,11 @@ export const GluuDropdown = <T extends DropdownValue = DropdownValue>({
</div>
</Box>
)}
{isOpen && showArrow && arrowAnchoredToTrigger && (
<div className={classes.arrow}>
<ArrowIcon />
</div>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type React from 'react'

export type DropdownPosition = 'top' | 'bottom' | 'left' | 'right'
export type DropdownPosition = 'top' | 'bottom' | 'bottom-end' | 'left' | 'right'

export type DropdownValue = string | number | boolean

Expand All @@ -13,6 +13,8 @@ export type GluuDropdownOption<T extends DropdownValue = DropdownValue> = {
icon?: React.ReactNode
metadata?: Record<string, string | number | boolean | null | undefined>
searchValue?: string
/** Keeps the menu open after this option is clicked, even when closeOnSelect is set. */
keepOpen?: boolean
}

export type GluuDropdownProps<T extends DropdownValue = DropdownValue> = {
Expand Down Expand Up @@ -43,6 +45,8 @@ export type GluuDropdownProps<T extends DropdownValue = DropdownValue> = {
selectedOption?: GluuDropdownOption<T> | GluuDropdownOption<T>[],
) => React.ReactNode
centerText?: boolean
/** Overrides the default option padding, which reserves 48px on the right for a checkmark slot. */
optionPadding?: string
}

export type DropdownState = {
Expand Down
1 change: 1 addition & 0 deletions admin-ui/app/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@
"platform": "Platform",
"authType": "Auth Type",
"showCedarLogs?": "Cedarling Log enabled?",
"cedarlingLogs?": "Cedarling logs?",
"reloginToViewCedarlingChanges": "Please Re-login to view the cedarling changes.",
"allAvailableHintsSelected": "All available hint options are selected",
"noMatchingOptions": "No matching options",
Expand Down
1 change: 1 addition & 0 deletions admin-ui/app/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@
"platform": "Plataforma",
"authType": "Tipo de Autenticación",
"showCedarLogs?": "¿Registro de Cedarling habilitado?",
"cedarlingLogs?": "¿Registros de Cedarling?",
"reloginToViewCedarlingChanges": "Por favor, vuelve a iniciar sesión para ver los cambios de Cedarling.",
"allAvailableHintsSelected": "Todas las opciones de sugerencia disponibles están seleccionadas",
"noMatchingOptions": "No hay opciones coincidentes",
Expand Down
1 change: 1 addition & 0 deletions admin-ui/app/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,7 @@
"platform": "Plateforme",
"authType": "Type d'authentification",
"showCedarLogs?": "Cedarling Log est-il activé?",
"cedarlingLogs?": "Journaux Cedarling ?",
"reloginToViewCedarlingChanges": "Veuillez vous reconnecter pour voir les modifications de cedarling.",
"allAvailableHintsSelected": "Toutes les options d'indice disponibles sont sélectionnées.",
"noMatchingOptions": "Aucune option correspondante",
Expand Down
1 change: 1 addition & 0 deletions admin-ui/app/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@
"platform": "Plataforma",
"authType": "Tipo de autenticação",
"showCedarLogs?": "O Cedarling Log está habilitado?",
"cedarlingLogs?": "Logs do Cedarling?",
"reloginToViewCedarlingChanges": "Por favor, faça login novamente para ver as alterações do cedarling.",
"allAvailableHintsSelected": "Todas as opções de dica disponíveis estão selecionadas",
"noMatchingOptions": "Nenhuma opção correspondente",
Expand Down
2 changes: 1 addition & 1 deletion admin-ui/app/routes/Apps/Gluu/GluuNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const GluuNavBar = () => {
</Box>
</Box>
)}
position="bottom"
position="bottom-end"
/>
)}
</Box>
Expand Down
4 changes: 4 additions & 0 deletions admin-ui/app/routes/Apps/Gluu/__tests__/GluuNavBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const createTestStore = (userinfo: UserInfo | null): Store =>
}),
})

jest.mock('@/utils/hooks/useCedarlingLogToggle', () => ({
useCedarlingLogToggle: () => ({ enabled: false, toggle: jest.fn(), isSaving: false }),
}))

const renderNavBar = (userinfo: UserInfo | null) => {
const store = createTestStore(userinfo)
const Wrapper = ({ children }: { children: ReactNode }) => (
Expand Down
36 changes: 35 additions & 1 deletion admin-ui/app/routes/components/Dropdowns/DropdownProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { useAppNavigation, ROUTES } from '@/helpers/navigation'
import { auditLogoutLogs } from 'Redux/features/sessionSlice'
import { MANUAL_LOGOUT } from '@/audit/messages'
import { GluuDropdown, type GluuDropdownOption } from 'Components'
import Box from '@mui/material/Box'
import Switch from '@mui/material/Switch'
import { useCedarlingLogToggle } from '@/utils/hooks/useCedarlingLogToggle'
import type { DropdownProfileProps } from './types'

const DropdownProfile = ({ trigger, renderTrigger, position = 'bottom' }: DropdownProfileProps) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { navigateToRoute } = useAppNavigation()
const { enabled: cedarLogsEnabled, toggle: toggleCedarLogs } = useCedarlingLogToggle()

const handleLogout = useCallback(() => {
dispatch(auditLogoutLogs({ message: MANUAL_LOGOUT }))
Expand All @@ -25,6 +29,35 @@ const DropdownProfile = ({ trigger, renderTrigger, position = 'bottom' }: Dropdo
navigateToRoute(ROUTES.PROFILE)
},
},
{
value: 'cedarLogs',
label: (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flex: 1,
gap: 2,
whiteSpace: 'nowrap',
}}
>
{t('fields.cedarlingLogs?')}
<Switch
size="small"
checked={cedarLogsEnabled}
slotProps={{ input: { 'aria-label': t('fields.cedarlingLogs?') } }}
/>
</Box>
),
searchValue: t('fields.cedarlingLogs?'),
// The menu stays open so the switch's new position and the resulting toast are
// both visible without reopening the dropdown.
keepOpen: true,
onClick: () => {
toggleCedarLogs()
},
},
{
value: 'logout',
label: t('menus.signout'),
Expand All @@ -33,7 +66,7 @@ const DropdownProfile = ({ trigger, renderTrigger, position = 'bottom' }: Dropdo
},
},
],
[t, navigateToRoute, handleLogout],
[t, navigateToRoute, handleLogout, cedarLogsEnabled, toggleCedarLogs],
)

return (
Expand All @@ -43,6 +76,7 @@ const DropdownProfile = ({ trigger, renderTrigger, position = 'bottom' }: Dropdo
options={options}
position={position}
minWidth={182}
optionPadding="12px"
showArrow={true}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ 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 Switch from '@mui/material/Switch'
import { useCedarlingLogToggle } from '@/utils/hooks/useCedarlingLogToggle'
import { useStyles } from './styles/MobileProfileDropdown.style'
import type { MobileProfileDropdownProps } from './types'

Expand Down Expand Up @@ -89,6 +91,8 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow

const onChangeTheme = useThemePersistence(userInfo)

const { enabled: cedarLogsEnabled, toggle: toggleCedarLogs } = useCedarlingLogToggle()

const handleProfile = useCallback(() => {
setIsOpen(false)
navigateToRoute(ROUTES.PROFILE)
Expand Down Expand Up @@ -219,6 +223,20 @@ const MobileProfileDropdown = ({ userInfo, renderTrigger }: MobileProfileDropdow
/>
</div>

<hr className={classes.divider} />

<div className={classes.row}>
<GluuText variant="span" className={classes.rowLabel}>
{t('fields.cedarlingLogs?')}
</GluuText>
<Switch
size="small"
checked={cedarLogsEnabled}
onChange={toggleCedarLogs}
slotProps={{ input: { 'aria-label': t('fields.cedarlingLogs?') } }}
/>
</div>

<button type="button" className={classes.signOut} onClick={handleLogout}>
<GluuText variant="span" className={classes.signOutText}>
{t('menus.signout')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import { DropdownProfile } from '../DropdownProfile'
import sessionReducer, { auditLogoutLogs } from 'Redux/features/sessionSlice'
import { ROUTES } from '@/helpers/navigation'

const mockToggleCedarLogs = jest.fn()

jest.mock('@/utils/hooks/useCedarlingLogToggle', () => ({
useCedarlingLogToggle: () => ({
enabled: false,
toggle: mockToggleCedarLogs,
isSaving: false,
}),
}))
Comment thread
faisalsiddique4400 marked this conversation as resolved.

const mockNavigateToRoute = jest.fn()

jest.mock('@/helpers/navigation', () => ({
Expand Down
Loading
Loading