mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 08:18:43 -03:30
Merge pull request #7800 from AlexSCorey/7789-UserTokenDetails
Adds User Token Details page
Reviewed-by: Daniel Sami
https://github.com/dsesami
This commit is contained in:
@@ -44,7 +44,7 @@ function ApplicationLookup({ i18n, onChange, value, label }) {
|
|||||||
actionsResponse?.data?.related_search_fields || []
|
actionsResponse?.data?.related_search_fields || []
|
||||||
).map(val => val.slice(0, -8)),
|
).map(val => val.slice(0, -8)),
|
||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse?.data?.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ function User({ i18n, setBreadcrumb, me }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
if (['edit', 'add'].some(name => location.pathname.includes(name))) {
|
if (
|
||||||
|
['edit', 'add', 'tokens'].some(name => location.pathname.includes(name))
|
||||||
|
) {
|
||||||
showCardHeader = false;
|
showCardHeader = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +133,11 @@ function User({ i18n, setBreadcrumb, me }) {
|
|||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
<Route path="/users/:id/tokens">
|
<Route path="/users/:id/tokens">
|
||||||
<UserTokens id={Number(match.params.id)} />
|
<UserTokens
|
||||||
|
user={user}
|
||||||
|
setBreadcrumb={setBreadcrumb}
|
||||||
|
id={Number(match.params.id)}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
|
|||||||
117
awx/ui_next/src/screens/User/UserToken/UserToken.jsx
Normal file
117
awx/ui_next/src/screens/User/UserToken/UserToken.jsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect, useCallback } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
import RoutedTabs from '../../../components/RoutedTabs';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import { TokensAPI } from '../../../api';
|
||||||
|
import useRequest from '../../../util/useRequest';
|
||||||
|
import UserTokenDetail from '../UserTokenDetail';
|
||||||
|
|
||||||
|
function UserToken({ i18n, setBreadcrumb, user }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { id, tokenId } = useParams();
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
request: fetchToken,
|
||||||
|
result: { token, actions },
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const [response, actionsResponse] = await Promise.all([
|
||||||
|
TokensAPI.readDetail(tokenId),
|
||||||
|
TokensAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
setBreadcrumb(user, response.data);
|
||||||
|
return {
|
||||||
|
token: response.data,
|
||||||
|
actions: actionsResponse.data.actions.POST,
|
||||||
|
};
|
||||||
|
}, [setBreadcrumb, user, tokenId]),
|
||||||
|
{ token: null, actions: null }
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
fetchToken();
|
||||||
|
}, [fetchToken]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Tokens`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/users/${id}/tokens`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/users/${id}/tokens/${tokenId}/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let showCardHeader = true;
|
||||||
|
|
||||||
|
if (location.pathname.endsWith('edit')) {
|
||||||
|
showCardHeader = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && error) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={error}>
|
||||||
|
{error.response.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Token not found.`)}{' '}
|
||||||
|
<Link to="/users/:id/tokens">
|
||||||
|
{i18n._(t`View all tokens.`)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
|
||||||
|
<Switch>
|
||||||
|
<Redirect
|
||||||
|
from="/users/:id/tokens/:tokenId"
|
||||||
|
to="/users/:id/tokens/:tokenId/details"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
{token && (
|
||||||
|
<Route path="/users/:id/tokens/:tokenId/details">
|
||||||
|
<UserTokenDetail canEditOrDelete={actions} token={token} />
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
|
<Route key="not-found" path="*">
|
||||||
|
{!isLoading && (
|
||||||
|
<ContentError isNotFound>
|
||||||
|
{id && (
|
||||||
|
<Link to={`/users/${id}/tokens`}>{i18n._(t`View Tokens`)}</Link>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(UserToken);
|
||||||
101
awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx
Normal file
101
awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { TokensAPI } from '../../../api';
|
||||||
|
import UserToken from './UserToken';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Tokens');
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useParams: () => ({
|
||||||
|
id: 1,
|
||||||
|
tokenId: 2,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
describe('<UserToken/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
const user = {
|
||||||
|
id: 1,
|
||||||
|
type: 'user',
|
||||||
|
url: '/api/v2/users/1/',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2020-06-19T12:55:13.138692Z',
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Alex',
|
||||||
|
last_name: 'Corey',
|
||||||
|
email: 'a@g.com',
|
||||||
|
};
|
||||||
|
test('should call api for token details and actions', async () => {
|
||||||
|
TokensAPI.readDetail.mockResolvedValue({
|
||||||
|
id: 2,
|
||||||
|
type: 'o_auth2_access_token',
|
||||||
|
url: '/api/v2/tokens/2/',
|
||||||
|
|
||||||
|
summary_fields: {
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Alex',
|
||||||
|
last_name: 'Corey',
|
||||||
|
},
|
||||||
|
application: {
|
||||||
|
id: 3,
|
||||||
|
name: 'hg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2020-06-23T19:56:38.422053Z',
|
||||||
|
modified: '2020-06-23T19:56:38.441353Z',
|
||||||
|
description: 'cdfsg',
|
||||||
|
scope: 'read',
|
||||||
|
});
|
||||||
|
TokensAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { POST: true } },
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserToken setBreadcrumb={jest.fn()} user={user} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('UserToken').length).toBe(1);
|
||||||
|
});
|
||||||
|
test('should call api for token details and actions', async () => {
|
||||||
|
TokensAPI.readDetail.mockResolvedValue({
|
||||||
|
id: 2,
|
||||||
|
type: 'o_auth2_access_token',
|
||||||
|
url: '/api/v2/tokens/2/',
|
||||||
|
|
||||||
|
summary_fields: {
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Alex',
|
||||||
|
last_name: 'Corey',
|
||||||
|
},
|
||||||
|
application: {
|
||||||
|
id: 3,
|
||||||
|
name: 'hg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2020-06-23T19:56:38.422053Z',
|
||||||
|
modified: '2020-06-23T19:56:38.441353Z',
|
||||||
|
description: 'cdfsg',
|
||||||
|
scope: 'read',
|
||||||
|
});
|
||||||
|
TokensAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { POST: true } },
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserToken setBreadcrumb={jest.fn()} user={user} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(TokensAPI.readDetail).toBeCalledWith(2);
|
||||||
|
expect(TokensAPI.readOptions).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui_next/src/screens/User/UserToken/index.js
Normal file
1
awx/ui_next/src/screens/User/UserToken/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './UserToken';
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
|
import {
|
||||||
|
DetailList,
|
||||||
|
Detail,
|
||||||
|
UserDateDetail,
|
||||||
|
} from '../../../components/DetailList';
|
||||||
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
|
import { TokensAPI } from '../../../api';
|
||||||
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { toTitleCase } from '../../../util/strings';
|
||||||
|
|
||||||
|
function UserTokenDetail({ token, canEditOrDelete, i18n }) {
|
||||||
|
const { scope, description, created, modified, summary_fields } = token;
|
||||||
|
const history = useHistory();
|
||||||
|
const { id, tokenId } = useParams();
|
||||||
|
const { request: deleteToken, isLoading, error: deleteError } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await TokensAPI.destroy(tokenId);
|
||||||
|
history.push(`/users/${id}/tokens`);
|
||||||
|
}, [tokenId, id, history])
|
||||||
|
);
|
||||||
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<DetailList>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Application`)}
|
||||||
|
value={summary_fields?.application?.name}
|
||||||
|
dataCy="application-token-detail-name"
|
||||||
|
/>
|
||||||
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
|
<Detail label={i18n._(t`Scope`)} value={toTitleCase(scope)} />
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Created`)}
|
||||||
|
date={created}
|
||||||
|
user={summary_fields.user}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last Modified`)}
|
||||||
|
date={modified}
|
||||||
|
user={summary_fields.user}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{canEditOrDelete && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
component={Link}
|
||||||
|
to={`/users/${id}/tokens/${tokenId}/details`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
<DeleteButton
|
||||||
|
name={summary_fields?.application?.name}
|
||||||
|
modalTitle={i18n._(t`Delete User Token`)}
|
||||||
|
onConfirm={deleteToken}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
{error && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to user token.`)}
|
||||||
|
<ErrorDetail error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(UserTokenDetail);
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { TokensAPI } from '../../../api';
|
||||||
|
import UserTokenDetail from './UserTokenDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Tokens');
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useParams: () => ({
|
||||||
|
id: 1,
|
||||||
|
tokenId: 2,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
describe('<UserTokenDetail/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
const token = {
|
||||||
|
id: 2,
|
||||||
|
type: 'o_auth2_access_token',
|
||||||
|
url: '/api/v2/tokens/2/',
|
||||||
|
|
||||||
|
summary_fields: {
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Alex',
|
||||||
|
last_name: 'Corey',
|
||||||
|
},
|
||||||
|
application: {
|
||||||
|
id: 3,
|
||||||
|
name: 'hg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2020-06-23T19:56:38.422053Z',
|
||||||
|
modified: '2020-06-23T19:56:38.441353Z',
|
||||||
|
description: 'cdfsg',
|
||||||
|
scope: 'read',
|
||||||
|
};
|
||||||
|
test('should call api for token details and actions', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenDetail canEditOrDelete token={token} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('UserTokenDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
test('should call api for token details and actions', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenDetail canEditOrDelete token={token} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('Detail[label="Application"]').prop('value')).toBe(
|
||||||
|
'hg'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe(
|
||||||
|
'cdfsg'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('Read');
|
||||||
|
expect(wrapper.find('UserDateDetail[label="Created"]').prop('date')).toBe(
|
||||||
|
'2020-06-23T19:56:38.422053Z'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('UserDateDetail[label="Last Modified"]').prop('date')
|
||||||
|
).toBe('2020-06-23T19:56:38.441353Z');
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(1);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(1);
|
||||||
|
});
|
||||||
|
test('should not render edit or delete buttons', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenDetail canEditOrDelete={false} token={token} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
test('should delete token properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenDetail canEditOrDelete token={token} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => wrapper.find('DeleteButton').prop('onConfirm')());
|
||||||
|
expect(TokensAPI.destroy).toBeCalledWith(2);
|
||||||
|
});
|
||||||
|
test('should throw deletion error', async () => {
|
||||||
|
TokensAPI.destroy.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/tokens',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenDetail canEditOrDelete token={token} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => wrapper.find('DeleteButton').prop('onConfirm')());
|
||||||
|
expect(TokensAPI.destroy).toBeCalledWith(2);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui_next/src/screens/User/UserTokenDetail/index.js
Normal file
1
awx/ui_next/src/screens/User/UserTokenDetail/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './UserTokenDetail';
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { toTitleCase } from '../../../util/strings';
|
import { toTitleCase } from '../../../util/strings';
|
||||||
|
|
||||||
import { formatDateStringUTC } from '../../../util/dates';
|
import { formatDateString } from '../../../util/dates';
|
||||||
import DataListCell from '../../../components/DataListCell';
|
import DataListCell from '../../../components/DataListCell';
|
||||||
|
|
||||||
const Label = styled.b`
|
const Label = styled.b`
|
||||||
@@ -22,6 +23,7 @@ const NameLabel = styled.b`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function UserTokenListItem({ i18n, token, isSelected, onSelect }) {
|
function UserTokenListItem({ i18n, token, isSelected, onSelect }) {
|
||||||
|
const { id } = useParams();
|
||||||
const labelId = `check-action-${token.id}`;
|
const labelId = `check-action-${token.id}`;
|
||||||
return (
|
return (
|
||||||
<DataListItem key={token.id} aria-labelledby={labelId} id={`${token.id}`}>
|
<DataListItem key={token.id} aria-labelledby={labelId} id={`${token.id}`}>
|
||||||
@@ -41,10 +43,14 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) {
|
|||||||
{token.summary_fields?.application?.name ? (
|
{token.summary_fields?.application?.name ? (
|
||||||
<span>
|
<span>
|
||||||
<NameLabel>{i18n._(t`Application`)}</NameLabel>
|
<NameLabel>{i18n._(t`Application`)}</NameLabel>
|
||||||
{token.summary_fields.application.name}
|
<Link to={`/users/${id}/tokens/${token.id}/details`}>
|
||||||
|
{token.summary_fields.application.name}
|
||||||
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
i18n._(t`Personal access token`)
|
<Link to={`/users/${id}/tokens/${token.id}/details`}>
|
||||||
|
{i18n._(t`Personal access token`)}
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell aria-label={i18n._(t`scope`)} key={token.scope}>
|
<DataListCell aria-label={i18n._(t`scope`)} key={token.scope}>
|
||||||
@@ -53,7 +59,7 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) {
|
|||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell aria-label={i18n._(t`expiration`)} key="expiration">
|
<DataListCell aria-label={i18n._(t`expiration`)} key="expiration">
|
||||||
<Label>{i18n._(t`Expires`)}</Label>
|
<Label>{i18n._(t`Expires`)}</Label>
|
||||||
{formatDateStringUTC(token.expires)}
|
{formatDateString(token.expires)}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { Switch, Route, useParams } from 'react-router-dom';
|
import { Switch, Route, useParams } from 'react-router-dom';
|
||||||
import UserTokenAdd from '../UserTokenAdd';
|
import UserTokenAdd from '../UserTokenAdd';
|
||||||
import UserTokenList from '../UserTokenList';
|
import UserTokenList from '../UserTokenList';
|
||||||
|
import UserToken from '../UserToken';
|
||||||
|
|
||||||
function UserTokens() {
|
function UserTokens({ setBreadcrumb, user }) {
|
||||||
const { id: userId } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route key="add" path="/users/:id/tokens/add">
|
<Route key="add" path="/users/:id/tokens/add">
|
||||||
<UserTokenAdd id={Number(userId)} />
|
<UserTokenAdd id={Number(id)} />
|
||||||
|
</Route>
|
||||||
|
<Route key="token" path="/users/:id/tokens/:tokenId">
|
||||||
|
<UserToken user={user} setBreadcrumb={setBreadcrumb} id={Number(id)} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route key="list" path="/users/:id/tokens">
|
<Route key="list" path="/users/:id/tokens">
|
||||||
<UserTokenList id={Number(userId)} />
|
<UserTokenList id={Number(id)} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function Users({ i18n }) {
|
|||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
|
||||||
const addUserBreadcrumb = useCallback(
|
const addUserBreadcrumb = useCallback(
|
||||||
user => {
|
(user, token) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,10 @@ function Users({ i18n }) {
|
|||||||
[`/users/${user.id}/organizations`]: i18n._(t`Organizations`),
|
[`/users/${user.id}/organizations`]: i18n._(t`Organizations`),
|
||||||
[`/users/${user.id}/tokens`]: i18n._(t`Tokens`),
|
[`/users/${user.id}/tokens`]: i18n._(t`Tokens`),
|
||||||
[`/users/${user.id}/tokens/add`]: i18n._(t`Create user token`),
|
[`/users/${user.id}/tokens/add`]: i18n._(t`Create user token`),
|
||||||
|
[`/users/${user.id}/tokens/${token && token.id}`]: `Application Name`,
|
||||||
|
[`/users/${user.id}/tokens/${token && token.id}/details`]: i18n._(
|
||||||
|
t`Details`
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n]
|
||||||
|
|||||||
Reference in New Issue
Block a user