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: 24 additions & 6 deletions admin-ui/app/redux/features/healthSlice.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createSlice } from '@reduxjs/toolkit'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import reducerRegistry from 'Redux/reducers/ReducerRegistry'

type HealthStatus = 'Running' | 'Not present'
type HealthStatus = 'Running' | 'Not present' | 'Down'

export type HealthServiceKey =
| 'jans-lock'
Expand Down Expand Up @@ -33,6 +33,17 @@ const initialState: HealthState = {
loading: false,
}

interface HealthStatusResponsePayload {
data?: {
status?: HealthStatus
db_status?: HealthStatus
}
}

interface HealthServerStatusResponsePayload {
data?: HealthStatusResponse
}

const healthSlice = createSlice({
name: 'health',
initialState,
Expand All @@ -43,14 +54,21 @@ const healthSlice = createSlice({
getHealthServerStatus: (state) => {
state.loading = true
},
getHealthStatusResponse: (state, action) => {
getHealthStatusResponse: (state, action: PayloadAction<HealthStatusResponsePayload | null>) => {
state.loading = false
if (action.payload?.data) {
state.serverStatus = action.payload.data.status
state.dbStatus = action.payload.data.db_status
if (action.payload.data.status) {
state.serverStatus = action.payload.data.status
}
if (action.payload.data.db_status) {
state.dbStatus = action.payload.data.db_status
}
}
},
getHealthServerStatusResponse: (state, action) => {
getHealthServerStatusResponse: (
state,
action: PayloadAction<HealthServerStatusResponsePayload | null>,
) => {
state.loading = false
if (action.payload?.data) {
state.health = action.payload.data
Expand Down
71 changes: 52 additions & 19 deletions admin-ui/app/redux/features/initSlice.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
import reducerRegistry from 'Redux/reducers/ReducerRegistry'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface InitState {
scripts: any[]
clients: any[]
scopes: any[]
attributes: any[]
interface GenericItem {
[key: string]: string | number | boolean | string[] | number[] | boolean[] | null
}

export interface InitState {
scripts: GenericItem[]
clients: GenericItem[]
scopes: GenericItem[]
attributes: GenericItem[]
totalClientsEntries: number
isTimeout: boolean
loadingScripts: boolean
isLoading?: boolean
isLoading: boolean
}

interface ActionPayload {
[key: string]: string | number | boolean | string[] | number[] | boolean[] | null
}

interface ScriptsResponsePayload {
data?: {
entries?: GenericItem[]
}
}

interface ClientsResponsePayload {
data?: {
entries?: GenericItem[]
totalEntriesCount?: number
}
}

interface ScopesResponsePayload {
data?: GenericItem[]
}

interface AttributesResponsePayload {
data?: {
entries?: GenericItem[]
}
}

interface ApiTimeoutPayload {
isTimeout: boolean
}

const initialState: InitState = {
Expand All @@ -20,46 +55,44 @@ const initialState: InitState = {
totalClientsEntries: 0,
isTimeout: false,
loadingScripts: false,
isLoading: false,
}

const initSlice = createSlice({
name: 'init',
initialState,
reducers: {
getScripts: (state, _action: PayloadAction<{ action?: Record<string, unknown> }>) => {
getScripts: (state, _action: PayloadAction<{ action?: ActionPayload }>) => {
state.loadingScripts = true
},
getScriptsResponse: (state, action: PayloadAction<{ data?: { entries?: any[] } }>) => {
getScriptsResponse: (state, action: PayloadAction<ScriptsResponsePayload>) => {
state.loadingScripts = false
if (action.payload?.data) {
state.scripts = action.payload.data?.entries || []
state.scripts = action.payload.data.entries || []
}
},
getClients: () => {},
getClientsResponse: (
state,
action: PayloadAction<{ data?: { entries?: any[]; totalEntriesCount?: number } }>,
) => {
getClientsResponse: (state, action: PayloadAction<ClientsResponsePayload>) => {
if (action.payload?.data) {
state.clients = action.payload.data?.entries || []
state.clients = action.payload.data.entries || []
state.totalClientsEntries = action.payload.data.totalEntriesCount || 0
}
},
getScopes: () => {},
getScopesResponse: (state, action: PayloadAction<{ data?: any[] }>) => {
getScopesResponse: (state, action: PayloadAction<ScopesResponsePayload>) => {
if (action.payload?.data) {
state.scopes = action.payload.data
}
},
getAttributes: () => {},
getAttributesResponse: (state, action: PayloadAction<{ data?: { entries?: any[] } }>) => {
getAttributesResponse: (state, action: PayloadAction<AttributesResponsePayload>) => {
if (action.payload?.data) {
state.attributes = action.payload.data?.entries || []
state.attributes = action.payload.data.entries || []
}
},
handleApiTimeout: (state, action: PayloadAction<{ isTimeout?: boolean }>) => {
handleApiTimeout: (state, action: PayloadAction<ApiTimeoutPayload>) => {
state.isLoading = false
state.isTimeout = action.payload.isTimeout || false
state.isTimeout = action.payload.isTimeout
},
},
})
Expand Down
116 changes: 82 additions & 34 deletions admin-ui/app/routes/Dashboards/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import CrossIcon from 'Images/svg/cross.svg'
import SetTitle from 'Utils/SetTitle'
import styles from './styles'
import type { HealthState } from 'Redux/features/healthSlice'
import type { AuthState } from 'Redux/features/types/authTypes'
import type { InitState } from 'Redux/features/initSlice'
import type { LicenseDetailsState } from 'Redux/features/licenseDetailsSlice'
import type { CedarPermissionsState } from '@/cedarling/types'
import type { UserAction, ActionData } from 'Utils/PermChecker'

import { formatDate } from 'Utils/Util'
import UsersIcon from '@/components/SVG/menu/Users'
Expand All @@ -34,43 +39,81 @@ import { useAppNavigation, ROUTES } from '@/helpers/navigation'
interface DashboardHealthRootState {
healthReducer: HealthState
}

interface StatDataItem {
month: number | string
mau?: number
authz_code_access_token_count?: number
client_credentials_access_token_count?: number
}

interface LockDetailItem {
monthly_active_users?: number
monthly_active_clients?: number
}

interface LockDetail {
monthly_active_users?: number
monthly_active_clients?: number
}

interface RootState {
mauReducer: {
stat: StatDataItem[]
loading: boolean
}
initReducer: InitState
lockReducer: {
lockDetail: LockDetailItem[] | LockDetail
loading: boolean
}
authReducer: AuthState
licenseDetailsReducer: LicenseDetailsState
healthReducer: HealthState
cedarPermissions: CedarPermissionsState
}

// Constants moved outside component for better performance
const FETCHING_LICENSE_DETAILS = 'Fetch license details'

function DashboardPage() {
const { t } = useTranslation()
const dispatch = useDispatch()
const userAction = useMemo(() => ({}), [])
const userAction = useMemo(() => ({ action_message: '', action_data: null }) as UserAction, [])
const options = useMemo(() => ({}), [])
const isTabletOrMobile = useMediaQuery({ query: '(max-width: 1224px)' })
const isMobile = useMediaQuery({ maxWidth: 767 })
const { classes } = styles()
const [mauCount, setMauCount] = useState(null)
const [tokenCount, setTokenCount] = useState(null)
const [mauCount, setMauCount] = useState<number | null>(null)
const [tokenCount, setTokenCount] = useState<number | null>(null)

const [requestStates, setRequestStates] = useState({
licenseRequested: false,
clientsRequested: false,
})
const statData = useSelector((state: any) => state.mauReducer.stat)
const loading = useSelector((state: any) => state.mauReducer.loading)
const clients = useSelector((state: any) => state.initReducer.clients)
const lock = useSelector((state: any) => state.lockReducer.lockDetail)
const { isUserInfoFetched } = useSelector((state: any) => state.authReducer)
const totalClientsEntries = useSelector((state: any) => state.initReducer.totalClientsEntries)
const license = useSelector((state: any) => state.licenseDetailsReducer.item)
const statData = useSelector((state: RootState) => state.mauReducer.stat)
const loading = useSelector((state: RootState) => state.mauReducer.loading)
const clients = useSelector((state: RootState) => state.initReducer.clients)
const lock = useSelector((state: RootState) => state.lockReducer.lockDetail)
const { isUserInfoFetched } = useSelector((state: RootState) => state.authReducer)
const totalClientsEntries = useSelector(
(state: RootState) => state.initReducer.totalClientsEntries,
)
const license = useSelector((state: RootState) => state.licenseDetailsReducer.item)
const serverStatus = useSelector(
(state: DashboardHealthRootState) => state.healthReducer.serverStatus,
)
const serverHealth = useSelector((state: DashboardHealthRootState) => state.healthReducer.health)
const dbStatus = useSelector((state: DashboardHealthRootState) => state.healthReducer.dbStatus)
const access_token = useSelector((state: any) => state.authReducer.token?.access_token)
const permissions = useSelector((state: any) => state.authReducer.permissions)
const access_token = useSelector((state: RootState) => state.authReducer.token?.access_token)
const permissions = useSelector((state: RootState) => state.authReducer.permissions)

const { hasCedarReadPermission, authorizeHelper } = useCedarling()
const { navigateToRoute } = useAppNavigation()
const cedarInitialized = useSelector((state: any) => state.cedarPermissions?.initialized)
const cedarIsInitializing = useSelector((state: any) => state.cedarPermissions?.isInitializing)
const cedarInitialized = useSelector((state: RootState) => state.cedarPermissions?.initialized)
const cedarIsInitializing = useSelector(
(state: RootState) => state.cedarPermissions?.isInitializing,
)

const dashboardResourceId = useMemo(() => ADMIN_UI_RESOURCES.Dashboard, [])
const dashboardScopes = useMemo(
Expand Down Expand Up @@ -108,7 +151,9 @@ function DashboardPage() {
const formattedMonth =
currentMonth > 9 ? currentMonth.toString() : '0' + currentMonth.toString()
const yearMonth = currentYear.toString() + formattedMonth
const currentMonthData = statData.find(({ month }: any) => month.toString() === yearMonth)
const currentMonthData = statData.find(
(item: StatDataItem) => item.month.toString() === yearMonth,
)

const mau = currentMonthData?.mau
const token =
Expand All @@ -132,8 +177,8 @@ function DashboardPage() {
!requestStates.licenseRequested
) {
setRequestStates((prev) => ({ ...prev, licenseRequested: true }))
buildPayload(userAction as any, FETCHING_LICENSE_DETAILS, options as any)
dispatch(getLicenseDetails({} as any))
buildPayload(userAction as UserAction, FETCHING_LICENSE_DETAILS, options as ActionData)
dispatch(getLicenseDetails())
}
}, [
access_token,
Expand All @@ -155,8 +200,8 @@ function DashboardPage() {
!requestStates.clientsRequested
) {
setRequestStates((prev) => ({ ...prev, clientsRequested: true }))
buildPayload(userAction as any, 'Fetch openid connect clients', {} as any)
dispatch(getClients({ action: userAction } as any))
buildPayload(userAction as UserAction, 'Fetch openid connect clients', {} as ActionData)
dispatch(getClients())
}
}, [
access_token,
Expand All @@ -167,11 +212,6 @@ function DashboardPage() {
userAction,
])

const isUp = useCallback((status: any) => {
if (!status) return false
return status.toUpperCase() === 'ONLINE' || status.toUpperCase() === 'RUNNING'
}, [])

const summaryData = useMemo(() => {
const baseData = [
{
Expand All @@ -191,16 +231,17 @@ function DashboardPage() {
},
]

if (lock && lock.length > 0) {
if (lock && Array.isArray(lock) && lock.length > 0) {
const lockItem = lock[0] as LockDetailItem
baseData.push(
{
text: t('dashboard.mau_users'),
value: lock[0]?.monthly_active_users ?? 0,
value: lockItem?.monthly_active_users ?? 0,
icon: <JansLockUsers className={classes.summaryIcon} style={{ top: '8px' }} />,
},
{
text: t('dashboard.mau_clients'),
value: lock[0]?.monthly_active_clients ?? 0,
value: lockItem?.monthly_active_clients ?? 0,
icon: <JansLockClients className={classes.summaryIcon} style={{ top: '8px' }} />,
},
)
Expand Down Expand Up @@ -276,25 +317,32 @@ function DashboardPage() {
const getClassName = useCallback(
(key: string) => {
const value = getStatusValue(key)
return isUp(value) ? classes.checkText : classes.crossText
if (!value) return classes.crossText
const statusUpper = String(value).toUpperCase()
return statusUpper === 'RUNNING' || statusUpper === 'ONLINE'
? classes.checkText
: classes.crossText
},
[getStatusValue, isUp, classes.checkText, classes.crossText],
[getStatusValue, classes.checkText, classes.crossText],
)

const getStatusText = useCallback(
(key: string) => {
const value = getStatusValue(key)
return isUp(value) ? 'Running' : 'Down'
if (!value) return 'Unknown'
return value
},
[getStatusValue, isUp],
[getStatusValue],
)

const getStatusIcon = useCallback(
(key: string) => {
const value = getStatusValue(key)
return isUp(value) ? CheckIcon : CrossIcon
if (!value) return CrossIcon
const statusUpper = String(value).toUpperCase()
return statusUpper === 'RUNNING' || statusUpper === 'ONLINE' ? CheckIcon : CrossIcon
},
[getStatusValue, isUp],
[getStatusValue],
)

const StatusCard = useMemo(
Expand Down Expand Up @@ -351,7 +399,7 @@ function DashboardPage() {
dispatch(
auditLogoutLogs({
message: 'Logging out due to insufficient permissions for Admin UI access.',
} as any),
}),
)
} else {
navigateToRoute(ROUTES.LOGOUT)
Expand Down
Loading
Loading