Fix job relaunch where credentials are needed

This commit is contained in:
mabashian
2021-01-19 11:34:19 -05:00
parent fb62e0ec2c
commit 7f78018386
3 changed files with 202 additions and 27 deletions

View File

@@ -102,17 +102,20 @@ class LaunchButton extends React.Component {
async launchWithParams(params) { async launchWithParams(params) {
try { try {
const { history, resource } = this.props; const { history, resource } = this.props;
const jobPromise = let jobPromise;
resource.type === 'workflow_job_template'
? WorkflowJobTemplatesAPI.launch(resource.id, params || {}) if (resource.type === 'job_template') {
: JobTemplatesAPI.launch(resource.id, params || {}); jobPromise = JobTemplatesAPI.launch(resource.id, params || {});
} else if (resource.type === 'workflow_job_template') {
jobPromise = WorkflowJobTemplatesAPI.launch(resource.id, params || {});
} else if (resource.type === 'job') {
jobPromise = JobsAPI.relaunch(resource.id, params || {});
} else if (resource.type === 'workflow_job') {
jobPromise = WorkflowJobsAPI.relaunch(resource.id, params || {});
}
const { data: job } = await jobPromise; const { data: job } = await jobPromise;
history.push( history.push(`/jobs/${job.id}/output`);
`/${
resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs'
}/${job.id}/output`
);
} catch (launchError) { } catch (launchError) {
this.setState({ launchError }); this.setState({ launchError });
} }
@@ -129,20 +132,15 @@ class LaunchButton extends React.Component {
readRelaunch = InventorySourcesAPI.readLaunchUpdate( readRelaunch = InventorySourcesAPI.readLaunchUpdate(
resource.inventory_source resource.inventory_source
); );
relaunch = InventorySourcesAPI.launchUpdate(resource.inventory_source);
} else if (resource.type === 'project_update') { } else if (resource.type === 'project_update') {
// We'll need to handle the scenario where the project no longer exists // We'll need to handle the scenario where the project no longer exists
readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project); readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project);
relaunch = ProjectsAPI.launchUpdate(resource.project);
} else if (resource.type === 'workflow_job') { } else if (resource.type === 'workflow_job') {
readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id); readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id);
relaunch = WorkflowJobsAPI.relaunch(resource.id);
} else if (resource.type === 'ad_hoc_command') { } else if (resource.type === 'ad_hoc_command') {
readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id); readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id);
relaunch = AdHocCommandsAPI.relaunch(resource.id);
} else if (resource.type === 'job') { } else if (resource.type === 'job') {
readRelaunch = JobsAPI.readRelaunch(resource.id); readRelaunch = JobsAPI.readRelaunch(resource.id);
relaunch = JobsAPI.relaunch(resource.id);
} }
try { try {
@@ -151,11 +149,22 @@ class LaunchButton extends React.Component {
!relaunchConfig.passwords_needed_to_start || !relaunchConfig.passwords_needed_to_start ||
relaunchConfig.passwords_needed_to_start.length === 0 relaunchConfig.passwords_needed_to_start.length === 0
) { ) {
if (resource.type === 'inventory_update') {
relaunch = InventorySourcesAPI.launchUpdate(
resource.inventory_source
);
} else if (resource.type === 'project_update') {
relaunch = ProjectsAPI.launchUpdate(resource.project);
} else if (resource.type === 'workflow_job') {
relaunch = WorkflowJobsAPI.relaunch(resource.id);
} else if (resource.type === 'ad_hoc_command') {
relaunch = AdHocCommandsAPI.relaunch(resource.id);
} else if (resource.type === 'job') {
relaunch = JobsAPI.relaunch(resource.id);
}
const { data: job } = await relaunch; const { data: job } = await relaunch;
history.push(`/jobs/${job.id}/output`); history.push(`/jobs/${job.id}/output`);
} else { } else {
// TODO: restructure (async?) to send launch command after prompts
// TODO: does relaunch need different prompt treatment than launch?
this.setState({ this.setState({
showLaunchPrompt: true, showLaunchPrompt: true,
launchConfig: relaunchConfig, launchConfig: relaunchConfig,

View File

@@ -4,10 +4,16 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import { sleep } from '../../../testUtils/testUtils'; import { sleep } from '../../../testUtils/testUtils';
import LaunchButton from './LaunchButton'; import LaunchButton from './LaunchButton';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api'; import {
InventorySourcesAPI,
JobsAPI,
JobTemplatesAPI,
ProjectsAPI,
WorkflowJobsAPI,
WorkflowJobTemplatesAPI,
} from '../../api';
jest.mock('../../api/models/WorkflowJobTemplates'); jest.mock('../../api');
jest.mock('../../api/models/JobTemplates');
describe('LaunchButton', () => { describe('LaunchButton', () => {
JobTemplatesAPI.readLaunch.mockResolvedValue({ JobTemplatesAPI.readLaunch.mockResolvedValue({
@@ -22,10 +28,14 @@ describe('LaunchButton', () => {
}, },
}); });
const children = ({ handleLaunch }) => ( const launchButton = ({ handleLaunch }) => (
<button type="submit" onClick={() => handleLaunch()} /> <button type="submit" onClick={() => handleLaunch()} />
); );
const relaunchButton = ({ handleRelaunch }) => (
<button type="submit" onClick={() => handleRelaunch()} />
);
const resource = { const resource = {
id: 1, id: 1,
type: 'job_template', type: 'job_template',
@@ -35,7 +45,7 @@ describe('LaunchButton', () => {
test('renders the expected content', () => { test('renders the expected content', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<LaunchButton resource={resource}>{children}</LaunchButton> <LaunchButton resource={resource}>{launchButton}</LaunchButton>
); );
expect(wrapper).toHaveLength(1); expect(wrapper).toHaveLength(1);
}); });
@@ -51,7 +61,7 @@ describe('LaunchButton', () => {
}, },
}); });
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<LaunchButton resource={resource}>{children}</LaunchButton>, <LaunchButton resource={resource}>{launchButton}</LaunchButton>,
{ {
context: { context: {
router: { history }, router: { history },
@@ -87,7 +97,7 @@ describe('LaunchButton', () => {
type: 'workflow_job_template', type: 'workflow_job_template',
}} }}
> >
{children} {launchButton}
</LaunchButton>, </LaunchButton>,
{ {
context: { context: {
@@ -100,12 +110,162 @@ describe('LaunchButton', () => {
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1); expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
await sleep(0); await sleep(0);
expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1, {}); expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1, {});
expect(history.location.pathname).toEqual('/jobs/workflow/9000/output'); expect(history.location.pathname).toEqual('/jobs/9000/output');
});
test('should relaunch job correctly', async () => {
JobsAPI.readRelaunch.mockResolvedValue({
data: {
can_start_without_user_input: true,
},
});
const history = createMemoryHistory({
initialEntries: ['/jobs/9000'],
});
JobsAPI.relaunch.mockResolvedValue({
data: {
id: 9000,
},
});
const wrapper = mountWithContexts(
<LaunchButton
resource={{
id: 1,
type: 'job',
}}
>
{relaunchButton}
</LaunchButton>,
{
context: {
router: { history },
},
}
);
const button = wrapper.find('button');
button.prop('onClick')();
expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1);
await sleep(0);
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1);
expect(history.location.pathname).toEqual('/jobs/9000/output');
});
test('should relaunch workflow job correctly', async () => {
WorkflowJobsAPI.readRelaunch.mockResolvedValue({
data: {
can_start_without_user_input: true,
},
});
const history = createMemoryHistory({
initialEntries: ['/jobs/9000'],
});
WorkflowJobsAPI.relaunch.mockResolvedValue({
data: {
id: 9000,
},
});
const wrapper = mountWithContexts(
<LaunchButton
resource={{
id: 1,
type: 'workflow_job',
}}
>
{relaunchButton}
</LaunchButton>,
{
context: {
router: { history },
},
}
);
const button = wrapper.find('button');
button.prop('onClick')();
expect(WorkflowJobsAPI.readRelaunch).toHaveBeenCalledWith(1);
await sleep(0);
expect(WorkflowJobsAPI.relaunch).toHaveBeenCalledWith(1);
expect(history.location.pathname).toEqual('/jobs/9000/output');
});
test('should relaunch project sync correctly', async () => {
ProjectsAPI.readLaunchUpdate.mockResolvedValue({
data: {
can_start_without_user_input: true,
},
});
const history = createMemoryHistory({
initialEntries: ['/jobs/9000'],
});
ProjectsAPI.launchUpdate.mockResolvedValue({
data: {
id: 9000,
},
});
const wrapper = mountWithContexts(
<LaunchButton
resource={{
id: 1,
project: 5,
type: 'project_update',
}}
>
{relaunchButton}
</LaunchButton>,
{
context: {
router: { history },
},
}
);
const button = wrapper.find('button');
button.prop('onClick')();
expect(ProjectsAPI.readLaunchUpdate).toHaveBeenCalledWith(5);
await sleep(0);
expect(ProjectsAPI.launchUpdate).toHaveBeenCalledWith(5);
expect(history.location.pathname).toEqual('/jobs/9000/output');
});
test('should relaunch project sync correctly', async () => {
InventorySourcesAPI.readLaunchUpdate.mockResolvedValue({
data: {
can_start_without_user_input: true,
},
});
const history = createMemoryHistory({
initialEntries: ['/jobs/9000'],
});
InventorySourcesAPI.launchUpdate.mockResolvedValue({
data: {
id: 9000,
},
});
const wrapper = mountWithContexts(
<LaunchButton
resource={{
id: 1,
inventory_source: 5,
type: 'inventory_update',
}}
>
{relaunchButton}
</LaunchButton>,
{
context: {
router: { history },
},
}
);
const button = wrapper.find('button');
button.prop('onClick')();
expect(InventorySourcesAPI.readLaunchUpdate).toHaveBeenCalledWith(5);
await sleep(0);
expect(InventorySourcesAPI.launchUpdate).toHaveBeenCalledWith(5);
expect(history.location.pathname).toEqual('/jobs/9000/output');
}); });
test('displays error modal after unsuccessful launch', async () => { test('displays error modal after unsuccessful launch', async () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<LaunchButton resource={resource}>{children}</LaunchButton> <LaunchButton resource={resource}>{launchButton}</LaunchButton>
); );
JobTemplatesAPI.launch.mockRejectedValue( JobTemplatesAPI.launch.mockRejectedValue(
new Error({ new Error({

View File

@@ -6,6 +6,7 @@ import { t } from '@lingui/macro';
import useRequest from '../../util/useRequest'; import useRequest from '../../util/useRequest';
import { UnifiedJobsAPI } from '../../api'; import { UnifiedJobsAPI } from '../../api';
import ContentError from '../../components/ContentError'; import ContentError from '../../components/ContentError';
import ContentLoading from '../../components/ContentLoading';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
const NOT_FOUND = 'not found'; const NOT_FOUND = 'not found';
@@ -46,8 +47,13 @@ function JobTypeRedirect({ id, path, view, i18n }) {
); );
} }
if (isLoading || !job?.id) { if (isLoading || !job?.id) {
// TODO show loading state return (
return <div>Loading...</div>; <PageSection>
<Card>
<ContentLoading />
</Card>
</PageSection>
);
} }
const type = JOB_TYPE_URL_SEGMENTS[job.type]; const type = JOB_TYPE_URL_SEGMENTS[job.type];
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />; return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;