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 all 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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Cypress screenshots
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
Expand Down
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/[email protected]...@iqss/[email protected]) (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({
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>
)
}
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>
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
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')
})
})
Loading
Loading