Skip to content

Commit

Permalink
Merge pull request #479 from IQSS/feat/470-user-panel-api-token
Browse files Browse the repository at this point in the history
Feat/470 user panel api token
  • Loading branch information
g-saracca authored Sep 17, 2024
2 parents 4dc5ff1 + b31b1ab commit db18ff9
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/design-system/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
- **Stack:** NEW Stack element to manage layouts.
- **TransferList:** NEW TransferList component to transfer items between two list, also sortable.
- **Table:** extend Props Interface to accept `bordered` prop to add or remove borders on all sides of the table and cells. Defaults to true.
- **Tab:** extend Props Interface to accept `disabled` prop to disable the tab.

# [1.1.0](https://github.com/IQSS/dataverse-frontend/compare/@iqss/dataverse-design-system@1.0.1...@iqss/dataverse-design-system@1.1.0) (2024-03-12)

Expand Down
5 changes: 3 additions & 2 deletions packages/design-system/src/lib/components/tabs/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Tab as TabBS } from 'react-bootstrap'
export interface TabProps {
title: string
eventKey: string
disabled?: boolean
}

export function Tab({ title, eventKey, children }: PropsWithChildren<TabProps>) {
export function Tab({ title, eventKey, disabled = false, children }: PropsWithChildren<TabProps>) {
return (
<TabBS title={title} eventKey={eventKey}>
<TabBS title={title} eventKey={eventKey} disabled={disabled}>
{children}
</TabBS>
)
Expand Down
16 changes: 16 additions & 0 deletions packages/design-system/src/lib/stories/tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ export const Default: Story = {
</Tabs>
)
}

export const SomeTabDisabled: Story = {
render: () => (
<Tabs defaultActiveKey="key-1">
<Tabs.Tab eventKey="key-1" title="Tab 1">
<ExampleContent title="Content 1" />
</Tabs.Tab>
<Tabs.Tab eventKey="key-2" title="Tab 2" disabled>
<ExampleContent title="Content 2" />
</Tabs.Tab>
<Tabs.Tab eventKey="key-3" title="Tab 3">
<ExampleContent title="Content 3" />
</Tabs.Tab>
</Tabs>
)
}
18 changes: 18 additions & 0 deletions public/locales/en/account.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"pageTitle": "Account",
"tabs": {
"myData": "My Data",
"notifications": "Notifications",
"accountInformation": "Account Information",
"apiToken": "API Token"
},
"apiToken": {
"helperText": "Your API Token is valid for a year. Check out our <anchor>API Guide</anchor> for more information on using your API Token with the Dataverse APIs.",
"notCreatedApiToken": "API Token for Dataverse Admin has not been created.",
"expirationDate": "Expiration date",
"copyToClipboard": "Copy to Clipboard",
"recreateToken": "Recreate Token",
"revokeToken": "Revoke Token",
"createToken": "Create Token"
}
}
3 changes: 2 additions & 1 deletion public/locales/en/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"navigation": {
"addData": "Add Data",
"newCollection": "New Collection",
"newDataset": "New Dataset"
"newDataset": "New Dataset",
"apiToken": "API Token"
}
}
5 changes: 5 additions & 0 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UploadDatasetFilesFactory } from './sections/upload-dataset-files/Uploa
import { EditDatasetMetadataFactory } from './sections/edit-dataset-metadata/EditDatasetMetadataFactory'
import { DatasetNonNumericVersion } from './dataset/domain/models/Dataset'
import { CreateCollectionFactory } from './sections/create-collection/CreateCollectionFactory'
import { AccountFactory } from './sections/account/AccountFactory'

const router = createBrowserRouter(
[
Expand Down Expand Up @@ -49,6 +50,10 @@ const router = createBrowserRouter(
{
path: Route.FILES,
element: FileFactory.create()
},
{
path: Route.ACCOUNT,
element: AccountFactory.create()
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion src/sections/Route.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export enum Route {
EDIT_DATASET_METADATA = '/datasets/edit-metadata',
FILES = '/files',
COLLECTIONS = '/collections/:collectionId',
CREATE_COLLECTION = '/collections/:ownerCollectionId/create'
CREATE_COLLECTION = '/collections/:ownerCollectionId/create',
ACCOUNT = '/account'
}

export const RouteWithParams = {
Expand Down
10 changes: 10 additions & 0 deletions src/sections/account/Account.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module';

.tab-container {
padding: 1rem;
}

.helper-text {
color: $dv-subtext-color;
font-size: 14px;
}
59 changes: 59 additions & 0 deletions src/sections/account/Account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Tabs } from '@iqss/dataverse-design-system'
import { useLoading } from '../loading/LoadingContext'
import { AccountHelper, AccountPanelTabKey } from './AccountHelper'
import { ApiTokenSection } from './api-token-section/ApiTokenSection'
import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator'
import { UpwardHierarchyNodeMother } from '../../../tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother'
import styles from './Account.module.scss'

const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS

interface AccountProps {
defaultActiveTabKey: AccountPanelTabKey
}

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

const rootHierarchy = UpwardHierarchyNodeMother.createCollection({
name: 'Root',
id: 'root'
})

useEffect(() => {
setIsLoading(false)
}, [setIsLoading])

return (
<section>
<BreadcrumbsGenerator hierarchy={rootHierarchy} withActionItem actionItemText="Account" />

<header>
<h1>{t('pageTitle')}</h1>
</header>

<Tabs defaultActiveKey={defaultActiveTabKey}>
<Tabs.Tab eventKey={tabsKeys.myData} title={t('tabs.myData')} disabled>
<div className={styles['tab-container']}></div>
</Tabs.Tab>
<Tabs.Tab eventKey={tabsKeys.notifications} title={t('tabs.notifications')} disabled>
<div className={styles['tab-container']}></div>
</Tabs.Tab>
<Tabs.Tab
eventKey={tabsKeys.accountInformation}
title={t('tabs.accountInformation')}
disabled>
<div className={styles['tab-container']}></div>
</Tabs.Tab>
<Tabs.Tab eventKey={tabsKeys.apiToken} title={t('tabs.apiToken')}>
<div className={styles['tab-container']}>
<ApiTokenSection />
</div>
</Tabs.Tab>
</Tabs>
</section>
)
}
17 changes: 17 additions & 0 deletions src/sections/account/AccountFactory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ReactElement } from 'react'
import { useSearchParams } from 'react-router-dom'
import { AccountHelper } from './AccountHelper'
import { Account } from './Account'

export class AccountFactory {
static create(): ReactElement {
return <AccountWithSearchParams />
}
}

function AccountWithSearchParams() {
const [searchParams] = useSearchParams()
const defaultActiveTabKey = AccountHelper.defineSelectedTabKey(searchParams)

return <Account defaultActiveTabKey={defaultActiveTabKey} />
}
22 changes: 22 additions & 0 deletions src/sections/account/AccountHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class AccountHelper {
static ACCOUNT_PANEL_TABS_KEYS = {
myData: 'myData',
notifications: 'notifications',
accountInformation: 'accountInformation',
apiToken: 'apiToken'
} as const

static ACCOUNT_PANEL_TAB_QUERY_KEY = 'tab'

public static defineSelectedTabKey(searchParams: URLSearchParams): AccountPanelTabKey {
const tabValue = searchParams.get(this.ACCOUNT_PANEL_TAB_QUERY_KEY)

return (
this.ACCOUNT_PANEL_TABS_KEYS[tabValue as keyof typeof this.ACCOUNT_PANEL_TABS_KEYS] ??
this.ACCOUNT_PANEL_TABS_KEYS.myData
)
}
}

export type AccountPanelTabKey =
(typeof AccountHelper.ACCOUNT_PANEL_TABS_KEYS)[keyof typeof AccountHelper.ACCOUNT_PANEL_TABS_KEYS]
32 changes: 32 additions & 0 deletions src/sections/account/api-token-section/ApiTokenSection.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module';

.exp-date {
display: flex;
gap: 1.5rem;
align-items: center;
padding-left: 0.5rem;
font-weight: bold;

time {
font-weight: normal;
}

@media (min-width: 768px) {
gap: 3rem;
}
}

.api-token {
padding: 0.5rem 1rem;
background-color: #f7f7f9;
border: solid 1px $dv-border-color;
border-radius: 4px;
}

.btns-wrapper {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
padding-top: 1rem;
}
71 changes: 71 additions & 0 deletions src/sections/account/api-token-section/ApiTokenSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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'

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

const apiToken = '999fff-666rrr-this-is-not-a-real-token-123456'
const expirationDate = '2025-09-04'

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

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>
{apiToken ? (
<>
<p className={styles['exp-date']}>
{t('expirationDate')} <time dateTime={expirationDate}>{expirationDate}</time>
</p>
<div className={styles['api-token']}>
<code data-testid="api-token">{apiToken}</code>
</div>
<div className={styles['btns-wrapper']} role="group">
<Button variant="secondary" onClick={copyToClipboard}>
{t('copyToClipboard')}
</Button>
<Button variant="secondary" disabled>
{t('recreateToken')}
</Button>
<Button variant="secondary" disabled>
{t('revokeToken')}
</Button>
</div>
</>
) : (
<>
<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>
{t('createToken')}
</Button>
</div>
</>
)}
</>
)
}
6 changes: 6 additions & 0 deletions src/sections/layout/header/LoggedInHeaderActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Route, RouteWithParams } from '../../Route.enum'
import { User } from '../../../users/domain/models/User'
import { CollectionRepository } from '../../../collection/domain/repositories/CollectionRepository'
import { ROOT_COLLECTION_ALIAS } from '../../../collection/domain/models/Collection'
import { AccountHelper } from '../../account/AccountHelper'

const currentPage = 0

Expand Down Expand Up @@ -56,6 +57,11 @@ export const LoggedInHeaderActions = ({
</Navbar.Dropdown.Item>
</Navbar.Dropdown>
<Navbar.Dropdown title={user.displayName} id="dropdown-user">
<Navbar.Dropdown.Item
as={Link}
to={`${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.apiToken}`}>
{t('navigation.apiToken')}
</Navbar.Dropdown.Item>
<Navbar.Dropdown.Item href="#" onClick={onLogoutClick}>
{t('logOut')}
</Navbar.Dropdown.Item>
Expand Down
22 changes: 22 additions & 0 deletions src/stories/account/Account.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Meta, StoryObj } from '@storybook/react'
import { Account } from '../../sections/account/Account'
import { WithI18next } from '../WithI18next'
import { WithLayout } from '../WithLayout'
import { WithLoggedInUser } from '../WithLoggedInUser'
import { AccountHelper } from '../../sections/account/AccountHelper'

const meta: Meta<typeof Account> = {
title: 'Pages/Account',
component: Account,
decorators: [WithI18next, WithLayout, WithLoggedInUser],
parameters: {
// Sets the delay for all stories.
chromatic: { delay: 15000, pauseAnimationAtEnd: true }
}
}
export default meta
type Story = StoryObj<typeof Account>

export const APITokenTab: Story = {
render: () => <Account defaultActiveTabKey={AccountHelper.ACCOUNT_PANEL_TABS_KEYS.apiToken} />
}
14 changes: 14 additions & 0 deletions tests/component/sections/account/Account.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Account } from '../../../../src/sections/account/Account'

describe('Account', () => {
it('should render the correct breadcrumbs', () => {
cy.mountAuthenticated(<Account />)

cy.findByRole('link', { name: 'Root' }).should('exist')

cy.get('li[aria-current="page"]')
.should('exist')
.should('have.text', 'Account')
.should('have.class', 'active')
})
})
24 changes: 24 additions & 0 deletions tests/component/sections/account/ApiTokenSection.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ApiTokenSection } from '../../../../src/sections/account/api-token-section/ApiTokenSection'

describe('ApiTokenSection', () => {
beforeEach(() => {
cy.mountAuthenticated(<ApiTokenSection />)
})

it('should copy the api token to the clipboard', () => {
cy.window().then((win) => {
cy.stub(win.navigator.clipboard, 'writeText').resolves()

cy.findByRole('button', { name: /Copy to Clipboard/ }).click()

cy.get('[data-testid="api-token"]').then(($element) => {
const textToCopy = $element.text()

// eslint-disable-next-line @typescript-eslint/unbound-method
cy.wrap(win.navigator.clipboard.writeText).should('be.calledWith', textToCopy)
})
})
})

// TODO: When we get the api token from the use case, we could mock the response and test more things.
})

0 comments on commit db18ff9

Please sign in to comment.