diff --git a/admin-ui/app/redux/features/authSlice.ts b/admin-ui/app/redux/features/authSlice.ts index 08fc750013..94df83d006 100644 --- a/admin-ui/app/redux/features/authSlice.ts +++ b/admin-ui/app/redux/features/authSlice.ts @@ -51,7 +51,7 @@ const authSlice = createSlice({ setAuthState: (state, action: PayloadAction<{ state: boolean }>) => { state.isAuthenticated = action.payload?.state }, - getUserInfo: (state, _action: PayloadAction) => {}, + getUserInfo: (_state, _action: PayloadAction) => {}, getUserInfoResponse: ( state, action: PayloadAction<{ @@ -74,7 +74,7 @@ const authSlice = createSlice({ state.isAuthenticated = true } }, - getAPIAccessToken: (state, _action: PayloadAction) => {}, + getAPIAccessToken: (_state, _action: PayloadAction) => {}, getAPIAccessTokenResponse: ( state, action: PayloadAction<{ access_token?: string; scopes?: string[]; issuer?: string }>, @@ -89,7 +89,7 @@ const authSlice = createSlice({ state.isAuthenticated = true } }, - getUserLocation: (state, _action: PayloadAction) => {}, + getUserLocation: (_state, _action: PayloadAction) => {}, getUserLocationResponse: (state, action: PayloadAction<{ location?: Location }>) => { if (action.payload?.location) { state.location = action.payload.location diff --git a/admin-ui/app/redux/sagas/AuthSaga.ts b/admin-ui/app/redux/sagas/AuthSaga.ts index 39967525fb..fa6d8ffd68 100644 --- a/admin-ui/app/redux/sagas/AuthSaga.ts +++ b/admin-ui/app/redux/sagas/AuthSaga.ts @@ -1,7 +1,5 @@ // @ts-nocheck -/** - * Auth Sagas - */ + import { all, call, fork, put, select, takeEvery } from 'redux-saga/effects' import { getOAuth2ConfigResponse, @@ -56,13 +54,21 @@ function* getOAuth2ConfigWorker({ payload }) { yield put(getOAuth2ConfigResponse()) } -function* putConfigWorker({ payload }) { +export function* putConfigWorker({ payload }) { try { + // Extract metadata (if any) from payload + const { _meta, ...configData } = payload const token = yield select((state) => state.authReducer.token.access_token) - const response = yield call(putServerConfiguration, { token, props: payload }) + const response = yield call(putServerConfiguration, { token, props: configData }) if (response) { yield put(getOAuth2ConfigResponse({ config: response })) - yield put(updateToast(true, 'success')) + + // If cedarlingLogType changed, show specific toast; otherwise show generic success + if (_meta?.cedarlingLogTypeChanged && _meta?.toastMessage) { + yield put(updateToast(true, 'success', _meta.toastMessage)) + } else { + yield put(updateToast(true, 'success')) + } return } } catch (error) { diff --git a/admin-ui/app/redux/sagas/__tests__/AuthSaga.test.ts b/admin-ui/app/redux/sagas/__tests__/AuthSaga.test.ts new file mode 100644 index 0000000000..fd6a330891 --- /dev/null +++ b/admin-ui/app/redux/sagas/__tests__/AuthSaga.test.ts @@ -0,0 +1,249 @@ +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { throwError } from 'redux-saga-test-plan/providers' +import { getOAuth2ConfigResponse, putConfigWorkerResponse } from '../../features/authSlice' +import { updateToast } from '../../features/toastSlice' +import { putServerConfiguration } from '../../api/backend-api' +import * as AuthSaga from '../AuthSaga' + +const { putConfigWorker } = AuthSaga + +describe('AuthSaga - putConfigWorker', () => { + const mockToken = 'mock-access-token' + const mockConfig = { + sessionTimeoutInMins: 30, + acrValues: 'simple_password_auth', + cedarlingLogType: 'OFF', + additionalParameters: [], + } + const mockResponse = { + ...mockConfig, + postLogoutRedirectUri: 'https://example.com/logout', + } + + const mockState = { + authReducer: { + token: { + access_token: mockToken, + }, + }, + } + + describe('when cedarlingLogType changes', () => { + // eslint-disable-next-line jest/expect-expect + it('should dispatch updateToast with custom message on success', () => { + const customToastMessage = 'Please relogin to view cedarling changes' + const action = { + type: 'auth/putConfigWorker', + payload: { + ...mockConfig, + _meta: { + cedarlingLogTypeChanged: true, + toastMessage: customToastMessage, + }, + }, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .put(getOAuth2ConfigResponse({ config: mockResponse })) + .put(updateToast(true, 'success', customToastMessage)) + .put(putConfigWorkerResponse()) + .run() + }) + + // eslint-disable-next-line jest/expect-expect + it('should not dispatch custom toast message on error', () => { + const customToastMessage = 'Please relogin to view cedarling changes' + const mockError = new Error('Network error') + const action = { + type: 'auth/putConfigWorker', + payload: { + ...mockConfig, + _meta: { + cedarlingLogTypeChanged: true, + toastMessage: customToastMessage, + }, + }, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), throwError(mockError)]]) + .not.put(updateToast(true, 'success', customToastMessage)) + .put(updateToast(true, 'error')) + .put(putConfigWorkerResponse()) + .run() + }) + }) + + describe('when cedarlingLogType does not change', () => { + // eslint-disable-next-line jest/expect-expect + it('should dispatch generic success toast when no metadata provided', () => { + const action = { + type: 'auth/putConfigWorker', + payload: mockConfig, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .put(getOAuth2ConfigResponse({ config: mockResponse })) + .put(updateToast(true, 'success')) + .put(putConfigWorkerResponse()) + .run() + }) + + // eslint-disable-next-line jest/expect-expect + it('should dispatch generic success toast when cedarlingLogTypeChanged is false', () => { + const action = { + type: 'auth/putConfigWorker', + payload: { + ...mockConfig, + _meta: { + cedarlingLogTypeChanged: false, + }, + }, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .put(getOAuth2ConfigResponse({ config: mockResponse })) + .put(updateToast(true, 'success')) + .put(putConfigWorkerResponse()) + .run() + }) + + // eslint-disable-next-line jest/expect-expect + it('should dispatch generic success toast when toastMessage is undefined', () => { + const action = { + type: 'auth/putConfigWorker', + payload: { + ...mockConfig, + _meta: { + cedarlingLogTypeChanged: true, + toastMessage: undefined, + }, + }, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .put(getOAuth2ConfigResponse({ config: mockResponse })) + .put(updateToast(true, 'success')) + .put(putConfigWorkerResponse()) + .run() + }) + }) + + describe('error handling', () => { + // eslint-disable-next-line jest/expect-expect + it('should dispatch error toast on failure', () => { + const mockError = new Error('API Error') + const action = { + type: 'auth/putConfigWorker', + payload: mockConfig, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), throwError(mockError)]]) + .not.put(getOAuth2ConfigResponse({ config: mockResponse })) + .put(updateToast(true, 'error')) + .put(putConfigWorkerResponse()) + .run() + }) + }) + + describe('metadata extraction', () => { + // eslint-disable-next-line jest/expect-expect + it('should not send _meta to the API', () => { + const action = { + type: 'auth/putConfigWorker', + payload: { + ...mockConfig, + _meta: { + cedarlingLogTypeChanged: true, + toastMessage: 'Test message', + }, + }, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .call(putServerConfiguration, { + token: mockToken, + props: mockConfig, // Should NOT include _meta + }) + .run() + }) + + // eslint-disable-next-line jest/expect-expect + it('should handle payload without _meta correctly', () => { + const action = { + type: 'auth/putConfigWorker', + payload: mockConfig, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .call(putServerConfiguration, { + token: mockToken, + props: mockConfig, + }) + .put(getOAuth2ConfigResponse({ config: mockResponse })) + .put(updateToast(true, 'success')) + .run() + }) + }) + + describe('response handling', () => { + // eslint-disable-next-line jest/expect-expect + it('should handle successful response with config update', () => { + const action = { + type: 'auth/putConfigWorker', + payload: mockConfig, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .put(getOAuth2ConfigResponse({ config: mockResponse })) + .run() + }) + + // eslint-disable-next-line jest/expect-expect + it('should always call putConfigWorkerResponse in finally block', () => { + const action = { + type: 'auth/putConfigWorker', + payload: mockConfig, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), mockResponse]]) + .put(putConfigWorkerResponse()) + .run() + }) + + // eslint-disable-next-line jest/expect-expect + it('should call putConfigWorkerResponse even on error', () => { + const mockError = new Error('API Error') + const action = { + type: 'auth/putConfigWorker', + payload: mockConfig, + } + + return expectSaga(putConfigWorker, action) + .withState(mockState) + .provide([[matchers.call.fn(putServerConfiguration), throwError(mockError)]]) + .put(putConfigWorkerResponse()) + .run() + }) + }) +}) diff --git a/admin-ui/app/routes/Apps/Gluu/GluuCommitFooter.tsx b/admin-ui/app/routes/Apps/Gluu/GluuCommitFooter.tsx index 15b38831f2..617738f890 100644 --- a/admin-ui/app/routes/Apps/Gluu/GluuCommitFooter.tsx +++ b/admin-ui/app/routes/Apps/Gluu/GluuCommitFooter.tsx @@ -13,6 +13,10 @@ interface GluuCommitFooterProps { save?: boolean back?: boolean } + disableButtons?: { + save?: boolean + back?: boolean + } type?: 'button' | 'submit' disableBackButton?: boolean cancelHandler?: () => void @@ -25,6 +29,7 @@ function GluuCommitFooter({ saveHandler, extraLabel, hideButtons, + disableButtons, type = 'button', backButtonLabel, backButtonHandler, @@ -54,6 +59,7 @@ function GluuCommitFooter({ type="button" onClick={disableBackButton ? cancelHandler : goBack} className="d-flex m-1 mx-5" + disabled={disableButtons?.back} > {!disableBackButton && } {backButtonLabel || t('actions.cancel')} @@ -84,6 +90,7 @@ function GluuCommitFooter({ color={`primary-${selectedTheme}`} style={{ ...applicationStyle.buttonStyle, ...applicationStyle.buttonFlexIconStyles }} className="ms-auto px-4" + disabled={disableButtons?.save} > {t('actions.apply')} @@ -97,6 +104,7 @@ function GluuCommitFooter({ style={{ ...applicationStyle.buttonStyle, ...applicationStyle.buttonFlexIconStyles }} className="ms-auto px-4" onClick={saveHandler} + disabled={disableButtons?.save} > {t('actions.apply')} diff --git a/admin-ui/app/utils/pagingUtils.ts b/admin-ui/app/utils/pagingUtils.ts new file mode 100644 index 0000000000..6387ab8e3f --- /dev/null +++ b/admin-ui/app/utils/pagingUtils.ts @@ -0,0 +1,45 @@ +export const getPagingSize = (defaultSize: number = 10): number => { + // Guard against SSR/test environments where localStorage is unavailable + if (typeof window === 'undefined' || !window.localStorage) { + return defaultSize + } + + try { + const stored = localStorage.getItem('gluu.pagingSize') + + if (!stored) return defaultSize + + const parsed = parseInt(stored, 10) + // Only return if it's a valid positive integer (>0) + if (!isNaN(parsed) && parsed > 0) { + return parsed + } + + return defaultSize + } catch (error) { + // Silently handle localStorage errors (quota exceeded, privacy mode, etc.) + console.warn('Failed to read paging size from localStorage:', error) + return defaultSize + } +} + +export const savePagingSize = (size: number): void => { + // Validate and coerce input to a positive integer + const validSize = Math.floor(size) + if (validSize <= 0) { + console.warn('Invalid paging size:', size, '- must be a positive integer') + return + } + + // Guard against SSR/test environments where localStorage is unavailable + if (typeof window === 'undefined' || !window.localStorage) { + return + } + + try { + localStorage.setItem('gluu.pagingSize', String(validSize)) + } catch (error) { + // Silently handle localStorage errors (quota exceeded, privacy mode, etc.) + console.warn('Failed to save paging size to localStorage:', error) + } +} diff --git a/admin-ui/plugins/admin/components/CustomScripts/ScriptListPage.tsx b/admin-ui/plugins/admin/components/CustomScripts/ScriptListPage.tsx index 138557bd0f..ccc26507af 100644 --- a/admin-ui/plugins/admin/components/CustomScripts/ScriptListPage.tsx +++ b/admin-ui/plugins/admin/components/CustomScripts/ScriptListPage.tsx @@ -27,6 +27,7 @@ import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' import { adminUiFeatures } from 'Plugins/admin/helper/utils' import customColors from '@/customColors' +import { getPagingSize } from '@/utils/pagingUtils' import { RootState, UserAction } from './types' function ScriptListTable(): JSX.Element { @@ -39,7 +40,7 @@ function ScriptListTable(): JSX.Element { const [myActions, setMyActions] = useState([]) const [item, setItem] = useState({}) const [modal, setModal] = useState(false) - const pageSize = localStorage.getItem('paggingSize') || 10 + const pageSize = getPagingSize() const [limit, setLimit] = useState(pageSize) const [pattern, setPattern] = useState('') const [type, setType] = useState('person_authentication') @@ -192,7 +193,7 @@ function ScriptListTable(): JSX.Element { isFreeAction: true, onClick: handleGoToCustomScriptAddPage, }) - actions.push((rowData: any) => ({ + actions.push((_rowData: any) => ({ icon: 'edit', iconProps: { color: 'primary', @@ -209,7 +210,7 @@ function ScriptListTable(): JSX.Element { } if (canRead) { - actions.push((rowData: any) => ({ + actions.push((_rowData: any) => ({ icon: 'visibility', iconProps: { color: 'primary', @@ -271,7 +272,7 @@ function ScriptListTable(): JSX.Element { } if (canDelete) { - actions.push((rowData: any) => ({ + actions.push((_rowData: any) => ({ icon: () => , iconProps: { color: 'primary', diff --git a/admin-ui/plugins/admin/components/Permissions/UiPermListPage.js b/admin-ui/plugins/admin/components/Permissions/UiPermListPage.js index 8238cc653b..682dd04d16 100644 --- a/admin-ui/plugins/admin/components/Permissions/UiPermListPage.js +++ b/admin-ui/plugins/admin/components/Permissions/UiPermListPage.js @@ -27,6 +27,7 @@ import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' import { isEmpty } from 'lodash' import customColors from '@/customColors' +import { getPagingSize } from '@/utils/pagingUtils' function UiPermListPage() { const { hasCedarPermission, authorize } = useCedarling() @@ -49,7 +50,7 @@ function UiPermListPage() { // Constants const userAction = {} - const pageSize = localStorage.getItem('paggingSize') || 10 + const pageSize = getPagingSize() SetTitle(t('menus.securityDropdown.capabilities')) diff --git a/admin-ui/plugins/admin/components/Roles/UiRoleListPage.js b/admin-ui/plugins/admin/components/Roles/UiRoleListPage.js index 374a7621d5..01980eeaae 100644 --- a/admin-ui/plugins/admin/components/Roles/UiRoleListPage.js +++ b/admin-ui/plugins/admin/components/Roles/UiRoleListPage.js @@ -17,18 +17,18 @@ import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' import { toast } from 'react-toastify' import customColors from '@/customColors' +import { getPagingSize } from '@/utils/pagingUtils' function UiRoleListPage() { const { hasCedarPermission, authorize } = useCedarling() const apiRoles = useSelector((state) => state.apiRoleReducer.items) const loading = useSelector((state) => state.apiRoleReducer.loading) - const { permissions: cedarPermissions } = useSelector((state) => state.cedarPermissions) const [modal, setModal] = useState(false) const myActions = [], options = [], userAction = {}, - pageSize = localStorage.getItem('paggingSize') || 10, + pageSize = getPagingSize(), theme = useContext(ThemeContext), selectedTheme = theme.state.theme, themeColors = getThemeColor(selectedTheme), diff --git a/admin-ui/plugins/admin/components/Settings/SettingsPage.js b/admin-ui/plugins/admin/components/Settings/SettingsPage.js index 1e9197898b..7096fa4c4e 100644 --- a/admin-ui/plugins/admin/components/Settings/SettingsPage.js +++ b/admin-ui/plugins/admin/components/Settings/SettingsPage.js @@ -1,8 +1,8 @@ -import React, { useState, useContext, useEffect } from 'react' +import React, { useEffect, useMemo, useCallback, useState, useContext } from 'react' import { useTranslation } from 'react-i18next' -import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' -import GluuToogleRow from 'Routes/Apps/Gluu/GluuToogleRow' -import { SETTINGS } from 'Utils/ApiResources' +import { useFormik } from 'formik' +import { useDispatch, useSelector } from 'react-redux' +import * as Yup from 'yup' import { Card, CardBody, @@ -13,156 +13,107 @@ import { InputGroup, CustomInput, Form, - Button, } from 'Components' +import GluuLabel from 'Routes/Apps/Gluu/GluuLabel' +import GluuToogleRow from 'Routes/Apps/Gluu/GluuToogleRow' +import GluuInputRow from 'Routes/Apps/Gluu/GluuInputRow' +import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' +import GluuProperties from 'Routes/Apps/Gluu/GluuProperties' +import GluuCommitFooter from 'Routes/Apps/Gluu/GluuCommitFooter' +import { SETTINGS } from 'Utils/ApiResources' import SetTitle from 'Utils/SetTitle' import applicationStyle from 'Routes/Apps/Gluu/styles/applicationstyle' import { ThemeContext } from 'Context/theme/themeContext' import { putConfigWorker } from 'Redux/features/authSlice' -import GluuInputRow from 'Routes/Apps/Gluu/GluuInputRow' -import { useFormik } from 'formik' -import { useDispatch, useSelector } from 'react-redux' -import * as Yup from 'yup' -import { SIMPLE_PASSWORD_AUTH } from 'Plugins/auth-server/common/Constants' import { getScripts } from 'Redux/features/initSlice' -import GluuLoader from 'Routes/Apps/Gluu/GluuLoader' -import GluuProperties from 'Routes/Apps/Gluu/GluuProperties' -import packageJson from '../../../../package.json' +import { SIMPLE_PASSWORD_AUTH } from 'Plugins/auth-server/common/Constants' import { CedarlingLogType } from '@/cedarling' -import { updateToast } from '@/redux/features/toastSlice' - -const levels = [1, 5, 10, 20] +import { getPagingSize, savePagingSize as savePagingSizeToStorage } from 'Utils/pagingUtils' +import { buildPayload } from 'Utils/PermChecker' +import packageJson from '../../../../package.json' function SettingsPage() { const { t } = useTranslation() const dispatch = useDispatch() + const theme = useContext(ThemeContext) + const selectedTheme = theme?.state?.theme || 'darkBlack' const loadingScripts = useSelector((state) => state.initReducer.loadingScripts) const loadingConfig = useSelector((state) => state.authReducer?.loadingConfig) - const theme = useContext(ThemeContext) - const selectedTheme = theme.state.theme - const [paggingSize, setPaggingSize] = useState(localStorage.getItem('paggingSize') || 10) - SetTitle(t('titles.application_settings')) + const config = useSelector((state) => state.authReducer?.config) || {} + const scripts = useSelector((state) => state.initReducer.scripts) - useEffect(() => { - dispatch(getScripts({ action: {} })) - }, []) + const pagingSizeOptions = useMemo(() => [1, 5, 10, 20], []) - return ( - - - - - + const defaultPagingSize = useMemo(() => { + // Use the third option (10) as default, or fallback to first option if array is too short + return pagingSizeOptions[2] || pagingSizeOptions[0] || 10 + }, [pagingSizeOptions]) - - - - - { - return element == paggingSize - }) - ] - } - onChange={(value) => { - const size = levels[value.target.options.selectedIndex] - setPaggingSize(size) - localStorage.setItem('paggingSize', size) - }} - > - {levels.map((item, key) => ( - - ))} - - - - + const savedPagingSize = useMemo(() => getPagingSize(defaultPagingSize), [defaultPagingSize]) + const [baselinePagingSize, setBaselinePagingSize] = useState(savedPagingSize) + const [currentPagingSize, setCurrentPagingSize] = useState(savedPagingSize) - - - - - - + SetTitle(t('titles.application_settings')) - {!loadingScripts && } - - - - + const configApiUrl = useMemo( + () => + typeof window !== 'undefined' && window.configApiBaseUrl ? window.configApiBaseUrl : 'N/A', + [], ) -} -function SettingsForm() { - const { t } = useTranslation() - const theme = useContext(ThemeContext) - const selectedTheme = theme.state.theme - const loadingConfig = useSelector((state) => state.authReducer?.loadingConfig) - const additionalParameters = - useSelector((state) => state.authReducer?.config?.additionalParameters) || [] - const acrValues = useSelector((state) => state.authReducer?.config?.acrValues) - const sessionTimeout = - useSelector((state) => state.authReducer?.config?.sessionTimeoutInMins) || 5 - const cedarlingLogType = - useSelector((state) => state.authReducer?.config?.cedarlingLogType) || CedarlingLogType.OFF - const scripts = useSelector((state) => state.initReducer.scripts) - const dispatch = useDispatch() + useEffect(() => { + const userAction = {} + const options = {} + buildPayload(userAction, 'Fetch custom scripts', options) + dispatch(getScripts({ action: userAction })) + }, [dispatch]) - const authScripts = scripts - .filter((item) => item.scriptType == 'person_authentication') - .filter((item) => item.enabled) - .map((item) => item.name) + const handlePagingSizeChange = useCallback((size) => { + setCurrentPagingSize(size) + }, []) - authScripts.push(SIMPLE_PASSWORD_AUTH) + const transformToFormValues = useCallback((configData) => { + return { + sessionTimeoutInMins: configData?.sessionTimeoutInMins || 30, + acrValues: configData?.acrValues || '', + cedarlingLogType: configData?.cedarlingLogType || CedarlingLogType.OFF, + additionalParameters: (configData?.additionalParameters || []).map((param) => ({ ...param })), + } + }, []) + + const initialValues = useMemo( + () => transformToFormValues(config), + [config, transformToFormValues], + ) + + const authScripts = useMemo(() => { + const names = (scripts || []) + .filter((s) => s.scriptType === 'person_authentication' && s.enabled) + .map((s) => s.name) + return Array.from(new Set([...names, SIMPLE_PASSWORD_AUTH])) + }, [scripts]) const formik = useFormik({ - initialValues: { - sessionTimeoutInMins: sessionTimeout, - acrValues: acrValues || '', - cedarlingLogType, - }, + initialValues, + enableReinitialize: true, onSubmit: (values) => { - dispatch(putConfigWorker(values)) - !(values?.cedarlingLogType === cedarlingLogType) && - dispatch(updateToast(true, 'success', t('fields.reloginToViewCedarlingChanges'))) + savePagingSizeToStorage(currentPagingSize) + + const cedarlingLogTypeChanged = values?.cedarlingLogType !== config?.cedarlingLogType + + dispatch( + putConfigWorker({ + ...values, + _meta: { + cedarlingLogTypeChanged, + toastMessage: cedarlingLogTypeChanged + ? t('fields.reloginToViewCedarlingChanges') + : undefined, + }, + }), + ) + + setBaselinePagingSize(currentPagingSize) }, validationSchema: Yup.object().shape({ sessionTimeoutInMins: Yup.number() @@ -171,96 +122,195 @@ function SettingsForm() { }), }) + const { resetForm } = formik + + const isPagingSizeChanged = currentPagingSize !== baselinePagingSize + const isFormChanged = formik.dirty || isPagingSizeChanged + const hasErrors = !formik.isValid + + const handleCancel = useCallback(() => { + const resetValues = transformToFormValues(config) + resetForm({ values: resetValues }) + setCurrentPagingSize(baselinePagingSize) + }, [config, transformToFormValues, baselinePagingSize, resetForm]) + + const additionalParametersOptions = useMemo(() => { + return (formik.values.additionalParameters || []).map((param) => ({ + key: param.key || '', + value: param.value || '', + })) + }, [formik.values.additionalParameters]) + + const formGroupRowStyle = useMemo(() => ({ justifyContent: 'space-between' }), []) + + const labelContainerStyle = useMemo( + () => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + paddingRight: '15px', + }), + [], + ) + return ( -
- + + + + + + + + + + + + + + + + + + + handlePagingSizeChange(parseInt(e.target.value, 10))} + > + {pagingSizeOptions.map((option) => ( + + ))} + + + + + + {!loadingScripts && ( + <> + - - - - - - - {authScripts.map((item) => ( - - ))} - - - - + + + + + + + {authScripts.map((item) => ( + + ))} + + + + - - + + + + { + formik.setFieldValue( + 'cedarlingLogType', + event.target.checked ? CedarlingLogType.STD_OUT : CedarlingLogType.OFF, + ) + }} + /> + + - - { - formik.setFieldValue( - 'cedarlingLogType', - event.target.checked ? CedarlingLogType.STD_OUT : CedarlingLogType.OFF, - ) - }} - /> - - +
+ +
-
- -
- - + + + )} + +
+
+
+
) } diff --git a/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx b/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx index 02973a4f79..375e02fb4d 100644 --- a/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx +++ b/admin-ui/plugins/auth-server/components/Sessions/SessionListPage.tsx @@ -37,6 +37,7 @@ import GetAppIcon from '@mui/icons-material/GetApp' import ViewColumnIcon from '@mui/icons-material/ViewColumn' import { DeleteOutlined } from '@mui/icons-material' import customColors from '@/customColors' +import { getPagingSize } from '@/utils/pagingUtils' import { useGetSessions, useDeleteSession, @@ -170,7 +171,7 @@ const SessionListPage: React.FC = () => { updatedColumns: [], }) - const pageSize = Number.parseInt(localStorage.getItem('paggingSize') || '10', 10) + const pageSize = getPagingSize() const toggle = () => setModal(!modal) const theme = useContext(ThemeContext) const selectedTheme = theme?.state?.theme || 'default' diff --git a/admin-ui/plugins/schema/components/Person/AttributeListPage.tsx b/admin-ui/plugins/schema/components/Person/AttributeListPage.tsx index 54fdef734a..92825f7494 100644 --- a/admin-ui/plugins/schema/components/Person/AttributeListPage.tsx +++ b/admin-ui/plugins/schema/components/Person/AttributeListPage.tsx @@ -27,6 +27,7 @@ import { adminUiFeatures } from 'Plugins/admin/helper/utils' import customColors from '@/customColors' import styled from 'styled-components' import { LIMIT_ID, PATTERN_ID } from 'Plugins/admin/common/Constants' +import { getPagingSize } from '@/utils/pagingUtils' import type { JansAttribute, GetAttributesOptions } from 'Plugins/schema/types' import type { Dispatch } from '@reduxjs/toolkit' import type { @@ -66,9 +67,7 @@ function AttributeListPage(): JSX.Element { useEffect(() => {}, [cedarPermissions]) const options: GetAttributesOptions = {} - const pageSize = localStorage.getItem('paggingSize') - ? parseInt(localStorage.getItem('paggingSize')!) - : 10 + const pageSize = getPagingSize() const [limit, setLimit] = useState(pageSize) const [pageNumber, setPageNumber] = useState(0) const [pattern, setPattern] = useState(null) diff --git a/admin-ui/plugins/services/Components/Configuration/LdapListPage.js b/admin-ui/plugins/services/Components/Configuration/LdapListPage.js index 1a47cca31e..ca0c589d69 100644 --- a/admin-ui/plugins/services/Components/Configuration/LdapListPage.js +++ b/admin-ui/plugins/services/Components/Configuration/LdapListPage.js @@ -27,6 +27,7 @@ import SetTitle from 'Utils/SetTitle' import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' import customColors from '@/customColors' +import { getPagingSize } from '@/utils/pagingUtils' function LdapListPage() { const { hasCedarPermission, authorize } = useCedarling() @@ -60,7 +61,7 @@ function LdapListPage() { // Constants const userAction = {} - const pageSize = localStorage.getItem('paggingSize') || 10 + const pageSize = getPagingSize() SetTitle(t('titles.ldap_authentication')) diff --git a/admin-ui/plugins/services/Components/Configuration/SqlListPage.js b/admin-ui/plugins/services/Components/Configuration/SqlListPage.js index 447f514d23..9a4f479d86 100644 --- a/admin-ui/plugins/services/Components/Configuration/SqlListPage.js +++ b/admin-ui/plugins/services/Components/Configuration/SqlListPage.js @@ -25,6 +25,7 @@ import SetTitle from 'Utils/SetTitle' import { ThemeContext } from 'Context/theme/themeContext' import getThemeColor from 'Context/theme/config' import customColors from '@/customColors' +import { getPagingSize } from '@/utils/pagingUtils' function SqlListPage() { const { hasCedarPermission, authorize } = useCedarling() @@ -56,7 +57,7 @@ function SqlListPage() { const navigate = useNavigate() const [item, setItem] = useState({}) const [modal, setModal] = useState(false) - const pageSize = localStorage.getItem('paggingSize') || 10 + const pageSize = getPagingSize() const [alertObj, setAlertObj] = useState({ severity: '', message: '',