Skip to content

Commit cd8bee9

Browse files
deo002heliocastro
authored andcommitted
refactor(tokens): Migrate tokens table to tanstack
1 parent 65e02b8 commit cd8bee9

File tree

1 file changed

+151
-96
lines changed

1 file changed

+151
-96
lines changed

src/app/[locale]/preferences/components/TokensTable.tsx

Lines changed: 151 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -10,86 +10,49 @@
1010

1111
'use client'
1212

13-
import { AccessToken, Embedded, HttpStatus } from '@/object-types'
13+
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table'
14+
import { getSession, signOut, useSession } from 'next-auth/react'
15+
import { useTranslations } from 'next-intl'
16+
import { SW360Table } from 'next-sw360'
17+
import React, { ReactNode, useEffect, useMemo, useState } from 'react'
18+
import { Button, Spinner } from 'react-bootstrap'
19+
import { AccessToken, Embedded, ErrorDetails, HttpStatus } from '@/object-types'
1420
import MessageService from '@/services/message.service'
1521
import { ApiUtils, CommonUtils } from '@/utils/index'
16-
import { getSession, signOut } from 'next-auth/react'
17-
import { useTranslations } from 'next-intl'
18-
import { Table, _ } from 'next-sw360'
19-
import React, { ReactNode, useCallback, useEffect, useState, type JSX } from 'react'
20-
import { Button } from 'react-bootstrap'
22+
23+
type EmbeddedAccessTokens = Embedded<AccessToken, 'sw360:restApiTokens'>
2124

2225
interface Props {
2326
generatedToken: string
2427
}
2528

2629
const TokensTable = ({ generatedToken }: Props): ReactNode => {
2730
const t = useTranslations('default')
28-
const [tableData, setTableData] = useState<(string | JSX.Element)[][]>([])
29-
30-
const fetchData = useCallback(async (url: string) => {
31-
try {
32-
const session = await getSession()
33-
if (CommonUtils.isNullOrUndefined(session)) return signOut()
34-
const response = await ApiUtils.GET(url, session.user.access_token)
35-
36-
if (response.status === HttpStatus.OK) {
37-
const data: Embedded<AccessToken, 'sw360:restApiTokens'> = (await response.json()) as Embedded<
38-
AccessToken,
39-
'sw360:restApiTokens'
40-
>
31+
const session = useSession()
32+
const [revoked, setRevoked] = useState(false)
4133

42-
const tableData = Object.values(data._embedded['sw360:restApiTokens']).map((token: AccessToken) => {
43-
const expirationDate = new Date(
44-
Date.parse(token.createdOn + ' +0000') +
45-
token.numberOfDaysValid * 24 * 60 * 60 * 1000 -
46-
new Date().getTimezoneOffset() * 60000,
47-
)
48-
49-
return [
50-
token.name,
51-
new Date(Date.parse(token.createdOn + ' +0000') - new Date().getTimezoneOffset() * 60000)
52-
.toISOString()
53-
.slice(0, 19)
54-
.replace('T', ' '),
55-
expirationDate.toISOString().slice(0, 19).replace('T', ' '),
56-
'[' + token.authorities.join(', ') + ']',
57-
_(
58-
<Button
59-
variant='danger'
60-
onClick={() => {
61-
revokeToken(token.name).catch((error) => console.error(error))
62-
}}
63-
>
64-
{t('Revoke Token')}
65-
</Button>,
66-
),
67-
]
68-
})
69-
setTableData(tableData)
70-
} else if (response.status === HttpStatus.UNAUTHORIZED) {
71-
await signOut()
72-
} else {
73-
setTableData([])
74-
}
75-
} catch (error) {
76-
console.error('Error fetching data:', error)
77-
setTableData([])
34+
useEffect(() => {
35+
if (session.status === 'unauthenticated') {
36+
void signOut()
7837
}
79-
}, [])
38+
}, [
39+
session,
40+
])
8041

8142
const revokeToken = async (tokenName: string) => {
8243
try {
8344
const session = await getSession()
8445
if (CommonUtils.isNullOrUndefined(session)) return signOut()
8546
const response = await ApiUtils.DELETE(
86-
CommonUtils.createUrlWithParams('users/tokens', { name: tokenName }),
47+
CommonUtils.createUrlWithParams('users/tokens', {
48+
name: tokenName,
49+
}),
8750
session.user.access_token,
8851
)
8952

9053
if (response.status === HttpStatus.NO_CONTENT) {
9154
MessageService.success(t('Revoke token sucessfully'))
92-
await fetchData('users/tokens')
55+
setRevoked(!revoked)
9356
} else if (response.status === HttpStatus.UNAUTHORIZED) {
9457
await signOut()
9558
} else {
@@ -101,47 +64,139 @@ const TokensTable = ({ generatedToken }: Props): ReactNode => {
10164
}
10265
}
10366

67+
const columns = useMemo<ColumnDef<AccessToken>[]>(
68+
() => [
69+
{
70+
id: 'name',
71+
header: t('Token Name'),
72+
cell: ({ row }) => <>{row.original.name}</>,
73+
},
74+
{
75+
id: 'createdOn',
76+
name: t('Created on'),
77+
cell: ({ row }) => (
78+
<>
79+
{new Date(
80+
Date.parse(row.original.createdOn + ' +0000') - new Date().getTimezoneOffset() * 60000,
81+
)
82+
.toISOString()
83+
.slice(0, 19)
84+
.replace('T', ' ')}
85+
</>
86+
),
87+
},
88+
{
89+
id: 'expiration-date',
90+
header: t('Expiration Date'),
91+
cell: ({ row }) => {
92+
const expirationDate = new Date(
93+
Date.parse(row.original.createdOn + ' +0000') +
94+
row.original.numberOfDaysValid * 24 * 60 * 60 * 1000 -
95+
new Date().getTimezoneOffset() * 60000,
96+
)
97+
return <>{expirationDate.toISOString().slice(0, 19).replace('T', ' ')}</>
98+
},
99+
},
100+
{
101+
id: 'authorities',
102+
header: t('Authorities'),
103+
cell: ({ row }) => <>{'[' + row.original.authorities.join(', ') + ']'}</>,
104+
},
105+
{
106+
id: 'action',
107+
cell: ({ row }) => (
108+
<Button
109+
variant='danger'
110+
onClick={() => {
111+
void revokeToken(row.original.name)
112+
}}
113+
>
114+
{t('Revoke Token')}
115+
</Button>
116+
),
117+
meta: {
118+
width: '10%',
119+
},
120+
},
121+
],
122+
[
123+
t,
124+
generatedToken,
125+
revoked,
126+
],
127+
)
128+
const [componentData, setComponentData] = useState<AccessToken[]>(() => [])
129+
const memoizedData = useMemo(
130+
() => componentData,
131+
[
132+
componentData,
133+
],
134+
)
135+
const [showProcessing, setShowProcessing] = useState(false)
136+
104137
useEffect(() => {
105-
fetchData('users/tokens').catch((error) => {
106-
console.error(error)
107-
})
108-
}, [fetchData, generatedToken])
109-
110-
const columns = [
111-
{
112-
id: 'token-name',
113-
name: t('Token Name'),
114-
sort: false,
115-
},
116-
{
117-
id: 'created-on',
118-
name: t('Created on'),
119-
sort: false,
120-
},
121-
{
122-
id: 'expiration-date',
123-
name: t('Expiration Date'),
124-
sort: false,
125-
},
126-
{
127-
id: 'authorities',
128-
name: t('Authorities'),
129-
sort: false,
130-
},
131-
{
132-
id: 'action',
133-
sort: false,
134-
width: '10%',
135-
},
136-
]
138+
if (session.status === 'loading') return
139+
const controller = new AbortController()
140+
const signal = controller.signal
141+
142+
const timeLimit = componentData.length !== 0 ? 700 : 0
143+
const timeout = setTimeout(() => {
144+
setShowProcessing(true)
145+
}, timeLimit)
146+
147+
void (async () => {
148+
try {
149+
if (CommonUtils.isNullOrUndefined(session.data)) return signOut()
150+
const response = await ApiUtils.GET('users/tokens', session.data.user.access_token, signal)
151+
if (response.status !== HttpStatus.OK) {
152+
const err = (await response.json()) as ErrorDetails
153+
throw new Error(err.message)
154+
}
155+
156+
const data = (await response.json()) as EmbeddedAccessTokens
157+
setComponentData(
158+
CommonUtils.isNullOrUndefined(data['_embedded']['sw360:restApiTokens'])
159+
? []
160+
: data['_embedded']['sw360:restApiTokens'],
161+
)
162+
} catch (error) {
163+
if (error instanceof DOMException && error.name === 'AbortError') {
164+
return
165+
}
166+
const message = error instanceof Error ? error.message : String(error)
167+
MessageService.error(message)
168+
} finally {
169+
clearTimeout(timeout)
170+
setShowProcessing(false)
171+
}
172+
})()
173+
174+
return () => controller.abort()
175+
}, [
176+
session,
177+
revoked,
178+
generatedToken,
179+
])
180+
181+
const table = useReactTable({
182+
data: memoizedData,
183+
columns,
184+
getCoreRowModel: getCoreRowModel(),
185+
})
137186

138187
return (
139-
<div className='row'>
140-
{tableData.length > 0 && (
141-
<Table
142-
columns={columns}
143-
data={tableData}
144-
/>
188+
<div className='mb-3'>
189+
{table ? (
190+
<>
191+
<SW360Table
192+
table={table}
193+
showProcessing={showProcessing}
194+
/>
195+
</>
196+
) : (
197+
<div className='col-12 mt-1 text-center'>
198+
<Spinner className='spinner' />
199+
</div>
145200
)}
146201
</div>
147202
)

0 commit comments

Comments
 (0)