mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 02:50:02 -03:30
Adds Alert Modal, Breadcrumb, Nested Tabs and Refactors PR.
This commit is contained in:
parent
3ea37e1c79
commit
f8a754cf44
@ -3,6 +3,7 @@ import { shape, string, number, arrayOf } from 'prop-types';
|
||||
import { Tab, Tabs as PFTabs } from '@patternfly/react-core';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
|
||||
const Tabs = styled(PFTabs)`
|
||||
--pf-c-tabs__button--PaddingLeft: 20px;
|
||||
@ -62,7 +63,15 @@ function RoutedTabs(props) {
|
||||
eventKey={tab.id}
|
||||
key={tab.id}
|
||||
link={tab.link}
|
||||
title={tab.name}
|
||||
title={
|
||||
tab.isNestedTabs ? (
|
||||
<>
|
||||
<CaretLeftIcon /> {tab.name}
|
||||
</>
|
||||
) : (
|
||||
tab.name
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
@ -61,9 +61,11 @@ class Inventories extends Component {
|
||||
t`Create New Group`
|
||||
),
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/details`]: i18n._(t`Details`),
|
||||
group.id}`]: `${group && group.name}`,
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/edit`]: `${group && group.name}`,
|
||||
group.id}/details`]: i18n._(t`Group Details`),
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/edit`]: i18n._(t`Edit Details`),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
||||
@ -57,7 +57,11 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
</CardHeader>
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit') || location.pathname.endsWith('add')) {
|
||||
if (
|
||||
location.pathname.endsWith('edit') ||
|
||||
location.pathname.endsWith('add') ||
|
||||
location.pathname.includes('groups/')
|
||||
) {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
@ -127,6 +131,7 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
<InventoryGroups
|
||||
location={location}
|
||||
match={match}
|
||||
history={history}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
inventory={inventory}
|
||||
/>
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { CardHeader } from '@patternfly/react-core';
|
||||
|
||||
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
|
||||
import { GroupsAPI } from '@api';
|
||||
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
|
||||
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
||||
|
||||
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
||||
|
||||
function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) {
|
||||
function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
|
||||
const [inventoryGroup, setInventoryGroup] = useState(null);
|
||||
const [hasContentLoading, setContentLoading] = useState(true);
|
||||
const [hasContentError, setHasContentError] = useState(false);
|
||||
@ -32,64 +32,101 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory }) {
|
||||
|
||||
loadData();
|
||||
}, [match.params.groupId, setBreadcrumb, inventory]);
|
||||
|
||||
const tabsArray = [
|
||||
{
|
||||
name: i18n._(t`Return to Groups`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups`,
|
||||
id: 99,
|
||||
isNestedTabs: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
|
||||
inventoryGroup.id}/details`,
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`RelatedGroups`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
|
||||
inventoryGroup.id}/nested_groups`,
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Hosts`),
|
||||
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
|
||||
inventoryGroup.id}/nested_hosts`,
|
||||
id: 2,
|
||||
},
|
||||
];
|
||||
if (hasContentError) {
|
||||
return <ContentError />;
|
||||
}
|
||||
if (hasContentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
return (
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/inventory/:id/groups/:groupId"
|
||||
to="/inventories/inventory/:id/groups/:groupId/details"
|
||||
exact
|
||||
let cardHeader = hasContentLoading ? null : (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton
|
||||
linkTo={`/inventories/inventory/${inventory.id}/group`}
|
||||
/>
|
||||
{inventoryGroup && [
|
||||
<Route
|
||||
key="edit"
|
||||
path="/inventories/inventory/:id/groups/:groupId/edit"
|
||||
render={() => {
|
||||
return (
|
||||
<InventoryGroupEdit
|
||||
inventory={inventory}
|
||||
inventoryGroup={inventoryGroup}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/inventory/:id/groups/:groupId/details"
|
||||
render={() => {
|
||||
return (
|
||||
<InventoryGroupDetail
|
||||
inventory={inventory}
|
||||
inventoryGroup={inventoryGroup}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
</CardHeader>
|
||||
);
|
||||
if (
|
||||
!history.location.pathname.includes('groups/') ||
|
||||
history.location.pathname.endsWith('edit')
|
||||
) {
|
||||
cardHeader = null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{cardHeader}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/inventory/:id/groups/:groupId"
|
||||
to="/inventories/inventory/:id/groups/:groupId/details"
|
||||
exact
|
||||
/>
|
||||
{inventoryGroup && [
|
||||
<Route
|
||||
key="edit"
|
||||
path="/inventories/inventory/:id/groups/:groupId/edit"
|
||||
render={() => {
|
||||
return (
|
||||
<InventoryGroupEdit
|
||||
inventory={inventory}
|
||||
inventoryGroup={inventoryGroup}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/inventory/:id/groups/:groupId/details"
|
||||
render={() => {
|
||||
return <InventoryGroupDetail inventoryGroup={inventoryGroup} />;
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
<Route
|
||||
key="not-found"
|
||||
path="*"
|
||||
render={() =>
|
||||
!hasContentLoading && (
|
||||
<ContentError>
|
||||
{match.params.id && (
|
||||
<Link
|
||||
to={`/inventories/inventory/${match.params.id}/details`}
|
||||
>
|
||||
{i18n._(t`View Inventory Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
)
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
</Switch>
|
||||
render={() => {
|
||||
return (
|
||||
!hasContentLoading && (
|
||||
<ContentError>
|
||||
{inventory && (
|
||||
<Link to={`/inventories/inventory/${inventory.id}/details`}>
|
||||
{i18n._(t`View Inventory Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { GroupsAPI } from '@api';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import InventoryGroup from './InventoryGroup';
|
||||
|
||||
jest.mock('@api');
|
||||
GroupsAPI.readDetail.mockResolvedValue({
|
||||
data: {
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
variables: 'bizz: buzz',
|
||||
summary_fields: {
|
||||
created_by: { id: 1, name: 'Athena' },
|
||||
modified_by: { id: 1, name: 'Apollo' },
|
||||
},
|
||||
},
|
||||
});
|
||||
describe('<InventoryGroup />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
const inventory = { id: 1, name: 'Foo' };
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/groups/1/details'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroup inventory={inventory} setBreadcrumb={() => {}} />,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: {
|
||||
params: { id: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('renders successfully', async () => {
|
||||
await act(async () => {
|
||||
waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe(
|
||||
1
|
||||
);
|
||||
});
|
||||
test('expect Return to Groups tab to exist', async () => {
|
||||
await act(async () => {
|
||||
waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe(
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { GroupsAPI } from '@api';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
|
||||
import ContentError from '@components/ContentError';
|
||||
import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm';
|
||||
|
||||
function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) {
|
||||
@ -21,15 +21,14 @@ function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) {
|
||||
const handleCancel = () => {
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups`);
|
||||
};
|
||||
if (error) {
|
||||
return <ContentError />;
|
||||
}
|
||||
return (
|
||||
<InventoryGroupForm
|
||||
error={error}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<Card>
|
||||
<InventoryGroupForm
|
||||
error={error}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(InventoryGroupsAdd));
|
||||
|
||||
@ -13,7 +13,7 @@ describe('<InventoryGroupAdd />', () => {
|
||||
let history;
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/1/groups'],
|
||||
initialEntries: ['/inventories/inventory/1/groups'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
@ -34,14 +34,16 @@ describe('<InventoryGroupAdd />', () => {
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('InventoryGroupEdit renders successfully', () => {
|
||||
test('InventoryGroupAdd renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('cancel should navigate user to Inventory Groups List', async () => {
|
||||
await act(async () => {
|
||||
waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||
});
|
||||
expect(history.location.pathname).toEqual('/inventories/1/groups');
|
||||
expect(history.location.pathname).toEqual(
|
||||
'/inventories/inventory/1/groups'
|
||||
);
|
||||
});
|
||||
test('handleSubmit should call api', async () => {
|
||||
await act(async () => {
|
||||
|
||||
@ -5,14 +5,21 @@ import { CardBody, Button } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter, Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { VariablesInput } from '@components/CodeMirrorInput';
|
||||
import ContentError from '@components/ContentError';
|
||||
import { VariablesInput as CodeMirrorInput } from '@components/CodeMirrorInput';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import { formatDateString } from '@util/dates';
|
||||
|
||||
import { GroupsAPI } from '@api';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
|
||||
const VariablesInput = styled(CodeMirrorInput)`
|
||||
.pf-c-form__label {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
margin: 20px 0;
|
||||
`;
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -26,6 +33,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
try {
|
||||
await GroupsAPI.destroy(inventoryGroup.id);
|
||||
history.push(`/inventories/inventory/${match.params.id}/groups`);
|
||||
@ -34,7 +42,17 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
||||
}
|
||||
};
|
||||
if (error) {
|
||||
return <ContentError />;
|
||||
return (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
isOpen={error}
|
||||
onClose={() => setError(false)}
|
||||
>
|
||||
{i18n._(t`Failed to delete group ${inventoryGroup.name}.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
if (isDeleteModalOpen) {
|
||||
return (
|
||||
@ -77,21 +95,14 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
||||
label={i18n._(t`Description`)}
|
||||
value={inventoryGroup.description}
|
||||
/>
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Variables`)}
|
||||
value={
|
||||
<VariablesInput
|
||||
css="margin: 20px 0"
|
||||
id="inventoryGroup-variables"
|
||||
readOnly
|
||||
value={inventoryGroup.variables}
|
||||
rows={4}
|
||||
label=""
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DetailList>
|
||||
<VariablesInput
|
||||
id="inventoryGroup-variables"
|
||||
readOnly
|
||||
value={inventoryGroup.variables}
|
||||
rows={4}
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
<DetailList>
|
||||
<Detail
|
||||
label={i18n._(t`Created`)}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { GroupsAPI } from '@api';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
|
||||
import InventoryGroupDetail from './InventoryGroupDetail';
|
||||
@ -28,19 +29,24 @@ const inventoryGroup = {
|
||||
};
|
||||
describe('<InventoryGroupDetail />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/groups/1/edit'],
|
||||
});
|
||||
wrapper = mountWithContexts(
|
||||
<MemoryRouter
|
||||
initialEntries={['/inventories/inventory/1/groups/1/edit']}
|
||||
>
|
||||
<Route
|
||||
path="/inventories/inventory/:id/groups/:groupId"
|
||||
component={() => (
|
||||
<InventoryGroupDetail inventoryGroup={inventoryGroup} />
|
||||
)}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
<Route
|
||||
path="/inventories/inventory/:id/groups/:groupId"
|
||||
component={() => (
|
||||
<InventoryGroupDetail inventoryGroup={inventoryGroup} />
|
||||
)}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
}
|
||||
);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
@ -51,20 +57,22 @@ describe('<InventoryGroupDetail />', () => {
|
||||
test('InventoryGroupDetail renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('should open delete modal and then call api to delete the group', () => {
|
||||
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||
test('should open delete modal and then call api to delete the group', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'Modal', el => el.length === 1);
|
||||
expect(wrapper.find('Modal').length).toBe(1);
|
||||
wrapper.find('button[aria-label="confirm delete"]').simulate('click');
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="confirm delete"]').simulate('click');
|
||||
});
|
||||
expect(GroupsAPI.destroy).toBeCalledWith(1);
|
||||
});
|
||||
test('should navigate user to edit form on edit button click', async () => {
|
||||
wrapper.find('button[aria-label="Edit"]').prop('onClick');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Router')
|
||||
.at(1)
|
||||
.prop('history').location.pathname
|
||||
).toEqual('/inventories/inventory/1/groups/1/edit');
|
||||
expect(history.location.pathname).toEqual(
|
||||
'/inventories/inventory/1/groups/1/edit'
|
||||
);
|
||||
});
|
||||
test('details shoudld render with the proper values', () => {
|
||||
expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo');
|
||||
|
||||
@ -14,11 +14,15 @@ function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) {
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups`);
|
||||
history.push(
|
||||
`/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details`
|
||||
);
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups`);
|
||||
history.push(
|
||||
`/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details`
|
||||
);
|
||||
};
|
||||
return (
|
||||
<InventoryGroupForm
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user