Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type OrganizationPolicyApiToken } from 'qovery-typescript-axios'
import { useState } from 'react'
import { dateUTCString } from '@qovery/shared/util-dates'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { PolicyApiTokenList } from './policy-api-token-list'

Expand Down Expand Up @@ -31,6 +33,48 @@ describe('PolicyApiTokenList', () => {
jest.clearAllMocks()
})

afterEach(() => {
jest.useRealTimers()
})

it('should show last usage next to creation, relative to when the list loaded', async () => {
jest.useFakeTimers().setSystemTime(new Date('2026-10-13T12:00:00Z'))
const lastUsedAt = '2026-10-13T11:55:00Z'
const tokens = [{ ...policyApiToken, last_used_at: lastUsedAt }]
const onDelete = jest.fn()

function TestPage() {
const [renderCount, setRenderCount] = useState(0)
return (
<>
<button onClick={() => setRenderCount(renderCount + 1)}>Render {renderCount}</button>
<PolicyApiTokenList policyApiTokens={tokens} onDelete={onDelete} />
</>
)
}

const { userEvent } = renderWithProviders(<TestPage />)

const lastUsed = screen.getByText('Last used 5 minutes ago')
expect(lastUsed).toHaveAttribute('title', dateUTCString(lastUsedAt))
expect(lastUsed.previousElementSibling).toHaveTextContent('Created since')

jest.setSystemTime(new Date('2026-10-13T13:00:00Z'))
await userEvent.click(screen.getByRole('button', { name: 'Render 0' }))

expect(screen.getByRole('button', { name: 'Render 1' })).toBeInTheDocument()
expect(screen.getByText('Last used 5 minutes ago')).toBeInTheDocument()
})

it.each([null, undefined])('should hide last usage when the timestamp is %s', (lastUsedAt) => {
renderWithProviders(
<PolicyApiTokenList policyApiTokens={[{ ...policyApiToken, last_used_at: lastUsedAt }]} onDelete={jest.fn()} />
)

expect(screen.getByTestId(`policy-token-list-${policyApiToken.id}`)).toBeInTheDocument()
expect(screen.queryByText(/Last used/)).not.toBeInTheDocument()
})

it('should render an empty state', () => {
renderWithProviders(<PolicyApiTokenList policyApiTokens={[]} onDelete={jest.fn()} />)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { formatDistanceStrict } from 'date-fns'
import { type OrganizationPolicyApiToken } from 'qovery-typescript-axios'
import { useState } from 'react'
import { Button, EmptyState, Icon, Skeleton, Tooltip, Truncate, useModal } from '@qovery/shared/ui'
import { dateMediumLocalFormat, dateUTCString } from '@qovery/shared/util-dates'
import { upperCaseFirstLetter } from '@qovery/shared/util-js'
Expand All @@ -12,6 +14,7 @@ export interface PolicyApiTokenListProps {

export function PolicyApiTokenList({ policyApiTokens, onDelete }: PolicyApiTokenListProps) {
const { openModal, closeModal } = useModal()
const [loadedAt] = useState(() => new Date())

if (policyApiTokens.length === 0) {
return (
Expand Down Expand Up @@ -53,10 +56,15 @@ export function PolicyApiTokenList({ policyApiTokens, onDelete }: PolicyApiToken
)}
{token.creator_name && <span className="mr-3 inline-block">Created by {token.creator_name}</span>}
{token.created_at && (
<span className="inline-block" title={dateUTCString(token.created_at)}>
<span className="mr-3 inline-block" title={dateUTCString(token.created_at)}>
Created since {dateMediumLocalFormat(token.created_at)}
</span>
)}
{token.last_used_at && (
<span className="inline-block" title={dateUTCString(token.last_used_at)}>
Last used {formatDistanceStrict(new Date(token.last_used_at), loadedAt, { addSuffix: true })}
</span>
)}
</p>
</div>
<div className="flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type OrganizationApiToken } from 'qovery-typescript-axios'
import { useState } from 'react'
import { dateUTCString } from '@qovery/shared/util-dates'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import * as useApiTokensModule from '../hooks/use-api-tokens/use-api-tokens'
import * as useDeleteApiTokenModule from '../hooks/use-delete-api-token/use-delete-api-token'
Expand Down Expand Up @@ -37,6 +39,51 @@ describe('SettingsApiToken', () => {
})
})

afterEach(() => {
jest.useRealTimers()
})

it('should show last usage next to creation, relative to when the list loaded', async () => {
jest.useFakeTimers().setSystemTime(new Date('2026-10-13T12:00:00Z'))
const lastUsedAt = '2026-10-13T10:00:00Z'
useApiTokensMockSpy.mockReturnValue({
data: [{ id: 'token-1', name: 'My token', created_at: '2024-01-01T00:00:00Z', last_used_at: lastUsedAt }],
})

function TestPage() {
const [renderCount, setRenderCount] = useState(0)
return (
<>
<button onClick={() => setRenderCount(renderCount + 1)}>Render {renderCount}</button>
<SettingsApiToken />
</>
)
}

const { userEvent } = renderWithProviders(<TestPage />)

const lastUsed = screen.getByText('Last used 2 hours ago')
expect(lastUsed).toHaveAttribute('title', dateUTCString(lastUsedAt))
expect(lastUsed.previousElementSibling).toHaveTextContent('Created since')

jest.setSystemTime(new Date('2026-10-13T13:00:00Z'))
await userEvent.click(screen.getByRole('button', { name: 'Render 0' }))

expect(screen.getByRole('button', { name: 'Render 1' })).toBeInTheDocument()
expect(screen.getByText('Last used 2 hours ago')).toBeInTheDocument()
})

it.each([null, undefined])('should hide last usage when the timestamp is %s', (lastUsedAt) => {
useApiTokensMockSpy.mockReturnValue({
data: [{ id: 'token-1', name: 'My token', last_used_at: lastUsedAt }],
})

renderWithProviders(<SettingsApiToken />)

expect(screen.getByTestId('token-list-token-1')).toBeInTheDocument()
expect(screen.queryByText(/Last used/)).not.toBeInTheDocument()
})

it('should render empty state', () => {
useApiTokensMockSpy.mockReturnValue({ data: [] })

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useParams } from '@tanstack/react-router'
import { formatDistanceStrict } from 'date-fns'
import { type OrganizationApiToken } from 'qovery-typescript-axios'
import { Suspense } from 'react'
import { Suspense, useState } from 'react'
import { SettingsHeading } from '@qovery/shared/console-shared'
import { useModal, useModalConfirmation } from '@qovery/shared/ui'
import { BlockContent, Button, Icon, Section, Skeleton, Tooltip, Truncate } from '@qovery/shared/ui'
Expand All @@ -18,13 +19,14 @@ interface PageOrganizationApiProps {

function PageOrganizationApi(props: PageOrganizationApiProps) {
const { apiTokens, onDelete } = props
const [loadedAt] = useState(() => new Date())
Comment thread
erebe marked this conversation as resolved.

return (
<div className="max-w-content-with-navigation-left">
<BlockContent title="Token List" classNameContent="p-0">
{apiTokens.length > 0 ? (
<ul>
{apiTokens.map((token: OrganizationApiToken) => (
{apiTokens.map((token) => (
<li
data-testid={`token-list-${token.id}`}
key={token.id}
Expand All @@ -45,10 +47,15 @@ function PageOrganizationApi(props: PageOrganizationApiProps) {
<p className="text-xs text-neutral-subtle">
<span className="mr-3 inline-block">Role: {upperCaseFirstLetter(token.role_name)}</span>
{token.created_at && (
<span className="inline-block" title={dateUTCString(token.created_at)}>
<span className="mr-3 inline-block" title={dateUTCString(token.created_at)}>
Created since {dateMediumLocalFormat(token.created_at)}
</span>
)}
{token.last_used_at && (
<span className="inline-block" title={dateUTCString(token.last_used_at)}>
Last used {formatDistanceStrict(new Date(token.last_used_at), loadedAt, { addSuffix: true })}
</span>
)}
</p>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"mermaid": "11.6.0",
"monaco-editor": "0.53.0",
"posthog-js": "1.345.1",
"qovery-typescript-axios": "1.1.1010",
"qovery-typescript-axios": "1.1.1011",
"react": "19.3.0",
"react-country-flag": "3.0.2",
"react-datepicker": "4.12.0",
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6285,7 +6285,7 @@ __metadata:
prettier: 3.2.5
prettier-plugin-tailwindcss: 0.5.14
pretty-quick: 4.0.0
qovery-typescript-axios: 1.1.1010
qovery-typescript-axios: 1.1.1011
qovery-ws-typescript-axios: 0.1.666
react: 19.3.0
react-country-flag: 3.0.2
Expand Down Expand Up @@ -24656,12 +24656,12 @@ __metadata:
languageName: node
linkType: hard

"qovery-typescript-axios@npm:1.1.1010":
version: 1.1.1010
resolution: "qovery-typescript-axios@npm:1.1.1010"
"qovery-typescript-axios@npm:1.1.1011":
version: 1.1.1011
resolution: "qovery-typescript-axios@npm:1.1.1011"
dependencies:
axios: 1.20.0
checksum: a1957c4f49a2c1d131d3826751e0e650f8494c8819e8d1dba0f0989d69f76f7ef6588a3636e950a92cfb5c46b8380fa17863a47ced60d046f181ca31c1e94c01
checksum: 2cb3f535fab0de6a47beae6e800871eaa3b075aa2258d78e8f2addb45f2e352db7f6e56e989c10e68a3ec6b770dd73e297238faf91678f36afb3f015ffe80783
languageName: node
linkType: hard

Expand Down
Loading