Show client id/secret in modal after successful application add

This commit is contained in:
mabashian
2020-11-24 16:09:32 -05:00
parent be08e0ce69
commit 957ab9bf7c
7 changed files with 131 additions and 44 deletions

View File

@@ -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);
} }

View File

@@ -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();

View File

@@ -80,6 +80,7 @@ function ApplicationDetails({
application.authorization_grant_type application.authorization_grant_type
)} )}
/> />
<Detail label={i18n._(t`Client ID`)} value={application.client_id} />
<Detail <Detail
label={i18n._(t`Redirect uris`)} label={i18n._(t`Redirect uris`)}
value={application.redirect_uris} value={application.redirect_uris}

View File

@@ -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'
); );

View File

@@ -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;

View File

@@ -2,13 +2,19 @@ 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 { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import {
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';
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 +42,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 +53,48 @@ 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)}
>
<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>
)}
</> </>
); );
} }

View File

@@ -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
);
});
}); });