Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
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
2 changes: 1 addition & 1 deletion admin-ui/app/utilities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ declare const require: {
useSubdirectories: boolean,
regExp: RegExp,
) => {
keys: () => string[];
keys: () => string[]
(id: string): any
}
}
Expand Down
46 changes: 43 additions & 3 deletions admin-ui/app/utils/dayjsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ dayjs.extend(customParseFormat)
// Re-export types for convenience
export type { Dayjs } from 'dayjs'

export const DATE_FORMATS = {
DATE_ONLY: 'YYYY-MM-DD',
DATETIME_SECONDS: 'YYYY-MM-DD HH:mm:ss',
DATETIME_AMPM: 'YYYY-MM-DD h:mm:ss A',
DATETIME_LONG: 'ddd, MMM DD, YYYY h:mm:ss A',
TOKEN_DATETIME: 'YYYY/DD/MM HH:mm:ss',
} as const

/**
* Calculate the difference between two dates
* @param dateA - The first date
* @param dateB - The second date
* @param unit - Optional unit of comparison (e.g., 'month', 'day', 'year')
* @returns The difference between the dates
*/
export const diffDate = (
dateA: string | number | Date | Dayjs,
dateB: string | number | Date | Dayjs,
unit?: OpUnitType,
): number => {
return dayjs(dateA).diff(dateB, unit)
}

/**
* Check if a date is the same as or before another date
* @param date - The date to check
Expand Down Expand Up @@ -71,7 +94,10 @@ export const isSameDate = (
}

/**
* Check if a date is valid
* Lenient date validity check using dayjs(date).isValid().
* Accepts string | number | Date | Dayjs | null | undefined; null/undefined return false.
* Note: this normalizes overflow dates (e.g. '2024-02-30') and is not suitable for strict format validation.
* For strict checks use parseDateStrict instead.
* @param date - The date to validate
* @returns true if date is valid
*/
Expand All @@ -89,7 +115,10 @@ export const getCurrentDate = (): Dayjs => {
}

/**
* Create a dayjs object from a date
* Create a Dayjs instance from a value.
* Accepts string | number | Date | Dayjs | null; when date is null/undefined, returns the current date/time.
* If a format is provided, parsing is lenient (non-strict) and may normalize overflow dates; use parseDateStrict for strict validation.
* Always returns a Dayjs instance.
* @param date - The date to parse (optional, defaults to current date)
* @param format - Optional format string for parsing
* @returns Dayjs object
Expand All @@ -107,6 +136,17 @@ export const createDate = (
return dayjs(date)
}

/**
* Parse a date string strictly - returns null if the date doesn't match the exact format
* @param date - The date string to parse
* @param format - The exact format to match
* @returns Dayjs object if valid, null if invalid
*/
export const parseDateStrict = (date: string, format: string): Dayjs | null => {
const parsed = dayjs(date, format, true)
return parsed.isValid() ? parsed : null
}

/**
* Format a date string
* @param date - The date to format
Expand All @@ -115,7 +155,7 @@ export const createDate = (
*/
export const formatDate = (
date: string | number | Date | Dayjs | null | undefined,
format = 'YYYY-MM-DD',
format: string = DATE_FORMATS.DATE_ONLY,
): string => {
if (date == null) return ''
const d = dayjs(date)
Expand Down
2 changes: 1 addition & 1 deletion admin-ui/config/webpack.config.client.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const webpackConfig: WebpackConfig & { devServer?: DevServerConfig } = {
priority: 17,
},
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios|formik|yup)[\\/]/,
test: /[\\/]node_modules[\\/](lodash|dayjs|axios|formik|yup)[\\/]/,
name: 'utils-vendor',
chunks: 'all',
priority: 16,
Expand Down
2 changes: 1 addition & 1 deletion admin-ui/config/webpack.config.client.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const webpackConfig: WebpackConfig & { devServer?: DevServerConfig } = {
priority: 17,
},
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios|formik|yup)[\\/]/,
test: /[\\/]node_modules[\\/](lodash|dayjs|axios|formik|yup)[\\/]/,
name: 'utils-vendor',
chunks: 'all',
priority: 16,
Expand Down
2 changes: 1 addition & 1 deletion admin-ui/docs/CODE_SPLITTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This document outlines the comprehensive code splitting strategy implemented in
- **Material-UI**: `@mui/*`, `@emotion/*`
- **Redux**: `@reduxjs/*`, `redux`, `redux-saga`, `redux-persist`
- **Charts**: `recharts`, `react-ace`, `ace-builds`
- **Utilities**: `lodash`, `moment`, `dayjs`, `axios`, `formik`, `yup`
- **Utilities**: `lodash`, `dayjs`, `axios`, `formik`, `yup`

#### Plugin-based Splitting

Expand Down
1 change: 0 additions & 1 deletion admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"node-fetch": "^3.3.1",
"openapi-merge-cli": "^1.3.2",
"path-browserify": "^1.0.1",
Expand Down
19 changes: 10 additions & 9 deletions admin-ui/plugins/admin/components/Assets/JansAssetListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
getAssetTypes,
} from 'Plugins/admin/redux/features/AssetSlice'
import customColors from '../../../../app/customColors'
import moment from 'moment'
import { Document, RootState, SearchEvent } from './types'
import { formatDate } from '@/utils/dayjsUtils'
import { Document, RootState } from './types'
import { DeleteAssetSagaPayload } from 'Plugins/admin/redux/features/types'
import { ADMIN_UI_RESOURCES } from '@/cedarling/utility'
import { CEDAR_RESOURCE_SCOPES } from '@/cedarling/constants/resourceScopes'
Expand Down Expand Up @@ -78,12 +78,13 @@ const JansAssetListPage: React.FC = () => {
)

const handleOptionsChange = useCallback(
(event: SearchEvent) => {
if (event.target.name === 'limit') {
memoLimit = Number(event.target.value)
} else if (event.target.name === 'pattern') {
memoPattern = String(event.target.value) || undefined
if (event.keyCode === 13) {
(event: React.ChangeEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>) => {
const target = event.target as HTMLInputElement
if (target.name === 'limit') {
memoLimit = Number(target.value)
} else if (target.name === 'pattern') {
memoPattern = String(target.value) || undefined
if ('keyCode' in event && event.keyCode === 13) {
const newOptions = {
limit: limit,
pattern: memoPattern,
Expand Down Expand Up @@ -305,7 +306,7 @@ const JansAssetListPage: React.FC = () => {
field: 'creationDate',
render: (rowData: Document) => (
<div style={{ wordWrap: 'break-word', maxWidth: '420px' }}>
{moment(rowData.creationDate).format('YYYY-MM-DD')}
{rowData.creationDate ? formatDate(rowData.creationDate, 'YYYY-MM-DD') : ''}
</div>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import { Box, Grid, MenuItem, Paper, TablePagination, TextField, Tooltip } from
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import getThemeColor from 'Context/theme/config'
import moment from 'moment'
import { deleteClientToken, getTokenByClient } from '../../redux/features/oidcSlice'
import ClientActiveTokenDetailPage from './ClientActiveTokenDetailPage'
import { Button } from 'Components'
import dayjs from 'dayjs'
import { formatDate, diffDate, createDate } from '@/utils/dayjsUtils'
import PropTypes from 'prop-types'
import { Button as MaterialButton } from '@mui/material'
import FilterListIcon from '@mui/icons-material/FilterList'
Expand Down Expand Up @@ -71,8 +70,8 @@ function ClientActiveTokens({ client }) {
setPageNumber(page)
let conditionquery = `clnId=${client.inum}`
if (pattern.dateAfter && pattern.dateBefore) {
conditionquery += `,${searchFilter}>${dayjs(pattern.dateAfter).format('YYYY-MM-DD')}`
conditionquery += `,${searchFilter}<${dayjs(pattern.dateBefore).format('YYYY-MM-DD')}`
conditionquery += `,${searchFilter}>${formatDate(pattern.dateAfter, 'YYYY-MM-DD')}`
conditionquery += `,${searchFilter}<${formatDate(pattern.dateBefore, 'YYYY-MM-DD')}`
}

getTokens(startCount, limit, conditionquery)
Expand Down Expand Up @@ -114,8 +113,8 @@ function ClientActiveTokens({ client }) {
const startCount = pageNumber * limit
let conditionquery = `clnId=${client.inum}`
if (pattern.dateAfter && pattern.dateBefore) {
conditionquery += `,${searchFilter}>${dayjs(pattern.dateAfter).format('YYYY-MM-DD')}`
conditionquery += `,${searchFilter}<${dayjs(pattern.dateBefore).format('YYYY-MM-DD')}`
conditionquery += `,${searchFilter}>${formatDate(pattern.dateAfter, 'YYYY-MM-DD')}`
conditionquery += `,${searchFilter}<${formatDate(pattern.dateBefore, 'YYYY-MM-DD')}`
}
getTokens(startCount, limit, conditionquery)
}
Expand All @@ -132,8 +131,8 @@ function ClientActiveTokens({ client }) {
const startCount = pageNumber * limit
let conditionquery = `clnId=${client.inum}`
if (pattern.dateAfter && pattern.dateBefore) {
conditionquery += `,${searchFilter}>${dayjs(pattern.dateAfter).format('YYYY-MM-DD')}`
conditionquery += `,${searchFilter}<${dayjs(pattern.dateBefore).format('YYYY-MM-DD')}`
conditionquery += `,${searchFilter}>${formatDate(pattern.dateAfter, 'YYYY-MM-DD')}`
conditionquery += `,${searchFilter}<${formatDate(pattern.dateBefore, 'YYYY-MM-DD')}`
}
getTokens(startCount, limit, conditionquery)
}
Expand Down Expand Up @@ -202,6 +201,13 @@ function ClientActiveTokens({ client }) {
const result = updatedToken?.items?.length
? updatedToken.items
.map((item) => {
const expirationDate = item.expirationDate
? formatDate(item.expirationDate, 'YYYY/DD/MM HH:mm:ss')
: ''
const creationDate = item.creationDate
? formatDate(item.creationDate, 'YYYY/DD/MM HH:mm:ss')
: ''

return {
id: item.tokenCode,
tokenCode: item.tokenCode,
Expand All @@ -210,15 +216,16 @@ function ClientActiveTokens({ client }) {
deletable: item.deletable,
attributes: item.attributes,
grantType: item.grantType,
expirationDate: moment(item.expirationDate).format('YYYY/DD/MM HH:mm:ss'),
creationDate: moment(item.creationDate).format('YYYY/DD/MM HH:mm:ss'),
expirationDate,
creationDate,
}
})
.sort((a, b) => {
return moment(b.creationDate, 'YYYY/DD/MM HH:mm:ss').diff(
moment(a.creationDate, 'YYYY/DD/MM HH:mm:ss'),
)
})
.sort((a, b) =>
diffDate(
createDate(b.creationDate, 'YYYY/DD/MM HH:mm:ss'),
createDate(a.creationDate, 'YYYY/DD/MM HH:mm:ss'),
),
)
: []
setData(result)
} else if (!updatedToken || (updatedToken && !updatedToken.items)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
import isEmpty from 'lodash/isEmpty'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import moment from 'moment'
import { formatDate } from '@/utils/dayjsUtils'
import AceEditor from 'react-ace'
import { Card, Col, Container, FormGroup } from 'Components'
import GluuLabel from 'Routes/Apps/Gluu/GluuLabel'
Expand Down Expand Up @@ -416,7 +416,9 @@ function ClientCibaParUmaPanel({
<FormGroup row>
<GluuLabel label={t('fields.creationTime')} size={3} />
<Col sm={9} className="top-5">
{moment(selectedUMA?.creationDate).format('ddd, MMM DD, YYYY h:mm:ss A')}
{selectedUMA?.creationDate
? formatDate(selectedUMA.creationDate, 'ddd, MMM DD, YYYY h:mm:ss A')
: ''}
</Col>
</FormGroup>
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { Col, FormGroup, Input, Card, CardBody, Accordion } from 'Components'
import moment from 'moment'
import { formatDate } from '@/utils/dayjsUtils'
import GluuLabel from 'Routes/Apps/Gluu/GluuLabel'
import customColors from '@/customColors'
import type { JwkItemProps } from '../types'
Expand Down Expand Up @@ -56,7 +56,7 @@ const JwkItem = React.memo(function JwkItem({ item, index }: JwkItemProps): Reac
data-testid="exp"
name="exp"
readOnly
defaultValue={item.exp != null ? moment(item.exp).format(DATE_FORMAT) : ''}
defaultValue={item.exp != null ? formatDate(item.exp, DATE_FORMAT) : ''}
/>
</Col>
<GluuLabel label="use" size={1} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import JwkListPage from './JwkListPage'
import { Provider } from 'react-redux'
import i18n from '../../../../../../app/i18n'
import { I18nextProvider } from 'react-i18next'
import moment from 'moment'
import { formatDate } from '../../../../../../app/utils/dayjsUtils'
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { mockJwksConfig } from '../__fixtures__/jwkTestData'
import { DATE_FORMAT } from '../constants'
import { useJwkApi } from '../hooks'

jest.mock('../hooks', () => ({
useJwkApi: jest.fn(() => ({
Expand Down Expand Up @@ -45,14 +46,13 @@ describe('JwkListPage', () => {
expect(screen.getByTestId('kty')).toHaveValue(firstKey?.kty ?? '')
expect(screen.getByTestId('use')).toHaveValue(firstKey?.use ?? '')
expect(screen.getByTestId('alg')).toHaveValue(firstKey?.alg ?? '')
if (firstKey?.exp != null) {
expect(screen.getByTestId('exp')).toHaveValue(moment(firstKey.exp).format(DATE_FORMAT))
}
const expectedExp = firstKey?.exp != null ? formatDate(firstKey.exp, DATE_FORMAT) : ''
expect(screen.getByTestId('exp')).toHaveValue(expectedExp)
})

it('should handle undefined exp gracefully', () => {
const { useJwkApi } = require('../hooks')
useJwkApi.mockReturnValue({
const mockedUseJwkApi = useJwkApi as jest.Mock
mockedUseJwkApi.mockReturnValue({
jwks: {
keys: [{ ...mockJwksConfig.keys[0], exp: undefined }],
},
Expand All @@ -67,8 +67,8 @@ describe('JwkListPage', () => {
})

it('should handle null exp gracefully', () => {
const { useJwkApi } = require('../hooks')
useJwkApi.mockReturnValue({
const mockedUseJwkApi = useJwkApi as jest.Mock
mockedUseJwkApi.mockReturnValue({
jwks: {
keys: [{ ...mockJwksConfig.keys[0], exp: null }],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import KeysPage from './KeysPage'
import { Provider } from 'react-redux'
import i18n from '../../../../../app/i18n'
import { I18nextProvider } from 'react-i18next'
import moment from 'moment'
import { formatDate } from '@/utils/dayjsUtils'
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import {
mockJwksConfig,
Expand Down Expand Up @@ -50,7 +50,7 @@ describe('KeysPage', () => {
expect(screen.getByTestId('use')).toHaveValue(firstKey?.use ?? '')
expect(screen.getByTestId('alg')).toHaveValue(firstKey?.alg ?? '')
if (firstKey?.exp != null) {
expect(screen.getByTestId('exp')).toHaveValue(moment(firstKey.exp).format(DATE_FORMAT))
expect(screen.getByTestId('exp')).toHaveValue(formatDate(firstKey.exp, DATE_FORMAT))
}
})

Expand Down
14 changes: 11 additions & 3 deletions admin-ui/plugins/auth-server/components/Scopes/ScopeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { SCOPE } from 'Utils/ApiResources'
import { useTranslation } from 'react-i18next'
import { ThemeContext } from 'Context/theme/themeContext'
import getThemeColor from 'Context/theme/config'
import moment from 'moment'
import { formatDate } from '@/utils/dayjsUtils'
import { adminUiFeatures } from 'Plugins/admin/helper/utils'
import customColors from '@/customColors'
import type { ScopeFormProps, ScopeFormValues, ScopeClient, ExtendedScope } from './types'
Expand Down Expand Up @@ -605,7 +605,11 @@ const ScopeForm: React.FC<ScopeFormProps> = ({
<GluuLabel label="fields.creationDate" size={4} />
<Col sm={8}>
<Input
defaultValue={moment(scope.creationDate).format('YYYY-MM-DD HH:mm:ss')}
defaultValue={
scope.creationDate
? formatDate(scope.creationDate, 'YYYY-MM-DD HH:mm:ss')
: ''
}
disabled={true}
/>
</Col>
Expand Down Expand Up @@ -672,7 +676,11 @@ const ScopeForm: React.FC<ScopeFormProps> = ({
<GluuLabel label="fields.creationDate" size={4} />
<Col sm={8}>
<Input
defaultValue={moment(scope.creationDate).format('YYYY-MM-DD HH:mm:ss')}
defaultValue={
scope.creationDate
? formatDate(scope.creationDate, 'YYYY-MM-DD HH:mm:ss')
: ''
}
disabled={true}
/>
</Col>
Expand Down
Loading
Loading