mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03: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:
commit
09d6da117a
@ -6,8 +6,22 @@ class WorkflowJobTemplates extends Base {
|
||||
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) {
|
||||
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) {
|
||||
|
||||
@ -11,7 +11,7 @@ const DetailList = ({ children, stacked, ...props }) => (
|
||||
export default styled(DetailList)`
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
${props =>
|
||||
props.stacked
|
||||
? `
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
JobTemplatesAPI,
|
||||
ProjectsAPI,
|
||||
WorkflowJobsAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '@api';
|
||||
|
||||
class LaunchButton extends React.Component {
|
||||
@ -46,13 +47,24 @@ class LaunchButton extends React.Component {
|
||||
|
||||
async handleLaunch() {
|
||||
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 {
|
||||
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
|
||||
resource.id
|
||||
);
|
||||
const { data: launchConfig } = await readLaunch;
|
||||
|
||||
if (launchConfig.can_start_without_user_input) {
|
||||
const { data: job } = await JobTemplatesAPI.launch(resource.id);
|
||||
history.push(`/jobs/${job.id}`);
|
||||
const { data: job } = await launchJob;
|
||||
history.push(
|
||||
`/${
|
||||
resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs'
|
||||
}/${job.id}/output`
|
||||
);
|
||||
} else {
|
||||
this.setState({ promptError: true });
|
||||
}
|
||||
|
||||
@ -4,9 +4,10 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { sleep } from '@testUtils/testUtils';
|
||||
|
||||
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', () => {
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
@ -23,7 +24,7 @@ describe('LaunchButton', () => {
|
||||
id: 1,
|
||||
type: 'job_template',
|
||||
};
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
@ -35,6 +36,7 @@ describe('LaunchButton', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/jobs/9000'],
|
||||
});
|
||||
|
||||
JobTemplatesAPI.launch.mockResolvedValue({
|
||||
data: {
|
||||
id: 9000,
|
||||
@ -53,10 +55,48 @@ describe('LaunchButton', () => {
|
||||
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
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');
|
||||
});
|
||||
|
||||
test('displays error modal after unsuccessful launch', async () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
);
|
||||
JobTemplatesAPI.launch.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
@ -69,9 +109,6 @@ describe('LaunchButton', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
);
|
||||
expect(wrapper.find('Modal').length).toBe(0);
|
||||
wrapper.find('button').prop('onClick')();
|
||||
await sleep(0);
|
||||
|
||||
@ -374,9 +374,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
"componentStyle": ComponentStyle {
|
||||
"componentId": "DetailList-sc-12g7m4-0",
|
||||
"isStatic": false,
|
||||
"lastClassName": "eYaZBv",
|
||||
"lastClassName": "iAtits",
|
||||
"rules": Array [
|
||||
"display:grid;grid-gap:20px;align-items:flex-start;",
|
||||
"display:grid;grid-gap:20px;align-items:center;",
|
||||
[Function],
|
||||
],
|
||||
},
|
||||
@ -394,15 +394,15 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<DetailList
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
className="DetailList-sc-12g7m4-0 iAtits"
|
||||
stacked={true}
|
||||
>
|
||||
<TextList
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
className="DetailList-sc-12g7m4-0 iAtits"
|
||||
component="dl"
|
||||
>
|
||||
<dl
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
className="DetailList-sc-12g7m4-0 iAtits"
|
||||
data-pf-content={true}
|
||||
>
|
||||
<Detail
|
||||
@ -548,9 +548,9 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
"componentStyle": ComponentStyle {
|
||||
"componentId": "DetailList-sc-12g7m4-0",
|
||||
"isStatic": false,
|
||||
"lastClassName": "eYaZBv",
|
||||
"lastClassName": "iAtits",
|
||||
"rules": Array [
|
||||
"display:grid;grid-gap:20px;align-items:flex-start;",
|
||||
"display:grid;grid-gap:20px;align-items:center;",
|
||||
[Function],
|
||||
],
|
||||
},
|
||||
@ -568,15 +568,15 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<DetailList
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
className="DetailList-sc-12g7m4-0 iAtits"
|
||||
stacked={true}
|
||||
>
|
||||
<TextList
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
className="DetailList-sc-12g7m4-0 iAtits"
|
||||
component="dl"
|
||||
>
|
||||
<dl
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
className="DetailList-sc-12g7m4-0 iAtits"
|
||||
data-pf-content={true}
|
||||
>
|
||||
<Detail
|
||||
|
||||
@ -9,7 +9,7 @@ import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
import FullPage from '@components/FullPage';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
|
||||
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
||||
import { Visualizer } from './WorkflowJobTemplateVisualizer';
|
||||
|
||||
@ -43,8 +43,25 @@ class WorkflowJobTemplate extends Component {
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
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 });
|
||||
setBreadcrumb(data);
|
||||
} catch (err) {
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
@ -54,7 +71,12 @@ class WorkflowJobTemplate extends Component {
|
||||
|
||||
render() {
|
||||
const { i18n, location, match } = this.props;
|
||||
const { contentError, hasContentLoading, template } = this.state;
|
||||
const {
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
template,
|
||||
webHookKey,
|
||||
} = this.state;
|
||||
|
||||
const tabsArray = [
|
||||
{ name: i18n._(t`Details`), link: `${match.url}/details` },
|
||||
@ -108,8 +130,8 @@ class WorkflowJobTemplate extends Component {
|
||||
path="/templates/workflow_job_template/:id/details"
|
||||
render={() => (
|
||||
<WorkflowJobTemplateDetail
|
||||
hasTemplateLoading={hasContentLoading}
|
||||
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 { withRouter } from 'react-router-dom';
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { DetailList } from '@components/DetailList';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Button,
|
||||
TextList,
|
||||
TextListItem,
|
||||
TextListVariants,
|
||||
TextListItemVariants,
|
||||
Label,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
class WorkflowJobTemplateDetail extends Component {
|
||||
render() {
|
||||
return (
|
||||
<CardBody>
|
||||
<DetailList gutter="sm" />
|
||||
</CardBody>
|
||||
);
|
||||
import { CardBody, CardActionsRow } from '@components/Card';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
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 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');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user