Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/470 user panel api token #479

Merged
merged 13 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
g-saracca marked this conversation as resolved.
Show resolved Hide resolved
},
"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;
}
58 changes: 58 additions & 0 deletions src/sections/account/Account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'react-router-dom'
import { Tabs } from '@iqss/dataverse-design-system'
import { useLoading } from '../loading/LoadingContext'
import { AccountHelper } 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'
import { useEffect } from 'react'

const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS

export const Account = () => {
const { t } = useTranslation('account')
const { setIsLoading } = useLoading()
const [searchParams] = useSearchParams()
const defaultActiveTabKey = AccountHelper.defineSelectedTabKey(searchParams)

const rootHierarchy = UpwardHierarchyNodeMother.createCollection({
ekraffmiller marked this conversation as resolved.
Show resolved Hide resolved
name: 'Root',
id: 'root'
})

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

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

<header>
<h1>{t('pageTitle')}</h1>
ekraffmiller marked this conversation as resolved.
Show resolved Hide resolved
</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>
)
}
8 changes: 8 additions & 0 deletions src/sections/account/AccountFactory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactElement } from 'react'
import { Account } from './Account'

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

static ACCOUNT_PANEL_TAB_QUERY_KEY = 'tab'

public static defineSelectedTabKey(searchParams: URLSearchParams): string {
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
)
}
}
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>
ekraffmiller marked this conversation as resolved.
Show resolved Hide resolved
</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
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.
})
Loading