Skip to content

Commit

Permalink
Implement an option to choose a job type on relaunch (issue ansible#1…
Browse files Browse the repository at this point in the history
…4177)

- for all three relaunch scenarios (job list, detail and output)
- write js tests
  • Loading branch information
Sasa Jovicic committed Feb 2, 2024
1 parent 2fa5116 commit 02f77b5
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 29 deletions.
8 changes: 7 additions & 1 deletion awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3491,11 +3491,17 @@ class JobRelaunchSerializer(BaseSerializer):
choices=[('all', _('No change to job limit')), ('failed', _('All failed and unreachable hosts'))],
write_only=True,
)
job_type = serializers.ChoiceField(
required=False,
allow_null=True,
choices=NEW_JOB_TYPE_CHOICES,
write_only=True,
)
credential_passwords = VerbatimField(required=True, write_only=True)

class Meta:
model = Job
fields = ('passwords_needed_to_start', 'retry_counts', 'hosts', 'credential_passwords')
fields = ('passwords_needed_to_start', 'retry_counts', 'hosts', 'job_type', 'credential_passwords')

def validate_credential_passwords(self, value):
pnts = self.instance.passwords_needed_to_start
Expand Down
3 changes: 3 additions & 0 deletions awx/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3406,6 +3406,7 @@ def post(self, request, *args, **kwargs):

copy_kwargs = {}
retry_hosts = serializer.validated_data.get('hosts', None)
job_type = serializer.validated_data.get('job_type', None)
if retry_hosts and retry_hosts != 'all':
if obj.status in ACTIVE_STATES:
return Response(
Expand All @@ -3426,6 +3427,8 @@ def post(self, request, *args, **kwargs):
)
copy_kwargs['limit'] = ','.join(retry_host_list)

if job_type:
copy_kwargs['job_type'] = job_type
new_job = obj.copy_unified_job(**copy_kwargs)
result = new_job.signal_start(**serializer.validated_data['credential_passwords'])
if not result:
Expand Down
27 changes: 18 additions & 9 deletions awx/ui/src/components/JobList/JobListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,24 @@ function JobListItem({
) : (
<LaunchButton resource={job}>
{({ handleRelaunch, isLaunching }) => (
<Button
ouiaId={`${job.id}-relaunch-button`}
variant="plain"
onClick={() => handleRelaunch()}
aria-label={t`Relaunch`}
isDisabled={isLaunching}
>
<RocketIcon />
</Button>
(job.type === 'job' && (
<ReLaunchDropDown
handleRelaunch={handleRelaunch}
isLaunching={isLaunching}
id={`relaunch-job-${job.id}`}
isRelaunchJobType
/>
)) || (
<Button
ouiaId={`${job.id}-relaunch-button`}
variant="plain"
onClick={() => handleRelaunch()}
aria-label={t`Relaunch`}
isDisabled={isLaunching}
>
<RocketIcon />
</Button>
)
)}
</LaunchButton>
)}
Expand Down
21 changes: 21 additions & 0 deletions awx/ui/src/components/JobList/JobListItem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,27 @@ describe('<JobListItem />', () => {
.at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});

test('dropdown receives isRelaunchJobType prop for job relaunch', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{
...mockJob,
type: 'job',
job_type: 'run'
}}
onSelect={() => {}}
isSelected
/>
</tbody>
</table>
);

const relaunchDropDown = wrapper.find('ReLaunchDropDown');
expect(relaunchDropDown.prop('isRelaunchJobType')).toBe(true);
});
});

describe('<JobListItem with failed job />', () => {
Expand Down
43 changes: 41 additions & 2 deletions awx/ui/src/components/LaunchButton/ReLaunchDropDown.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RocketIcon } from '@patternfly/react-icons';

function ReLaunchDropDown({
isPrimary = false,
isRelaunchJobType = false,
handleRelaunch,
isLaunching,
id = 'relaunch-job',
Expand Down Expand Up @@ -62,14 +63,52 @@ function ReLaunchDropDown({
</DropdownItem>,
];

const dropdownItemsJobType = [
<DropdownItem
ouiaId={`${ouiaId}-on`}
aria-label={t`Relaunch on`}
key="relaunch_on"
component="div"
isPlainText
>
{t`Relaunch with job type`}
</DropdownItem>,
<DropdownSeparator key="separator" />,
<DropdownItem
ouiaId={`${ouiaId}-run-type`}
key="relaunch_run_type"
aria-label={t`Relaunch with job type run`}
component="button"
onClick={() => {
handleRelaunch({ job_type: 'run' });
}}
isDisabled={isLaunching}
>
{t`Run`}
</DropdownItem>,

<DropdownItem
ouiaId={`${ouiaId}-check-type`}
key="relaunch_check_type"
aria-label={t`Relaunch with job type check`}
component="button"
onClick={() => {
handleRelaunch({ job_type: 'check' });
}}
isDisabled={isLaunching}
>
{t`Check`}
</DropdownItem>,
];

if (isPrimary) {
return (
<Dropdown
ouiaId={ouiaId}
position={DropdownPosition.left}
direction={DropdownDirection.up}
isOpen={isOpen}
dropdownItems={dropdownItems}
dropdownItems={isRelaunchJobType ? dropdownItemsJobType : dropdownItems}
toggle={
<DropdownToggle
toggleIndicator={null}
Expand All @@ -92,7 +131,7 @@ function ReLaunchDropDown({
isPlain
position={DropdownPosition.right}
isOpen={isOpen}
dropdownItems={dropdownItems}
dropdownItems={isRelaunchJobType ? dropdownItemsJobType : dropdownItems}
toggle={
<DropdownToggle
toggleIndicator={null}
Expand Down
20 changes: 20 additions & 0 deletions awx/ui/src/components/LaunchButton/ReLaunchDropDown.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,24 @@ describe('ReLaunchDropDown', () => {
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
});

test('dropdown with isRelaunchJobType renders job type items', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown isRelaunchJobType handleRelaunch={handleRelaunch} />
);

wrapper.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);

wrapper
.find('DropdownItem[aria-label="Relaunch with job type run"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ job_type: 'run' });

wrapper
.find('DropdownItem[aria-label="Relaunch with job type check"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ job_type: 'check' });
});
});
26 changes: 18 additions & 8 deletions awx/ui/src/screens/Job/JobDetail/JobDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,14 +579,24 @@ function JobDetail({ job, inventorySourceLabels }) {
) : (
<LaunchButton resource={job} aria-label={t`Relaunch`}>
{({ handleRelaunch, isLaunching }) => (
<Button
ouiaId="job-detail-relaunch-button"
type="submit"
onClick={() => handleRelaunch()}
isDisabled={isLaunching}
>
{t`Relaunch`}
</Button>
(job.type === 'job' && (
<ReLaunchDropDown
handleRelaunch={handleRelaunch}
isLaunching={isLaunching}
id={`relaunch-job-${job.id}`}
isRelaunchJobType
isPrimary
/>
)) || (
<Button
ouiaId="job-detail-relaunch-button"
type="submit"
onClick={() => handleRelaunch()}
isDisabled={isLaunching}
>
{t`Relaunch`}
</Button>
)
)}
</LaunchButton>
))}
Expand Down
28 changes: 28 additions & 0 deletions awx/ui/src/screens/Job/JobDetail/JobDetail.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,4 +608,32 @@ describe('<JobDetail />', () => {
);
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0);
});

test('should render ReLaunchDropDown for job type "job"', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
type: 'job',
}}
/>
);

expect(wrapper.find('ReLaunchDropDown').length).toBe(1);
expect(wrapper.find('Button[ouiaId="job-detail-relaunch-button"]').length).toBe(0);
});

test('should render Button for other job types', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
type: 'project_update',
}}
/>
);

expect(wrapper.find('Button[ouiaId="job-detail-relaunch-button"]').length).toBe(1);
expect(wrapper.find('ReLaunchDropDown').length).toBe(0);
});
});
27 changes: 18 additions & 9 deletions awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,24 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
) : (
<LaunchButton resource={job}>
{({ handleRelaunch, isLaunching }) => (
<Button
ouiaId="job-output-relaunch-button"
variant="plain"
onClick={() => handleRelaunch()}
aria-label={t`Relaunch`}
isDisabled={isLaunching}
>
<RocketIcon />
</Button>
(job.type === 'job' && (
<ReLaunchDropDown
handleRelaunch={handleRelaunch}
isLaunching={isLaunching}
ouiaId="job-output-relaunch-dropdown"
isRelaunchJobType
/>
)) || (
<Button
ouiaId="job-output-relaunch-button"
variant="plain"
onClick={() => handleRelaunch()}
aria-label={t`Relaunch`}
isDisabled={isLaunching}
>
<RocketIcon />
</Button>
)
)}
</LaunchButton>
)}
Expand Down
32 changes: 32 additions & 0 deletions awx/ui/src/screens/Job/JobOutput/shared/OutputToolbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,36 @@ describe('<OutputToolbar />', () => {
);
expect(wrapper.find('DeleteButton').length).toBe(0);
});

test('should render ReLaunchDropDown for job type "job"', () => {
wrapper = mountWithContexts(
<OutputToolbar
job={{
...mockJobData,
type: 'job',
}}
jobStatus="successful"
onDelete={() => {}}
/>
);

expect(wrapper.find('ReLaunchDropDown').length).toBe(1);
});

test('should render original relaunch button for other job types', () => {
wrapper = mountWithContexts(
<OutputToolbar
job={{
...mockJobData,
type: 'system_job',
}}
jobStatus="successful"
onDelete={() => {}}
/>
);

expect(wrapper.find('ReLaunchDropDown').length).toBe(0);
expect(wrapper.find('Button[aria-label="Relaunch"]').length).toBe(1);
expect(wrapper.find('RocketIcon').length).toBe(1);
});
});

0 comments on commit 02f77b5

Please sign in to comment.