mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 00:38:45 -03:30
Merge pull request #7742 from nixocio/ui_rebased_issue_7640
Add details page for instance group Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
26
awx/ui_next/src/components/DetailList/DetailBadge.jsx
Normal file
26
awx/ui_next/src/components/DetailList/DetailBadge.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { node } from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Badge } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import _Detail from './Detail';
|
||||||
|
|
||||||
|
const Detail = styled(_Detail)`
|
||||||
|
word-break: break-word;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function DetailBadge({ label, content, dataCy = null }) {
|
||||||
|
return (
|
||||||
|
<Detail
|
||||||
|
label={label}
|
||||||
|
dataCy={dataCy}
|
||||||
|
value={<Badge isRead>{content}</Badge>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DetailBadge.propTypes = {
|
||||||
|
label: node.isRequired,
|
||||||
|
content: node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailBadge;
|
||||||
@@ -2,3 +2,4 @@ export { default as DetailList } from './DetailList';
|
|||||||
export { default as Detail, DetailName, DetailValue } from './Detail';
|
export { default as Detail, DetailName, DetailValue } from './Detail';
|
||||||
export { default as DeletedDetail } from './DeletedDetail';
|
export { default as DeletedDetail } from './DeletedDetail';
|
||||||
export { default as UserDateDetail } from './UserDateDetail';
|
export { default as UserDateDetail } from './UserDateDetail';
|
||||||
|
export { default as DetailBadge } from './DetailBadge';
|
||||||
|
|||||||
131
awx/ui_next/src/screens/InstanceGroup/ContainerGroup.jsx
Normal file
131
awx/ui_next/src/screens/InstanceGroup/ContainerGroup.jsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
import { InstanceGroupsAPI } from '../../api';
|
||||||
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
|
import ContentError from '../../components/ContentError';
|
||||||
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
|
|
||||||
|
import ContainerGroupDetails from './ContainerGroupDetails';
|
||||||
|
import ContainerGroupEdit from './ContainerGroupEdit';
|
||||||
|
import Jobs from './Jobs';
|
||||||
|
|
||||||
|
function ContainerGroup({ i18n, setBreadcrumb }) {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
error: contentError,
|
||||||
|
request: fetchInstanceGroups,
|
||||||
|
result: instanceGroup,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await InstanceGroupsAPI.readDetail(id);
|
||||||
|
return data;
|
||||||
|
}, [id])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchInstanceGroups();
|
||||||
|
}, [fetchInstanceGroups, pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceGroup) {
|
||||||
|
setBreadcrumb(instanceGroup);
|
||||||
|
}
|
||||||
|
}, [instanceGroup, setBreadcrumb]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to instance groups`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: '/instance_groups',
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/instance_groups/container_group/${id}/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Jobs`),
|
||||||
|
link: `/instance_groups/container_group/${id}/jobs`,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isLoading && contentError) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={contentError}>
|
||||||
|
{contentError.response?.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Container group not found.`)}
|
||||||
|
{''}
|
||||||
|
<Link to="/instance_groups">
|
||||||
|
{i18n._(t`View all instance groups`)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cardHeader = <RoutedTabs tabsArray={tabsArray} />;
|
||||||
|
if (pathname.endsWith('edit')) {
|
||||||
|
cardHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
{cardHeader}
|
||||||
|
{isLoading && <ContentLoading />}
|
||||||
|
{!isLoading && instanceGroup && (
|
||||||
|
<Switch>
|
||||||
|
<Redirect
|
||||||
|
from="/instance_groups/container_group/:id"
|
||||||
|
to="/instance_groups/container_group/:id/details"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
{instanceGroup && (
|
||||||
|
<>
|
||||||
|
<Route path="/instance_groups/container_group/:id/edit">
|
||||||
|
<ContainerGroupEdit />
|
||||||
|
</Route>
|
||||||
|
<Route path="/instance_groups/container_group/:id/details">
|
||||||
|
<ContainerGroupDetails />
|
||||||
|
</Route>
|
||||||
|
<Route path="/instance_groups/container_group/:id/jobs">
|
||||||
|
<Jobs />
|
||||||
|
</Route>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ContainerGroup);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { InstanceGroupsAPI } from '../../api';
|
||||||
|
|
||||||
|
import ContainerGroup from './ContainerGroup';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useRouteMatch: () => ({
|
||||||
|
url: '/instance_groups/container_group',
|
||||||
|
}),
|
||||||
|
useParams: () => ({ id: 42 }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('<ContainerGroup/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
test('should render details properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ContainerGroup setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ContainerGroup').length).toBe(1);
|
||||||
|
expect(InstanceGroupsAPI.readDetail).toBeCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', async () => {
|
||||||
|
const expectedTabs = ['Back to instance groups', 'Details', 'Jobs'];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ContainerGroup setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/instance_groups/container_group/42/foobar'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ContainerGroup setBreadcrumb={() => {}} />, {
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function ContainerGroupAdd() {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<div>Add container group</div>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContainerGroupAdd;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ContainerGroupAdd';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function ContainerGroupDetails() {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<div>Container group details</div>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContainerGroupDetails;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ContainerGroupDetails';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function ContainerGroupEdit() {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<div>Edit container group</div>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContainerGroupEdit;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ContainerGroupEdit';
|
||||||
@@ -1,25 +1,140 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
import {
|
||||||
|
Link,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
import { InstanceGroupsAPI } from '../../api';
|
||||||
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
|
import ContentError from '../../components/ContentError';
|
||||||
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
|
|
||||||
import InstanceGroupDetails from './InstanceGroupDetails';
|
import InstanceGroupDetails from './InstanceGroupDetails';
|
||||||
import InstanceGroupEdit from './InstanceGroupEdit';
|
import InstanceGroupEdit from './InstanceGroupEdit';
|
||||||
|
import Jobs from './Jobs';
|
||||||
|
import Instances from './Instances';
|
||||||
|
|
||||||
|
function InstanceGroup({ i18n, setBreadcrumb }) {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
error: contentError,
|
||||||
|
request: fetchInstanceGroups,
|
||||||
|
result: instanceGroup,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await InstanceGroupsAPI.readDetail(id);
|
||||||
|
return data;
|
||||||
|
}, [id])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchInstanceGroups();
|
||||||
|
}, [fetchInstanceGroups, pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceGroup) {
|
||||||
|
setBreadcrumb(instanceGroup);
|
||||||
|
}
|
||||||
|
}, [instanceGroup, setBreadcrumb]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to instance groups`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: '/instance_groups',
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/instance_groups/${id}/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Instances`),
|
||||||
|
link: `/instance_groups/${id}/instances`,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Jobs`),
|
||||||
|
link: `/instance_groups/${id}/jobs`,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isLoading && contentError) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={contentError}>
|
||||||
|
{contentError.response?.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Instance group not found.`)}
|
||||||
|
{''}
|
||||||
|
<Link to="/instance_groups">
|
||||||
|
{i18n._(t`View all instance groups`)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cardHeader = <RoutedTabs tabsArray={tabsArray} />;
|
||||||
|
if (pathname.endsWith('edit')) {
|
||||||
|
cardHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
function InstanceGroup() {
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<PageSection>
|
||||||
<Redirect
|
<Card>
|
||||||
from="/instance_groups/:id"
|
{cardHeader}
|
||||||
to="/instance_groups/:id/details"
|
{isLoading && <ContentLoading />}
|
||||||
exact
|
{!isLoading && instanceGroup && (
|
||||||
/>
|
<Switch>
|
||||||
<Route path="/instance_groups/:id/edit">
|
<Redirect
|
||||||
<InstanceGroupEdit />
|
from="/instance_groups/:id"
|
||||||
</Route>
|
to="/instance_groups/:id/details"
|
||||||
<Route path="/instance_groups/:id/details">
|
exact
|
||||||
<InstanceGroupDetails />
|
/>
|
||||||
</Route>
|
{instanceGroup && (
|
||||||
</Switch>
|
<>
|
||||||
|
<Route path="/instance_groups/:id/edit">
|
||||||
|
<InstanceGroupEdit instanceGroup={instanceGroup} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/instance_groups/:id/details">
|
||||||
|
<InstanceGroupDetails instanceGroup={instanceGroup} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/instance_groups/:id/instances">
|
||||||
|
<Instances />
|
||||||
|
</Route>
|
||||||
|
<Route path="/instance_groups/:id/jobs">
|
||||||
|
<Jobs />
|
||||||
|
</Route>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InstanceGroup;
|
export default withI18n()(InstanceGroup);
|
||||||
|
|||||||
63
awx/ui_next/src/screens/InstanceGroup/InstanceGroup.test.jsx
Normal file
63
awx/ui_next/src/screens/InstanceGroup/InstanceGroup.test.jsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { InstanceGroupsAPI } from '../../api';
|
||||||
|
|
||||||
|
import InstanceGroup from './InstanceGroup';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useRouteMatch: () => ({
|
||||||
|
url: '/instance_groups',
|
||||||
|
}),
|
||||||
|
useParams: () => ({ id: 42 }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('<InstanceGroup/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
test('should render details properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InstanceGroup setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('InstanceGroup').length).toBe(1);
|
||||||
|
expect(InstanceGroupsAPI.readDetail).toBeCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', async () => {
|
||||||
|
const expectedTabs = [
|
||||||
|
'Back to instance groups',
|
||||||
|
'Details',
|
||||||
|
'Instances',
|
||||||
|
'Jobs',
|
||||||
|
];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InstanceGroup setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/instance_groups/42/foobar'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InstanceGroup setBreadcrumb={() => {}} />, {
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ function InstanceGroupAdd() {
|
|||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<div>Instance Group Add</div>
|
<div>Add instance group</div>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,134 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import 'styled-components/macro';
|
||||||
|
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
|
import {
|
||||||
|
Detail,
|
||||||
|
DetailList,
|
||||||
|
UserDateDetail,
|
||||||
|
DetailBadge,
|
||||||
|
} from '../../../components/DetailList';
|
||||||
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { InstanceGroupsAPI } from '../../../api';
|
||||||
|
|
||||||
|
function InstanceGroupDetails({ instanceGroup, i18n }) {
|
||||||
|
const { id, name } = instanceGroup;
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const {
|
||||||
|
request: deleteInstanceGroup,
|
||||||
|
isLoading,
|
||||||
|
error: deleteError,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await InstanceGroupsAPI.destroy(id);
|
||||||
|
history.push(`/instance_groups`);
|
||||||
|
}, [id, history])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
const isAvailable = item => {
|
||||||
|
return (
|
||||||
|
(item.policy_instance_minimum || item.policy_instance_percentage) &&
|
||||||
|
item.capacity
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function InstanceGroupDetails() {
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<CardBody>
|
||||||
<Card>
|
<DetailList>
|
||||||
<div>Instance Group Details</div>
|
<Detail
|
||||||
</Card>
|
label={i18n._(t`Name`)}
|
||||||
</PageSection>
|
value={name}
|
||||||
|
dataCy="instance-group-detail-name"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Type`)}
|
||||||
|
value={
|
||||||
|
instanceGroup.is_containerized
|
||||||
|
? i18n._(t`Container group`)
|
||||||
|
: i18n._(t`Instance group`)
|
||||||
|
}
|
||||||
|
dataCy="instance-group-type"
|
||||||
|
/>
|
||||||
|
<DetailBadge
|
||||||
|
label={i18n._(t`Policy instance minimum`)}
|
||||||
|
dataCy="instance-group-policy-instance-minimum"
|
||||||
|
content={instanceGroup.policy_instance_minimum}
|
||||||
|
/>
|
||||||
|
<DetailBadge
|
||||||
|
label={i18n._(t`Policy instance percentage`)}
|
||||||
|
dataCy="instance-group-policy-instance-percentage"
|
||||||
|
content={`${instanceGroup.policy_instance_percentage} %`}
|
||||||
|
/>
|
||||||
|
{isAvailable(instanceGroup) ? (
|
||||||
|
<DetailBadge
|
||||||
|
label={i18n._(t`Used capacity`)}
|
||||||
|
content={`${100 - instanceGroup.percent_capacity_remaining} %`}
|
||||||
|
dataCy="instance-group-used-capacity"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Used capacity`)}
|
||||||
|
value={<span css="color: red">{i18n._(t`Unavailable`)}</span>}
|
||||||
|
dataCy="instance-group-used-capacity"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Created`)}
|
||||||
|
date={instanceGroup.created}
|
||||||
|
user={instanceGroup.summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last Modified`)}
|
||||||
|
date={instanceGroup.modified}
|
||||||
|
user={instanceGroup.summary_fields.modified_by}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
|
||||||
|
<CardActionsRow>
|
||||||
|
{instanceGroup.summary_fields.user_capabilities &&
|
||||||
|
instanceGroup.summary_fields.user_capabilities.edit && (
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`edit`)}
|
||||||
|
component={Link}
|
||||||
|
to={`/instance_groups/${id}/edit`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{name !== 'tower' &&
|
||||||
|
instanceGroup.summary_fields.user_capabilities &&
|
||||||
|
instanceGroup.summary_fields.user_capabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={name}
|
||||||
|
modalTitle={i18n._(t`Delete instance group`)}
|
||||||
|
onConfirm={deleteInstanceGroup}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
{error && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
onClose={dismissError}
|
||||||
|
title={i18n._(t`Error`)}
|
||||||
|
variant="error"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InstanceGroupDetails;
|
export default withI18n()(InstanceGroupDetails);
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
|
import { InstanceGroupsAPI } from '../../../api';
|
||||||
|
|
||||||
|
import InstanceGroupDetails from './InstanceGroupDetails';
|
||||||
|
|
||||||
|
jest.mock('../../../api');
|
||||||
|
|
||||||
|
const instanceGroups = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
type: 'instance_group',
|
||||||
|
url: '/api/v2/instance_groups/1/',
|
||||||
|
capacity: 10,
|
||||||
|
policy_instance_minimum: 10,
|
||||||
|
policy_instance_percentage: 50,
|
||||||
|
percent_capacity_remaining: 60,
|
||||||
|
is_containerized: false,
|
||||||
|
created: '2020-07-21T18:41:02.818081Z',
|
||||||
|
modified: '2020-07-24T20:32:03.121079Z',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Bar',
|
||||||
|
type: 'instance_group',
|
||||||
|
url: '/api/v2/instance_groups/2/',
|
||||||
|
capacity: 0,
|
||||||
|
policy_instance_minimum: 0,
|
||||||
|
policy_instance_percentage: 0,
|
||||||
|
percent_capacity_remaining: 0,
|
||||||
|
is_containerized: true,
|
||||||
|
created: '2020-07-21T18:41:02.818081Z',
|
||||||
|
modified: '2020-07-24T20:32:03.121079Z',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: false,
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function expectDetailToMatch(wrapper, label, value) {
|
||||||
|
const detail = wrapper.find(`Detail[label="${label}"]`);
|
||||||
|
expect(detail).toHaveLength(1);
|
||||||
|
expect(detail.prop('value')).toEqual(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<InstanceGroupDetails/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
test('should render details properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InstanceGroupDetails instanceGroup={instanceGroups[0]} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
expectDetailToMatch(wrapper, 'Name', instanceGroups[0].name);
|
||||||
|
expectDetailToMatch(wrapper, 'Type', `Instance group`);
|
||||||
|
const dates = wrapper.find('UserDateDetail');
|
||||||
|
expect(dates).toHaveLength(2);
|
||||||
|
expect(dates.at(0).prop('date')).toEqual(instanceGroups[0].created);
|
||||||
|
expect(dates.at(1).prop('date')).toEqual(instanceGroups[0].modified);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('DetailBadge[label="Used capacity"]').prop('content')
|
||||||
|
).toBe(`${100 - instanceGroups[0].percent_capacity_remaining} %`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DetailBadge[label="Policy instance minimum"]')
|
||||||
|
.prop('content')
|
||||||
|
).toBe(instanceGroups[0].policy_instance_minimum);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DetailBadge[label="Policy instance percentage"]')
|
||||||
|
.prop('content')
|
||||||
|
).toBe(`${instanceGroups[0].policy_instance_percentage} %`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expected api call is made for delete', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/instance_groups/1/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InstanceGroupDetails instanceGroup={instanceGroups[0]} />,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||||
|
});
|
||||||
|
expect(InstanceGroupsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(history.location.pathname).toBe('/instance_groups');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render delete button for tower instance group', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InstanceGroupDetails instanceGroup={instanceGroups[1]} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render delete button', async () => {
|
||||||
|
instanceGroups[0].summary_fields.user_capabilities.delete = false;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InstanceGroupDetails instanceGroup={instanceGroups[0]} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render edit button', async () => {
|
||||||
|
instanceGroups[0].summary_fields.user_capabilities.edit = false;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InstanceGroupDetails instanceGroup={instanceGroups[0]} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ function InstanceGroupEdit() {
|
|||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<div>Instance Group Edit</div>
|
<div>Edit instance group</div>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
|||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
ToolbarAddButton,
|
|
||||||
} from '../../../components/PaginatedDataList';
|
} from '../../../components/PaginatedDataList';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import DatalistToolbar from '../../../components/DataListToolbar';
|
import DatalistToolbar from '../../../components/DataListToolbar';
|
||||||
|
import AddDropDownButton from '../../../components/AddDropDownButton';
|
||||||
|
|
||||||
import InstanceGroupListItem from './InstanceGroupListItem';
|
import InstanceGroupListItem from './InstanceGroupListItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('instance_group', {
|
const QS_CONFIG = getQSConfig('instance_group', {
|
||||||
@@ -137,6 +138,27 @@ function InstanceGroupList({ i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addButtonOptions = [
|
||||||
|
{
|
||||||
|
label: i18n._(t`Instance group`),
|
||||||
|
url: '/instance_groups/add',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n._(t`Container group`),
|
||||||
|
url: '/instance_groups/container_group/add',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const addButton = (
|
||||||
|
<AddDropDownButton key="add" dropdownItems={addButtonOptions} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDetailUrl = item => {
|
||||||
|
return item.is_containerized
|
||||||
|
? `${match.url}/container_group/${item.id}/details`
|
||||||
|
: `${match.url}/${item.id}/details`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -160,14 +182,7 @@ function InstanceGroupList({ i18n }) {
|
|||||||
}
|
}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
...(canAdd
|
...(canAdd ? [addButton] : []),
|
||||||
? [
|
|
||||||
<ToolbarAddButton
|
|
||||||
key="add"
|
|
||||||
linkTo={`${match.url}/add`}
|
|
||||||
/>,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
@@ -183,16 +198,12 @@ function InstanceGroupList({ i18n }) {
|
|||||||
key={instanceGroup.id}
|
key={instanceGroup.id}
|
||||||
value={instanceGroup.name}
|
value={instanceGroup.name}
|
||||||
instanceGroup={instanceGroup}
|
instanceGroup={instanceGroup}
|
||||||
detailUrl={`${match.url}/${instanceGroup.id}/details`}
|
detailUrl={getDetailUrl(instanceGroup)}
|
||||||
onSelect={() => handleSelect(instanceGroup)}
|
onSelect={() => handleSelect(instanceGroup)}
|
||||||
isSelected={selected.some(row => row.id === instanceGroup.id)}
|
isSelected={selected.some(row => row.id === instanceGroup.id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
emptyStateControls={
|
emptyStateControls={canAdd && addButton}
|
||||||
canAdd && (
|
|
||||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -162,7 +162,11 @@ function InstanceGroupListItem({
|
|||||||
aria-label={i18n._(t`Edit instance group`)}
|
aria-label={i18n._(t`Edit instance group`)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/instance_groups/${instanceGroup.id}/edit`}
|
to={
|
||||||
|
isContainerGroup(instanceGroup)
|
||||||
|
? `/instance_groups/container_group/${instanceGroup.id}/edit`
|
||||||
|
: `/instance_groups/${instanceGroup.id}/edit`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import { Route, Switch } from 'react-router-dom';
|
|||||||
import InstanceGroupAdd from './InstanceGroupAdd';
|
import InstanceGroupAdd from './InstanceGroupAdd';
|
||||||
import InstanceGroupList from './InstanceGroupList';
|
import InstanceGroupList from './InstanceGroupList';
|
||||||
import InstanceGroup from './InstanceGroup';
|
import InstanceGroup from './InstanceGroup';
|
||||||
|
|
||||||
|
import ContainerGroupAdd from './ContainerGroupAdd';
|
||||||
|
import ContainerGroup from './ContainerGroup';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import Breadcrumbs from '../../components/Breadcrumbs';
|
||||||
|
|
||||||
function InstanceGroups({ i18n }) {
|
function InstanceGroups({ i18n }) {
|
||||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
'/instance_groups': i18n._(t`Instance Groups`),
|
'/instance_groups': i18n._(t`Instance groups`),
|
||||||
'/instance_groups/add': i18n._(t`Create Instance Groups`),
|
'/instance_groups/add': i18n._(t`Create instance group`),
|
||||||
|
'/instance_groups/container_group/add': i18n._(t`Create container group`),
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildBreadcrumbConfig = useCallback(
|
const buildBreadcrumbConfig = useCallback(
|
||||||
@@ -20,9 +24,30 @@ function InstanceGroups({ i18n }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBreadcrumbConfig({
|
setBreadcrumbConfig({
|
||||||
'/instance_groups': i18n._(t`Instance Groups`),
|
'/instance_groups': i18n._(t`Instance group`),
|
||||||
'/instance_groups/add': i18n._(t`Create Instance Groups`),
|
'/instance_groups/add': i18n._(t`Create instance group`),
|
||||||
|
'/instance_groups/container_group/add': i18n._(
|
||||||
|
t`Create container group`
|
||||||
|
),
|
||||||
|
|
||||||
|
[`/instance_groups/${instanceGroups.id}/details`]: i18n._(t`Details`),
|
||||||
|
[`/instance_groups/${instanceGroups.id}/instances`]: i18n._(
|
||||||
|
t`Instances`
|
||||||
|
),
|
||||||
|
[`/instance_groups/${instanceGroups.id}/jobs`]: i18n._(t`Jobs`),
|
||||||
|
[`/instance_groups/${instanceGroups.id}/edit`]: i18n._(t`Edit details`),
|
||||||
[`/instance_groups/${instanceGroups.id}`]: `${instanceGroups.name}`,
|
[`/instance_groups/${instanceGroups.id}`]: `${instanceGroups.name}`,
|
||||||
|
|
||||||
|
[`/instance_groups/container_group/${instanceGroups.id}/details`]: i18n._(
|
||||||
|
t`Details`
|
||||||
|
),
|
||||||
|
[`/instance_groups/container_group/${instanceGroups.id}/jobs`]: i18n._(
|
||||||
|
t`Jobs`
|
||||||
|
),
|
||||||
|
[`/instance_groups/container_group/${instanceGroups.id}/edit`]: i18n._(
|
||||||
|
t`Edit details`
|
||||||
|
),
|
||||||
|
[`/instance_groups/container_group/${instanceGroups.id}`]: `${instanceGroups.name}`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n]
|
||||||
@@ -31,6 +56,12 @@ function InstanceGroups({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route path="/instance_groups/container_group/add">
|
||||||
|
<ContainerGroupAdd />
|
||||||
|
</Route>
|
||||||
|
<Route path="/instance_groups/container_group/:id">
|
||||||
|
<ContainerGroup setBreadcrumb={buildBreadcrumbConfig} />
|
||||||
|
</Route>
|
||||||
<Route path="/instance_groups/add">
|
<Route path="/instance_groups/add">
|
||||||
<InstanceGroupAdd />
|
<InstanceGroupAdd />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function Instances() {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<div>Instances</div>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Instances;
|
||||||
1
awx/ui_next/src/screens/InstanceGroup/Instances/index.js
Normal file
1
awx/ui_next/src/screens/InstanceGroup/Instances/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Instances';
|
||||||
14
awx/ui_next/src/screens/InstanceGroup/Jobs/Jobs.jsx
Normal file
14
awx/ui_next/src/screens/InstanceGroup/Jobs/Jobs.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function Jobs() {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<div>Jobs</div>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Jobs;
|
||||||
1
awx/ui_next/src/screens/InstanceGroup/Jobs/index.js
Normal file
1
awx/ui_next/src/screens/InstanceGroup/Jobs/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Jobs';
|
||||||
Reference in New Issue
Block a user