diff --git a/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.spec.tsx
index 35a7826bbed..31d486a01fa 100644
--- a/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.spec.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.spec.tsx
@@ -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'
@@ -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 (
+ <>
+
+
+ >
+ )
+ }
+
+ const { userEvent } = renderWithProviders()
+
+ 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(
+
+ )
+
+ expect(screen.getByTestId(`policy-token-list-${policyApiToken.id}`)).toBeInTheDocument()
+ expect(screen.queryByText(/Last used/)).not.toBeInTheDocument()
+ })
+
it('should render an empty state', () => {
renderWithProviders()
diff --git a/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.tsx b/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.tsx
index f9ee442ada6..67103cdf219 100644
--- a/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-api-token/policy-api-token/policy-api-token-list.tsx
@@ -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'
@@ -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 (
@@ -53,10 +56,15 @@ export function PolicyApiTokenList({ policyApiTokens, onDelete }: PolicyApiToken
)}
{token.creator_name && Created by {token.creator_name}}
{token.created_at && (
-
+
Created since {dateMediumLocalFormat(token.created_at)}
)}
+ {token.last_used_at && (
+
+ Last used {formatDistanceStrict(new Date(token.last_used_at), loadedAt, { addSuffix: true })}
+
+ )}
diff --git a/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.spec.tsx
index cb19ed636c7..2b531fb558c 100644
--- a/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.spec.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.spec.tsx
@@ -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'
@@ -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 (
+ <>
+
+
+ >
+ )
+ }
+
+ const { userEvent } = renderWithProviders(
)
+
+ 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(
)
+
+ expect(screen.getByTestId('token-list-token-1')).toBeInTheDocument()
+ expect(screen.queryByText(/Last used/)).not.toBeInTheDocument()
+ })
+
it('should render empty state', () => {
useApiTokensMockSpy.mockReturnValue({ data: [] })
diff --git a/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.tsx b/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.tsx
index c794dc8bcc6..ef5d0cd1694 100644
--- a/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-api-token/settings-api-token.tsx
@@ -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'
@@ -18,13 +19,14 @@ interface PageOrganizationApiProps {
function PageOrganizationApi(props: PageOrganizationApiProps) {
const { apiTokens, onDelete } = props
+ const [loadedAt] = useState(() => new Date())
return (
{apiTokens.length > 0 ? (
- {apiTokens.map((token: OrganizationApiToken) => (
+ {apiTokens.map((token) => (
-
Role: {upperCaseFirstLetter(token.role_name)}
{token.created_at && (
-
+
Created since {dateMediumLocalFormat(token.created_at)}
)}
+ {token.last_used_at && (
+
+ Last used {formatDistanceStrict(new Date(token.last_used_at), loadedAt, { addSuffix: true })}
+
+ )}
diff --git a/package.json b/package.json
index 25c491e7ab9..e7af72cca59 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/yarn.lock b/yarn.lock
index 06eda2225d7..6a9c4a5161a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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
@@ -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