Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"homepage": "/dashboard",
"dependencies": {
"@devtron-labs/devtron-fe-common-lib": "1.20.6-pre-51",
"@devtron-labs/devtron-fe-common-lib": "1.20.6-pre-53",
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rjsf/core": "^5.13.3",
"@rjsf/utils": "^5.13.3",
Expand All @@ -23,7 +23,6 @@
"moment": "^2.29.4",
"query-string": "^7.1.1",
"react": "^17.0.2",
"react-dates": "^21.8.0",
"react-dom": "^17.0.2",
"react-ga4": "^1.4.1",
"react-gtm-module": "^2.0.11",
Expand Down Expand Up @@ -74,7 +73,6 @@
"@types/jest": "^27.4.1",
"@types/node": "20.11.0",
"@types/react": "17.0.39",
"@types/react-dates": "^21.8.6",
"@types/react-dom": "17.0.13",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ import {
ButtonStyleType,
ButtonVariantType,
ComponentSizeType,
DATE_TIME_FORMATS,
FeatureTitleWithInfo,
GenericFilterEmptyState,
} from '@devtron-labs/devtron-fe-common-lib'

import { ReactComponent as Trash } from '../../../../assets/icons/ic-delete-interactive.svg'
import { ReactComponent as Key } from '../../../../assets/icons/ic-key-bulb.svg'
import { ReactComponent as Edit } from '../../../../assets/icons/ic-pencil.svg'
import { HEADER_TEXT, MomentDateFormat } from '../../../../config'
import { HEADER_TEXT } from '../../../../config'
import { APITokenListType, TokenListType } from './apiToken.type'
import { isTokenExpired } from './apiToken.utils'
import DeleteAPITokenModal from './DeleteAPITokenModal'
Expand Down Expand Up @@ -126,7 +127,9 @@ const APITokenList = ({ tokenList, renderSearchToken, reload }: APITokenListType
</div>
<div className="dc__ellipsis-right">
{list.lastUsedAt
? moment(list.lastUsedAt).format(MomentDateFormat)
? moment(list.lastUsedAt).format(
DATE_TIME_FORMATS.WEEKDAY_WITH_DATE_MONTH_AND_YEAR,
)
: 'Never used'}
</div>
<div>{list.lastUsedByIp ? list.lastUsedByIp : '-'}</div>
Expand All @@ -136,7 +139,7 @@ const APITokenList = ({ tokenList, renderSearchToken, reload }: APITokenListType
) : (
<>
{isTokenExpired(list.expireAtInMs) ? 'Expired on ' : ''}
{moment(list.expireAtInMs).format(MomentDateFormat)}
{moment(list.expireAtInMs).format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT)}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import APITokenList from './APITokenList'
import CreateAPIToken from './CreateAPIToken'
import EditAPIToken from './EditAPIToken'
import { getGeneratedAPITokenList } from './service'
import { ExpirationDateSelectOptionType } from './types'

import './apiToken.scss'

Expand All @@ -48,7 +49,7 @@ const ApiTokens = () => {
const [errorStatusCode, setErrorStatusCode] = useState(0)
const [showGenerateModal, setShowGenerateModal] = useState(false)
const [showRegenerateTokenModal, setShowRegenerateTokenModal] = useState(false)
const [selectedExpirationDate, setSelectedExpirationDate] = useState<{ label: string; value: number }>({
const [selectedExpirationDate, setSelectedExpirationDate] = useState<ExpirationDateSelectOptionType>({
label: '30 days',
value: 30,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { useEffect, useState } from 'react'
import { useHistory, useRouteMatch } from 'react-router-dom'
import { Moment } from 'moment'
import dayjs from 'dayjs'

import {
CustomInput,
Expand Down Expand Up @@ -49,6 +49,7 @@ import ExpirationDate from './ExpirationDate'
import GenerateActionButton from './GenerateActionButton'
import GenerateModal from './GenerateModal'
import { createGeneratedAPIToken } from './service'
import { ExpirationDateSelectOptionType } from './types'
import { ValidationRules } from './validationRules'

const showStatus = !!importComponentFromFELibrary('StatusHeaderCell', null, 'function')
Expand Down Expand Up @@ -103,7 +104,7 @@ const CreateAPIToken = ({
isSaveDisabled,
allowManageAllAccess,
} = usePermissionConfiguration()
const [customDate, setCustomDate] = useState<Moment>(null)
const [customDate, setCustomDate] = useState<Date>(dayjs().add(1, 'day').toDate())
const [tokenResponse, setTokenResponse] = useState<TokenResponseType>({
success: false,
token: '',
Expand Down Expand Up @@ -147,11 +148,11 @@ const CreateAPIToken = ({
}
}

const onCustomDateChange = (event) => {
setCustomDate(event)
const onCustomDateChange = (date: Date) => {
setCustomDate(date)
setFormData({
...formData,
expireAtInMs: event.valueOf(),
expireAtInMs: date.valueOf(),
dateType: 'Custom',
})

Expand All @@ -167,12 +168,15 @@ const CreateAPIToken = ({
history.push(`${match.path.split('create')[0]}list`)
}

const onChangeSelectFormData = (selectedOption: { label: string; value: number }) => {
const onChangeSelectFormData = (selectedOption: ExpirationDateSelectOptionType) => {
setSelectedExpirationDate(selectedOption)
const parsedMilliseconds = selectedOption.value === 0 ? 0 : getDateInMilliseconds(selectedOption.value)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getDateInMilliseconds is being called with selectedOption.value which can be either a number (days) or a Date object (when "Custom" is selected), but the function only accepts numbers. This will cause incorrect behavior.

The logic should be:

const parsedMilliseconds = selectedOption.value === 0 
    ? 0 
    : typeof selectedOption.value === 'number' 
        ? getDateInMilliseconds(selectedOption.value) 
        : selectedOption.value.valueOf()

Copilot uses AI. Check for mistakes.

setFormData({
...formData,
expireAtInMs: selectedOption.value === 0 ? 0 : getDateInMilliseconds(selectedOption.value),
dateType: selectedOption.label,
expireAtInMs:
typeof selectedOption.value === 'number' ? parsedMilliseconds : selectedOption.value.valueOf(),
dateType: selectedOption.label as string,
})
}

Expand Down Expand Up @@ -283,7 +287,7 @@ const CreateAPIToken = ({
placeholder="Enter a description to remember where you have used this token"
error={formDataErrorObj.invalidDescription && formDataErrorObj.invalidDescriptionMessage}
/>
<label className="form__row">
<div className="form__row">
<div className="flex left">
<ExpirationDate
selectedExpirationDate={selectedExpirationDate}
Expand All @@ -299,7 +303,7 @@ const CreateAPIToken = ({
Custom expiration can't be blank. Please select a date.
</span>
)}
</label>
</div>
<div className="dc__border-top" />
<PermissionConfigurationForm showUserPermissionGroupSelector isAddMode />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import React, { useEffect, useMemo, useState } from 'react'
import { useHistory, useParams, useRouteMatch } from 'react-router-dom'
import dayjs from 'dayjs'
import moment from 'moment'

import {
Expand All @@ -26,6 +27,7 @@ import {
ButtonVariantType,
ClipboardButton,
CustomInput,
DATE_TIME_FORMATS,
ErrorScreenManager,
Icon,
InfoBlock,
Expand All @@ -39,7 +41,6 @@ import {
} from '@devtron-labs/devtron-fe-common-lib'

import { importComponentFromFELibrary } from '../../../../components/common'
import { MomentDateFormat } from '../../../../config'
import { API_COMPONENTS } from '../../../../config/constantMessaging'
import { createOrUpdateUser, getUserById } from '../authorization.service'
import { getDefaultUserStatusAndTimeout } from '../libUtils'
Expand Down Expand Up @@ -89,7 +90,7 @@ const EditAPIToken = ({
const { serverMode } = useMainContext()
const [loader, setLoader] = useState(false)

const [customDate, setCustomDate] = useState<number>(undefined)
const [customDate, setCustomDate] = useState<Date>(dayjs().add(1, 'day').toDate())
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false)
const [invalidDescription, setInvalidDescription] = useState(false)

Expand Down Expand Up @@ -177,7 +178,7 @@ const EditAPIToken = ({
return (
<span className="fw-6 cn-9">
This token {isTokenExpired(editData.expireAtInMs) ? 'expired' : 'expires'} on&nbsp;
{moment(editData.expireAtInMs).format(MomentDateFormat)}.
{moment(editData.expireAtInMs).format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT)}.
</span>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,56 @@
/* eslint-disable react/prop-types */
import moment from 'moment'

import { InfoBlock, SelectPicker } from '@devtron-labs/devtron-fe-common-lib'
import {
ComponentSizeType,
DATE_TIME_FORMATS,
DateTimePicker,
InfoBlock,
SelectPicker,
} from '@devtron-labs/devtron-fe-common-lib'

import { SingleDatePickerComponent } from '../../../../components/common'
import { MomentDateFormat } from '../../../../config'
import { getDateInMilliseconds, getOptions } from './apiToken.utils'
import { ExpirationDateProps } from './types'

const ExpirationDate = ({ selectedExpirationDate, onChangeSelectFormData, handleDatesChange, customDate }) => (
const ExpirationDate = ({
selectedExpirationDate,
onChangeSelectFormData,
handleDatesChange,
customDate,
}: ExpirationDateProps) => (
<div className="w-100">
<div className="flex left bottom dc__gap-16">
<div className="w-200">
<SelectPicker
<SelectPicker<number | Date, false>
label="Expiration"
required
inputId="token-expiry-duration"
value={selectedExpirationDate}
options={getOptions(customDate)}
classNamePrefix="select-token-expiry-duration"
onChange={onChangeSelectFormData}
size={ComponentSizeType.large}
/>
</div>

{selectedExpirationDate.label !== 'Custom' && selectedExpirationDate.label !== 'No expiration' && (
<span className="fs-13 fw-4 cn-9">
<span>This token will expire on</span>&nbsp;
{moment(getDateInMilliseconds(selectedExpirationDate.value)).format(MomentDateFormat)}
{moment(getDateInMilliseconds(selectedExpirationDate.value)).format(
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM,
)}
Comment on lines +55 to +57
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getDateInMilliseconds expects a number (days), but selectedExpirationDate.value can be either a number or a Date when "Custom" is selected. This will cause incorrect calculations or runtime errors when a custom date is selected.

The code should check the type before calling getDateInMilliseconds:

{moment(typeof selectedExpirationDate.value === 'number' 
    ? getDateInMilliseconds(selectedExpirationDate.value) 
    : selectedExpirationDate.value
).format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM)}
Suggested change
{moment(getDateInMilliseconds(selectedExpirationDate.value)).format(
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM,
)}
{typeof selectedExpirationDate.value === 'number'
? moment(getDateInMilliseconds(selectedExpirationDate.value)).format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM)
: ''}

Copilot uses AI. Check for mistakes.
</span>
)}
{selectedExpirationDate.label === 'No expiration' && (
<span className="ml-16 fs-13 fw-4 cn-9">The token will never expire!</span>
)}
{selectedExpirationDate.label === 'Custom' && (
<div className="w-200 ml-16">
<SingleDatePickerComponent
date={customDate}
handleDatesChange={handleDatesChange}
readOnly
isTodayBlocked
/>
</div>
<DateTimePicker
id="expiration-date-picker"
date={customDate}
onChange={handleDatesChange}
isTodayBlocked
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DateTimePicker component is missing the hideTimeSelect prop. Since this is for selecting an expiration date (not time), the time selection should be hidden for consistency with other date pickers in the codebase and to avoid confusion.

Add:

<DateTimePicker
    id="expiration-date-picker"
    date={customDate}
    onChange={handleDatesChange}
    isTodayBlocked
    hideTimeSelect
/>
Suggested change
isTodayBlocked
isTodayBlocked
hideTimeSelect

Copilot uses AI. Check for mistakes.
/>
)}
</div>
{selectedExpirationDate.label === 'No expiration' && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import ExpirationDate from './ExpirationDate'
import GenerateActionButton from './GenerateActionButton'
import GenerateModal from './GenerateModal'
import { updateGeneratedAPIToken } from './service'
import { ExpirationDateProps, ExpirationDateSelectOptionType } from './types'

const RegeneratedModal = ({
close,
Expand All @@ -38,7 +39,7 @@ const RegeneratedModal = ({
}: RegenerateModalType) => {
const [loader, setLoader] = useState(false)
const [showGenerateModal, setShowGenerateModal] = useState(false)
const [selectedExpirationDate, setSelectedExpirationDate] = useState<{ label: string; value: number }>({
const [selectedExpirationDate, setSelectedExpirationDate] = useState<ExpirationDateSelectOptionType>({
label: '30 days',
value: 30,
})
Expand All @@ -48,18 +49,22 @@ const RegeneratedModal = ({
)
const [invalidCustomDate, setInvalidCustomDate] = useState(false)

const onChangeSelectFormData = (selectedOption: { label: string; value: number }) => {
setRegeneratedExpireAtInMs(selectedOption.value === 0 ? 0 : getDateInMilliseconds(selectedOption.value))
const onChangeSelectFormData: ExpirationDateProps['onChangeSelectFormData'] = (selectedOption) => {
const parsedMilliseconds = selectedOption.value === 0 ? 0 : getDateInMilliseconds(selectedOption.value)

setRegeneratedExpireAtInMs(
typeof selectedOption.value === 'number' ? parsedMilliseconds : selectedOption.value.valueOf(),
)
Comment on lines +53 to +57
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getDateInMilliseconds is being called with selectedOption.value which can be either a number (days) or a Date object (when "Custom" is selected), but the function only accepts numbers. This will cause incorrect behavior.

The logic should be:

const parsedMilliseconds = selectedOption.value === 0 
    ? 0 
    : typeof selectedOption.value === 'number' 
        ? getDateInMilliseconds(selectedOption.value) 
        : selectedOption.value.valueOf()
Suggested change
const parsedMilliseconds = selectedOption.value === 0 ? 0 : getDateInMilliseconds(selectedOption.value)
setRegeneratedExpireAtInMs(
typeof selectedOption.value === 'number' ? parsedMilliseconds : selectedOption.value.valueOf(),
)
const parsedMilliseconds = selectedOption.value === 0
? 0
: typeof selectedOption.value === 'number'
? getDateInMilliseconds(selectedOption.value)
: selectedOption.value.valueOf()
setRegeneratedExpireAtInMs(parsedMilliseconds)

Copilot uses AI. Check for mistakes.
setSelectedExpirationDate(selectedOption)

if (selectedOption.label === 'Custom' && invalidCustomDate) {
setInvalidCustomDate(false)
}
}

const handleDatesChange = (event): void => {
setCustomDate(event)
setRegeneratedExpireAtInMs(event.valueOf())
const handleDatesChange = (date: Date): void => {
setCustomDate(date)
setRegeneratedExpireAtInMs(date.valueOf())

if (invalidCustomDate) {
setInvalidCustomDate(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import React from 'react'

import { GenericModalProps } from '@devtron-labs/devtron-fe-common-lib'

import { ExpirationDateSelectOptionType } from './types'

export interface FormType {
name: string
description: string
Expand All @@ -36,8 +38,8 @@ export interface GenerateTokenType {
showGenerateModal: boolean
setShowGenerateModal: React.Dispatch<React.SetStateAction<boolean>>
handleGenerateTokenActionButton: () => void
setSelectedExpirationDate
selectedExpirationDate
setSelectedExpirationDate: React.Dispatch<React.SetStateAction<ExpirationDateSelectOptionType>>
selectedExpirationDate: ExpirationDateSelectOptionType
reload: () => void
}

Expand Down Expand Up @@ -95,8 +97,8 @@ export interface RegenerateModalType {
close: () => void
setShowRegeneratedModal: React.Dispatch<React.SetStateAction<boolean>>
editData: EditDataType
customDate: number
setCustomDate: React.Dispatch<React.SetStateAction<number>>
customDate: Date
setCustomDate: React.Dispatch<React.SetStateAction<Date>>
reload: () => void
redirectToTokenList: () => void
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@
* limitations under the License.
*/

import { SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib'

import { TokenListType } from './apiToken.type'

export function getOptions(customDate) {
return [
{ value: 7, label: '7 days' },
{ value: 30, label: '30 days' },
{ value: 60, label: '60 days' },
{ value: 90, label: '90 days' },
{ value: customDate, label: 'Custom' },
{ value: 0, label: 'No expiration' },
]
}
export const getOptions = (customDate: Date): SelectPickerOptionType<number | Date>[] => [
{ value: 7, label: '7 days' },
{ value: 30, label: '30 days' },
{ value: 60, label: '60 days' },
{ value: 90, label: '90 days' },
{ value: customDate, label: 'Custom' },
{ value: 0, label: 'No expiration' },
]

const millisecondsInDay = 86400000

Expand Down
10 changes: 10 additions & 0 deletions src/Pages/GlobalConfigurations/Authorization/APITokens/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib'

export type ExpirationDateSelectOptionType = SelectPickerOptionType<number | Date>

export interface ExpirationDateProps {
selectedExpirationDate: ExpirationDateSelectOptionType
onChangeSelectFormData: (value: ExpirationDateSelectOptionType) => void
handleDatesChange: (date: Date) => void
customDate: Date
}
Loading