Skip to content

Commit

Permalink
Merge pull request #531 from IQSS/api-token-usecase-integration
Browse files Browse the repository at this point in the history
API Token use cases integration for account page
  • Loading branch information
ofahimIQSS authored Nov 8, 2024
2 parents 110d696 + 4cedb79 commit 64b8367
Show file tree
Hide file tree
Showing 23 changed files with 805 additions and 22 deletions.
6 changes: 4 additions & 2 deletions src/sections/account/Account.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useTranslation } from 'react-i18next'
import { Tabs } from '@iqss/dataverse-design-system'
import { AccountHelper, AccountPanelTabKey } from './AccountHelper'
import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository'
import { ApiTokenSection } from './api-token-section/ApiTokenSection'
import styles from './Account.module.scss'

const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS

interface AccountProps {
defaultActiveTabKey: AccountPanelTabKey
userRepository: UserJSDataverseRepository
}

export const Account = ({ defaultActiveTabKey }: AccountProps) => {
export const Account = ({ defaultActiveTabKey, userRepository }: AccountProps) => {
const { t } = useTranslation('account')

return (
Expand All @@ -34,7 +36,7 @@ export const Account = ({ defaultActiveTabKey }: AccountProps) => {
</Tabs.Tab>
<Tabs.Tab eventKey={tabsKeys.apiToken} title={t('tabs.apiToken')}>
<div className={styles['tab-container']}>
<ApiTokenSection />
<ApiTokenSection repository={userRepository} />
</div>
</Tabs.Tab>
</Tabs>
Expand Down
5 changes: 4 additions & 1 deletion src/sections/account/AccountFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { ReactElement } from 'react'
import { useSearchParams } from 'react-router-dom'
import { AccountHelper } from './AccountHelper'
import { Account } from './Account'
import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository'

const userRepository = new UserJSDataverseRepository()

export class AccountFactory {
static create(): ReactElement {
Expand All @@ -13,5 +16,5 @@ function AccountWithSearchParams() {
const [searchParams] = useSearchParams()
const defaultActiveTabKey = AccountHelper.defineSelectedTabKey(searchParams)

return <Account defaultActiveTabKey={defaultActiveTabKey} />
return <Account defaultActiveTabKey={defaultActiveTabKey} userRepository={userRepository} />
}
84 changes: 72 additions & 12 deletions src/sections/account/api-token-section/ApiTokenSection.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,78 @@
import { Trans, useTranslation } from 'react-i18next'
import { useEffect, useState } from 'react'
import { useGetApiToken } from './useGetCurrentApiToken'
import { useRecreateApiToken } from './useRecreateApiToken'
import { useRevokeApiToken } from './useRevokeApiToken'
import { ApiTokenSectionSkeleton } from './ApiTokenSectionSkeleton'
import { TokenInfo } from '@/users/domain/models/TokenInfo'
import { DateHelper } from '@/shared/helpers/DateHelper'
import { UserRepository } from '@/users/domain/repositories/UserRepository'
import { Button } from '@iqss/dataverse-design-system'
import { Alert } from '@iqss/dataverse-design-system'
import accountStyles from '../Account.module.scss'
import styles from './ApiTokenSection.module.scss'

export const ApiTokenSection = () => {
interface ApiTokenSectionProps {
repository: UserRepository
}

export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => {
const { t } = useTranslation('account', { keyPrefix: 'apiToken' })
const [currentApiTokenInfo, setCurrentApiTokenInfo] = useState<TokenInfo>()

const { error: getError, apiTokenInfo, isLoading } = useGetApiToken(repository)

useEffect(() => {
setCurrentApiTokenInfo(apiTokenInfo)
}, [apiTokenInfo])

const {
recreateToken,
isRecreating,
error: recreatingError,
apiTokenInfo: updatedTokenInfo
} = useRecreateApiToken(repository)

useEffect(() => {
if (updatedTokenInfo) {
setCurrentApiTokenInfo(updatedTokenInfo)
}
}, [updatedTokenInfo])

const handleCreateToken = () => {
void recreateToken()
}

// TODO: When we have the use cases we need to mock stub to unit test this with or without token
const apiToken = '999fff-666rrr-this-is-not-a-real-token-123456'
const expirationDate = '2025-09-04'
const { revokeToken, isRevoking, error: revokingError } = useRevokeApiToken(repository)

const handleRevokeToken = async () => {
await revokeToken()
setCurrentApiTokenInfo({
apiToken: '',
expirationDate: new Date(0)
})
}

const copyToClipboard = () => {
navigator.clipboard.writeText(apiToken).catch(
navigator.clipboard.writeText(currentApiTokenInfo?.apiToken ?? '').catch(
/* istanbul ignore next */ (error) => {
console.error('Failed to copy text:', error)
}
)
}

if (isLoading || isRecreating || isRevoking) {
return <ApiTokenSectionSkeleton data-testid="loadingSkeleton" />
}

if (getError || recreatingError || revokingError) {
return (
<Alert variant="danger" dismissible={false}>
{getError || recreatingError || revokingError}
</Alert>
)
}

return (
<>
<p className={accountStyles['helper-text']}>
Expand All @@ -35,22 +90,27 @@ export const ApiTokenSection = () => {
}}
/>
</p>
{apiToken ? (
{currentApiTokenInfo?.apiToken ? (
<>
<p className={styles['exp-date']}>
{t('expirationDate')} <time dateTime={expirationDate}>{expirationDate}</time>
{t('expirationDate')}{' '}
<time
data-testid="expiration-date"
dateTime={DateHelper.toISO8601Format(currentApiTokenInfo.expirationDate)}>
{DateHelper.toISO8601Format(currentApiTokenInfo.expirationDate)}
</time>
</p>
<div className={styles['api-token']}>
<code data-testid="api-token">{apiToken}</code>
<code data-testid="api-token">{currentApiTokenInfo.apiToken}</code>
</div>
<div className={styles['btns-wrapper']} role="group">
<Button variant="secondary" onClick={copyToClipboard}>
{t('copyToClipboard')}
</Button>
<Button variant="secondary" disabled>
<Button variant="secondary" onClick={handleCreateToken}>
{t('recreateToken')}
</Button>
<Button variant="secondary" disabled>
<Button variant="secondary" onClick={handleRevokeToken}>
{t('revokeToken')}
</Button>
</div>
Expand All @@ -60,8 +120,8 @@ export const ApiTokenSection = () => {
<div className={styles['api-token']}>
<code data-testid="api-token">{t('notCreatedApiToken')}</code>
</div>
<div className={styles['btns-wrapper']} role="group">
<Button variant="secondary" disabled>
<div className={styles['btns-wrapper']} data-testid="noApiToken" role="group">
<Button data-testid="createApi" variant="secondary" onClick={handleCreateToken}>
{t('createToken')}
</Button>
</div>
Expand Down
49 changes: 49 additions & 0 deletions src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Trans, useTranslation } from 'react-i18next'
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
import { Button } from '@iqss/dataverse-design-system'
import accountStyles from '../Account.module.scss'
import styles from './ApiTokenSection.module.scss'

export const ApiTokenSectionSkeleton = () => {
const { t } = useTranslation('account', { keyPrefix: 'apiToken' })

return (
<>
<p className={accountStyles['helper-text']}>
<Trans
t={t}
i18nKey="helperText"
components={{
anchor: (
<a
href="http://guides.dataverse.org/en/latest/api"
target="_blank"
rel="noreferrer"
/>
)
}}
/>
</p>
<SkeletonTheme>
<div data-testid="loadingSkeleton">
<p className={styles['exp-date']}>
{t('expirationDate')}{' '}
<time data-testid="expiration-date">
<Skeleton width={100} />
</time>
</p>
<div className={styles['api-token']}>
<code data-testid="api-token">
<Skeleton width={350} />
</code>
</div>
<div className={styles['btns-wrapper']} role="group">
<Button variant="secondary">{t('copyToClipboard')}</Button>
<Button variant="secondary">{t('recreateToken')}</Button>
<Button variant="secondary">{t('revokeToken')}</Button>
</div>
</div>
</SkeletonTheme>
</>
)
}
49 changes: 49 additions & 0 deletions src/sections/account/api-token-section/useGetCurrentApiToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useState, useEffect, useCallback } from 'react'
import { TokenInfo } from '@/users/domain/models/TokenInfo'
import { UserRepository } from '@/users/domain/repositories/UserRepository'
import { getCurrentApiToken } from '@/users/domain/useCases/getCurrentApiToken'

interface UseGetApiTokenResult {
apiTokenInfo: TokenInfo
isLoading: boolean
error: string | null
}

export const useGetApiToken = (repository: UserRepository): UseGetApiTokenResult => {
const [apiTokenInfo, setApiTokenInfo] = useState<TokenInfo>({
apiToken: '',
expirationDate: new Date(0)
})
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)

const fetchTokenInfo = useCallback(async () => {
try {
setIsLoading(true)
const tokenInfo = await getCurrentApiToken(repository)
setApiTokenInfo({
apiToken: tokenInfo.apiToken,
expirationDate: tokenInfo.expirationDate
})
setError(null)
} catch (err) {
const errorMessage =
err instanceof Error && err.message
? err.message
: 'Something went wrong getting the current api token. Try again later.'
setError(
errorMessage ===
'There was an error when reading the resource. Reason was: [404] Token not found.'
? null
: errorMessage
)
} finally {
setIsLoading(false)
}
}, [repository])

useEffect(() => {
void fetchTokenInfo()
}, [fetchTokenInfo])
return { isLoading, error, apiTokenInfo }
}
36 changes: 36 additions & 0 deletions src/sections/account/api-token-section/useRecreateApiToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from 'react'
import { recreateApiToken } from '@/users/domain/useCases/recreateApiToken'
import { TokenInfo } from '@/users/domain/models/TokenInfo'
import { UserRepository } from '@/users/domain/repositories/UserRepository'

interface UseRecreateApiTokenResult {
recreateToken: () => Promise<void>
isRecreating: boolean
error: string | null
apiTokenInfo: TokenInfo | null
}

export const useRecreateApiToken = (repository: UserRepository): UseRecreateApiTokenResult => {
const [isRecreating, setIsRecreating] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const [apiTokenInfo, setApiTokenInfo] = useState<TokenInfo | null>(null)

const recreateToken = async () => {
setIsRecreating(true)
setError(null)
try {
const newTokenInfo = await recreateApiToken(repository)
setApiTokenInfo(newTokenInfo)
} catch (err) {
const errorMessage =
err instanceof Error && err.message
? err.message
: 'Something went wrong creating the api token. Try again later.'
setError(errorMessage)
} finally {
setIsRecreating(false)
}
}

return { recreateToken, isRecreating, error, apiTokenInfo }
}
32 changes: 32 additions & 0 deletions src/sections/account/api-token-section/useRevokeApiToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react'
import { revokeApiToken } from '@/users/domain/useCases/revokeApiToken'
import { UserRepository } from '@/users/domain/repositories/UserRepository'

interface UseRevokeApiTokenResult {
revokeToken: () => Promise<void>
isRevoking: boolean
error: string | null
}

export const useRevokeApiToken = (repository: UserRepository): UseRevokeApiTokenResult => {
const [isRevoking, setIsRevoking] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)

const revokeToken = async () => {
setIsRevoking(true)
setError(null)
try {
await revokeApiToken(repository)
} catch (err) {
const errorMessage =
err instanceof Error && err.message
? err.message
: 'Something went wrong revoking the api token. Try again later.'
setError(errorMessage)
} finally {
setIsRevoking(false)
}
}

return { revokeToken, isRevoking, error }
}
Loading

0 comments on commit 64b8367

Please sign in to comment.