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:
softwarefactory-project-zuul[bot] 2020-07-30 20:07:54 +00:00 committed by GitHub
commit c0a0e16ba0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 830 additions and 48 deletions

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

View File

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

View 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);

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './ContainerGroupAdd';

View File

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

View File

@ -0,0 +1 @@
export { default } from './ContainerGroupDetails';

View File

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

View File

@ -0,0 +1 @@
export { default } from './ContainerGroupEdit';

View File

@ -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: (
<>
<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 (
<Switch>
<Redirect
from="/instance_groups/:id"
to="/instance_groups/:id/details"
exact
/>
<Route path="/instance_groups/:id/edit">
<InstanceGroupEdit />
</Route>
<Route path="/instance_groups/:id/details">
<InstanceGroupDetails />
</Route>
</Switch>
<PageSection>
<Card>
{cardHeader}
{isLoading && <ContentLoading />}
{!isLoading && instanceGroup && (
<Switch>
<Redirect
from="/instance_groups/:id"
to="/instance_groups/:id/details"
exact
/>
{instanceGroup && (
<>
<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);

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

View File

@ -5,7 +5,7 @@ function InstanceGroupAdd() {
return (
<PageSection>
<Card>
<div>Instance Group Add</div>
<div>Add instance group</div>
</Card>
</PageSection>
);

View File

@ -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 (
<PageSection>
<Card>
<div>Instance Group Details</div>
</Card>
</PageSection>
<CardBody>
<DetailList>
<Detail
label={i18n._(t`Name`)}
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);

View File

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

View File

@ -5,7 +5,7 @@ function InstanceGroupEdit() {
return (
<PageSection>
<Card>
<div>Instance Group Edit</div>
<div>Edit instance group</div>
</Card>
</PageSection>
);

View File

@ -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 = (
<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 (
<>
<PageSection>
@ -160,14 +182,7 @@ function InstanceGroupList({ i18n }) {
}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
? [
<ToolbarAddButton
key="add"
linkTo={`${match.url}/add`}
/>,
]
: []),
...(canAdd ? [addButton] : []),
<ToolbarDeleteButton
key="delete"
onDelete={handleDelete}
@ -183,16 +198,12 @@ function InstanceGroupList({ i18n }) {
key={instanceGroup.id}
value={instanceGroup.name}
instanceGroup={instanceGroup}
detailUrl={`${match.url}/${instanceGroup.id}/details`}
detailUrl={getDetailUrl(instanceGroup)}
onSelect={() => handleSelect(instanceGroup)}
isSelected={selected.some(row => row.id === instanceGroup.id)}
/>
)}
emptyStateControls={
canAdd && (
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
)
}
emptyStateControls={canAdd && addButton}
/>
</Card>
</PageSection>

View File

@ -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`
}
>
<PencilAltIcon />
</Button>

View File

@ -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 }) {
<>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<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">
<InstanceGroupAdd />
</Route>

View File

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

View File

@ -0,0 +1 @@
export { default } from './Instances';

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

View File

@ -0,0 +1 @@
export { default } from './Jobs';