Skip to content

Commit

Permalink
feat:current api usecase integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ChengShi-1 committed Oct 28, 2024
1 parent 02bc675 commit 6ade2c3
Show file tree
Hide file tree
Showing 18 changed files with 626 additions and 21 deletions.
9 changes: 3 additions & 6 deletions src/sections/account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import { AccountHelper, AccountPanelTabKey } from './AccountHelper'
import { ApiTokenSection } from './api-token-section/ApiTokenSection'
import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator'
import styles from './Account.module.scss'
import {
DvObjectType,
UpwardHierarchyNode
} from '../../shared/hierarchy/domain/models/UpwardHierarchyNode'
import { ROOT_COLLECTION_ALIAS } from '../../collection/domain/models/Collection'
import { ApiTokenInfoJSDataverseRepository } from '@/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository'

const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS

Expand All @@ -21,6 +17,7 @@ interface AccountProps {
export const Account = ({ defaultActiveTabKey }: AccountProps) => {
const { t } = useTranslation('account')
const { setIsLoading } = useLoading()
const repository = new ApiTokenInfoJSDataverseRepository()

const rootHierarchy = new UpwardHierarchyNode(

Check failure on line 22 in src/sections/account/Account.tsx

View workflow job for this annotation

GitHub Actions / lint

Cannot find name 'UpwardHierarchyNode'.
'Root',
Expand Down Expand Up @@ -55,7 +52,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={repository} />
</div>
</Tabs.Tab>
</Tabs>
Expand Down
85 changes: 73 additions & 12 deletions src/sections/account/api-token-section/ApiTokenSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,80 @@ import { Trans, useTranslation } from 'react-i18next'
import { Button } from '@iqss/dataverse-design-system'
import accountStyles from '../Account.module.scss'
import styles from './ApiTokenSection.module.scss'
import { useEffect, useState } from 'react'
import { Alert } from '@iqss/dataverse-design-system'
import { TokenInfo } from '@/users/domain/models/TokenInfo'
import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository'
import ApiTokenSectionSkeleton from './ApiTokenSectionSkeleton'
import { useGetApiToken } from './useGetCurrentApiToken'
import { useRecreateApiToken } from './useRecreateApiToken'
import { useRevokeApiToken } from './useRevokeApiToken'
interface ApiTokenSectionProps {
repository: ApiTokenInfoRepository
}

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

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

const getError =
error !== 'There was an error when reading the resource. Reason was: [404] Token not found.'
? error
: null

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

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

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

const handleCreateToken = () => {
initiateRecreateToken()
}

// 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: ''
})
}

const copyToClipboard = () => {
navigator.clipboard.writeText(apiToken).catch(
navigator.clipboard.writeText(apiTokenInfo.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 +93,25 @@ 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={currentApiTokenInfo.expirationDate}>
{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 +121,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
51 changes: 51 additions & 0 deletions src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
import 'react-loading-skeleton/dist/skeleton.css'
import { Trans, useTranslation } from 'react-i18next'
import accountStyles from '../Account.module.scss'
import { Button } from '@iqss/dataverse-design-system'
import styles from './ApiTokenSection.module.scss'

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>
</>
)
}
export default ApiTokenSectionSkeleton
43 changes: 43 additions & 0 deletions src/sections/account/api-token-section/useGetCurrentApiToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useState, useEffect, useCallback } from 'react'
import { TokenInfo } from '@/users/domain/models/TokenInfo'
import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository'
import { getCurrentApiToken } from '@/users/domain/useCases/getCurrentApiToken'

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

export const useGetApiToken = (repository: ApiTokenInfoRepository): UseGetApiTokenResult => {
const [apiTokenInfo, setApiTokenInfo] = useState<TokenInfo>({
apiToken: '',
expirationDate: ''
})
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 : 'Failed to fetch API token.'
console.error(errorMessage)
setError(errorMessage)
} finally {
setIsLoading(false)
}
}, [repository])

useEffect(() => {
void fetchTokenInfo()
}, [fetchTokenInfo])

return { error, apiTokenInfo, isLoading }
}
47 changes: 47 additions & 0 deletions src/sections/account/api-token-section/useRecreateApiToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState, useEffect } from 'react'
import { recreateApiToken } from '@/users/domain/useCases/recreateApiToken'
import { TokenInfo } from '@/users/domain/models/TokenInfo'
import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository'

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

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

const initiateRecreateToken = () => {
setShouldRecreate(true)
}

useEffect(() => {
const recreateToken = async () => {
setIsRecreating(true)
setError(null)

try {
const newTokenInfo = await recreateApiToken(repository)
setApiTokenInfo(newTokenInfo)
} catch (err) {
console.error('Error recreating token:', err)
setError('Failed to recreate API token.')
} finally {
setIsRecreating(false)
setShouldRecreate(false)
}
}
if (shouldRecreate) {
void recreateToken()
}
}, [shouldRecreate, repository])

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

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

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

const revokeToken = useCallback(async () => {
setIsRevoking(true)
setError(null)

try {
await revokeApiToken(repository)
} catch (err) {
console.error('There was an error revoking Api token:', err)
setError('Failed to revoke API token.')
} finally {
setIsRevoking(false)
}
}, [repository])

return { revokeToken, isRevoking, error }
}
3 changes: 3 additions & 0 deletions src/shared/helpers/DateHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ export class DateHelper {
day: '2-digit'
})
}
static toISO8601Format(date: Date): string {
return date.toISOString().split('T')[0]
}
}
4 changes: 4 additions & 0 deletions src/users/domain/models/TokenInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface TokenInfo {
apiToken: string
expirationDate: string
}
7 changes: 7 additions & 0 deletions src/users/domain/repositories/ApiTokenInfoRepository.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TokenInfo } from '../.././domain/models/TokenInfo'

export interface ApiTokenInfoRepository {
getCurrentApiToken(): Promise<TokenInfo>
recreateApiToken(): Promise<TokenInfo>
deleteApiToken(): Promise<void>
}
6 changes: 6 additions & 0 deletions src/users/domain/useCases/getCurrentApiToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TokenInfo } from '../models/TokenInfo'
import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository'

export function getCurrentApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise<TokenInfo> {
return apiTokenRepository.getCurrentApiToken()
}
6 changes: 6 additions & 0 deletions src/users/domain/useCases/recreateApiToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TokenInfo } from '../models/TokenInfo'
import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository'

export function recreateApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise<TokenInfo> {
return apiTokenRepository.recreateApiToken()
}
5 changes: 5 additions & 0 deletions src/users/domain/useCases/revokeApiToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository'

export function revokeApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise<void> {
return apiTokenRepository.deleteApiToken()
}
Loading

0 comments on commit 6ade2c3

Please sign in to comment.