Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import LoggingPage from './LoggingPage'
import { Provider } from 'react-redux'
import AppTestWrapper from 'Routes/Apps/Gluu/Tests/Components/AppTestWrapper.test'
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { Logging } from 'JansConfigApi'

// Mock the orval hooks
jest.mock('JansConfigApi', () => ({
useGetConfigLogging: jest.fn(),
usePutConfigLogging: jest.fn(),
}))

const permissions = [
'https://jans.io/oauth/config/logging.readonly',
Expand All @@ -13,37 +21,76 @@ const permissions = [

const AUTH_STATE = {
permissions: permissions,
token: {
access_token: 'test-token',
},
config: {
clientId: 'test-client-id',
},
userinfo: {
inum: 'test-inum',
name: 'Test User',
},
}
const logging = {

const logging: Logging = {
loggingLevel: 'TRACE',
loggingLayout: 'text',
httpLoggingEnabled: true,
disableJdkLogger: false,
enabledOAuthAuditLogging: false,
}
const LOGGING_STATE = {
logging: logging,
loading: false,
}

const store = configureStore({
reducer: combineReducers({
authReducer: (state = AUTH_STATE) => state,
loggingReducer: (state = LOGGING_STATE) => state,
cedarPermissions: (state = { permissions: {} }) => state,
noReducer: (state = {}) => state,
}),
})

const Wrapper = ({ children }) => (
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})

interface WrapperProps {
children: React.ReactNode
}

const Wrapper: React.FC<WrapperProps> = ({ children }) => (
<AppTestWrapper>
<Provider store={store}>{children}</Provider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>{children}</Provider>
</QueryClientProvider>
</AppTestWrapper>
)

it('Should render Acr page properly', () => {
it('Should render Logging page properly', () => {
const { useGetConfigLogging, usePutConfigLogging } = require('JansConfigApi')

// Mock the GET hook to return logging data
useGetConfigLogging.mockReturnValue({
data: logging,
isLoading: false,
error: null,
})

// Mock the PUT mutation hook
usePutConfigLogging.mockReturnValue({
mutateAsync: jest.fn(),
isPending: false,
isError: false,
error: null,
})

render(<LoggingPage />, {
wrapper: Wrapper,
})

expect(screen.getByTestId('loggingLayout')).toHaveDisplayValue(logging.loggingLayout)
expect(screen.getByTestId('loggingLevel')).toHaveDisplayValue(logging.loggingLevel)
expect(screen.getByTestId('httpLoggingEnabled')).toBeChecked()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,103 @@ import GluuCommitDialog from 'Routes/Apps/Gluu/GluuCommitDialog'
import GluuFormFooter from 'Routes/Apps/Gluu/GluuFormFooter'
import { JSON_CONFIG } from 'Utils/ApiResources'
import { loggingValidationSchema } from './validations'
import { LOG_LEVELS, LOG_LAYOUTS, getLoggingInitialValues } from './utils'
import {
LOG_LEVELS,
LOG_LAYOUTS,
getLoggingInitialValues,
getMergedValues,
getChangedFields,
} from './utils'
import type { LoggingFormValues } from './utils'
import applicationStyle from 'Routes/Apps/Gluu/styles/applicationstyle'
import { useDispatch, useSelector } from 'react-redux'
import { useSelector } from 'react-redux'
import { Formik } from 'formik'
import { useNavigate } from 'react-router-dom'
import {
getLoggingConfig,
editLoggingConfig,
} from 'Plugins/auth-server/redux/features/loggingSlice'
import { useGetConfigLogging, usePutConfigLogging, type Logging } from 'JansConfigApi'
import { LOGGING_READ, LOGGING_WRITE } from 'Utils/PermChecker'
import { useCedarling } from '@/cedarling'
import { useTranslation } from 'react-i18next'
import SetTitle from 'Utils/SetTitle'
import GluuToogleRow from 'Routes/Apps/Gluu/GluuToogleRow'
import { getChangedFields, getMergedValues } from '@/helpers'
import { useLoggingActions, type ModifiedFields } from './hooks/useLoggingActions'
import type { RootState } from '@/cedarling/types'

interface PendingValues {
mergedValues: Logging
changedFields: ModifiedFields
}

function LoggingPage() {
function LoggingPage(): React.ReactElement {
const { t } = useTranslation()
const navigate = useNavigate()
const { hasCedarPermission, authorize } = useCedarling()
const logging = useSelector((state) => state.loggingReducer.logging)
const loading = useSelector((state) => state.loggingReducer.loading)
const { permissions: cedarPermissions } = useSelector((state) => state.cedarPermissions)

const dispatch = useDispatch()
const { permissions: cedarPermissions } = useSelector(
(state: RootState) => state.cedarPermissions,
)
const { logLoggingUpdate } = useLoggingActions()

const [showCommitDialog, setShowCommitDialog] = useState(false)
const [pendingValues, setPendingValues] = useState(null)
const [localLogging, setLocalLogging] = useState(null)
const [pendingValues, setPendingValues] = useState<PendingValues | null>(null)
const [localLogging, setLocalLogging] = useState<Logging | null>(null)
const [permissionsInitialized, setPermissionsInitialized] = useState(false)

// React Query hooks for data fetching and mutation
// Only fetch data after permissions are initialized and user has read permission
const { data: logging, isLoading: isLoadingData } = useGetConfigLogging({
query: {
enabled: permissionsInitialized && hasCedarPermission(LOGGING_READ),
},
})
const updateLogging = usePutConfigLogging()

useEffect(() => {
const initPermissions = async () => {
const permissions = [LOGGING_READ, LOGGING_WRITE]
for (const permission of permissions) {
await authorize([permission])
let isMounted = true

const initPermissions = async (): Promise<void> => {
try {
// Run permission checks in parallel for better performance
await Promise.all([authorize([LOGGING_READ]), authorize([LOGGING_WRITE])])

if (isMounted) {
setPermissionsInitialized(true)
}
} catch (error) {
if (isMounted) {
console.error('Failed to authorize permissions:', error)
setPermissionsInitialized(true) // Set anyway to unblock UI
}
}
}

initPermissions()
dispatch(getLoggingConfig())
}, [dispatch, authorize])

return () => {
isMounted = false
}
}, [authorize])

useEffect(() => {
if (logging) {
setLocalLogging(logging)
}
}, [logging])

useEffect(() => {}, [cedarPermissions])

const initialValues = useMemo(() => getLoggingInitialValues(localLogging), [localLogging])
const initialValues: LoggingFormValues = useMemo(
() => getLoggingInitialValues(localLogging),
[localLogging],
)

const levels = useMemo(() => [...LOG_LEVELS], [])
const logLayouts = useMemo(() => [...LOG_LAYOUTS], [])
const levels = LOG_LEVELS
const logLayouts = LOG_LAYOUTS
SetTitle('Logging')

const handleSubmit = useCallback(
(values) => {
(values: LoggingFormValues): void => {
if (!localLogging) {
console.error('Cannot submit: logging data not loaded')
return
}

const mergedValues = getMergedValues(localLogging, values)
const changedFields = getChangedFields(localLogging, mergedValues)

Expand All @@ -74,28 +114,39 @@ function LoggingPage() {
)

const handleAccept = useCallback(
(userMessage) => {
if (pendingValues) {
const { mergedValues, changedFields } = pendingValues

const opts = {}
opts['logging'] = JSON.stringify(mergedValues)

dispatch(
editLoggingConfig({
data: opts,
otherFields: { userMessage, changedFields },
}),
async (userMessage: string): Promise<void> => {
if (!pendingValues) return

const { mergedValues, changedFields } = pendingValues

try {
// Update API first
const result = await updateLogging.mutateAsync({ data: mergedValues })

// Update local state with server response
setLocalLogging(result)

// Log audit action (non-blocking - errors are logged but don't fail the operation)
logLoggingUpdate(mergedValues, userMessage, changedFields).catch((error) =>
console.error('Audit logging failed:', error),
)

// Success: close dialog and clear pending
setShowCommitDialog(false)
setPendingValues(null)
} catch (error) {
// Keep dialog open on error so user can retry or cancel
console.error('Failed to update logging configuration:', error)
// Note: React Query mutation will handle showing error toast if configured
}
},
[pendingValues, dispatch],
[pendingValues, updateLogging, logLoggingUpdate],
)

const isLoading = isLoadingData || updateLogging.isPending

return (
<GluuLoader blocking={loading}>
<GluuLoader blocking={isLoading}>
<Card style={applicationStyle.mainCard}>
<CardBody style={{ minHeight: 500 }}>
<GluuViewWrapper canShow={hasCedarPermission(LOGGING_READ)}>
Expand All @@ -121,7 +172,9 @@ function LoggingPage() {
name="loggingLevel"
data-testid="loggingLevel"
value={formik.values.loggingLevel}
onChange={(e) => formik.setFieldValue('loggingLevel', e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
formik.setFieldValue('loggingLevel', e.target.value)
}
>
<option value="">{t('actions.choose')}...</option>
{levels.map((item, key) => (
Expand All @@ -147,7 +200,9 @@ function LoggingPage() {
name="loggingLayout"
data-testid="loggingLayout"
value={formik.values.loggingLayout}
onChange={(e) => formik.setFieldValue('loggingLayout', e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
formik.setFieldValue('loggingLayout', e.target.value)
}
>
<option value="">{t('actions.choose')}...</option>
{logLayouts.map((item, key) => (
Expand All @@ -162,7 +217,9 @@ function LoggingPage() {
<GluuToogleRow
label="fields.http_logging_enabled"
name="httpLoggingEnabled"
handler={(e) => formik.setFieldValue('httpLoggingEnabled', e.target.checked)}
handler={(e: React.ChangeEvent<HTMLInputElement>) =>
formik.setFieldValue('httpLoggingEnabled', e.target.checked)
}
lsize={5}
rsize={7}
value={formik.values.httpLoggingEnabled}
Expand All @@ -171,7 +228,9 @@ function LoggingPage() {
<GluuToogleRow
label="fields.disable_jdk_logger"
name="disableJdkLogger"
handler={(e) => formik.setFieldValue('disableJdkLogger', e.target.checked)}
handler={(e: React.ChangeEvent<HTMLInputElement>) =>
formik.setFieldValue('disableJdkLogger', e.target.checked)
}
lsize={5}
rsize={7}
doc_category={JSON_CONFIG}
Expand All @@ -180,7 +239,7 @@ function LoggingPage() {
<GluuToogleRow
label="fields.enabled_oAuth_audit_logging"
name="enabledOAuthAuditLogging"
handler={(e) =>
handler={(e: React.ChangeEvent<HTMLInputElement>) =>
formik.setFieldValue('enabledOAuthAuditLogging', e.target.checked)
}
lsize={5}
Expand All @@ -206,7 +265,7 @@ function LoggingPage() {
onApply={formik.handleSubmit}
disableApply={!formik.isValid || !formik.dirty}
applyButtonType="button"
isLoading={loading}
isLoading={isLoading}
/>
)}
</Form>
Expand Down
Loading
Loading