mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 20:30:46 -03:30
Adds Inventory Groups routing --sort of
Adds Inventory Groups Add Adds Inventory Groups Edit Adds Inventory Groups Form Adds api module for Groups Adds placeholder file for InventoryGroupsList. This was added to refine routing. Tgere are no tests for this file yet.
This commit is contained in:
parent
4dd4928aab
commit
c997fcfc2c
@ -27,7 +27,7 @@ class Inventories extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
setBreadCrumbConfig = inventory => {
|
||||
setBreadCrumbConfig = (inventory, group) => {
|
||||
const { i18n } = this.props;
|
||||
if (!inventory) {
|
||||
return;
|
||||
@ -57,6 +57,13 @@ class Inventories extends Component {
|
||||
),
|
||||
[`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
|
||||
[`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
|
||||
[`/inventories/inventory/${inventory.id}/groups/add`]: i18n._(
|
||||
t`Create New Group`
|
||||
),
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/details`]: i18n._(t`Details`),
|
||||
[`/inventories/inventory/${inventory.id}/groups/${group &&
|
||||
group.id}/edit`]: `${group && group.name}`,
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
||||
@ -123,7 +123,14 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
<Route
|
||||
key="groups"
|
||||
path="/inventories/inventory/:id/groups"
|
||||
render={() => <InventoryGroups inventory={inventory} />}
|
||||
render={() => (
|
||||
<InventoryGroups
|
||||
location={location}
|
||||
match={match}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
inventory={inventory}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
<Route
|
||||
key="hosts"
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
|
||||
import { GroupsAPI } from '@api';
|
||||
|
||||
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 }) {
|
||||
const [inventoryGroup, setInventoryGroup] = useState(null);
|
||||
const [hasContentLoading, setContentLoading] = useState(true);
|
||||
const [hasContentError, setHasContentError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const { data } = await GroupsAPI.readDetail(match.params.groupId);
|
||||
setInventoryGroup(data);
|
||||
setBreadcrumb(inventory, data);
|
||||
} catch (err) {
|
||||
setHasContentError(err);
|
||||
} finally {
|
||||
setContentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [match.params.groupId, setBreadcrumb, inventory]);
|
||||
|
||||
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
|
||||
/>
|
||||
{inventoryGroup && [
|
||||
<Route
|
||||
key="edit"
|
||||
path="/inventories/inventory/:id/groups/:groupId/edit"
|
||||
render={() => {
|
||||
return <InventoryGroupEdit 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>
|
||||
);
|
||||
}
|
||||
|
||||
export { InventoryGroups as _InventoryGroups };
|
||||
export default withI18n()(withRouter(InventoryGroups));
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryGroup';
|
||||
@ -0,0 +1,36 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { GroupsAPI } from '@api';
|
||||
|
||||
import ContentError from '@components/ContentError';
|
||||
import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm';
|
||||
|
||||
function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) {
|
||||
useEffect(() => setBreadcrumb(inventory), [inventory, setBreadcrumb]);
|
||||
const [error, setError] = useState(null);
|
||||
const handleSubmit = async values => {
|
||||
values.inventory = inventory.id;
|
||||
try {
|
||||
const { data } = await GroupsAPI.create(values);
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups/${data.id}`);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups`);
|
||||
};
|
||||
if (error) {
|
||||
return <ContentError />;
|
||||
}
|
||||
return (
|
||||
<InventoryGroupForm
|
||||
error={error}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(InventoryGroupsAdd));
|
||||
export { InventoryGroupsAdd as _InventoryGroupsAdd };
|
||||
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { GroupsAPI } from '@api';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
|
||||
import InventoryGroupAdd from './InventoryGroupAdd';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<InventoryGroupAdd />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/1/groups'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroupAdd
|
||||
setBreadcrumb={() => {}}
|
||||
inventory={{ inventory: { id: 1 } }}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('InventoryGroupEdit 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');
|
||||
});
|
||||
test('handleSubmit should call api', async () => {
|
||||
await act(async () => {
|
||||
waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('InventoryGroupForm').prop('handleSubmit')({
|
||||
name: 'Bar',
|
||||
description: 'Ansible',
|
||||
variables: 'ying: yang',
|
||||
});
|
||||
});
|
||||
|
||||
expect(GroupsAPI.create).toBeCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryGroupAdd';
|
||||
@ -0,0 +1,86 @@
|
||||
import React, { useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { CardBody, Button } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { VariablesInput } from '@components/CodeMirrorInput';
|
||||
import ContentError from '@components/ContentError';
|
||||
|
||||
import { GroupsAPI } from '@api';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
& > :not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
`;
|
||||
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
||||
const [error, setError] = useState(false);
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await GroupsAPI.destroy(inventoryGroup.id);
|
||||
history.push(`/inventories/inventory/${match.params.id}/groups`);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ContentError />;
|
||||
}
|
||||
return (
|
||||
<CardBody style={{ paddingTop: '20px' }}>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={inventoryGroup.name} />
|
||||
<Detail
|
||||
label={i18n._(t`Description`)}
|
||||
value={inventoryGroup.description}
|
||||
/>
|
||||
</DetailList>
|
||||
<VariablesInput
|
||||
css="margin: 20px 0"
|
||||
id="inventoryGroup-variables"
|
||||
readOnly
|
||||
value={inventoryGroup.variables}
|
||||
rows={4}
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
<DetailList>
|
||||
<Detail
|
||||
label={i18n._(t`Created`)}
|
||||
value={`${inventoryGroup.created} by ${inventoryGroup.summary_fields.created_by.username}`}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Modified`)}
|
||||
value={`${inventoryGroup.modified} by ${inventoryGroup.summary_fields.modified_by.username}`}
|
||||
/>
|
||||
</DetailList>
|
||||
<ActionButtonWrapper>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label={i18n._(t`Edit`)}
|
||||
onClick={() =>
|
||||
history.push(
|
||||
`/inventories/inventory/${match.params.id}/groups/${inventoryGroup.id}/edit`
|
||||
)
|
||||
}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</Button>
|
||||
</ActionButtonWrapper>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(InventoryGroupDetail));
|
||||
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { GroupsAPI } from '@api';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
|
||||
import InventoryGroupDetail from './InventoryGroupDetail';
|
||||
|
||||
jest.mock('@api');
|
||||
const inventoryGroup = {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
variables: 'bizz: buzz',
|
||||
id: 1,
|
||||
created: '10:00',
|
||||
modified: '12:00',
|
||||
summary_fields: {
|
||||
created_by: {
|
||||
username: 'James',
|
||||
},
|
||||
modified_by: {
|
||||
username: 'Bond',
|
||||
},
|
||||
},
|
||||
};
|
||||
describe('<InventoryGroupDetail />', () => {
|
||||
let wrapper;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<MemoryRouter
|
||||
initialEntries={['/inventories/inventory/1/groups/1/edit']}
|
||||
>
|
||||
<Route
|
||||
path="/inventories/inventory/:id/groups/:groupId"
|
||||
component={() => (
|
||||
<InventoryGroupDetail inventoryGroup={inventoryGroup} />
|
||||
)}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('InventoryGroupDetail renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('should call api to delete the group', () => {
|
||||
wrapper.find('button[aria-label="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');
|
||||
});
|
||||
test('details shoudld render with the proper values', () => {
|
||||
expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo');
|
||||
expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe(
|
||||
'Bar'
|
||||
);
|
||||
expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe(
|
||||
'10:00 by James'
|
||||
);
|
||||
expect(wrapper.find('Detail[label="Modified"]').prop('value')).toBe(
|
||||
'12:00 by Bond'
|
||||
);
|
||||
expect(wrapper.find('VariablesInput').prop('value')).toBe('bizz: buzz');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryGroupDetail';
|
||||
@ -0,0 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { GroupsAPI } from '@api';
|
||||
|
||||
import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm';
|
||||
|
||||
function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
await GroupsAPI.update(match.params.groupId, values);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups`);
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
history.push(`/inventories/inventory/${inventory.id}/groups`);
|
||||
};
|
||||
return (
|
||||
<InventoryGroupForm
|
||||
error={error}
|
||||
group={inventoryGroup}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(InventoryGroupEdit));
|
||||
export { InventoryGroupEdit as _InventoryGroupEdit };
|
||||
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { GroupsAPI } from '@api';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
|
||||
import InventoryGroupEdit from './InventoryGroupEdit';
|
||||
|
||||
jest.mock('@api');
|
||||
GroupsAPI.readDetail.mockResolvedValue({
|
||||
data: {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
variables: 'bizz: buzz',
|
||||
},
|
||||
});
|
||||
describe('<InventoryGroupEdit />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/1/groups'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroupEdit
|
||||
setBreadcrumb={jest.fn()}
|
||||
inventory={{ inventory: { id: 1 } }}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
match: {
|
||||
params: { groupId: 13 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('InventoryGroupEdit renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('cancel should navigate user to Inventory Groups List', async () => {
|
||||
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||
expect(history.location.pathname).toEqual('/inventories/1/groups');
|
||||
});
|
||||
test('handleSubmit should call api', async () => {
|
||||
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
|
||||
wrapper.find('InventoryGroupForm').prop('handleSubmit')({
|
||||
name: 'Bar',
|
||||
description: 'Ansible',
|
||||
variables: 'ying: yang',
|
||||
});
|
||||
expect(GroupsAPI.update).toBeCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryGroupEdit';
|
||||
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Formik } from 'formik';
|
||||
import { Form, Card, CardBody, CardHeader } from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import FormRow from '@components/FormRow';
|
||||
import FormField from '@components/FormField';
|
||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||
import { VariablesField } from '@components/CodeMirrorInput';
|
||||
import { required } from '@util/validators';
|
||||
|
||||
function InventoryGroupForm({
|
||||
i18n,
|
||||
error,
|
||||
group = {},
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
match,
|
||||
}) {
|
||||
const initialValues = {
|
||||
name: group.name || '',
|
||||
description: group.description || '',
|
||||
variables: group.variables || '---',
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="awx-c-card">
|
||||
<CardHeader>
|
||||
<CardCloseButton
|
||||
linkTo={`/inventories/inventory/${match.params.id}/groups`}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormRow css="grid-template-columns: repeat(auto-fit, minmax(300px, 500px));">
|
||||
<FormField
|
||||
id="inventoryGroup-name"
|
||||
name="name"
|
||||
type="text"
|
||||
label={i18n._(t`Name`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="inventoryGroup-description"
|
||||
name="description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<VariablesField
|
||||
id="host-variables"
|
||||
name="variables"
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
{error ? <div>error</div> : null}
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(withRouter(InventoryGroupForm));
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import InventoryGroupForm from './InventoryGroupForm';
|
||||
|
||||
const group = {
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
variables: 'ying: false',
|
||||
};
|
||||
describe('<InventoryGroupForm />', () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryGroupForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
group={group}
|
||||
/>
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('should render values for the fields that have them', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
expect(wrapper.find("FormGroup[label='Name']").length).toBe(1);
|
||||
expect(wrapper.find("FormGroup[label='Description']").length).toBe(1);
|
||||
expect(wrapper.find("VariablesField[label='Variables']").length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryGroupForm';
|
||||
@ -27,7 +27,7 @@ function InventoryGroupItem({
|
||||
onSelect,
|
||||
}) {
|
||||
const labelId = `check-action-${group.id}`;
|
||||
const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/detail`;
|
||||
const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`;
|
||||
const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`;
|
||||
|
||||
return (
|
||||
|
||||
@ -1,250 +1,50 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrashAltIcon } from '@patternfly/react-icons';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import { InventoriesAPI, GroupsAPI } from '@api';
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import styled from 'styled-components';
|
||||
import InventoryGroupItem from './InventoryGroupItem';
|
||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
const DeleteButton = styled(Button)`
|
||||
padding: 5px 8px;
|
||||
import InventoryGroupAdd from './InventoryGroupAdd/InventoryGroupAdd';
|
||||
|
||||
&:hover {
|
||||
background-color: #d9534f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: var(--pf-c-button--m-plain--Color);
|
||||
pointer-events: initial;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
function cannotDelete(item) {
|
||||
return !item.summary_fields.user_capabilities.delete;
|
||||
}
|
||||
|
||||
const useModal = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
function toggleModal() {
|
||||
setIsModalOpen(!isModalOpen);
|
||||
}
|
||||
|
||||
return {
|
||||
isModalOpen,
|
||||
toggleModal,
|
||||
};
|
||||
};
|
||||
|
||||
function InventoryGroups({ i18n, location, match }) {
|
||||
const [actions, setActions] = useState(null);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [deletionError, setDeletionError] = useState(null);
|
||||
const [groupCount, setGroupCount] = useState(0);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const { isModalOpen, toggleModal } = useModal();
|
||||
|
||||
const inventoryId = match.params.id;
|
||||
|
||||
const fetchGroups = (id, queryString) => {
|
||||
const params = parseQueryString(QS_CONFIG, queryString);
|
||||
return InventoriesAPI.readGroups(id, params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
{
|
||||
data: { actions: optionActions },
|
||||
},
|
||||
] = await Promise.all([
|
||||
fetchGroups(inventoryId, location.search),
|
||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
]);
|
||||
|
||||
setGroups(results);
|
||||
setGroupCount(count);
|
||||
setActions(optionActions);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [inventoryId, location]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...groups] : []);
|
||||
};
|
||||
|
||||
const handleSelect = row => {
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
setSelected(selected.filter(s => s.id !== row.id));
|
||||
} else {
|
||||
setSelected(selected.concat(row));
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = () => {
|
||||
const itemsUnableToDelete = selected
|
||||
.filter(cannotDelete)
|
||||
.map(item => item.name)
|
||||
.join(', ');
|
||||
|
||||
if (selected.some(cannotDelete)) {
|
||||
return (
|
||||
<div>
|
||||
{i18n._(
|
||||
t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}`
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (selected.length) {
|
||||
return i18n._(t`Delete`);
|
||||
}
|
||||
return i18n._(t`Select a row to delete`);
|
||||
};
|
||||
|
||||
const handleDelete = async option => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
/* eslint-disable no-await-in-loop, no-restricted-syntax */
|
||||
/* Delete groups sequentially to avoid api integrity errors */
|
||||
for (const group of selected) {
|
||||
if (option === 'delete') {
|
||||
await GroupsAPI.destroy(+group.id);
|
||||
} else if (option === 'promote') {
|
||||
await InventoriesAPI.promoteGroup(inventoryId, +group.id);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop, no-restricted-syntax */
|
||||
} catch (error) {
|
||||
setDeletionError(error);
|
||||
}
|
||||
|
||||
toggleModal();
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await fetchGroups(inventoryId, location.search);
|
||||
setGroups(results);
|
||||
setGroupCount(count);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
import InventoryGroup from './InventoryGroup/InventoryGroup';
|
||||
import InventoryGroupsList from './InventoryGroupsList';
|
||||
|
||||
function InventoryGroups({ setBreadcrumb, inventory, location, match }) {
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={groups}
|
||||
itemCount={groupCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => (
|
||||
<InventoryGroupItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
inventoryId={inventoryId}
|
||||
isSelected={selected.some(row => row.id === item.id)}
|
||||
onSelect={() => handleSelect(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||
<div>
|
||||
<DeleteButton
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={toggleModal}
|
||||
isDisabled={
|
||||
selected.length === 0 || selected.some(cannotDelete)
|
||||
}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</DeleteButton>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
canAdd && (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd && (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
|
||||
<Switch>
|
||||
{[
|
||||
<Route
|
||||
key="list"
|
||||
path="/inventories/inventory/:id/groups"
|
||||
render={() => {
|
||||
return <InventoryGroupsList location={location} match={match} />;
|
||||
}}
|
||||
/>,
|
||||
<Route
|
||||
key="add"
|
||||
path="/inventories/inventory/:id/groups/add"
|
||||
render={() => {
|
||||
return (
|
||||
<InventoryGroupAdd
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
inventory={inventory}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/inventory/:id/groups/:groupId/"
|
||||
render={() => (
|
||||
<InventoryGroup
|
||||
inventory={inventory}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setDeletionError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to delete one or more groups.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
<InventoryGroupsDeleteModal
|
||||
groups={selected}
|
||||
isModalOpen={isModalOpen}
|
||||
onClose={toggleModal}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>,
|
||||
]}
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export { InventoryGroups as _InventoryGroups };
|
||||
export default withI18n()(withRouter(InventoryGroups));
|
||||
|
||||
@ -0,0 +1,248 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrashAltIcon } from '@patternfly/react-icons';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import { InventoriesAPI, GroupsAPI } from '@api';
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import styled from 'styled-components';
|
||||
import InventoryGroupItem from './InventoryGroupItem';
|
||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
const DeleteButton = styled(Button)`
|
||||
padding: 5px 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #d9534f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: var(--pf-c-button--m-plain--Color);
|
||||
pointer-events: initial;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
function cannotDelete(item) {
|
||||
return !item.summary_fields.user_capabilities.delete;
|
||||
}
|
||||
|
||||
const useModal = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
function toggleModal() {
|
||||
setIsModalOpen(!isModalOpen);
|
||||
}
|
||||
|
||||
return {
|
||||
isModalOpen,
|
||||
toggleModal,
|
||||
};
|
||||
};
|
||||
|
||||
function InventoryGroupsList({ i18n, location, match }) {
|
||||
const [actions, setActions] = useState(null);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [deletionError, setDeletionError] = useState(null);
|
||||
const [groupCount, setGroupCount] = useState(0);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const { isModalOpen, toggleModal } = useModal();
|
||||
|
||||
const inventoryId = match.params.id;
|
||||
const fetchGroups = (id, queryString) => {
|
||||
const params = parseQueryString(QS_CONFIG, queryString);
|
||||
return InventoriesAPI.readGroups(id, params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
{
|
||||
data: { actions: optionActions },
|
||||
},
|
||||
] = await Promise.all([
|
||||
fetchGroups(inventoryId, location.search),
|
||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
]);
|
||||
|
||||
setGroups(results);
|
||||
setGroupCount(count);
|
||||
setActions(optionActions);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [inventoryId, location]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...groups] : []);
|
||||
};
|
||||
|
||||
const handleSelect = row => {
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
setSelected(selected.filter(s => s.id !== row.id));
|
||||
} else {
|
||||
setSelected(selected.concat(row));
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = () => {
|
||||
const itemsUnableToDelete = selected
|
||||
.filter(cannotDelete)
|
||||
.map(item => item.name)
|
||||
.join(', ');
|
||||
|
||||
if (selected.some(cannotDelete)) {
|
||||
return (
|
||||
<div>
|
||||
{i18n._(
|
||||
t`You do not have permission to delete the following Groups: ${itemsUnableToDelete}`
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (selected.length) {
|
||||
return i18n._(t`Delete`);
|
||||
}
|
||||
return i18n._(t`Select a row to delete`);
|
||||
};
|
||||
|
||||
const handleDelete = async option => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
/* eslint-disable no-await-in-loop, no-restricted-syntax */
|
||||
/* Delete groups sequentially to avoid api integrity errors */
|
||||
for (const group of selected) {
|
||||
if (option === 'delete') {
|
||||
await GroupsAPI.destroy(+group.id);
|
||||
} else if (option === 'promote') {
|
||||
await InventoriesAPI.promoteGroup(inventoryId, +group.id);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop, no-restricted-syntax */
|
||||
} catch (error) {
|
||||
setDeletionError(error);
|
||||
}
|
||||
|
||||
toggleModal();
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await fetchGroups(inventoryId, location.search);
|
||||
setGroups(results);
|
||||
setGroupCount(count);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const isAllSelected =
|
||||
selected.length > 0 && selected.length === groups.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={groups}
|
||||
itemCount={groupCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => (
|
||||
<InventoryGroupItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
inventoryId={inventoryId}
|
||||
isSelected={selected.some(row => row.id === item.id)}
|
||||
onSelect={() => handleSelect(item)}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||
<div>
|
||||
<DeleteButton
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={toggleModal}
|
||||
isDisabled={
|
||||
selected.length === 0 || selected.some(cannotDelete)
|
||||
}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</DeleteButton>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
canAdd && (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd && (
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setDeletionError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to delete one or more groups.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
<InventoryGroupsDeleteModal
|
||||
groups={selected}
|
||||
isModalOpen={isModalOpen}
|
||||
onClose={toggleModal}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default withI18n()(withRouter(InventoryGroupsList));
|
||||
Loading…
x
Reference in New Issue
Block a user