Skip to content

Commit

Permalink
Merge pull request #9225 from nixocio/ui_issue_8670
Browse files Browse the repository at this point in the history
Add relaunch against failed hosts

Reviewed-by: Mat Wilson <[email protected]>
             https://github.com/one-t
  • Loading branch information
softwarefactory-project-zuul[bot] authored Feb 16, 2021
2 parents 4e48118 + 33b1028 commit 56868db
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 38 deletions.
38 changes: 25 additions & 13 deletions awx/ui_next/src/components/JobList/JobListItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
import { RocketIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import { ActionsTd, ActionItem } from '../PaginatedTable';
import LaunchButton from '../LaunchButton';
import { LaunchButton, ReLaunchDropDown } from '../LaunchButton';
import StatusLabel from '../StatusLabel';
import { DetailList, Detail, LaunchedByDetail } from '../DetailList';
import ChipGroup from '../ChipGroup';
Expand Down Expand Up @@ -83,19 +83,31 @@ function JobListItem({
job.type !== 'system_job' &&
job.summary_fields?.user_capabilities?.start
}
tooltip={i18n._(t`Relaunch Job`)}
tooltip={
job.status === 'failed' && job.type === 'job'
? i18n._(t`Relaunch using host parameters`)
: i18n._(t`Relaunch Job`)
}
>
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button
variant="plain"
onClick={handleRelaunch}
aria-label={i18n._(t`Relaunch`)}
>
<RocketIcon />
</Button>
)}
</LaunchButton>
{job.status === 'failed' && job.type === 'job' ? (
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
)}
</LaunchButton>
) : (
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button
variant="plain"
onClick={handleRelaunch}
aria-label={i18n._(t`Relaunch`)}
>
<RocketIcon />
</Button>
)}
</LaunchButton>
)}
</ActionItem>
</ActionsTd>
</Tr>
Expand Down
84 changes: 84 additions & 0 deletions awx/ui_next/src/components/JobList/JobListItem.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ describe('<JobListItem />', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});

test('launch button shown to users with launch capabilities', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});

test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts(
<table>
Expand Down Expand Up @@ -92,3 +96,83 @@ describe('<JobListItem />', () => {
expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1);
});
});

describe('<JobListItem with failed job />', () => {
let wrapper;

beforeEach(() => {
const history = createMemoryHistory({
initialEntries: ['/jobs'],
});
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{ ...mockJob, status: 'failed' }}
isSelected
onSelect={() => {}}
/>
</tbody>
</table>,
{ context: { router: { history } } }
);
});

afterEach(() => {
wrapper.unmount();
});

test('launch button shown to users with launch capabilities', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});

test('dropdown should be displayed in case of failed job', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
const dropdown = wrapper.find('Dropdown');
expect(dropdown).toHaveLength(1);
expect(dropdown.find('DropdownItem')).toHaveLength(0);
dropdown.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);
});

test('dropdown should not be rendered for job type different of playbook run', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{
...mockJob,
status: 'failed',
type: 'project_update',
}}
onSelect={() => {}}
isSelected
/>
</tbody>
</table>
);
expect(wrapper.find('LaunchButton').length).toBe(1);
expect(wrapper.find('Dropdown')).toHaveLength(0);
});

test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{
...mockJob,
status: 'failed',
summary_fields: { user_capabilities: { start: false } },
}}
detailUrl={`/jobs/playbook/${mockJob.id}`}
onSelect={() => {}}
isSelected={false}
/>
</tbody>
</table>
);
expect(wrapper.find('LaunchButton').length).toBe(0);
});
});
4 changes: 2 additions & 2 deletions awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function LaunchButton({ resource, i18n, children, history }) {
}
};

const handleRelaunch = async () => {
const handleRelaunch = async params => {
let readRelaunch;
let relaunch;

Expand Down Expand Up @@ -125,7 +125,7 @@ function LaunchButton({ resource, i18n, children, history }) {
} else if (resource.type === 'ad_hoc_command') {
relaunch = AdHocCommandsAPI.relaunch(resource.id);
} else if (resource.type === 'job') {
relaunch = JobsAPI.relaunch(resource.id);
relaunch = JobsAPI.relaunch(resource.id, params || {});
}
const { data: job } = await relaunch;
history.push(`/jobs/${job.id}/output`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('LaunchButton', () => {
await act(() => button.prop('onClick')());
expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1);
await sleep(0);
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1);
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1, {});
expect(history.location.pathname).toEqual('/jobs/9000/output');
});

Expand Down
96 changes: 96 additions & 0 deletions awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Dropdown,
DropdownToggle,
DropdownItem,
DropdownPosition,
DropdownSeparator,
DropdownDirection,
} from '@patternfly/react-core';
import { RocketIcon } from '@patternfly/react-icons';

function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n }) {
const [isOpen, setIsOPen] = useState(false);

const onToggle = () => {
setIsOPen(prev => !prev);
};

const dropdownItems = [
<DropdownItem
aria-label={i18n._(t`Relaunch on`)}
key="relaunch_on"
component="div"
isPlainText
>
{i18n._(t`Relaunch on`)}
</DropdownItem>,
<DropdownSeparator key="separator" />,
<DropdownItem
key="relaunch_all"
aria-label={i18n._(t`Relaunch all hosts`)}
component="button"
onClick={() => {
handleRelaunch({ hosts: 'all' });
}}
>
{i18n._(t`All`)}
</DropdownItem>,

<DropdownItem
key="relaunch_failed"
aria-label={i18n._(t`Relaunch failed hosts`)}
component="button"
onClick={() => {
handleRelaunch({ hosts: 'failed' });
}}
>
{i18n._(t`Failed hosts`)}
</DropdownItem>,
];

if (isPrimary) {
return (
<Dropdown
position={DropdownPosition.left}
direction={DropdownDirection.up}
isOpen={isOpen}
dropdownItems={dropdownItems}
toggle={
<DropdownToggle
toggleIndicator={null}
onToggle={onToggle}
aria-label={i18n._(`relaunch jobs`)}
id="relaunch_jobs"
isPrimary
>
{i18n._(t`Relaunch`)}
</DropdownToggle>
}
/>
);
}

return (
<Dropdown
isPlain
position={DropdownPosition.right}
isOpen={isOpen}
dropdownItems={dropdownItems}
toggle={
<DropdownToggle
toggleIndicator={null}
onToggle={onToggle}
aria-label={i18n._(`relaunch jobs`)}
id="relaunch_jobs"
>
<RocketIcon />
</DropdownToggle>
}
/>
);
}

export default withI18n()(ReLaunchDropDown);
55 changes: 55 additions & 0 deletions awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import ReLaunchDropDown from './ReLaunchDropDown';

describe('ReLaunchDropDown', () => {
const handleRelaunch = jest.fn();

test('expected content is rendered on initialization', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
);

expect(wrapper.find('Dropdown')).toHaveLength(1);
});

test('dropdown have expected items and callbacks', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
);
expect(wrapper.find('DropdownItem')).toHaveLength(0);
wrapper.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);

wrapper
.find('DropdownItem[aria-label="Relaunch failed hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'failed' });

wrapper
.find('DropdownItem[aria-label="Relaunch all hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
});

test('dropdown isPrimary have expected items and callbacks', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown isPrimary handleRelaunch={handleRelaunch} />
);
expect(wrapper.find('DropdownItem')).toHaveLength(0);
wrapper.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);

wrapper
.find('DropdownItem[aria-label="Relaunch failed hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'failed' });

wrapper
.find('DropdownItem[aria-label="Relaunch all hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
});
});
3 changes: 2 additions & 1 deletion awx/ui_next/src/components/LaunchButton/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from './LaunchButton';
export { default as LaunchButton } from './LaunchButton';
export { default as ReLaunchDropDown } from './ReLaunchDropDown';
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import CredentialChip from '../CredentialChip';
import { timeOfDay, formatDateString } from '../../util/dates';

import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api';
import LaunchButton from '../LaunchButton';
import { LaunchButton } from '../LaunchButton';
import Sparkline from '../Sparkline';
import { toTitleCase } from '../../util/strings';
import CopyButton from '../CopyButton';
Expand Down
16 changes: 13 additions & 3 deletions awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import CredentialChip from '../../../components/CredentialChip';
import { VariablesInput as _VariablesInput } from '../../../components/CodeMirrorInput';
import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail';
import LaunchButton from '../../../components/LaunchButton';
import {
LaunchButton,
ReLaunchDropDown,
} from '../../../components/LaunchButton';
import StatusIcon from '../../../components/StatusIcon';
import { toTitleCase } from '../../../util/strings';
import { formatDateString } from '../../../util/dates';
Expand Down Expand Up @@ -346,15 +349,22 @@ function JobDetail({ job, i18n }) {
)}
<CardActionsRow>
{job.type !== 'system_job' &&
job.summary_fields.user_capabilities.start && (
job.summary_fields.user_capabilities.start &&
(job.status === 'failed' && job.type === 'job' ? (
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<ReLaunchDropDown isPrimary handleRelaunch={handleRelaunch} />
)}
</LaunchButton>
) : (
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
{({ handleRelaunch }) => (
<Button type="submit" onClick={handleRelaunch}>
{i18n._(t`Relaunch`)}
</Button>
)}
</LaunchButton>
)}
))}
{job.summary_fields.user_capabilities.delete && (
<DeleteButton
name={job.name}
Expand Down
Loading

0 comments on commit 56868db

Please sign in to comment.