diff --git a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx
index 0537fa813c..34f40645d7 100644
--- a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx
+++ b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx
@@ -8,7 +8,7 @@ import ApplicationForm from '../shared/ApplicationForm';
import { ApplicationsAPI } from '../../../api';
import { CardBody } from '../../../components/Card';
-function ApplicationAdd() {
+function ApplicationAdd({ onSuccessfulAdd }) {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
@@ -53,10 +53,9 @@ function ApplicationAdd() {
const handleSubmit = async ({ ...values }) => {
values.organization = values.organization.id;
try {
- const {
- data: { id },
- } = await ApplicationsAPI.create(values);
- history.push(`/applications/${id}/details`);
+ const { data } = await ApplicationsAPI.create(values);
+ onSuccessfulAdd(data);
+ history.push(`/applications/${data.id}/details`);
} catch (err) {
setSubmitError(err);
}
diff --git a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx
index 2bc0eba47e..dd480e8ab9 100644
--- a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx
+++ b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx
@@ -39,12 +39,16 @@ const options = {
},
};
+const onSuccessfulAdd = jest.fn();
+
describe('', () => {
let wrapper;
test('should render properly', async () => {
ApplicationsAPI.readOptions.mockResolvedValue(options);
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
expect(wrapper.find('ApplicationAdd').length).toBe(1);
expect(wrapper.find('ApplicationForm').length).toBe(1);
@@ -59,9 +63,12 @@ describe('', () => {
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
await act(async () => {
- wrapper = mountWithContexts(, {
- context: { router: { history } },
- });
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
});
await act(async () => {
@@ -124,6 +131,7 @@ describe('', () => {
redirect_uris: 'http://www.google.com',
});
expect(history.location.pathname).toBe('/applications/8/details');
+ expect(onSuccessfulAdd).toHaveBeenCalledWith({ id: 8 });
});
test('should cancel form properly', async () => {
@@ -134,9 +142,12 @@ describe('', () => {
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
await act(async () => {
- wrapper = mountWithContexts(, {
- context: { router: { history } },
- });
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
});
await act(async () => {
wrapper.find('Button[aria-label="Cancel"]').prop('onClick')();
@@ -157,7 +168,9 @@ describe('', () => {
ApplicationsAPI.create.mockRejectedValue(error);
ApplicationsAPI.readOptions.mockResolvedValue(options);
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
await act(async () => {
wrapper.find('Formik').prop('onSubmit')({
@@ -181,7 +194,9 @@ describe('', () => {
})
);
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx
index e6874b65a5..5e55cb1611 100644
--- a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx
+++ b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx
@@ -58,11 +58,12 @@ function ApplicationDetails({
}
+ dataCy="app-detail-organization"
/>
+
diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx
index 27fdb5f2f4..eebefb0ce0 100644
--- a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx
+++ b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx
@@ -120,6 +120,9 @@ describe('', () => {
expect(wrapper.find('Detail[label="Client type"]').prop('value')).toBe(
'Confidential'
);
+ expect(wrapper.find('Detail[label="Client ID"]').prop('value')).toBe(
+ 'b1dmj8xzkbFm1ZQ27ygw2ZeE9I0AXqqeL74fiyk4'
+ );
expect(wrapper.find('Button[aria-label="Edit"]').prop('to')).toBe(
'/applications/10/edit'
);
diff --git a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx
index 18268ba63a..13558bd2a0 100644
--- a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx
+++ b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
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 { ApplicationsAPI } from '../../../api';
import { CardBody } from '../../../components/Card';
@@ -29,22 +29,18 @@ function ApplicationEdit({
history.push(`/applications/${id}/details`);
};
return (
- <>
-
-
-
-
-
-
-
- >
+
+
+
+
+
);
}
export default ApplicationEdit;
diff --git a/awx/ui_next/src/screens/Application/Applications.jsx b/awx/ui_next/src/screens/Application/Applications.jsx
index 1842e5bd89..85995c8512 100644
--- a/awx/ui_next/src/screens/Application/Applications.jsx
+++ b/awx/ui_next/src/screens/Application/Applications.jsx
@@ -1,14 +1,26 @@
import React, { useState, useCallback } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
+import styled from 'styled-components';
import { Route, Switch } from 'react-router-dom';
-
+import {
+ Alert,
+ ClipboardCopy,
+ ClipboardCopyVariant,
+ Modal,
+} from '@patternfly/react-core';
import ApplicationsList from './ApplicationsList';
import ApplicationAdd from './ApplicationAdd';
import Application from './Application';
import Breadcrumbs from '../../components/Breadcrumbs';
+import { Detail, DetailList } from '../../components/DetailList';
+
+const ApplicationAlert = styled(Alert)`
+ margin-bottom: 20px;
+`;
function Applications({ i18n }) {
+ const [applicationModalSource, setApplicationModalSource] = useState(null);
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
'/applications': i18n._(t`Applications`),
'/applications/add': i18n._(t`Create New Application`),
@@ -36,7 +48,9 @@ function Applications({ i18n }) {
-
+ setApplicationModalSource(app)}
+ />
@@ -45,6 +59,57 @@ function Applications({ i18n }) {
+ {applicationModalSource && (
+ setApplicationModalSource(null)}
+ >
+ {applicationModalSource.client_secret && (
+
+ )}
+
+
+ {applicationModalSource.client_id && (
+
+ {applicationModalSource.client_id}
+
+ }
+ />
+ )}
+ {applicationModalSource.client_secret && (
+
+ {applicationModalSource.client_secret}
+
+ }
+ />
+ )}
+
+
+ )}
>
);
}
diff --git a/awx/ui_next/src/screens/Application/Applications.test.jsx b/awx/ui_next/src/screens/Application/Applications.test.jsx
index f309a2b60a..bb75e55deb 100644
--- a/awx/ui_next/src/screens/Application/Applications.test.jsx
+++ b/awx/ui_next/src/screens/Application/Applications.test.jsx
@@ -1,25 +1,48 @@
import React from 'react';
-
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import Applications from './Applications';
describe('', () => {
- let pageWrapper;
- let pageSections;
-
- beforeEach(() => {
- pageWrapper = mountWithContexts();
- pageSections = pageWrapper.find('PageSection');
- });
+ let wrapper;
afterEach(() => {
- pageWrapper.unmount();
+ wrapper.unmount();
});
- test('initially renders without crashing', () => {
- expect(pageWrapper.length).toBe(1);
+ test('renders successfully', () => {
+ wrapper = mountWithContexts();
+ const pageSections = wrapper.find('PageSection');
+ expect(wrapper.length).toBe(1);
expect(pageSections.length).toBe(1);
expect(pageSections.first().props().variant).toBe('light');
});
+
+ test('shows Application information modal after successful creation', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/applications/add'],
+ });
+ wrapper = mountWithContexts(, {
+ 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
+ );
+ });
});
diff --git a/awx/ui_next/src/screens/User/User.jsx b/awx/ui_next/src/screens/User/User.jsx
index c29948fe67..7fd0647b33 100644
--- a/awx/ui_next/src/screens/User/User.jsx
+++ b/awx/ui_next/src/screens/User/User.jsx
@@ -14,7 +14,6 @@ import { Card, PageSection } from '@patternfly/react-core';
import useRequest from '../../util/useRequest';
import { UsersAPI } from '../../api';
import ContentError from '../../components/ContentError';
-import ContentLoading from '../../components/ContentLoading';
import RoutedTabs from '../../components/RoutedTabs';
import UserDetail from './UserDetail';
import UserEdit from './UserEdit';
@@ -86,7 +85,7 @@ function User({ i18n, setBreadcrumb, me }) {
showCardHeader = false;
}
- if (contentError) {
+ if (!isLoading && contentError) {
return (
@@ -107,41 +106,34 @@ function User({ i18n, setBreadcrumb, me }) {
{showCardHeader && }
- {isLoading && }
- {!isLoading && user && (
-
-
- {user && (
-
-
-
- )}
- {user && (
-
-
-
- )}
-
+
+
+ {user && [
+
+
+ ,
+
+
+ ,
+
-
- {user && (
-
-
-
- )}
- {user && (
-
-
-
- )}
-
+ ,
+
+
+ ,
+
+
+ ,
+
-
-
+ ,
+ ]}
+
+ {!isLoading && (
{match.params.id && (
@@ -149,9 +141,9 @@ function User({ i18n, setBreadcrumb, me }) {
)}
-
-
- )}
+ )}
+
+
);
diff --git a/awx/ui_next/src/screens/User/User.test.jsx b/awx/ui_next/src/screens/User/User.test.jsx
index 5765aa2158..9046d4052c 100644
--- a/awx/ui_next/src/screens/User/User.test.jsx
+++ b/awx/ui_next/src/screens/User/User.test.jsx
@@ -22,10 +22,11 @@ async function getUsers() {
};
}
+UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
+UsersAPI.read.mockImplementation(getUsers);
+
describe('', () => {
test('initially renders successfully', async () => {
- UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
- UsersAPI.read.mockImplementation(getUsers);
const history = createMemoryHistory({
initialEntries: ['/users/1'],
});
@@ -49,8 +50,6 @@ describe('', () => {
});
test('tabs shown for users', async () => {
- UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
- UsersAPI.read.mockImplementation(getUsers);
const history = createMemoryHistory({
initialEntries: ['/users/1'],
});
@@ -81,9 +80,7 @@ describe('', () => {
expect(wrapper.find('Tabs TabButton').length).toEqual(6);
});
- test('should not now Tokens tab', async () => {
- UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
- UsersAPI.read.mockImplementation(getUsers);
+ test('should not show Tokens tab', async () => {
const history = createMemoryHistory({
initialEntries: ['/users/1'],
});
diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.jsx
index af6f0f0b16..e42a98b5a4 100644
--- a/awx/ui_next/src/screens/User/UserToken/UserToken.jsx
+++ b/awx/ui_next/src/screens/User/UserToken/UserToken.jsx
@@ -24,20 +24,16 @@ function UserToken({ i18n, setBreadcrumb, user }) {
isLoading,
error,
request: fetchToken,
- result: { token, actions },
+ result: { token },
} = useRequest(
useCallback(async () => {
- const [response, actionsResponse] = await Promise.all([
- TokensAPI.readDetail(tokenId),
- TokensAPI.readOptions(),
- ]);
+ const response = await TokensAPI.readDetail(tokenId);
setBreadcrumb(user, response.data);
return {
token: response.data,
- actions: actionsResponse.data.actions.POST,
};
}, [setBreadcrumb, user, tokenId]),
- { token: null, actions: null }
+ { token: null }
);
useEffect(() => {
fetchToken();
@@ -97,7 +93,7 @@ function UserToken({ i18n, setBreadcrumb, user }) {
/>
{token && (
-
+
)}
diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx
index 8e71f1b085..f8e551c7a7 100644
--- a/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx
+++ b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx
@@ -54,9 +54,6 @@ describe('', () => {
description: 'cdfsg',
scope: 'read',
});
- TokensAPI.readOptions.mockResolvedValue({
- data: { actions: { POST: true } },
- });
await act(async () => {
wrapper = mountWithContexts(
@@ -87,15 +84,11 @@ describe('', () => {
description: 'cdfsg',
scope: 'read',
});
- TokensAPI.readOptions.mockResolvedValue({
- data: { actions: { POST: true } },
- });
await act(async () => {
wrapper = mountWithContexts(
);
});
expect(TokensAPI.readDetail).toBeCalledWith(2);
- expect(TokensAPI.readOptions).toBeCalled();
});
});
diff --git a/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.jsx b/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.jsx
index 606171c028..ae7ea9a84f 100644
--- a/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.jsx
+++ b/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.jsx
@@ -6,22 +6,27 @@ import { TokensAPI, UsersAPI } from '../../../api';
import useRequest from '../../../util/useRequest';
import UserTokenFrom from '../shared/UserTokenForm';
-function UserTokenAdd() {
+function UserTokenAdd({ onSuccessfulAdd }) {
const history = useHistory();
const { id: userId } = useParams();
const { error: submitError, request: handleSubmit } = useRequest(
useCallback(
async formData => {
+ let response;
if (formData.application) {
- formData.application = formData.application?.id || null;
- await UsersAPI.createToken(userId, formData);
+ response = await UsersAPI.createToken(userId, {
+ ...formData,
+ application: formData.application?.id || null,
+ });
} 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]
)
);
diff --git a/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.test.jsx b/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.test.jsx
index 4323663c45..b74d2712c1 100644
--- a/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.test.jsx
+++ b/awx/ui_next/src/screens/User/UserTokenAdd/UserTokenAdd.test.jsx
@@ -18,29 +18,46 @@ jest.mock('react-router-dom', () => ({
}));
let wrapper;
+const onSuccessfulAdd = jest.fn();
+
describe('', () => {
+ afterEach(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
test('handleSubmit should post to api', async () => {
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
UsersAPI.createToken.mockResolvedValueOnce({ data: { id: 1 } });
const tokenData = {
- application: 1,
+ application: {
+ id: 1,
+ },
description: 'foo',
scope: 'read',
};
await act(async () => {
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 () => {
const history = createMemoryHistory({});
await act(async () => {
- wrapper = mountWithContexts(, {
- context: { router: { history } },
- });
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
});
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
@@ -48,51 +65,67 @@ describe('', () => {
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 tokenData = {
- application: 1,
+ application: {
+ id: 1,
+ },
description: 'foo',
scope: 'read',
};
+ const rtnData = {
+ id: 2,
+ token: 'abc',
+ refresh_token: 'def',
+ expires: '3020-03-28T14:26:48.099297Z',
+ };
UsersAPI.createToken.mockResolvedValueOnce({
- data: {
- id: 2,
- ...tokenData,
- },
+ data: rtnData,
});
await act(async () => {
- wrapper = mountWithContexts(, {
- context: { router: { history } },
- });
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
});
await waitForElement(wrapper, 'button[aria-label="Save"]');
await act(async () => {
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 tokenData = {
scope: 'read',
};
+ const rtnData = {
+ id: 2,
+ token: 'abc',
+ refresh_token: null,
+ expires: '3020-03-28T14:26:48.099297Z',
+ };
TokensAPI.create.mockResolvedValueOnce({
- data: {
- id: 2,
- ...tokenData,
- },
+ data: rtnData,
});
await act(async () => {
- wrapper = mountWithContexts(, {
- context: { router: { history } },
- });
+ wrapper = mountWithContexts(
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
});
await waitForElement(wrapper, 'button[aria-label="Save"]');
await act(async () => {
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);
});
});
diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx
index 4e6891767d..9285d05b43 100644
--- a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx
+++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx
@@ -1,8 +1,7 @@
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 { t } from '@lingui/macro';
-import { Button } from '@patternfly/react-core';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
@@ -14,11 +13,19 @@ import {
} from '../../../components/DetailList';
import ErrorDetail from '../../../components/ErrorDetail';
import { TokensAPI } from '../../../api';
+import { formatDateString } from '../../../util/dates';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { toTitleCase } from '../../../util/strings';
-function UserTokenDetail({ token, canEditOrDelete, i18n }) {
- const { scope, description, created, modified, summary_fields } = token;
+function UserTokenDetail({ token, i18n }) {
+ const {
+ scope,
+ description,
+ created,
+ modified,
+ expires,
+ summary_fields,
+ } = token;
const history = useHistory();
const { id, tokenId } = useParams();
const { request: deleteToken, isLoading, error: deleteError } = useRequest(
@@ -37,39 +44,43 @@ function UserTokenDetail({ token, canEditOrDelete, i18n }) {
value={summary_fields?.application?.name}
dataCy="application-token-detail-name"
/>
-
-
+
+
+
- {canEditOrDelete && (
- <>
-
-
- {i18n._(t`Delete`)}
-
- >
- )}
+
+ {i18n._(t`Delete`)}
+
{error && (
', () => {
description: 'cdfsg',
scope: 'read',
};
- test('should call api for token details and actions', async () => {
+ test('should render properly', async () => {
await act(async () => {
- wrapper = mountWithContexts(
-
- );
+ wrapper = mountWithContexts();
});
+
expect(wrapper.find('UserTokenDetail').length).toBe(1);
- });
- test('should call api for token details and actions', async () => {
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
- });
expect(wrapper.find('Detail[label="Application"]').prop('value')).toBe(
'hg'
@@ -65,23 +57,11 @@ describe('', () => {
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(
-
- );
- });
- 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(
-
- );
+ wrapper = mountWithContexts();
});
await act(async () =>
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
@@ -90,7 +70,7 @@ describe('', () => {
await act(async () => wrapper.find('DeleteButton').prop('onConfirm')());
expect(TokensAPI.destroy).toBeCalledWith(2);
});
- test('should throw deletion error', async () => {
+ test('should display error on failed deletion', async () => {
TokensAPI.destroy.mockRejectedValue(
new Error({
response: {
@@ -104,9 +84,7 @@ describe('', () => {
})
);
await act(async () => {
- wrapper = mountWithContexts(
-
- );
+ wrapper = mountWithContexts();
});
await act(async () =>
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx
index a60f4412b6..72ba8b31c5 100644
--- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx
+++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx
@@ -108,7 +108,7 @@ function UserTokenList({ i18n }) {
onRowClick={handleSelect}
toolbarSearchColumns={[
{
- name: i18n._(t`Name`),
+ name: i18n._(t`Application name`),
key: 'application__name__icontains',
isDefault: true,
},
@@ -119,7 +119,7 @@ function UserTokenList({ i18n }) {
]}
toolbarSortColumns={[
{
- name: i18n._(t`Name`),
+ name: i18n._(t`Application name`),
key: 'application__name',
},
{
diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx
index 52eb44a7f1..6ae8325723 100644
--- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx
+++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx
@@ -36,28 +36,33 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) {
/>
+
+ {token.summary_fields?.application
+ ? i18n._(t`Application access token`)
+ : i18n._(t`Personal access token`)}
+
+ ,
- {token.summary_fields?.application?.name ? (
+ {token.summary_fields?.application && (
{i18n._(t`Application`)}
-
+
{token.summary_fields.application.name}
- ) : (
-
- {i18n._(t`Personal access token`)}
-
)}
,
-
+
{toTitleCase(token.scope)}
,
-
+
{formatDateString(token.expires)}
,
diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx
index a91e2d1632..87bc06401c 100644
--- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx
+++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.test.jsx
@@ -21,7 +21,7 @@ const token = {
},
application: {
id: 1,
- name: 'app',
+ name: 'Foobar app',
},
},
created: '2020-06-23T15:06:43.188634Z',
@@ -44,22 +44,57 @@ describe('', () => {
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 () => {
wrapper = mountWithContexts(
);
});
expect(wrapper.find('DataListCheck').prop('checked')).toBe(false);
+ expect(wrapper.find('PFDataListCell[aria-label="Token type"]').text()).toBe(
+ 'Application access token'
+ );
expect(
- wrapper.find('PFDataListCell[aria-label="application name"]').text()
- ).toBe('Applicationapp');
- expect(wrapper.find('PFDataListCell[aria-label="scope"]').text()).toBe(
- 'ScopeRead'
+ wrapper.find('PFDataListCell[aria-label="Application name"]').text()
+ ).toContain('Foobar app');
+ expect(wrapper.find('PFDataListCell[aria-label="Scope"]').text()).toContain(
+ 'Read'
);
- expect(wrapper.find('PFDataListCell[aria-label="expiration"]').text()).toBe(
- 'Expires10/25/3019, 3:06:43 PM'
+ expect(
+ 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(
+
+ );
+ });
+ 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 () => {
diff --git a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx
index c73519d7f9..450bd41822 100644
--- a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx
+++ b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx
@@ -1,24 +1,99 @@
-import React from 'react';
+import React, { useCallback, useState } from '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 {
+ Alert,
+ ClipboardCopy,
+ ClipboardCopyVariant,
+ Modal,
+} from '@patternfly/react-core';
+import { formatDateString } from '../../../util/dates';
+import { Detail, DetailList } from '../../../components/DetailList';
import UserTokenAdd from '../UserTokenAdd';
import UserTokenList from '../UserTokenList';
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 onSuccessfulAdd = useCallback(token => setTokenModalSource(token), [
+ setTokenModalSource,
+ ]);
+
return (
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {tokenModalSource && (
+ setTokenModalSource(null)}
+ >
+
+
+ {tokenModalSource.token && (
+
+ {tokenModalSource.token}
+
+ }
+ />
+ )}
+ {tokenModalSource.refresh_token && (
+
+ {tokenModalSource.refresh_token}
+
+ }
+ />
+ )}
+
+
+
+ )}
+ >
);
}
diff --git a/awx/ui_next/src/screens/User/UserTokens/UserTokens.test.jsx b/awx/ui_next/src/screens/User/UserTokens/UserTokens.test.jsx
new file mode 100644
index 0000000000..7a66eadca7
--- /dev/null
+++ b/awx/ui_next/src/screens/User/UserTokens/UserTokens.test.jsx
@@ -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('', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('renders successfully', () => {
+ wrapper = mountWithContexts();
+ expect(wrapper.length).toBe(1);
+ });
+
+ test('shows Application information modal after successful creation', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/users/1/tokens/add'],
+ });
+ wrapper = mountWithContexts(, {
+ 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);
+ });
+});