diff --git a/admin-ui/app/components/GluuTable/GluuTable.tsx b/admin-ui/app/components/GluuTable/GluuTable.tsx index 2bd19ca590..7fe6f783d0 100644 --- a/admin-ui/app/components/GluuTable/GluuTable.tsx +++ b/admin-ui/app/components/GluuTable/GluuTable.tsx @@ -9,7 +9,7 @@ import GluuText from '@/routes/Apps/Gluu/GluuText' import { GluuButton } from '@/components/GluuButton' import { GluuSpinner } from '@/components/GluuSpinner' import { useStyles, TABLE_MIN_WIDTH, TABLE_RESPONSIVE_BREAKPOINT } from './GluuTable.style' -import { T_KEYS } from './constants' +import { T_KEYS, EMPTY_CELL_PLACEHOLDER } from './constants' import type { CellValue, ColumnKey, GluuTableProps, SortDirection } from './types' import { ChevronIcon } from '@/components/SVG' import { @@ -652,7 +652,9 @@ const GluuTable = (props: Readonly>) => { }) ) : ( - {String(value ?? '')} + {value === null || value === undefined || value === '' + ? EMPTY_CELL_PLACEHOLDER + : String(value)} )} diff --git a/admin-ui/app/components/GluuTable/constants.ts b/admin-ui/app/components/GluuTable/constants.ts index 7ab32a6d18..fc48bc5b24 100644 --- a/admin-ui/app/components/GluuTable/constants.ts +++ b/admin-ui/app/components/GluuTable/constants.ts @@ -4,6 +4,8 @@ export const COLUMN_WIDTHS = { PILL_LIST: 280, } as const +export const EMPTY_CELL_PLACEHOLDER = '—' + export const T_KEYS = { FIELDS_ACTIONS: 'fields.actions', FIELDS_OF: 'fields.of', diff --git a/admin-ui/app/components/Layout/Layout.tsx b/admin-ui/app/components/Layout/Layout.tsx index ce44af3d57..3a9ff4d068 100755 --- a/admin-ui/app/components/Layout/Layout.tsx +++ b/admin-ui/app/components/Layout/Layout.tsx @@ -13,7 +13,6 @@ import { LayoutNavbar } from './LayoutNavbar' import { LayoutSidebar } from './LayoutSidebar' import { PageConfigContext } from './PageConfigContext' import { ThemeClass } from './../Theme' -import { siteDescription, siteKeywords } from '@/config/site' import type { LayoutPartComponentType, LayoutProps, @@ -89,8 +88,8 @@ const initialLayoutState: LayoutState = { screenSize: (typeof window !== 'undefined' ? getScreenSize() : '') as ScreenSize, animationsDisabled: true, pageTitle: null, - pageDescription: siteDescription, - pageKeywords: siteKeywords, + pageDescription: 'Jans-server admin Ui', + pageKeywords: 'jans-server oauth jans gluu', } const Layout: React.FC = (props) => { diff --git a/admin-ui/app/components/icons/index.ts b/admin-ui/app/components/icons/index.ts index 533d06dc00..56b0f5add2 100644 --- a/admin-ui/app/components/icons/index.ts +++ b/admin-ui/app/components/icons/index.ts @@ -1,6 +1,5 @@ export { default as AccessTimeIcon } from '@mui/icons-material/AccessTime' export { default as Add } from '@mui/icons-material/Add' -export { default as AddCircleOutline } from '@mui/icons-material/AddCircleOutlineOutlined' export { default as CachedIcon } from '@mui/icons-material/Cached' export { default as Check } from '@mui/icons-material/Check' export { default as CheckCircleOutline } from '@mui/icons-material/CheckCircleOutlineOutlined' diff --git a/admin-ui/app/config/site.ts b/admin-ui/app/config/site.ts deleted file mode 100644 index 895ac3bd4e..0000000000 --- a/admin-ui/app/config/site.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const siteDescription = 'Jans-server admin Ui' -export const siteKeywords = 'jans-server oauth jans gluu' diff --git a/admin-ui/app/constants/ui.ts b/admin-ui/app/constants/ui.ts index 5ffb0c026e..24b39bfdff 100644 --- a/admin-ui/app/constants/ui.ts +++ b/admin-ui/app/constants/ui.ts @@ -120,6 +120,24 @@ export const ICON_SIZE = { export const ICON_BUTTON_SIZE = 32 +export const TOGGLE = { + TRACK_WIDTH: 50, + TRACK_HEIGHT: 24, + TRACK_RADIUS: 30, + THUMB_SIZE: 22, + THUMB_INSET: 1, + THUMB_CHECKED_LEFT: 27, + ICON_HEIGHT: 10, + CHECK_WIDTH: 14, + CHECK_LEFT: 8, + X_WIDTH: 10, + X_RIGHT: 10, + FOCUS_RING_BLUR: 2, + FOCUS_RING_SPREAD: 3, + ACTIVE_RING_BLUR: 5, + ACTIVE_RING_SPREAD: 5, +} as const + export const TOOLTIP = { ARROW_SIZE: 8, PADDING_VERTICAL: 8, diff --git a/admin-ui/app/layout/components/DefaultSidebar.tsx b/admin-ui/app/layout/components/DefaultSidebar.tsx index 922250de6c..dbb8fc745a 100755 --- a/admin-ui/app/layout/components/DefaultSidebar.tsx +++ b/admin-ui/app/layout/components/DefaultSidebar.tsx @@ -3,13 +3,13 @@ import { Link } from 'react-router-dom' import { useAppSelector } from '@/redux/hooks' import { Sidebar } from 'Components' import { LogoThemed } from 'Routes/components/LogoThemed/LogoThemed' -import GluuSuspenseLoader from 'Routes/Apps/Gluu/GluuSuspenseLoader' import GluuText from '@/routes/Apps/Gluu/GluuText' import GluuAppSidebar from 'Routes/Apps/Gluu/GluuAppSidebar' import type { DefaultSidebarProps } from './types' import { useTranslation } from 'react-i18next' import { ROUTES } from '@/helpers/navigation' import { useStyles } from './DefaultSidebar.style' +import { GluuSpinner } from '@/components/GluuSpinner' const DefaultSidebar: React.FC = () => { const { t } = useTranslation() @@ -26,7 +26,7 @@ const DefaultSidebar: React.FC = () => { ) : (
- +
) diff --git a/admin-ui/app/layout/default.tsx b/admin-ui/app/layout/default.tsx index bd4e8a00c6..11684304f1 100755 --- a/admin-ui/app/layout/default.tsx +++ b/admin-ui/app/layout/default.tsx @@ -7,7 +7,6 @@ import { STORAGE_KEYS } from '@/constants' import 'Styles/bootstrap.scss' import 'Styles/main.scss' -import 'Styles/plugins/plugins.css' import 'Styles/plugins/plugins.scss' import { RoutedNavbars, RoutedSidebars } from '../routes' diff --git a/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx b/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx index 239975bf8b..c02d0009fa 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuAutocomplete.tsx @@ -50,7 +50,7 @@ const GluuAutocomplete = ({ const { t } = useTranslation() const { state: themeState } = useTheme() const selectedTheme = themeState?.theme ?? DEFAULT_THEME - const themeColors = getThemeColor(selectedTheme) + const themeColors = React.useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) const { classes } = useStyles({ themeColors, allowCustom, @@ -63,7 +63,7 @@ const GluuAutocomplete = ({ const resolvedHelperText = helperText ?? t('messages.multi_select_hint') const optionValues = React.useMemo( - () => options.map((o) => (typeof o === 'string' ? o : o.value)), + () => [...new Set(options.map((o) => (typeof o === 'string' ? o : o.value)))], [options], ) const labelByValue = React.useMemo(() => { @@ -81,13 +81,62 @@ const GluuAutocomplete = ({ [labelByValue], ) - const selectedItems = Array.isArray(value) - ? value.filter((v): v is string => typeof v === 'string') - : [] + const selectedItems = React.useMemo( + () => + Array.isArray(value) + ? [...new Set(value.filter((v): v is string => typeof v === 'string'))] + : [], + [value], + ) const [inputValue, setInputValue] = React.useState('') const lockedPlacementRef = React.useRef(null) + const autocompleteOptions = React.useMemo( + () => + allowCustom + ? [...optionValues, ...selectedItems.filter((s) => !optionValues.includes(s))] + : optionValues, + [allowCustom, optionValues, selectedItems], + ) + + const popperSlotProps = React.useMemo( + () => ({ + paper: { className: classes.dropdownPaper }, + popper: { + placement: 'bottom-start' as const, + className: classes.popperRoot, + modifiers: [ + { + name: 'computeStyles', + options: { adaptive: false, gpuAcceleration: false }, + }, + { + name: 'sameWidth', + enabled: true, + phase: 'afterWrite' as const, + fn: sameWidthModifier, + }, + { + name: 'lockPlacement', + enabled: true, + phase: 'main' as const, + requires: ['flip'], + fn: ({ state }: ModifierArguments) => { + if (lockedPlacementRef.current == null) { + lockedPlacementRef.current = state.placement + } else if (state.placement !== lockedPlacementRef.current) { + state.placement = lockedPlacementRef.current + state.reset = true + } + }, + }, + ], + }, + }), + [classes.dropdownPaper, classes.popperRoot], + ) + const handleChange = useCallback( (_event: React.SyntheticEvent, newValue: string[]) => { const normalized = newValue @@ -161,11 +210,7 @@ const GluuAutocomplete = ({ onSearch?.(val) } }} - options={ - allowCustom - ? [...optionValues, ...selectedItems.filter((s) => !optionValues.includes(s))] - : optionValues - } + options={autocompleteOptions} value={selectedItems} isOptionEqualToValue={(option, val) => option === val} onChange={handleChange} @@ -178,41 +223,7 @@ const GluuAutocomplete = ({ disableCloseOnSelect disablePortal className={classes.autocompleteRoot} - slotProps={{ - paper: { - className: classes.dropdownPaper, - }, - popper: { - placement: 'bottom-start' as const, - className: classes.popperRoot, - modifiers: [ - { - name: 'computeStyles', - options: { adaptive: false, gpuAcceleration: false }, - }, - { - name: 'sameWidth', - enabled: true, - phase: 'afterWrite' as const, - fn: sameWidthModifier, - }, - { - name: 'lockPlacement', - enabled: true, - phase: 'main' as const, - requires: ['flip'], - fn: ({ state }: ModifierArguments) => { - if (lockedPlacementRef.current == null) { - lockedPlacementRef.current = state.placement - } else if (state.placement !== lockedPlacementRef.current) { - state.placement = lockedPlacementRef.current - state.reset = true - } - }, - }, - ], - }, - }} + slotProps={popperSlotProps} forcePopupIcon popupIcon={} filterOptions={filterOptions} @@ -268,8 +279,8 @@ const GluuAutocomplete = ({ const trimmed = inputValue.trim() if ( trimmed && - !optionValues.some((o) => - getDisplayLabel(o).toLowerCase().includes(trimmed.toLowerCase()), + !optionValues.some( + (o) => getDisplayLabel(o).toLowerCase() === trimmed.toLowerCase(), ) && !selectedItems.some( (s) => getDisplayLabel(s).toLowerCase() === trimmed.toLowerCase(), diff --git a/admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx b/admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx deleted file mode 100644 index d3059fcbfb..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuBooleanSelectBox.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import GluuLabel from './GluuLabel' -import GluuToggle from './GluuToggle' -import { Col, FormGroup, CustomInput, InputGroup } from 'Components' -import { useTranslation } from 'react-i18next' -import { useMemo } from 'react' -import type { JsonValue } from './types/common' -import type { GluuBooleanSelectBoxProps } from './types' - -const GluuBooleanSelectBox = >({ - label, - name, - value, - formik, - handler, - lsize, - rsize, - doc_category, - disabled, - toToggle = true, -}: GluuBooleanSelectBoxProps): JSX.Element => { - const { t } = useTranslation() - const normalizedValue = useMemo(() => { - return typeof value === 'string' ? value === 'true' : value - }, [value]) - return ( - - - - {!toToggle && ( - - - - - - - )} - {toToggle && ( - - )} - - - ) -} - -export default GluuBooleanSelectBox diff --git a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx index 24710d1c95..cbbbd2c1c1 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuCommitDialog.tsx @@ -200,7 +200,9 @@ const GluuCommitDialog = ({ {operations.map((operation, index) => (
{t('set')} - {operation.path} + + {operation.label || operation.path} + {t('to')} {operation.value === null || operation.value === '' diff --git a/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx b/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx index 147952353d..93b1bc1962 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuInlineInput.tsx @@ -3,7 +3,8 @@ import { Check, Close } from '@/components/icons' import { OPACITY } from '@/constants' import GluuLabel from './GluuLabel' import GluuToggle from './GluuToggle' -import { Typeahead } from 'react-bootstrap-typeahead' +import GluuAutocomplete from './GluuAutocomplete' +import type { AutocompleteOption } from './types/GluuAutocomplete.types' import applicationStyle from '@/routes/Apps/Gluu/styles/applicationStyle' import { useStyles } from '@/routes/Apps/Gluu/styles/GluuInlineInput.style' import { Col, FormGroup, Input, Button } from 'Components' @@ -137,8 +138,11 @@ const GluuInlineInput = ({ [value], ) - const filteredOptions = useMemo( - () => (Array.isArray(options) ? options.filter((item) => item != null) : []), + const filteredOptions = useMemo( + () => + Array.isArray(options) + ? options.filter((item) => item != null).map((item) => ({ value: item, label: item })) + : [], [options], ) const resolvedId = id || name @@ -173,21 +177,19 @@ const GluuInlineInput = ({ data-testid={resolvedId} name={name} handler={onValueChanged} - value={value as boolean} + value={Boolean(data)} disabled={disabled} /> )} {isArray && ( - )} diff --git a/admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx b/admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx deleted file mode 100644 index a2aecdbbcb..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuPermissionModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { useTheme } from 'Context/theme/themeContext' -import getThemeColor from '@/context/theme/config' -import { DEFAULT_THEME, THEME_DARK } from '@/context/theme/constants' -import GluuText from './GluuText' -import { GluuButton } from '@/components' -import { GluuModalShell } from '@/components/GluuModalShell' -import { useStyles } from './styles/GluuPermissionModal.style' -import type { GluuPermissionModalProps } from './types/GluuPermissionModal.types' - -const GluuPermissionModal = ({ handler, isOpen }: GluuPermissionModalProps) => { - const { t } = useTranslation() - const { state: themeState } = useTheme() - const selectedTheme = themeState?.theme ?? DEFAULT_THEME - const isDark = selectedTheme === THEME_DARK - const themeColors = useMemo(() => getThemeColor(selectedTheme), [selectedTheme]) - const { classes } = useStyles({ isDark, themeColors }) - - if (!isOpen) return null - - return ( - - - {t('dashboard.access_denied')} - -
- - 🚫 {t('dashboard.access_denied_message')} - - {t('dashboard.access_contact_admin')} -
-
- - {t('menus.signout')} - -
-
- ) -} - -export default GluuPermissionModal diff --git a/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx b/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx index 19822be18e..a20bacfde0 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuRemovableInputRow.tsx @@ -6,6 +6,7 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useTheme } from '@/context/theme/themeContext' import getThemeColor from '@/context/theme/config' +import { THEME_DARK } from '@/context/theme/constants' import { useStyles } from './styles/GluuRemovableInputRow.style' import type { GluuRemovableInputRowProps } from './types' import { Close as CloseIcon } from '@/components/icons' @@ -29,7 +30,8 @@ const GluuRemovableInputRow = ({ const { t } = useTranslation() const { state: themeState } = useTheme() const themeColors = useMemo(() => getThemeColor(themeState.theme), [themeState.theme]) - const { classes } = useStyles({ fontColor: themeColors.fontColor }) + const isDark = themeState.theme === THEME_DARK + const { classes } = useStyles({ fontColor: themeColors.fontColor, isDark }) const currentValue = formik.values[name as keyof TValues] as string | boolean | undefined const isChecked = (formik.values[name as keyof TValues] as boolean | undefined) ?? false @@ -61,7 +63,7 @@ const GluuRemovableInputRow = ({ formik={formik} value={isChecked} handler={(e: React.ChangeEvent) => { - setModifiedFields({ + setModifiedFields?.({ ...modifiedFields, [name]: e.target.checked, }) @@ -92,7 +94,7 @@ const GluuRemovableInputRow = ({ className={classes.input} value={(currentValue as string) ?? (value as string) ?? ''} onChange={(e: React.ChangeEvent) => { - setModifiedFields({ + setModifiedFields?.({ ...modifiedFields, [name]: e.target.value, }) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuRemovableSelectRow.tsx b/admin-ui/app/routes/Apps/Gluu/GluuRemovableSelectRow.tsx index 7de774b582..3582578ef7 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuRemovableSelectRow.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuRemovableSelectRow.tsx @@ -45,7 +45,7 @@ const GluuRemovableSelectRow = ({ name={name} value={currentValue} onChange={(e: React.ChangeEvent) => { - setModifiedFields({ + setModifiedFields?.({ ...modifiedFields, [name]: e.target.value, }) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx deleted file mode 100644 index 4e803968e9..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuSecretDetail.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useState, memo, useCallback, CSSProperties, useMemo } from 'react' -import { FormGroup, Label, Col } from 'Components' -import Toggle from 'react-toggle' -import GluuTooltip from './GluuTooltip' -import { useTranslation } from 'react-i18next' -import customColors from '@/customColors' -import type { GluuSecretDetailProps } from './types' -import { useStyles } from './styles/GluuSecretDetail.style' - -const defaultLabelStyle: CSSProperties = { fontWeight: 'bold', color: customColors.black } - -const GluuSecretDetail = ({ - label, - value, - doc_category, - doc_entry, - lsize = 6, - rsize = 6, - labelStyle, - rowClassName, -}: GluuSecretDetailProps) => { - const { t } = useTranslation() - const [up, setUp] = useState(false) - - const handleSecret = useCallback(() => { - setUp((prev) => !prev) - }, []) - - const appliedLabelStyle: CSSProperties = useMemo( - () => ({ ...defaultLabelStyle, ...labelStyle }), - [labelStyle], - ) - const { classes } = useStyles() - const appliedRowClassName = rowClassName || classes.rowDefault - - const valueStyle: CSSProperties = useMemo( - () => ({ fontWeight: 'bold', color: customColors.black }), - [], - ) - - return ( - - - - - {value !== '-' && } - {(value === '-' || up) && ( - - {value} - - )} - - - - ) -} - -export default memo(GluuSecretDetail) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSelectRow.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSelectRow.tsx index 9ffaa11999..569afa3b81 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuSelectRow.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuSelectRow.tsx @@ -46,6 +46,7 @@ const GluuSelectRow: React.FC = ({ inputPaddingTop, inputPaddingBottom, hideChooseOption = false, + reserveErrorSpace = false, }) => { const { t } = useTranslation() const { state: themeState } = useTheme() @@ -160,11 +161,11 @@ const GluuSelectRow: React.FC = ({
- {showError && errorMessage && ( + {(showError && errorMessage) || reserveErrorSpace ? ( - {errorMessage} + {showError && errorMessage ? errorMessage : ' '} - )} + ) : null} ) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx b/admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx deleted file mode 100644 index 569618f933..0000000000 --- a/admin-ui/app/routes/Apps/Gluu/GluuSuspenseLoader.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { GluuSpinner } from '@/components/GluuSpinner' - -const GluuSuspenseLoader = () => { - return -} - -export default GluuSuspenseLoader diff --git a/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx b/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx index 6a8f203b99..2c7544af0a 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuToggle.tsx @@ -1,52 +1,74 @@ -import React, { useEffect, useState } from 'react' -import type { FormikProps } from 'formik' -import Toggle from 'react-toggle' +import { useState } from 'react' +import type { ChangeEvent } from 'react' +import clsx from 'clsx' import type { JsonValue } from './types/common' - -type GluuToggleProps> = { - id?: string - name: string - formik?: FormikProps | null - value?: boolean - handler?: (event: React.ChangeEvent) => void - disabled?: boolean -} +import type { GluuToggleProps } from './types/GluuToggle.types' +import { useStyles } from './styles/GluuToggle.style' const GluuToggle = ,>({ id, name, formik, - value, + value = false, handler, - disabled, + disabled = false, }: GluuToggleProps) => { - const [checked, setChecked] = useState(value || false) + const { classes } = useStyles() + const [focused, setFocused] = useState(false) - useEffect(() => { - if (value !== undefined) { - setChecked(value) + const handleChange = (event: ChangeEvent) => { + if (formik) { + formik.handleChange(event) } - }, [value]) + handler?.(event) + } return ( - ) => { - setChecked(event.target.checked) - if (formik) { - formik.handleChange(event) - if (handler) { - handler(event) - } - } else { - handler?.(event) - } - }} - disabled={disabled} - checked={checked} - /> + +