mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge pull request #9225 from nixocio/ui_issue_8670
Add relaunch against failed hosts
Reviewed-by: Mat Wilson <mawilson@redhat.com>
https://github.com/one-t
This commit is contained in:
commit
56868dbedd
@ -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';
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -87,7 +87,7 @@ function LaunchButton({ resource, i18n, children, history }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRelaunch = async () => {
|
||||
const handleRelaunch = async params => {
|
||||
let readRelaunch;
|
||||
let relaunch;
|
||||
|
||||
@ -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`);
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
|
||||
96
awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
Normal file
96
awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
Normal file
@ -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);
|
||||
@ -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' });
|
||||
});
|
||||
});
|
||||
@ -1 +1,2 @@
|
||||
export { default } from './LaunchButton';
|
||||
export { default as LaunchButton } from './LaunchButton';
|
||||
export { default as ReLaunchDropDown } from './ReLaunchDropDown';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
@ -346,7 +349,14 @@ 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}>
|
||||
@ -354,7 +364,7 @@ function JobDetail({ job, i18n }) {
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
)}
|
||||
))}
|
||||
{job.summary_fields.user_capabilities.delete && (
|
||||
<DeleteButton
|
||||
name={job.name}
|
||||
|
||||
@ -11,7 +11,10 @@ import {
|
||||
} from '@patternfly/react-icons';
|
||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import DeleteButton from '../../../../components/DeleteButton';
|
||||
import LaunchButton from '../../../../components/LaunchButton';
|
||||
import {
|
||||
LaunchButton,
|
||||
ReLaunchDropDown,
|
||||
} from '../../../../components/LaunchButton';
|
||||
|
||||
const BadgeGroup = styled.div`
|
||||
margin-left: 20px;
|
||||
@ -125,18 +128,32 @@ const OutputToolbar = ({ i18n, job, jobStatus, onDelete, onCancel }) => {
|
||||
|
||||
{job.type !== 'system_job' &&
|
||||
job.summary_fields.user_capabilities?.start && (
|
||||
<Tooltip content={i18n._(t`Relaunch Job`)}>
|
||||
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={handleRelaunch}
|
||||
aria-label={i18n._(t`Relaunch`)}
|
||||
>
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
<Tooltip
|
||||
content={
|
||||
job.status === 'failed' && job.type === 'job'
|
||||
? i18n._(t`Relaunch using host parameters`)
|
||||
: i18n._(t`Relaunch Job`)
|
||||
}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
import styled from 'styled-components';
|
||||
import DataListCell from '../../../components/DataListCell';
|
||||
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import Sparkline from '../../../components/Sparkline';
|
||||
import { toTitleCase } from '../../../util/strings';
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ import {
|
||||
} from '../../../components/DetailList';
|
||||
import DeleteButton from '../../../components/DeleteButton';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||
import { JobTemplatesAPI } from '../../../api';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
UserDateDetail,
|
||||
} from '../../../components/DetailList';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import Sparkline from '../../../components/Sparkline';
|
||||
import { toTitleCase } from '../../../util/strings';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
WrenchIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import LaunchButton from '../../../components/LaunchButton';
|
||||
import { LaunchButton } from '../../../components/LaunchButton';
|
||||
import {
|
||||
WorkflowDispatchContext,
|
||||
WorkflowStateContext,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user