mirror of
https://github.com/ansible/awx.git
synced 2026-03-10 14:09:28 -02:30
Merge pull request #5979 from AlexSCorey/5814-WFJTDetailsView
Adds WFJT Details view Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -6,8 +6,22 @@ class WorkflowJobTemplates extends Base {
|
|||||||
this.baseUrl = '/api/v2/workflow_job_templates/';
|
this.baseUrl = '/api/v2/workflow_job_templates/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launch(id, data) {
|
||||||
|
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
readLaunch(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
||||||
|
}
|
||||||
|
|
||||||
readNodes(id, params) {
|
readNodes(id, params) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, { params });
|
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readWebhookKey(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/webhook_key/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
createNode(id, data) {
|
createNode(id, data) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const DetailList = ({ children, stacked, ...props }) => (
|
|||||||
export default styled(DetailList)`
|
export default styled(DetailList)`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
${props =>
|
${props =>
|
||||||
props.stacked
|
props.stacked
|
||||||
? `
|
? `
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
JobTemplatesAPI,
|
JobTemplatesAPI,
|
||||||
ProjectsAPI,
|
ProjectsAPI,
|
||||||
WorkflowJobsAPI,
|
WorkflowJobsAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
} from '@api';
|
} from '@api';
|
||||||
|
|
||||||
class LaunchButton extends React.Component {
|
class LaunchButton extends React.Component {
|
||||||
@@ -46,13 +47,24 @@ class LaunchButton extends React.Component {
|
|||||||
|
|
||||||
async handleLaunch() {
|
async handleLaunch() {
|
||||||
const { history, resource } = this.props;
|
const { history, resource } = this.props;
|
||||||
|
const readLaunch =
|
||||||
|
resource.type === 'workflow_job_template'
|
||||||
|
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
||||||
|
: JobTemplatesAPI.readLaunch(resource.id);
|
||||||
|
const launchJob =
|
||||||
|
resource.type === 'workflow_job_template'
|
||||||
|
? WorkflowJobTemplatesAPI.launch(resource.id)
|
||||||
|
: JobTemplatesAPI.launch(resource.id);
|
||||||
try {
|
try {
|
||||||
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
|
const { data: launchConfig } = await readLaunch;
|
||||||
resource.id
|
|
||||||
);
|
|
||||||
if (launchConfig.can_start_without_user_input) {
|
if (launchConfig.can_start_without_user_input) {
|
||||||
const { data: job } = await JobTemplatesAPI.launch(resource.id);
|
const { data: job } = await launchJob;
|
||||||
history.push(`/jobs/${job.id}`);
|
history.push(
|
||||||
|
`/${
|
||||||
|
resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs'
|
||||||
|
}/${job.id}/output`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.setState({ promptError: true });
|
this.setState({ promptError: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ 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 } from '@api';
|
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
|
||||||
|
|
||||||
jest.mock('@api');
|
jest.mock('@api/models/WorkflowJobTemplates');
|
||||||
|
jest.mock('@api/models/JobTemplates');
|
||||||
|
|
||||||
describe('LaunchButton', () => {
|
describe('LaunchButton', () => {
|
||||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||||
@@ -23,7 +24,7 @@ describe('LaunchButton', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
type: 'job_template',
|
type: 'job_template',
|
||||||
};
|
};
|
||||||
|
afterEach(() => jest.clearAllMocks());
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||||
@@ -35,6 +36,7 @@ describe('LaunchButton', () => {
|
|||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/jobs/9000'],
|
initialEntries: ['/jobs/9000'],
|
||||||
});
|
});
|
||||||
|
|
||||||
JobTemplatesAPI.launch.mockResolvedValue({
|
JobTemplatesAPI.launch.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
id: 9000,
|
id: 9000,
|
||||||
@@ -53,10 +55,48 @@ describe('LaunchButton', () => {
|
|||||||
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1);
|
expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1);
|
||||||
|
expect(history.location.pathname).toEqual('/jobs/9000/output');
|
||||||
|
});
|
||||||
|
test('should launch the correct job type', async () => {
|
||||||
|
WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
can_start_without_user_input: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/jobs/9000'],
|
||||||
|
});
|
||||||
|
JobTemplatesAPI.launch.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 9000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<LaunchButton
|
||||||
|
resource={{
|
||||||
|
id: 1,
|
||||||
|
type: 'workflow_job_template',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LaunchButton>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const button = wrapper.find('button');
|
||||||
|
button.prop('onClick')();
|
||||||
|
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||||
|
await sleep(0);
|
||||||
|
expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1);
|
||||||
expect(history.location.pathname).toEqual('/jobs/9000');
|
expect(history.location.pathname).toEqual('/jobs/9000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('displays error modal after unsuccessful launch', async () => {
|
test('displays error modal after unsuccessful launch', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||||
|
);
|
||||||
JobTemplatesAPI.launch.mockRejectedValue(
|
JobTemplatesAPI.launch.mockRejectedValue(
|
||||||
new Error({
|
new Error({
|
||||||
response: {
|
response: {
|
||||||
@@ -69,9 +109,6 @@ describe('LaunchButton', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('Modal').length).toBe(0);
|
expect(wrapper.find('Modal').length).toBe(0);
|
||||||
wrapper.find('button').prop('onClick')();
|
wrapper.find('button').prop('onClick')();
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
|
|||||||
@@ -374,9 +374,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
"componentStyle": ComponentStyle {
|
"componentStyle": ComponentStyle {
|
||||||
"componentId": "DetailList-sc-12g7m4-0",
|
"componentId": "DetailList-sc-12g7m4-0",
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"lastClassName": "eYaZBv",
|
"lastClassName": "iAtits",
|
||||||
"rules": Array [
|
"rules": Array [
|
||||||
"display:grid;grid-gap:20px;align-items:flex-start;",
|
"display:grid;grid-gap:20px;align-items:center;",
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -394,15 +394,15 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<DetailList
|
<DetailList
|
||||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
className="DetailList-sc-12g7m4-0 iAtits"
|
||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<TextList
|
<TextList
|
||||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
className="DetailList-sc-12g7m4-0 iAtits"
|
||||||
component="dl"
|
component="dl"
|
||||||
>
|
>
|
||||||
<dl
|
<dl
|
||||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
className="DetailList-sc-12g7m4-0 iAtits"
|
||||||
data-pf-content={true}
|
data-pf-content={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
@@ -548,9 +548,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
"componentStyle": ComponentStyle {
|
"componentStyle": ComponentStyle {
|
||||||
"componentId": "DetailList-sc-12g7m4-0",
|
"componentId": "DetailList-sc-12g7m4-0",
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"lastClassName": "eYaZBv",
|
"lastClassName": "iAtits",
|
||||||
"rules": Array [
|
"rules": Array [
|
||||||
"display:grid;grid-gap:20px;align-items:flex-start;",
|
"display:grid;grid-gap:20px;align-items:center;",
|
||||||
[Function],
|
[Function],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -568,15 +568,15 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<DetailList
|
<DetailList
|
||||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
className="DetailList-sc-12g7m4-0 iAtits"
|
||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<TextList
|
<TextList
|
||||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
className="DetailList-sc-12g7m4-0 iAtits"
|
||||||
component="dl"
|
component="dl"
|
||||||
>
|
>
|
||||||
<dl
|
<dl
|
||||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
className="DetailList-sc-12g7m4-0 iAtits"
|
||||||
data-pf-content={true}
|
data-pf-content={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import CardCloseButton from '@components/CardCloseButton';
|
|||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import FullPage from '@components/FullPage';
|
import FullPage from '@components/FullPage';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import { WorkflowJobTemplatesAPI } from '@api';
|
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
|
||||||
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
||||||
import { Visualizer } from './WorkflowJobTemplateVisualizer';
|
import { Visualizer } from './WorkflowJobTemplateVisualizer';
|
||||||
|
|
||||||
@@ -43,8 +43,25 @@ class WorkflowJobTemplate extends Component {
|
|||||||
this.setState({ contentError: null, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await WorkflowJobTemplatesAPI.readDetail(id);
|
const { data } = await WorkflowJobTemplatesAPI.readDetail(id);
|
||||||
setBreadcrumb(data);
|
if (data?.related?.webhook_key) {
|
||||||
|
const {
|
||||||
|
data: { webhook_key },
|
||||||
|
} = await WorkflowJobTemplatesAPI.readWebhookKey(id);
|
||||||
|
this.setState({ webHookKey: webhook_key });
|
||||||
|
}
|
||||||
|
if (data?.summary_fields?.webhook_credential) {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
summary_fields: { credential_type: name },
|
||||||
|
},
|
||||||
|
} = await CredentialsAPI.readDetail(
|
||||||
|
data.summary_fields.webhook_credential.id
|
||||||
|
);
|
||||||
|
data.summary_fields.webhook_credential.kind = name;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ template: data });
|
this.setState({ template: data });
|
||||||
|
setBreadcrumb(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ contentError: err });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -54,7 +71,12 @@ class WorkflowJobTemplate extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { i18n, location, match } = this.props;
|
const { i18n, location, match } = this.props;
|
||||||
const { contentError, hasContentLoading, template } = this.state;
|
const {
|
||||||
|
contentError,
|
||||||
|
hasContentLoading,
|
||||||
|
template,
|
||||||
|
webHookKey,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const tabsArray = [
|
const tabsArray = [
|
||||||
{ name: i18n._(t`Details`), link: `${match.url}/details` },
|
{ name: i18n._(t`Details`), link: `${match.url}/details` },
|
||||||
@@ -108,8 +130,8 @@ class WorkflowJobTemplate extends Component {
|
|||||||
path="/templates/workflow_job_template/:id/details"
|
path="/templates/workflow_job_template/:id/details"
|
||||||
render={() => (
|
render={() => (
|
||||||
<WorkflowJobTemplateDetail
|
<WorkflowJobTemplateDetail
|
||||||
hasTemplateLoading={hasContentLoading}
|
|
||||||
template={template}
|
template={template}
|
||||||
|
webHookKey={webHookKey}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import WorkflowJobTemplate from './WorkflowJobTemplate';
|
||||||
|
import { sleep } from '@testUtils/testUtils';
|
||||||
|
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
|
||||||
|
|
||||||
|
jest.mock('@api/models/WorkflowJobTemplates');
|
||||||
|
jest.mock('@api/models/Credentials');
|
||||||
|
|
||||||
|
describe('<WorkflowJobTemplate/>', () => {
|
||||||
|
const mockMe = {
|
||||||
|
is_super_user: true,
|
||||||
|
is_system_auditor: false,
|
||||||
|
};
|
||||||
|
beforeAll(() => {
|
||||||
|
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
description: 'Bar',
|
||||||
|
created: '2015-07-07T17:21:26.429745Z',
|
||||||
|
modified: '2019-08-11T19:47:37.980466Z',
|
||||||
|
extra_vars: '',
|
||||||
|
summary_fields: {
|
||||||
|
webhook_credential: { id: 1234567, name: 'Foo Webhook Credential' },
|
||||||
|
created_by: { id: 1, username: 'Athena' },
|
||||||
|
modified_by: { id: 1, username: 'Apollo' },
|
||||||
|
recent_jobs: [
|
||||||
|
{ id: 1, status: 'run' },
|
||||||
|
{ id: 2, status: 'run' },
|
||||||
|
{ id: 3, status: 'run' },
|
||||||
|
],
|
||||||
|
labels: {
|
||||||
|
results: [
|
||||||
|
{ name: 'Label 1', id: 1 },
|
||||||
|
{ name: 'Label 2', id: 2 },
|
||||||
|
{ name: 'Label 3', id: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
related: {
|
||||||
|
webhook_key: '/api/v2/workflow_job_templates/57/webhook_key/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({
|
||||||
|
data: { webhook_key: 'WebHook Key' },
|
||||||
|
});
|
||||||
|
CredentialsAPI.readDetail.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
summary_fields: {
|
||||||
|
credential_type: { name: 'Github Personal Access Token', id: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('calls api to get workflow job template data', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/templates/workflow_job_template/1'],
|
||||||
|
});
|
||||||
|
let wrapper;
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Route
|
||||||
|
path="/templates/workflow_job_template/:id"
|
||||||
|
component={() => (
|
||||||
|
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
route: {
|
||||||
|
location: history.location,
|
||||||
|
match: {
|
||||||
|
params: { id: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowJobTemplate').length).toBe(1);
|
||||||
|
expect(WorkflowJobTemplatesAPI.readDetail).toBeCalledWith('1');
|
||||||
|
wrapper.update();
|
||||||
|
await sleep(0);
|
||||||
|
expect(WorkflowJobTemplatesAPI.readWebhookKey).toBeCalledWith('1');
|
||||||
|
expect(CredentialsAPI.readDetail).toBeCalledWith(1234567);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +1,238 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { CardBody } from '@components/Card';
|
import { t } from '@lingui/macro';
|
||||||
import { DetailList } from '@components/DetailList';
|
import {
|
||||||
|
Chip,
|
||||||
|
ChipGroup,
|
||||||
|
Button,
|
||||||
|
TextList,
|
||||||
|
TextListItem,
|
||||||
|
TextListVariants,
|
||||||
|
TextListItemVariants,
|
||||||
|
Label,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
class WorkflowJobTemplateDetail extends Component {
|
import { CardBody, CardActionsRow } from '@components/Card';
|
||||||
render() {
|
import ContentLoading from '@components/ContentLoading';
|
||||||
return (
|
import { WorkflowJobTemplatesAPI } from '@api';
|
||||||
<CardBody>
|
import AlertModal from '@components/AlertModal';
|
||||||
<DetailList gutter="sm" />
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
</CardBody>
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
);
|
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||||
|
import LaunchButton from '@components/LaunchButton';
|
||||||
|
import DeleteButton from '@components/DeleteButton';
|
||||||
|
import { toTitleCase } from '@util/strings';
|
||||||
|
import { Sparkline } from '@components/Sparkline';
|
||||||
|
|
||||||
|
function WorkflowJobTemplateDetail({ template, i18n, webHookKey }) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
ask_inventory_on_launch,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
extra_vars,
|
||||||
|
created,
|
||||||
|
modified,
|
||||||
|
summary_fields,
|
||||||
|
related,
|
||||||
|
webhook_credential,
|
||||||
|
} = template;
|
||||||
|
const urlOrigin = window.location.origin;
|
||||||
|
const history = useHistory();
|
||||||
|
const [deletionError, setDeletionError] = useState(null);
|
||||||
|
const [hasContentLoading, setHasContentLoading] = useState(false);
|
||||||
|
const renderOptionsField =
|
||||||
|
template.allow_simultaneous || template.webhook_servicee;
|
||||||
|
|
||||||
|
const renderOptions = (
|
||||||
|
<TextList component={TextListVariants.ul}>
|
||||||
|
{template.allow_simultaneous && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{i18n._(t`- Enable Concurrent Jobs`)}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
{template.webhook_service && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{i18n._(t`- Webhooks`)}
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasContentLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setHasContentLoading(true);
|
||||||
|
try {
|
||||||
|
await WorkflowJobTemplatesAPI.destroy(id);
|
||||||
|
history.push(`/templates`);
|
||||||
|
} catch (error) {
|
||||||
|
setDeletionError(error);
|
||||||
|
}
|
||||||
|
setHasContentLoading(false);
|
||||||
|
};
|
||||||
|
const inventoryValue = (kind, inventoryId) => {
|
||||||
|
const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory';
|
||||||
|
|
||||||
|
return ask_inventory_on_launch ? (
|
||||||
|
<>
|
||||||
|
<Link to={`/inventories/${inventorykind}/${inventoryId}/details`}>
|
||||||
|
<Label>{summary_fields.inventory.name}</Label>
|
||||||
|
</Link>
|
||||||
|
<span> {i18n._(t`(Prompt on Launch)`)}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link to={`/inventories/${inventorykind}/${inventoryId}/details`}>
|
||||||
|
<Label>{summary_fields.inventory.name}</Label>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const canLaunch = summary_fields?.user_capabilities?.start;
|
||||||
|
const recentPlaybookJobs = summary_fields.recent_jobs.map(job => ({
|
||||||
|
...job,
|
||||||
|
type: 'job',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<DetailList gutter="sm">
|
||||||
|
<Detail label={i18n._(t`Name`)} value={name} dataCy="jt-detail-name" />
|
||||||
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
|
{summary_fields.recent_jobs?.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
css="display: flex; flex: 1;"
|
||||||
|
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||||
|
label={i18n._(t`Activity`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{summary_fields.organization && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Organization`)}
|
||||||
|
value={
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${summary_fields.organization.id}/details`}
|
||||||
|
>
|
||||||
|
<Label>{summary_fields.organization.name}</Label>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(type)} />
|
||||||
|
{summary_fields.inventory && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Inventory`)}
|
||||||
|
value={inventoryValue(
|
||||||
|
summary_fields.inventory.kind,
|
||||||
|
summary_fields.inventory.id
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderOptionsField && (
|
||||||
|
<Detail label={i18n._(t`Options`)} value={renderOptions} />
|
||||||
|
)}
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Webhook Service`)}
|
||||||
|
value={toTitleCase(template.webhook_service)}
|
||||||
|
/>
|
||||||
|
{related.webhook_receiver && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Webhook URL`)}
|
||||||
|
value={`${urlOrigin}${template.related.webhook_receiver}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail label={i18n._(t`Webhook Key`)} value={webHookKey} />
|
||||||
|
{webhook_credential && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Webhook Credentials`)}
|
||||||
|
value={
|
||||||
|
<Link
|
||||||
|
to={`/credentials/${summary_fields.webhook_credential.id}/details`}
|
||||||
|
>
|
||||||
|
<Label>{summary_fields.webhook_credential.name}</Label>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{summary_fields.labels?.results?.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Labels`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup>
|
||||||
|
{summary_fields.labels.results.map(l => (
|
||||||
|
<Chip key={l.id} isReadOnly>
|
||||||
|
{l.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<VariablesDetail
|
||||||
|
label={i18n._(t`Variables`)}
|
||||||
|
value={extra_vars}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Created`)}
|
||||||
|
date={created}
|
||||||
|
user={summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Modified`)}
|
||||||
|
date={modified}
|
||||||
|
user={summary_fields.modified_by}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{summary_fields.user_capabilities &&
|
||||||
|
summary_fields.user_capabilities.edit && (
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={`/templates/workflow_job_template/${id}/edit`}
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canLaunch && (
|
||||||
|
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
|
||||||
|
{({ handleLaunch }) => (
|
||||||
|
<Button variant="secondary" type="submit" onClick={handleLaunch}>
|
||||||
|
{i18n._(t`Launch`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</LaunchButton>
|
||||||
|
)}
|
||||||
|
{summary_fields.user_capabilities &&
|
||||||
|
summary_fields.user_capabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={name}
|
||||||
|
modalTitle={i18n._(t`Delete Workflow Job Template`)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
{deletionError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => setDeletionError(null)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete workflow job template.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export { WorkflowJobTemplateDetail as _WorkflowJobTemplateDetail };
|
export { WorkflowJobTemplateDetail as _WorkflowJobTemplateDetail };
|
||||||
export default withI18n()(withRouter(WorkflowJobTemplateDetail));
|
export default withI18n()(WorkflowJobTemplateDetail);
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
||||||
|
|
||||||
|
describe('<WorkflowJobTemplateDetail/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
let history;
|
||||||
|
const template = {
|
||||||
|
id: 1,
|
||||||
|
name: 'WFJT Template',
|
||||||
|
description: 'It is a wfjt template, yo!',
|
||||||
|
type: 'workflow_job_template',
|
||||||
|
extra_vars: '1: 2',
|
||||||
|
created: '2015-07-07T17:21:26.429745Z',
|
||||||
|
modified: '2019-08-11T19:47:37.980466Z',
|
||||||
|
related: { webhook_receiver: '/api/v2/workflow_job_templates/45/github/' },
|
||||||
|
summary_fields: {
|
||||||
|
created_by: { id: 1, username: 'Athena' },
|
||||||
|
modified_by: { id: 1, username: 'Apollo' },
|
||||||
|
organization: { id: 1, name: 'Org' },
|
||||||
|
inventory: { kind: 'Foo', id: 1, name: 'Bar' },
|
||||||
|
labels: {
|
||||||
|
results: [
|
||||||
|
{ name: 'Label 1', id: 1 },
|
||||||
|
{ name: 'Label 2', id: 2 },
|
||||||
|
{ name: 'Label 3', id: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
recent_jobs: [
|
||||||
|
{ id: 1, status: 'run' },
|
||||||
|
{ id: 2, status: 'run' },
|
||||||
|
{ id: 3, status: 'run' },
|
||||||
|
],
|
||||||
|
webhook_credential: { id: '1', name: 'Credentaial', kind: 'machine' },
|
||||||
|
user_capabilities: { edit: true, delete: true },
|
||||||
|
},
|
||||||
|
webhook_service: 'Github',
|
||||||
|
};
|
||||||
|
beforeEach(async () => {
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/templates/workflow_job_template/1/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = await mountWithContexts(
|
||||||
|
<Route
|
||||||
|
path="/templates/workflow_job_template/:id/details"
|
||||||
|
component={() => (
|
||||||
|
<WorkflowJobTemplateDetail
|
||||||
|
template={template}
|
||||||
|
webHookKey="Foo webhook key"
|
||||||
|
hasContentLoading={false}
|
||||||
|
onSetContentLoading={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
route: {
|
||||||
|
location: history.location,
|
||||||
|
match: {
|
||||||
|
params: { id: 1 },
|
||||||
|
path: '/templates/workflow_job_template/1/details',
|
||||||
|
url: '/templates/workflow_job_template/1/details',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
test('renders successfully', () => {
|
||||||
|
expect(wrapper.find(WorkflowJobTemplateDetail).length).toBe(1);
|
||||||
|
});
|
||||||
|
test('expect detail fields to render properly', () => {
|
||||||
|
const renderedValues = [
|
||||||
|
{
|
||||||
|
element: 'UserDateDetail[label="Created"]',
|
||||||
|
prop: 'date',
|
||||||
|
value: '2015-07-07T17:21:26.429745Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'UserDateDetail[label="Modified"]',
|
||||||
|
prop: 'date',
|
||||||
|
value: '2019-08-11T19:47:37.980466Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'Detail[label="Webhook URL"]',
|
||||||
|
prop: 'value',
|
||||||
|
value: 'http://127.0.0.1:3001/api/v2/workflow_job_templates/45/github/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "Detail[label='Webhook Service']",
|
||||||
|
prop: 'value',
|
||||||
|
value: 'Github',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'Detail[label="Webhook Key"]',
|
||||||
|
prop: 'value',
|
||||||
|
value: 'Foo webhook key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'Detail[label="Name"]',
|
||||||
|
value: 'WFJT Template',
|
||||||
|
prop: 'value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'Detail[label="Description"]',
|
||||||
|
prop: 'value',
|
||||||
|
value: 'It is a wfjt template, yo!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'Detail[label="Job Type"]',
|
||||||
|
prop: 'value',
|
||||||
|
value: 'Workflow Job Template',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const organization = wrapper
|
||||||
|
.find('Detail[label="Organization"]')
|
||||||
|
.find('span');
|
||||||
|
const inventory = wrapper.find('Detail[label="Inventory"]').find('a');
|
||||||
|
const labels = wrapper
|
||||||
|
.find('Detail[label="Labels"]')
|
||||||
|
.find('Chip[component="li"]');
|
||||||
|
const sparkline = wrapper.find('Sparkline__Link');
|
||||||
|
|
||||||
|
expect(organization.text()).toBe('Org');
|
||||||
|
expect(inventory.text()).toEqual('Bar');
|
||||||
|
expect(labels.length).toBe(3);
|
||||||
|
expect(sparkline.length).toBe(3);
|
||||||
|
|
||||||
|
const assertValue = value => {
|
||||||
|
expect(wrapper.find(`${value.element}`).prop(`${value.prop}`)).toEqual(
|
||||||
|
`${value.value}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderedValues.map(value => assertValue(value));
|
||||||
|
});
|
||||||
|
test('link out resource have the correct url', () => {
|
||||||
|
const inventory = wrapper.find('Detail[label="Inventory"]').find('Link');
|
||||||
|
const organization = wrapper
|
||||||
|
.find('Detail[label="Organization"]')
|
||||||
|
.find('Link');
|
||||||
|
expect(inventory.prop('to')).toEqual('/inventories/inventory/1/details');
|
||||||
|
expect(organization.prop('to')).toEqual('/organizations/1/details');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user