mirror of
https://github.com/ansible/awx.git
synced 2026-03-02 01:08:48 -03:30
Merge pull request #8684 from mabashian/8666-app-token-modal
Show modals with app/token info after creation Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -8,7 +8,7 @@ import ApplicationForm from '../shared/ApplicationForm';
|
|||||||
import { ApplicationsAPI } from '../../../api';
|
import { ApplicationsAPI } from '../../../api';
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
|
|
||||||
function ApplicationAdd() {
|
function ApplicationAdd({ onSuccessfulAdd }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [submitError, setSubmitError] = useState(null);
|
const [submitError, setSubmitError] = useState(null);
|
||||||
|
|
||||||
@@ -53,10 +53,9 @@ function ApplicationAdd() {
|
|||||||
const handleSubmit = async ({ ...values }) => {
|
const handleSubmit = async ({ ...values }) => {
|
||||||
values.organization = values.organization.id;
|
values.organization = values.organization.id;
|
||||||
try {
|
try {
|
||||||
const {
|
const { data } = await ApplicationsAPI.create(values);
|
||||||
data: { id },
|
onSuccessfulAdd(data);
|
||||||
} = await ApplicationsAPI.create(values);
|
history.push(`/applications/${data.id}/details`);
|
||||||
history.push(`/applications/${id}/details`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSubmitError(err);
|
setSubmitError(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,16 @@ const options = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSuccessfulAdd = jest.fn();
|
||||||
|
|
||||||
describe('<ApplicationAdd/>', () => {
|
describe('<ApplicationAdd/>', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
test('should render properly', async () => {
|
test('should render properly', async () => {
|
||||||
ApplicationsAPI.readOptions.mockResolvedValue(options);
|
ApplicationsAPI.readOptions.mockResolvedValue(options);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<ApplicationAdd />);
|
wrapper = mountWithContexts(
|
||||||
|
<ApplicationAdd onSuccessfulAdd={onSuccessfulAdd} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('ApplicationAdd').length).toBe(1);
|
expect(wrapper.find('ApplicationAdd').length).toBe(1);
|
||||||
expect(wrapper.find('ApplicationForm').length).toBe(1);
|
expect(wrapper.find('ApplicationForm').length).toBe(1);
|
||||||
@@ -59,9 +63,12 @@ describe('<ApplicationAdd/>', () => {
|
|||||||
|
|
||||||
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
|
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<ApplicationAdd />, {
|
wrapper = mountWithContexts(
|
||||||
context: { router: { history } },
|
<ApplicationAdd onSuccessfulAdd={onSuccessfulAdd} />,
|
||||||
});
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -124,6 +131,7 @@ describe('<ApplicationAdd/>', () => {
|
|||||||
redirect_uris: 'http://www.google.com',
|
redirect_uris: 'http://www.google.com',
|
||||||
});
|
});
|
||||||
expect(history.location.pathname).toBe('/applications/8/details');
|
expect(history.location.pathname).toBe('/applications/8/details');
|
||||||
|
expect(onSuccessfulAdd).toHaveBeenCalledWith({ id: 8 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should cancel form properly', async () => {
|
test('should cancel form properly', async () => {
|
||||||
@@ -134,9 +142,12 @@ describe('<ApplicationAdd/>', () => {
|
|||||||
|
|
||||||
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
|
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<ApplicationAdd />, {
|
wrapper = mountWithContexts(
|
||||||
context: { router: { history } },
|
<ApplicationAdd onSuccessfulAdd={onSuccessfulAdd} />,
|
||||||
});
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('Button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('Button[aria-label="Cancel"]').prop('onClick')();
|
||||||
@@ -157,7 +168,9 @@ describe('<ApplicationAdd/>', () => {
|
|||||||
ApplicationsAPI.create.mockRejectedValue(error);
|
ApplicationsAPI.create.mockRejectedValue(error);
|
||||||
ApplicationsAPI.readOptions.mockResolvedValue(options);
|
ApplicationsAPI.readOptions.mockResolvedValue(options);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<ApplicationAdd />);
|
wrapper = mountWithContexts(
|
||||||
|
<ApplicationAdd onSuccessfulAdd={onSuccessfulAdd} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('Formik').prop('onSubmit')({
|
wrapper.find('Formik').prop('onSubmit')({
|
||||||
@@ -181,7 +194,9 @@ describe('<ApplicationAdd/>', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<ApplicationAdd />);
|
wrapper = mountWithContexts(
|
||||||
|
<ApplicationAdd onSuccessfulAdd={onSuccessfulAdd} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|||||||
@@ -58,11 +58,12 @@ function ApplicationDetails({
|
|||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Name`)}
|
label={i18n._(t`Name`)}
|
||||||
value={application.name}
|
value={application.name}
|
||||||
dataCy="jt-detail-name"
|
dataCy="app-detail-name"
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Description`)}
|
label={i18n._(t`Description`)}
|
||||||
value={application.description}
|
value={application.description}
|
||||||
|
dataCy="app-detail-description"
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Organization`)}
|
label={i18n._(t`Organization`)}
|
||||||
@@ -73,20 +74,29 @@ function ApplicationDetails({
|
|||||||
{application.summary_fields.organization.name}
|
{application.summary_fields.organization.name}
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
dataCy="app-detail-organization"
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Authorization grant type`)}
|
label={i18n._(t`Authorization grant type`)}
|
||||||
value={getAuthorizationGrantType(
|
value={getAuthorizationGrantType(
|
||||||
application.authorization_grant_type
|
application.authorization_grant_type
|
||||||
)}
|
)}
|
||||||
|
dataCy="app-detail-authorization-grant-type"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Client ID`)}
|
||||||
|
value={application.client_id}
|
||||||
|
dataCy="app-detail-client-id"
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Redirect uris`)}
|
label={i18n._(t`Redirect uris`)}
|
||||||
value={application.redirect_uris}
|
value={application.redirect_uris}
|
||||||
|
dataCy="app-detail-redirect-uris"
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Client type`)}
|
label={i18n._(t`Client type`)}
|
||||||
value={getClientType(application.client_type)}
|
value={getClientType(application.client_type)}
|
||||||
|
dataCy="app-detail-client-type"
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ describe('<ApplicationDetails/>', () => {
|
|||||||
expect(wrapper.find('Detail[label="Client type"]').prop('value')).toBe(
|
expect(wrapper.find('Detail[label="Client type"]').prop('value')).toBe(
|
||||||
'Confidential'
|
'Confidential'
|
||||||
);
|
);
|
||||||
|
expect(wrapper.find('Detail[label="Client ID"]').prop('value')).toBe(
|
||||||
|
'b1dmj8xzkbFm1ZQ27ygw2ZeE9I0AXqqeL74fiyk4'
|
||||||
|
);
|
||||||
expect(wrapper.find('Button[aria-label="Edit"]').prop('to')).toBe(
|
expect(wrapper.find('Button[aria-label="Edit"]').prop('to')).toBe(
|
||||||
'/applications/10/edit'
|
'/applications/10/edit'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card } from '@patternfly/react-core';
|
||||||
import ApplicationForm from '../shared/ApplicationForm';
|
import ApplicationForm from '../shared/ApplicationForm';
|
||||||
import { ApplicationsAPI } from '../../../api';
|
import { ApplicationsAPI } from '../../../api';
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
@@ -29,22 +29,18 @@ function ApplicationEdit({
|
|||||||
history.push(`/applications/${id}/details`);
|
history.push(`/applications/${id}/details`);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<Card>
|
||||||
<PageSection>
|
<CardBody>
|
||||||
<Card>
|
<ApplicationForm
|
||||||
<CardBody>
|
onSubmit={handleSubmit}
|
||||||
<ApplicationForm
|
application={application}
|
||||||
onSubmit={handleSubmit}
|
onCancel={handleCancel}
|
||||||
application={application}
|
authorizationOptions={authorizationOptions}
|
||||||
onCancel={handleCancel}
|
clientTypeOptions={clientTypeOptions}
|
||||||
authorizationOptions={authorizationOptions}
|
submitError={submitError}
|
||||||
clientTypeOptions={clientTypeOptions}
|
/>
|
||||||
submitError={submitError}
|
</CardBody>
|
||||||
/>
|
</Card>
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</PageSection>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default ApplicationEdit;
|
export default ApplicationEdit;
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardCopyVariant,
|
||||||
|
Modal,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import ApplicationsList from './ApplicationsList';
|
import ApplicationsList from './ApplicationsList';
|
||||||
import ApplicationAdd from './ApplicationAdd';
|
import ApplicationAdd from './ApplicationAdd';
|
||||||
import Application from './Application';
|
import Application from './Application';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import Breadcrumbs from '../../components/Breadcrumbs';
|
||||||
|
import { Detail, DetailList } from '../../components/DetailList';
|
||||||
|
|
||||||
|
const ApplicationAlert = styled(Alert)`
|
||||||
|
margin-bottom: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
function Applications({ i18n }) {
|
function Applications({ i18n }) {
|
||||||
|
const [applicationModalSource, setApplicationModalSource] = useState(null);
|
||||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
'/applications': i18n._(t`Applications`),
|
'/applications': i18n._(t`Applications`),
|
||||||
'/applications/add': i18n._(t`Create New Application`),
|
'/applications/add': i18n._(t`Create New Application`),
|
||||||
@@ -36,7 +48,9 @@ function Applications({ i18n }) {
|
|||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/applications/add">
|
<Route path="/applications/add">
|
||||||
<ApplicationAdd />
|
<ApplicationAdd
|
||||||
|
onSuccessfulAdd={app => setApplicationModalSource(app)}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/applications/:id">
|
<Route path="/applications/:id">
|
||||||
<Application setBreadcrumb={buildBreadcrumbConfig} />
|
<Application setBreadcrumb={buildBreadcrumbConfig} />
|
||||||
@@ -45,6 +59,57 @@ function Applications({ i18n }) {
|
|||||||
<ApplicationsList />
|
<ApplicationsList />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
{applicationModalSource && (
|
||||||
|
<Modal
|
||||||
|
aria-label={i18n._(t`Application information`)}
|
||||||
|
isOpen
|
||||||
|
variant="medium"
|
||||||
|
title={i18n._(t`Application information`)}
|
||||||
|
onClose={() => setApplicationModalSource(null)}
|
||||||
|
>
|
||||||
|
{applicationModalSource.client_secret && (
|
||||||
|
<ApplicationAlert
|
||||||
|
variant="info"
|
||||||
|
isInline
|
||||||
|
title={i18n._(
|
||||||
|
t`This is the only time the client secret will be shown.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DetailList stacked>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
|
value={applicationModalSource.name}
|
||||||
|
/>
|
||||||
|
{applicationModalSource.client_id && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Client ID`)}
|
||||||
|
value={
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{applicationModalSource.client_id}
|
||||||
|
</ClipboardCopy>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{applicationModalSource.client_secret && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Client secret`)}
|
||||||
|
value={
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{applicationModalSource.client_secret}
|
||||||
|
</ClipboardCopy>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DetailList>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import Applications from './Applications';
|
import Applications from './Applications';
|
||||||
|
|
||||||
describe('<Applications />', () => {
|
describe('<Applications />', () => {
|
||||||
let pageWrapper;
|
let wrapper;
|
||||||
let pageSections;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
pageWrapper = mountWithContexts(<Applications />);
|
|
||||||
pageSections = pageWrapper.find('PageSection');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
pageWrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('renders successfully', () => {
|
||||||
expect(pageWrapper.length).toBe(1);
|
wrapper = mountWithContexts(<Applications />);
|
||||||
|
const pageSections = wrapper.find('PageSection');
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
expect(pageSections.length).toBe(1);
|
expect(pageSections.length).toBe(1);
|
||||||
expect(pageSections.first().props().variant).toBe('light');
|
expect(pageSections.first().props().variant).toBe('light');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows Application information modal after successful creation', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/applications/add'],
|
||||||
|
});
|
||||||
|
wrapper = mountWithContexts(<Applications />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Modal[title="Application information"]').length).toBe(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('ApplicationAdd')
|
||||||
|
.props()
|
||||||
|
.onSuccessfulAdd({
|
||||||
|
name: 'test',
|
||||||
|
client_id: 'foobar',
|
||||||
|
client_secret: 'aaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Modal[title="Application information"]').length).toBe(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { Card, PageSection } from '@patternfly/react-core';
|
|||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
import { UsersAPI } from '../../api';
|
import { UsersAPI } from '../../api';
|
||||||
import ContentError from '../../components/ContentError';
|
import ContentError from '../../components/ContentError';
|
||||||
import ContentLoading from '../../components/ContentLoading';
|
|
||||||
import RoutedTabs from '../../components/RoutedTabs';
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
import UserDetail from './UserDetail';
|
import UserDetail from './UserDetail';
|
||||||
import UserEdit from './UserEdit';
|
import UserEdit from './UserEdit';
|
||||||
@@ -86,7 +85,7 @@ function User({ i18n, setBreadcrumb, me }) {
|
|||||||
showCardHeader = false;
|
showCardHeader = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentError) {
|
if (!isLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -107,41 +106,34 @@ function User({ i18n, setBreadcrumb, me }) {
|
|||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
|
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
|
||||||
{isLoading && <ContentLoading />}
|
<Switch>
|
||||||
{!isLoading && user && (
|
<Redirect from="/users/:id" to="/users/:id/details" exact />
|
||||||
<Switch>
|
{user && [
|
||||||
<Redirect from="/users/:id" to="/users/:id/details" exact />
|
<Route path="/users/:id/edit" key="edit">
|
||||||
{user && (
|
<UserEdit user={user} />
|
||||||
<Route path="/users/:id/edit">
|
</Route>,
|
||||||
<UserEdit user={user} />
|
<Route path="/users/:id/details" key="details">
|
||||||
</Route>
|
<UserDetail user={user} />
|
||||||
)}
|
</Route>,
|
||||||
{user && (
|
<Route path="/users/:id/organizations" key="organizations">
|
||||||
<Route path="/users/:id/details">
|
|
||||||
<UserDetail user={user} />
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
<Route path="/users/:id/organizations">
|
|
||||||
<UserOrganizations id={Number(match.params.id)} />
|
<UserOrganizations id={Number(match.params.id)} />
|
||||||
</Route>
|
</Route>,
|
||||||
{user && (
|
<Route path="/users/:id/teams" key="teams">
|
||||||
<Route path="/users/:id/teams">
|
<UserTeams />
|
||||||
<UserTeams />
|
</Route>,
|
||||||
</Route>
|
<Route path="/users/:id/roles" key="roles">
|
||||||
)}
|
<UserRolesList user={user} />
|
||||||
{user && (
|
</Route>,
|
||||||
<Route path="/users/:id/roles">
|
<Route path="/users/:id/tokens" key="tokens">
|
||||||
<UserRolesList user={user} />
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
<Route path="/users/:id/tokens">
|
|
||||||
<UserTokens
|
<UserTokens
|
||||||
user={user}
|
user={user}
|
||||||
setBreadcrumb={setBreadcrumb}
|
setBreadcrumb={setBreadcrumb}
|
||||||
id={Number(match.params.id)}
|
id={Number(match.params.id)}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>,
|
||||||
<Route key="not-found" path="*">
|
]}
|
||||||
|
<Route key="not-found" path="*">
|
||||||
|
{!isLoading && (
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
{match.params.id && (
|
{match.params.id && (
|
||||||
<Link to={`/users/${match.params.id}/details`}>
|
<Link to={`/users/${match.params.id}/details`}>
|
||||||
@@ -149,9 +141,9 @@ function User({ i18n, setBreadcrumb, me }) {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</ContentError>
|
</ContentError>
|
||||||
</Route>
|
)}
|
||||||
</Switch>
|
</Route>
|
||||||
)}
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ async function getUsers() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
|
||||||
|
UsersAPI.read.mockImplementation(getUsers);
|
||||||
|
|
||||||
describe('<User />', () => {
|
describe('<User />', () => {
|
||||||
test('initially renders successfully', async () => {
|
test('initially renders successfully', async () => {
|
||||||
UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
|
|
||||||
UsersAPI.read.mockImplementation(getUsers);
|
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/users/1'],
|
initialEntries: ['/users/1'],
|
||||||
});
|
});
|
||||||
@@ -49,8 +50,6 @@ describe('<User />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('tabs shown for users', async () => {
|
test('tabs shown for users', async () => {
|
||||||
UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
|
|
||||||
UsersAPI.read.mockImplementation(getUsers);
|
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/users/1'],
|
initialEntries: ['/users/1'],
|
||||||
});
|
});
|
||||||
@@ -81,9 +80,7 @@ describe('<User />', () => {
|
|||||||
expect(wrapper.find('Tabs TabButton').length).toEqual(6);
|
expect(wrapper.find('Tabs TabButton').length).toEqual(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not now Tokens tab', async () => {
|
test('should not show Tokens tab', async () => {
|
||||||
UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
|
|
||||||
UsersAPI.read.mockImplementation(getUsers);
|
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/users/1'],
|
initialEntries: ['/users/1'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,20 +24,16 @@ function UserToken({ i18n, setBreadcrumb, user }) {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
request: fetchToken,
|
request: fetchToken,
|
||||||
result: { token, actions },
|
result: { token },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [response, actionsResponse] = await Promise.all([
|
const response = await TokensAPI.readDetail(tokenId);
|
||||||
TokensAPI.readDetail(tokenId),
|
|
||||||
TokensAPI.readOptions(),
|
|
||||||
]);
|
|
||||||
setBreadcrumb(user, response.data);
|
setBreadcrumb(user, response.data);
|
||||||
return {
|
return {
|
||||||
token: response.data,
|
token: response.data,
|
||||||
actions: actionsResponse.data.actions.POST,
|
|
||||||
};
|
};
|
||||||
}, [setBreadcrumb, user, tokenId]),
|
}, [setBreadcrumb, user, tokenId]),
|
||||||
{ token: null, actions: null }
|
{ token: null }
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchToken();
|
fetchToken();
|
||||||
@@ -97,7 +93,7 @@ function UserToken({ i18n, setBreadcrumb, user }) {
|
|||||||
/>
|
/>
|
||||||
{token && (
|
{token && (
|
||||||
<Route path="/users/:id/tokens/:tokenId/details">
|
<Route path="/users/:id/tokens/:tokenId/details">
|
||||||
<UserTokenDetail canEditOrDelete={actions} token={token} />
|
<UserTokenDetail token={token} />
|
||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ describe('<UserToken/>', () => {
|
|||||||
description: 'cdfsg',
|
description: 'cdfsg',
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
});
|
});
|
||||||
TokensAPI.readOptions.mockResolvedValue({
|
|
||||||
data: { actions: { POST: true } },
|
|
||||||
});
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<UserToken setBreadcrumb={jest.fn()} user={user} />
|
<UserToken setBreadcrumb={jest.fn()} user={user} />
|
||||||
@@ -87,15 +84,11 @@ describe('<UserToken/>', () => {
|
|||||||
description: 'cdfsg',
|
description: 'cdfsg',
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
});
|
});
|
||||||
TokensAPI.readOptions.mockResolvedValue({
|
|
||||||
data: { actions: { POST: true } },
|
|
||||||
});
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<UserToken setBreadcrumb={jest.fn()} user={user} />
|
<UserToken setBreadcrumb={jest.fn()} user={user} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(TokensAPI.readDetail).toBeCalledWith(2);
|
expect(TokensAPI.readDetail).toBeCalledWith(2);
|
||||||
expect(TokensAPI.readOptions).toBeCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,22 +6,27 @@ import { TokensAPI, UsersAPI } from '../../../api';
|
|||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import UserTokenFrom from '../shared/UserTokenForm';
|
import UserTokenFrom from '../shared/UserTokenForm';
|
||||||
|
|
||||||
function UserTokenAdd() {
|
function UserTokenAdd({ onSuccessfulAdd }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { id: userId } = useParams();
|
const { id: userId } = useParams();
|
||||||
const { error: submitError, request: handleSubmit } = useRequest(
|
const { error: submitError, request: handleSubmit } = useRequest(
|
||||||
useCallback(
|
useCallback(
|
||||||
async formData => {
|
async formData => {
|
||||||
|
let response;
|
||||||
if (formData.application) {
|
if (formData.application) {
|
||||||
formData.application = formData.application?.id || null;
|
response = await UsersAPI.createToken(userId, {
|
||||||
await UsersAPI.createToken(userId, formData);
|
...formData,
|
||||||
|
application: formData.application?.id || null,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await TokensAPI.create(formData);
|
response = await TokensAPI.create(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
history.push(`/users/${userId}/tokens`);
|
onSuccessfulAdd(response.data);
|
||||||
|
|
||||||
|
history.push(`/users/${userId}/tokens/${response.data.id}/details`);
|
||||||
},
|
},
|
||||||
[history, userId]
|
[history, userId, onSuccessfulAdd]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,29 +18,46 @@ jest.mock('react-router-dom', () => ({
|
|||||||
}));
|
}));
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
const onSuccessfulAdd = jest.fn();
|
||||||
|
|
||||||
describe('<UserTokenAdd />', () => {
|
describe('<UserTokenAdd />', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
test('handleSubmit should post to api', async () => {
|
test('handleSubmit should post to api', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<UserTokenAdd />);
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenAdd onSuccessfulAdd={onSuccessfulAdd} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
UsersAPI.createToken.mockResolvedValueOnce({ data: { id: 1 } });
|
UsersAPI.createToken.mockResolvedValueOnce({ data: { id: 1 } });
|
||||||
const tokenData = {
|
const tokenData = {
|
||||||
application: 1,
|
application: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
description: 'foo',
|
description: 'foo',
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
};
|
};
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('UserTokenForm').prop('handleSubmit')(tokenData);
|
wrapper.find('UserTokenForm').prop('handleSubmit')(tokenData);
|
||||||
});
|
});
|
||||||
expect(UsersAPI.createToken).toHaveBeenCalledWith(1, tokenData);
|
expect(UsersAPI.createToken).toHaveBeenCalledWith(1, {
|
||||||
|
application: 1,
|
||||||
|
description: 'foo',
|
||||||
|
scope: 'read',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should navigate to tokens list when cancel is clicked', async () => {
|
test('should navigate to tokens list when cancel is clicked', async () => {
|
||||||
const history = createMemoryHistory({});
|
const history = createMemoryHistory({});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<UserTokenAdd />, {
|
wrapper = mountWithContexts(
|
||||||
context: { router: { history } },
|
<UserTokenAdd onSuccessfulAdd={onSuccessfulAdd} />,
|
||||||
});
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
@@ -48,51 +65,67 @@ describe('<UserTokenAdd />', () => {
|
|||||||
expect(history.location.pathname).toEqual('/users/1/tokens');
|
expect(history.location.pathname).toEqual('/users/1/tokens');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('successful form submission should trigger redirect', async () => {
|
test('successful form submission with application', async () => {
|
||||||
const history = createMemoryHistory({});
|
const history = createMemoryHistory({});
|
||||||
const tokenData = {
|
const tokenData = {
|
||||||
application: 1,
|
application: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
description: 'foo',
|
description: 'foo',
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
};
|
};
|
||||||
|
const rtnData = {
|
||||||
|
id: 2,
|
||||||
|
token: 'abc',
|
||||||
|
refresh_token: 'def',
|
||||||
|
expires: '3020-03-28T14:26:48.099297Z',
|
||||||
|
};
|
||||||
UsersAPI.createToken.mockResolvedValueOnce({
|
UsersAPI.createToken.mockResolvedValueOnce({
|
||||||
data: {
|
data: rtnData,
|
||||||
id: 2,
|
|
||||||
...tokenData,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<UserTokenAdd />, {
|
wrapper = mountWithContexts(
|
||||||
context: { router: { history } },
|
<UserTokenAdd onSuccessfulAdd={onSuccessfulAdd} />,
|
||||||
});
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('UserTokenForm').prop('handleSubmit')(tokenData);
|
wrapper.find('UserTokenForm').prop('handleSubmit')(tokenData);
|
||||||
});
|
});
|
||||||
expect(history.location.pathname).toEqual('/users/1/tokens');
|
expect(history.location.pathname).toEqual('/users/1/tokens/2/details');
|
||||||
|
expect(onSuccessfulAdd).toHaveBeenCalledWith(rtnData);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successful submit form with application', async () => {
|
test('successful form submission without application', async () => {
|
||||||
const history = createMemoryHistory({});
|
const history = createMemoryHistory({});
|
||||||
const tokenData = {
|
const tokenData = {
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
};
|
};
|
||||||
|
const rtnData = {
|
||||||
|
id: 2,
|
||||||
|
token: 'abc',
|
||||||
|
refresh_token: null,
|
||||||
|
expires: '3020-03-28T14:26:48.099297Z',
|
||||||
|
};
|
||||||
TokensAPI.create.mockResolvedValueOnce({
|
TokensAPI.create.mockResolvedValueOnce({
|
||||||
data: {
|
data: rtnData,
|
||||||
id: 2,
|
|
||||||
...tokenData,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<UserTokenAdd />, {
|
wrapper = mountWithContexts(
|
||||||
context: { router: { history } },
|
<UserTokenAdd onSuccessfulAdd={onSuccessfulAdd} />,
|
||||||
});
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('UserTokenForm').prop('handleSubmit')(tokenData);
|
wrapper.find('UserTokenForm').prop('handleSubmit')(tokenData);
|
||||||
});
|
});
|
||||||
expect(history.location.pathname).toEqual('/users/1/tokens');
|
expect(history.location.pathname).toEqual('/users/1/tokens/2/details');
|
||||||
|
expect(onSuccessfulAdd).toHaveBeenCalledWith(rtnData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
import { useHistory, 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 { Button } from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
@@ -14,11 +13,19 @@ import {
|
|||||||
} from '../../../components/DetailList';
|
} from '../../../components/DetailList';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import { TokensAPI } from '../../../api';
|
import { TokensAPI } from '../../../api';
|
||||||
|
import { formatDateString } from '../../../util/dates';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { toTitleCase } from '../../../util/strings';
|
import { toTitleCase } from '../../../util/strings';
|
||||||
|
|
||||||
function UserTokenDetail({ token, canEditOrDelete, i18n }) {
|
function UserTokenDetail({ token, i18n }) {
|
||||||
const { scope, description, created, modified, summary_fields } = token;
|
const {
|
||||||
|
scope,
|
||||||
|
description,
|
||||||
|
created,
|
||||||
|
modified,
|
||||||
|
expires,
|
||||||
|
summary_fields,
|
||||||
|
} = token;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { id, tokenId } = useParams();
|
const { id, tokenId } = useParams();
|
||||||
const { request: deleteToken, isLoading, error: deleteError } = useRequest(
|
const { request: deleteToken, isLoading, error: deleteError } = useRequest(
|
||||||
@@ -37,39 +44,43 @@ function UserTokenDetail({ token, canEditOrDelete, i18n }) {
|
|||||||
value={summary_fields?.application?.name}
|
value={summary_fields?.application?.name}
|
||||||
dataCy="application-token-detail-name"
|
dataCy="application-token-detail-name"
|
||||||
/>
|
/>
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail
|
||||||
<Detail label={i18n._(t`Scope`)} value={toTitleCase(scope)} />
|
label={i18n._(t`Description`)}
|
||||||
|
value={description}
|
||||||
|
dataCy="application-token-detail-description"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Scope`)}
|
||||||
|
value={toTitleCase(scope)}
|
||||||
|
dataCy="application-token-detail-scope"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Expires`)}
|
||||||
|
value={formatDateString(expires)}
|
||||||
|
dataCy="application-token-detail-expires"
|
||||||
|
/>
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={i18n._(t`Created`)}
|
label={i18n._(t`Created`)}
|
||||||
date={created}
|
date={created}
|
||||||
user={summary_fields.user}
|
user={summary_fields.user}
|
||||||
|
dataCy="application-token-detail-created"
|
||||||
/>
|
/>
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={i18n._(t`Last Modified`)}
|
label={i18n._(t`Last Modified`)}
|
||||||
date={modified}
|
date={modified}
|
||||||
user={summary_fields.user}
|
user={summary_fields.user}
|
||||||
|
dataCy="application-token-detail-last-modified"
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{canEditOrDelete && (
|
<DeleteButton
|
||||||
<>
|
name={summary_fields?.application?.name}
|
||||||
<Button
|
modalTitle={i18n._(t`Delete User Token`)}
|
||||||
aria-label={i18n._(t`Edit`)}
|
onConfirm={deleteToken}
|
||||||
component={Link}
|
isDisabled={isLoading}
|
||||||
to={`/users/${id}/tokens/${tokenId}/details`}
|
>
|
||||||
>
|
{i18n._(t`Delete`)}
|
||||||
{i18n._(t`Edit`)}
|
</DeleteButton>
|
||||||
</Button>
|
|
||||||
<DeleteButton
|
|
||||||
name={summary_fields?.application?.name}
|
|
||||||
modalTitle={i18n._(t`Delete User Token`)}
|
|
||||||
onConfirm={deleteToken}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
>
|
|
||||||
{i18n._(t`Delete`)}
|
|
||||||
</DeleteButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardActionsRow>
|
</CardActionsRow>
|
||||||
{error && (
|
{error && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
|
|||||||
@@ -37,20 +37,12 @@ describe('<UserTokenDetail/>', () => {
|
|||||||
description: 'cdfsg',
|
description: 'cdfsg',
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
};
|
};
|
||||||
test('should call api for token details and actions', async () => {
|
test('should render properly', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<UserTokenDetail token={token} />);
|
||||||
<UserTokenDetail canEditOrDelete token={token} />
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('UserTokenDetail').length).toBe(1);
|
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(
|
expect(wrapper.find('Detail[label="Application"]').prop('value')).toBe(
|
||||||
'hg'
|
'hg'
|
||||||
@@ -65,23 +57,11 @@ describe('<UserTokenDetail/>', () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapper.find('UserDateDetail[label="Last Modified"]').prop('date')
|
wrapper.find('UserDateDetail[label="Last Modified"]').prop('date')
|
||||||
).toBe('2020-06-23T19:56:38.441353Z');
|
).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);
|
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 () => {
|
test('should delete token properly', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<UserTokenDetail token={token} />);
|
||||||
<UserTokenDetail canEditOrDelete token={token} />
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
await act(async () =>
|
await act(async () =>
|
||||||
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
||||||
@@ -90,7 +70,7 @@ describe('<UserTokenDetail/>', () => {
|
|||||||
await act(async () => wrapper.find('DeleteButton').prop('onConfirm')());
|
await act(async () => wrapper.find('DeleteButton').prop('onConfirm')());
|
||||||
expect(TokensAPI.destroy).toBeCalledWith(2);
|
expect(TokensAPI.destroy).toBeCalledWith(2);
|
||||||
});
|
});
|
||||||
test('should throw deletion error', async () => {
|
test('should display error on failed deletion', async () => {
|
||||||
TokensAPI.destroy.mockRejectedValue(
|
TokensAPI.destroy.mockRejectedValue(
|
||||||
new Error({
|
new Error({
|
||||||
response: {
|
response: {
|
||||||
@@ -104,9 +84,7 @@ describe('<UserTokenDetail/>', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<UserTokenDetail token={token} />);
|
||||||
<UserTokenDetail canEditOrDelete token={token} />
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
await act(async () =>
|
await act(async () =>
|
||||||
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function UserTokenList({ i18n }) {
|
|||||||
onRowClick={handleSelect}
|
onRowClick={handleSelect}
|
||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Application name`),
|
||||||
key: 'application__name__icontains',
|
key: 'application__name__icontains',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
@@ -119,7 +119,7 @@ function UserTokenList({ i18n }) {
|
|||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Application name`),
|
||||||
key: 'application__name',
|
key: 'application__name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,28 +36,33 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) {
|
|||||||
/>
|
/>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
|
<DataListCell aria-label={i18n._(t`Token type`)} key="type">
|
||||||
|
<Link to={`/users/${id}/tokens/${token.id}/details`}>
|
||||||
|
{token.summary_fields?.application
|
||||||
|
? i18n._(t`Application access token`)
|
||||||
|
: i18n._(t`Personal access token`)}
|
||||||
|
</Link>
|
||||||
|
</DataListCell>,
|
||||||
<DataListCell
|
<DataListCell
|
||||||
aria-label={i18n._(t`application name`)}
|
aria-label={i18n._(t`Application name`)}
|
||||||
key={token.id}
|
key="applicationName"
|
||||||
>
|
>
|
||||||
{token.summary_fields?.application?.name ? (
|
{token.summary_fields?.application && (
|
||||||
<span>
|
<span>
|
||||||
<NameLabel>{i18n._(t`Application`)}</NameLabel>
|
<NameLabel>{i18n._(t`Application`)}</NameLabel>
|
||||||
<Link to={`/users/${id}/tokens/${token.id}/details`}>
|
<Link
|
||||||
|
to={`/applications/${token.summary_fields.application.id}/details`}
|
||||||
|
>
|
||||||
{token.summary_fields.application.name}
|
{token.summary_fields.application.name}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
|
||||||
<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="scope">
|
||||||
<Label>{i18n._(t`Scope`)}</Label>
|
<Label>{i18n._(t`Scope`)}</Label>
|
||||||
{toTitleCase(token.scope)}
|
{toTitleCase(token.scope)}
|
||||||
</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>
|
||||||
{formatDateString(token.expires)}
|
{formatDateString(token.expires)}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const token = {
|
|||||||
},
|
},
|
||||||
application: {
|
application: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'app',
|
name: 'Foobar app',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created: '2020-06-23T15:06:43.188634Z',
|
created: '2020-06-23T15:06:43.188634Z',
|
||||||
@@ -44,22 +44,57 @@ describe('<UserTokenListItem />', () => {
|
|||||||
expect(wrapper.find('UserTokenListItem').length).toBe(1);
|
expect(wrapper.find('UserTokenListItem').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render proper data', async () => {
|
test('should render application access token row properly', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<UserTokenListItem isSelected={false} token={token} />
|
<UserTokenListItem isSelected={false} token={token} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('DataListCheck').prop('checked')).toBe(false);
|
expect(wrapper.find('DataListCheck').prop('checked')).toBe(false);
|
||||||
|
expect(wrapper.find('PFDataListCell[aria-label="Token type"]').text()).toBe(
|
||||||
|
'Application access token'
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('PFDataListCell[aria-label="application name"]').text()
|
wrapper.find('PFDataListCell[aria-label="Application name"]').text()
|
||||||
).toBe('Applicationapp');
|
).toContain('Foobar app');
|
||||||
expect(wrapper.find('PFDataListCell[aria-label="scope"]').text()).toBe(
|
expect(wrapper.find('PFDataListCell[aria-label="Scope"]').text()).toContain(
|
||||||
'ScopeRead'
|
'Read'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('PFDataListCell[aria-label="expiration"]').text()).toBe(
|
expect(
|
||||||
'Expires10/25/3019, 3:06:43 PM'
|
wrapper.find('PFDataListCell[aria-label="Expiration"]').text()
|
||||||
|
).toContain('10/25/3019, 3:06:43 PM');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render personal access token row properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<UserTokenListItem
|
||||||
|
isSelected={false}
|
||||||
|
token={{
|
||||||
|
...token,
|
||||||
|
refresh_token: null,
|
||||||
|
application: null,
|
||||||
|
scope: 'write',
|
||||||
|
summary_fields: {
|
||||||
|
user: token.summary_fields.user,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('DataListCheck').prop('checked')).toBe(false);
|
||||||
|
expect(wrapper.find('PFDataListCell[aria-label="Token type"]').text()).toBe(
|
||||||
|
'Personal access token'
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PFDataListCell[aria-label="Application name"]').text()
|
||||||
|
).toBe('');
|
||||||
|
expect(wrapper.find('PFDataListCell[aria-label="Scope"]').text()).toContain(
|
||||||
|
'Write'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PFDataListCell[aria-label="Expiration"]').text()
|
||||||
|
).toContain('10/25/3019, 3:06:43 PM');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be checked', async () => {
|
test('should be checked', async () => {
|
||||||
|
|||||||
@@ -1,24 +1,99 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { Switch, Route, useParams } from 'react-router-dom';
|
import { Switch, Route, useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardCopyVariant,
|
||||||
|
Modal,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { formatDateString } from '../../../util/dates';
|
||||||
|
import { Detail, DetailList } from '../../../components/DetailList';
|
||||||
import UserTokenAdd from '../UserTokenAdd';
|
import UserTokenAdd from '../UserTokenAdd';
|
||||||
import UserTokenList from '../UserTokenList';
|
import UserTokenList from '../UserTokenList';
|
||||||
import UserToken from '../UserToken';
|
import UserToken from '../UserToken';
|
||||||
|
|
||||||
function UserTokens({ setBreadcrumb, user }) {
|
const TokenAlert = styled(Alert)`
|
||||||
|
margin-bottom: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function UserTokens({ i18n, setBreadcrumb, user }) {
|
||||||
|
const [tokenModalSource, setTokenModalSource] = useState(null);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const onSuccessfulAdd = useCallback(token => setTokenModalSource(token), [
|
||||||
|
setTokenModalSource,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<>
|
||||||
<Route key="add" path="/users/:id/tokens/add">
|
<Switch>
|
||||||
<UserTokenAdd id={Number(id)} />
|
<Route key="add" path="/users/:id/tokens/add">
|
||||||
</Route>
|
<UserTokenAdd id={Number(id)} onSuccessfulAdd={onSuccessfulAdd} />
|
||||||
<Route key="token" path="/users/:id/tokens/:tokenId">
|
</Route>
|
||||||
<UserToken user={user} setBreadcrumb={setBreadcrumb} id={Number(id)} />
|
<Route key="token" path="/users/:id/tokens/:tokenId">
|
||||||
</Route>
|
<UserToken
|
||||||
<Route key="list" path="/users/:id/tokens">
|
user={user}
|
||||||
<UserTokenList id={Number(id)} />
|
setBreadcrumb={setBreadcrumb}
|
||||||
</Route>
|
id={Number(id)}
|
||||||
</Switch>
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route key="list" path="/users/:id/tokens">
|
||||||
|
<UserTokenList id={Number(id)} />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
{tokenModalSource && (
|
||||||
|
<Modal
|
||||||
|
aria-label={i18n._(t`Token information`)}
|
||||||
|
isOpen
|
||||||
|
variant="medium"
|
||||||
|
title={i18n._(t`Token information`)}
|
||||||
|
onClose={() => setTokenModalSource(null)}
|
||||||
|
>
|
||||||
|
<TokenAlert
|
||||||
|
variant="info"
|
||||||
|
isInline
|
||||||
|
title={i18n._(
|
||||||
|
t`This is the only time the token value and associated refresh token value will be shown.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DetailList stacked>
|
||||||
|
{tokenModalSource.token && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Token`)}
|
||||||
|
value={
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{tokenModalSource.token}
|
||||||
|
</ClipboardCopy>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tokenModalSource.refresh_token && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Refresh Token`)}
|
||||||
|
value={
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{tokenModalSource.refresh_token}
|
||||||
|
</ClipboardCopy>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Expires`)}
|
||||||
|
value={formatDateString(tokenModalSource.expires)}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
awx/ui_next/src/screens/User/UserTokens/UserTokens.test.jsx
Normal file
41
awx/ui_next/src/screens/User/UserTokens/UserTokens.test.jsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
|
import UserTokens from './UserTokens';
|
||||||
|
|
||||||
|
describe('<UserTokens />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders successfully', () => {
|
||||||
|
wrapper = mountWithContexts(<UserTokens />);
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows Application information modal after successful creation', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/users/1/tokens/add'],
|
||||||
|
});
|
||||||
|
wrapper = mountWithContexts(<UserTokens />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Modal[title="Token information"]').length).toBe(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('UserTokenAdd')
|
||||||
|
.props()
|
||||||
|
.onSuccessfulAdd({
|
||||||
|
expires: '3020-03-28T14:26:48.099297Z',
|
||||||
|
token: 'foobar',
|
||||||
|
refresh_token: 'aaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Modal[title="Token information"]').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user