diff --git a/awx/ui_next/src/components/DetailList/DetailBadge.jsx b/awx/ui_next/src/components/DetailList/DetailBadge.jsx new file mode 100644 index 0000000000..7447a896f8 --- /dev/null +++ b/awx/ui_next/src/components/DetailList/DetailBadge.jsx @@ -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 ( + {content}} + /> + ); +} +DetailBadge.propTypes = { + label: node.isRequired, + content: node.isRequired, +}; + +export default DetailBadge; diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index 8573f1a614..6a12824bad 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -2,3 +2,4 @@ export { default as DetailList } from './DetailList'; export { default as Detail, DetailName, DetailValue } from './Detail'; export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; +export { default as DetailBadge } from './DetailBadge'; diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroup.jsx new file mode 100644 index 0000000000..265c27c379 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroup.jsx @@ -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: ( + <> + + {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 ( + + + + {contentError.response?.status === 404 && ( + + {i18n._(t`Container group not found.`)} + {''} + + {i18n._(t`View all instance groups`)} + + + )} + + + + ); + } + + let cardHeader = ; + if (pathname.endsWith('edit')) { + cardHeader = null; + } + + return ( + + + {cardHeader} + {isLoading && } + {!isLoading && instanceGroup && ( + + + {instanceGroup && ( + <> + + + + + + + + + + + )} + + )} + + + ); +} + +export default withI18n()(ContainerGroup); diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroup.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroup.test.jsx new file mode 100644 index 0000000000..308b21f7a5 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroup.test.jsx @@ -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('', () => { + let wrapper; + test('should render details properly', async () => { + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + 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( {}} />); + }); + 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( {}} />, { + context: { + router: { + history, + }, + }, + }); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx new file mode 100644 index 0000000000..f4dd75787e --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ContainerGroupAdd() { + return ( + + +
Add container group
+
+
+ ); +} + +export default ContainerGroupAdd; diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/index.js b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/index.js new file mode 100644 index 0000000000..d693720aac --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/index.js @@ -0,0 +1 @@ +export { default } from './ContainerGroupAdd'; diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx new file mode 100644 index 0000000000..80db274d98 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ContainerGroupDetails() { + return ( + + +
Container group details
+
+
+ ); +} + +export default ContainerGroupDetails; diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/index.js b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/index.js new file mode 100644 index 0000000000..b1b7e0d8c5 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/index.js @@ -0,0 +1 @@ +export { default } from './ContainerGroupDetails'; diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx new file mode 100644 index 0000000000..5df56a03f3 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ContainerGroupEdit() { + return ( + + +
Edit container group
+
+
+ ); +} + +export default ContainerGroupEdit; diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/index.js b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/index.js new file mode 100644 index 0000000000..cb97abe8ab --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/index.js @@ -0,0 +1 @@ +export { default } from './ContainerGroupEdit'; diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx index 60c23ec31f..a42048f503 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx @@ -1,25 +1,140 @@ -import React from 'react'; -import { Route, Switch, Redirect } from 'react-router-dom'; +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 InstanceGroupDetails from './InstanceGroupDetails'; 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: ( + <> + + {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 ( + + + + {contentError.response?.status === 404 && ( + + {i18n._(t`Instance group not found.`)} + {''} + + {i18n._(t`View all instance groups`)} + + + )} + + + + ); + } + + let cardHeader = ; + if (pathname.endsWith('edit')) { + cardHeader = null; + } -function InstanceGroup() { return ( - - - - - - - - - + + + {cardHeader} + {isLoading && } + {!isLoading && instanceGroup && ( + + + {instanceGroup && ( + <> + + + + + + + + + + + + + + )} + + )} + + ); } -export default InstanceGroup; +export default withI18n()(InstanceGroup); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.test.jsx new file mode 100644 index 0000000000..68c47ed20c --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.test.jsx @@ -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('', () => { + let wrapper; + test('should render details properly', async () => { + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + 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( {}} />); + }); + 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( {}} />, { + context: { + router: { + history, + }, + }, + }); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.jsx index fb8f034a0f..f80b53be8e 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupAdd/InstanceGroupAdd.jsx @@ -5,7 +5,7 @@ function InstanceGroupAdd() { return ( -
Instance Group Add
+
Add instance group
); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index f88675ec81..8df5e5b863 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -1,14 +1,134 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; +import React, { useCallback } from 'react'; +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 ( - - -
Instance Group Details
-
-
+ + + + + + + {isAvailable(instanceGroup) ? ( + + ) : ( + {i18n._(t`Unavailable`)}} + dataCy="instance-group-used-capacity" + /> + )} + + + + + + + {instanceGroup.summary_fields.user_capabilities && + instanceGroup.summary_fields.user_capabilities.edit && ( + + )} + {name !== 'tower' && + instanceGroup.summary_fields.user_capabilities && + instanceGroup.summary_fields.user_capabilities.delete && ( + + {i18n._(t`Delete`)} + + )} + + {error && ( + + )} + ); } -export default InstanceGroupDetails; +export default withI18n()(InstanceGroupDetails); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx new file mode 100644 index 0000000000..7df34cf91d --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx @@ -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('', () => { + let wrapper; + test('should render details properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + 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( + , + { + 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( + + ); + }); + 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( + + ); + }); + 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( + + ); + }); + wrapper.update(); + + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx index a06b1db2e8..2f724479ee 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx @@ -5,7 +5,7 @@ function InstanceGroupEdit() { return ( -
Instance Group Edit
+
Edit instance group
); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx index 3f7f67da19..06e6e756d2 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx @@ -10,11 +10,12 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; import PaginatedDataList, { ToolbarDeleteButton, - ToolbarAddButton, } from '../../../components/PaginatedDataList'; import ErrorDetail from '../../../components/ErrorDetail'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; +import AddDropDownButton from '../../../components/AddDropDownButton'; + import InstanceGroupListItem from './InstanceGroupListItem'; 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 = ( + + ); + + const getDetailUrl = item => { + return item.is_containerized + ? `${match.url}/container_group/${item.id}/details` + : `${match.url}/${item.id}/details`; + }; + return ( <> @@ -160,14 +182,7 @@ function InstanceGroupList({ i18n }) { } qsConfig={QS_CONFIG} additionalControls={[ - ...(canAdd - ? [ - , - ] - : []), + ...(canAdd ? [addButton] : []), handleSelect(instanceGroup)} isSelected={selected.some(row => row.id === instanceGroup.id)} /> )} - emptyStateControls={ - canAdd && ( - - ) - } + emptyStateControls={canAdd && addButton} /> diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 2b494d99f7..93e334d367 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -162,7 +162,11 @@ function InstanceGroupListItem({ aria-label={i18n._(t`Edit instance group`)} variant="plain" component={Link} - to={`/instance_groups/${instanceGroup.id}/edit`} + to={ + isContainerGroup(instanceGroup) + ? `/instance_groups/container_group/${instanceGroup.id}/edit` + : `/instance_groups/${instanceGroup.id}/edit` + } > diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx index 336d6e5037..4fbdd5d9b2 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx @@ -6,12 +6,16 @@ import { Route, Switch } from 'react-router-dom'; import InstanceGroupAdd from './InstanceGroupAdd'; import InstanceGroupList from './InstanceGroupList'; import InstanceGroup from './InstanceGroup'; + +import ContainerGroupAdd from './ContainerGroupAdd'; +import ContainerGroup from './ContainerGroup'; import Breadcrumbs from '../../components/Breadcrumbs'; function InstanceGroups({ i18n }) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ - '/instance_groups': i18n._(t`Instance Groups`), - '/instance_groups/add': i18n._(t`Create Instance Groups`), + '/instance_groups': i18n._(t`Instance groups`), + '/instance_groups/add': i18n._(t`Create instance group`), + '/instance_groups/container_group/add': i18n._(t`Create container group`), }); const buildBreadcrumbConfig = useCallback( @@ -20,9 +24,30 @@ function InstanceGroups({ i18n }) { return; } setBreadcrumbConfig({ - '/instance_groups': i18n._(t`Instance Groups`), - '/instance_groups/add': i18n._(t`Create Instance Groups`), + '/instance_groups': i18n._(t`Instance group`), + '/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/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] @@ -31,6 +56,12 @@ function InstanceGroups({ i18n }) { <> + + + + + + diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/Instances.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/Instances.jsx new file mode 100644 index 0000000000..b41760edd5 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/Instances.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function Instances() { + return ( + + +
Instances
+
+
+ ); +} + +export default Instances; diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/index.js b/awx/ui_next/src/screens/InstanceGroup/Instances/index.js new file mode 100644 index 0000000000..b018ebb049 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/index.js @@ -0,0 +1 @@ +export { default } from './Instances'; diff --git a/awx/ui_next/src/screens/InstanceGroup/Jobs/Jobs.jsx b/awx/ui_next/src/screens/InstanceGroup/Jobs/Jobs.jsx new file mode 100644 index 0000000000..aad6a4061d --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Jobs/Jobs.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function Jobs() { + return ( + + +
Jobs
+
+
+ ); +} + +export default Jobs; diff --git a/awx/ui_next/src/screens/InstanceGroup/Jobs/index.js b/awx/ui_next/src/screens/InstanceGroup/Jobs/index.js new file mode 100644 index 0000000000..9fc254c85c --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Jobs/index.js @@ -0,0 +1 @@ +export { default } from './Jobs';