Merge pull request #6709 from marshmalien/6530-wf-node-wf

Add workflow details to node view modal

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-04-16 20:54:25 +00:00
committed by GitHub
11 changed files with 464 additions and 26 deletions

View File

@@ -16,6 +16,7 @@ class JobTemplates extends SchedulesMixin(
this.disassociateLabel = this.disassociateLabel.bind(this);
this.readCredentials = this.readCredentials.bind(this);
this.readAccessList = this.readAccessList.bind(this);
this.readWebhookKey = this.readWebhookKey.bind(this);
}
launch(id, data) {
@@ -82,6 +83,10 @@ class JobTemplates extends SchedulesMixin(
destroySurvey(id) {
return this.http.delete(`${this.baseUrl}${id}/survey_spec/`);
}
readWebhookKey(id) {
return this.http.get(`${this.baseUrl}${id}/webhook_key/`);
}
}
export default JobTemplates;

View File

@@ -2,10 +2,15 @@ import React from 'react';
import { node, string } from 'prop-types';
import { Trans } from '@lingui/macro';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { formatDateString } from '@util/dates';
import Detail from './Detail';
import _Detail from './Detail';
import { SummaryFieldUser } from '../../types';
const Detail = styled(_Detail)`
word-break: break-word;
`;
function UserDateDetail({ label, date, user, dataCy = null }) {
const dateStr = formatDateString(date);
const username = user ? user.username : '';

View File

@@ -6,8 +6,9 @@ import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { toTitleCase } from '@util/strings';
import { Chip, ChipGroup } from '@patternfly/react-core';
import { Chip, ChipGroup, Divider } from '@patternfly/react-core';
import { VariablesDetail } from '@components/CodeMirrorInput';
import CredentialChip from '@components/CredentialChip';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import PromptProjectDetail from './PromptProjectDetail';
@@ -172,7 +173,6 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
/>
)}
{/* TODO: Add JT, WFJT, Inventory Source Details */}
{details?.type === 'project' && (
<PromptProjectDetail resource={details} />
)}
@@ -200,6 +200,7 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
{hasPromptData(launchConfig) && hasOverrides && (
<>
<Divider css="margin-top: var(--pf-global--spacer--lg)" />
<PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader>
<DetailList aria-label="Prompt Overrides">
{overrides?.job_type && (
@@ -211,14 +212,16 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
{overrides?.credentials && (
<Detail
fullWidth
label={i18n._(t`Credential`)}
label={i18n._(t`Credentials`)}
rows={4}
value={
<ChipGroup numChips={5}>
{overrides.credentials.map(cred => (
<Chip key={cred.id} isReadOnly>
{cred.name}
</Chip>
<CredentialChip
key={cred.id}
credential={cred}
isReadOnly
/>
))}
</ChipGroup>
}

View File

@@ -24,12 +24,14 @@ function PromptJobTemplateDetail({ i18n, resource }) {
job_type,
limit,
playbook,
related,
scm_branch,
skip_tags,
summary_fields,
url,
use_fact_cache,
verbosity,
webhook_key,
webhook_service,
} = resource;
const VERBOSITY = {
@@ -114,23 +116,49 @@ function PromptJobTemplateDetail({ i18n, resource }) {
value={diff_mode ? 'On' : 'Off'}
/>
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
{host_config_key && (
<React.Fragment>
<Detail label={i18n._(t`Host Config Key`)} value={host_config_key} />
<Detail
label={i18n._(t`Provisioning Callback URL`)}
value={`${window.location.origin + url}callback/`}
/>
</React.Fragment>
<Detail label={i18n._(t`Host Config Key`)} value={host_config_key} />
{related?.callback && (
<Detail
label={i18n._(t`Provisioning Callback URL`)}
value={`${window.location.origin}${related.callback}`}
/>
)}
<Detail
label={i18n._(t`Webhook Service`)}
value={toTitleCase(webhook_service)}
/>
{related.webhook_receiver && (
<Detail
label={i18n._(t`Webhook URL`)}
value={`${window.location.origin}${related.webhook_receiver}`}
/>
)}
<Detail label={i18n._(t`Webhook Key`)} value={webhook_key} />
{summary_fields?.webhook_credential && (
<Detail
fullWidth
label={i18n._(t`Webhook Credential`)}
value={
<CredentialChip
key={summary_fields.webhook_credential?.id}
credential={summary_fields.webhook_credential}
isReadOnly
/>
}
/>
)}
{optionsList && <Detail label={i18n._(t`Options`)} value={optionsList} />}
{summary_fields?.credentials?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Credentials`)}
value={summary_fields.credentials.map(chip => (
<CredentialChip key={chip.id} credential={chip} isReadOnly />
))}
value={
<ChipGroup numChips={5}>
{summary_fields.credentials.map(cred => (
<CredentialChip key={cred.id} credential={cred} isReadOnly />
))}
</ChipGroup>
}
/>
)}
{summary_fields?.labels?.results?.length > 0 && (

View File

@@ -5,6 +5,7 @@ import mockData from './data.job_template.json';
const mockJT = {
...mockData,
webhook_key: 'PiM3n2',
instance_groups: [
{
id: 1,
@@ -49,9 +50,24 @@ describe('PromptJobTemplateDetail', () => {
assertDetail('Show Changes', 'Off');
assertDetail('Job Slicing', '1');
assertDetail('Host Config Key', 'a1b2c3');
assertDetail('Webhook Service', 'Github');
assertDetail('Webhook Key', 'PiM3n2');
expect(wrapper.find('StatusIcon')).toHaveLength(2);
expect(wrapper.find('Detail[label="Webhook URL"] dd').text()).toEqual(
expect.stringContaining('/api/v2/job_templates/7/github/')
);
expect(
wrapper.find('Detail[label="Provisioning Callback URL"] dd').text()
).toEqual(expect.stringContaining('/api/v2/job_templates/7/callback/'));
expect(
wrapper
.find('Detail[label="Webhook Credential"]')
.containsAllMatchingElements([
<span>
<strong>Github Token:</strong>GitHub Cred
</span>,
])
).toEqual(true);
expect(
wrapper.find('Detail[label="Credentials"]').containsAllMatchingElements([
<span>

View File

@@ -1,8 +1,120 @@
import React from 'react';
import { CardBody } from '@components/Card';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
function PromptWFJobTemplateDetail() {
return <CardBody>Coming soon :)</CardBody>;
import { Chip, ChipGroup, List, ListItem } from '@patternfly/react-core';
import CredentialChip from '@components/CredentialChip';
import { Detail } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput';
import Sparkline from '@components/Sparkline';
import { toTitleCase } from '@util/strings';
function PromptWFJobTemplateDetail({ i18n, resource }) {
const {
allow_simultaneous,
extra_vars,
limit,
related,
scm_branch,
summary_fields,
webhook_key,
webhook_service,
} = resource;
let optionsList = '';
if (allow_simultaneous || webhook_service) {
optionsList = (
<List>
{allow_simultaneous && (
<ListItem>{i18n._(t`Enable Concurrent Jobs`)}</ListItem>
)}
{webhook_service && <ListItem>{i18n._(t`Enable Webhooks`)}</ListItem>}
</List>
);
}
const inventoryKind =
summary_fields?.inventory?.kind === 'smart'
? 'smart_inventory'
: 'inventory';
const recentJobs = summary_fields.recent_jobs.map(job => ({
...job,
type: 'job',
}));
return (
<>
{summary_fields.recent_jobs?.length > 0 && (
<Detail
value={<Sparkline jobs={recentJobs} />}
label={i18n._(t`Activity`)}
/>
)}
{summary_fields?.inventory && (
<Detail
label={i18n._(t`Inventory`)}
value={
<Link
to={`/${inventoryKind}/${summary_fields.inventory?.id}/details`}
>
{summary_fields.inventory?.name}
</Link>
}
/>
)}
<Detail label={i18n._(t`Source Control Branch`)} value={scm_branch} />
<Detail label={i18n._(t`Limit`)} value={limit} />
<Detail
label={i18n._(t`Webhook Service`)}
value={toTitleCase(webhook_service)}
/>
<Detail label={i18n._(t`Webhook Key`)} value={webhook_key} />
{related.webhook_receiver && (
<Detail
label={i18n._(t`Webhook URL`)}
value={`${window.location.origin}${related.webhook_receiver}`}
/>
)}
{optionsList && <Detail label={i18n._(t`Options`)} value={optionsList} />}
{summary_fields?.webhook_credential && (
<Detail
fullWidth
label={i18n._(t`Webhook Credential`)}
value={
<CredentialChip
key={summary_fields.webhook_credential?.id}
credential={summary_fields.webhook_credential}
isReadOnly
/>
}
/>
)}
{summary_fields?.labels?.results?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Labels`)}
value={
<ChipGroup numChips={5}>
{summary_fields.labels.results.map(label => (
<Chip key={label.id} isReadOnly>
{label.name}
</Chip>
))}
</ChipGroup>
}
/>
)}
{extra_vars && (
<VariablesDetail
label={i18n._(t`Variables`)}
rows={4}
value={extra_vars}
/>
)}
</>
);
}
export default PromptWFJobTemplateDetail;
export default withI18n()(PromptWFJobTemplateDetail);

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import PromptWFJobTemplateDetail from './PromptWFJobTemplateDetail';
import mockData from './data.workflow_template.json';
const mockWF = {
...mockData,
webhook_key: 'Pim3mRXT0',
};
describe('PromptWFJobTemplateDetail', () => {
let wrapper;
beforeAll(() => {
wrapper = mountWithContexts(
<PromptWFJobTemplateDetail resource={mockWF} />
);
});
afterAll(() => {
wrapper.unmount();
});
test('should render successfully', () => {
expect(wrapper.find('PromptWFJobTemplateDetail')).toHaveLength(1);
});
test('should render expected details', () => {
function assertDetail(label, value) {
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
}
expect(wrapper.find('StatusIcon')).toHaveLength(1);
assertDetail('Inventory', 'Mock Smart Inv');
assertDetail('Source Control Branch', '/bar/');
assertDetail('Limit', 'hosts1,hosts2');
assertDetail('Webhook Service', 'Github');
assertDetail('Webhook Key', 'Pim3mRXT0');
expect(wrapper.find('Detail[label="Webhook URL"] dd').text()).toEqual(
expect.stringContaining('/api/v2/workflow_job_templates/47/github/')
);
expect(
wrapper
.find('Detail[label="Options"]')
.containsAllMatchingElements([
<li>Enable Concurrent Jobs</li>,
<li>Enable Webhooks</li>,
])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Webhook Credential"]')
.containsAllMatchingElements([
<span>
<strong>Github Token:</strong>github
</span>,
])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Labels"]')
.containsAllMatchingElements([<span>L_10o0</span>, <span>L_20o0</span>])
).toEqual(true);
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
'---\nmock: data'
);
});
});

View File

@@ -16,6 +16,8 @@
"schedules": "/api/v2/job_templates/7/schedules/",
"activity_stream": "/api/v2/job_templates/7/activity_stream/",
"launch": "/api/v2/job_templates/7/launch/",
"webhook_key": "/api/v2/job_templates/7/webhook_key/",
"webhook_receiver": "/api/v2/job_templates/7/github/",
"notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/",
"notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/",
"notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/",
@@ -24,7 +26,9 @@
"object_roles": "/api/v2/job_templates/7/object_roles/",
"instance_groups": "/api/v2/job_templates/7/instance_groups/",
"slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/",
"copy": "/api/v2/job_templates/7/copy/"
"copy": "/api/v2/job_templates/7/copy/",
"callback": "/api/v2/job_templates/7/callback/",
"webhook_credential": "/api/v2/credentials/8/"
},
"summary_fields": {
"inventory": {
@@ -64,6 +68,14 @@
"status": "successful",
"failed": false
},
"webhook_credential": {
"id": 8,
"name": "GitHub Cred",
"description": "",
"kind": "github_token",
"cloud": false,
"credential_type_id": 12
},
"created_by": {
"id": 1,
"username": "admin",
@@ -123,6 +135,12 @@
"status": "successful",
"finished": "2019-10-01T14:34:35.142483Z",
"type": "job"
},
{
"id": 13,
"status": "successful",
"finished": "2019-10-01T14:34:35.142483Z",
"type": "job"
}
],
"extra_credentials": [],
@@ -174,5 +192,7 @@
"diff_mode": false,
"allow_simultaneous": true,
"custom_virtualenv": null,
"job_slice_count": 1
"job_slice_count": 1,
"webhook_service": "github",
"webhook_credential": 8
}

View File

@@ -0,0 +1,156 @@
{
"id": 47,
"type": "workflow_job_template",
"url": "/api/v2/workflow_job_templates/47/",
"related": {
"created_by": "/api/v2/users/8/",
"modified_by": "/api/v2/users/1/",
"last_job": "/api/v2/workflow_jobs/226/",
"workflow_jobs": "/api/v2/workflow_job_templates/47/workflow_jobs/",
"schedules": "/api/v2/workflow_job_templates/47/schedules/",
"launch": "/api/v2/workflow_job_templates/47/launch/",
"webhook_key": "/api/v2/workflow_job_templates/47/webhook_key/",
"webhook_receiver": "/api/v2/workflow_job_templates/47/github/",
"workflow_nodes": "/api/v2/workflow_job_templates/47/workflow_nodes/",
"labels": "/api/v2/workflow_job_templates/47/labels/",
"activity_stream": "/api/v2/workflow_job_templates/47/activity_stream/",
"notification_templates_started": "/api/v2/workflow_job_templates/47/notification_templates_started/",
"notification_templates_success": "/api/v2/workflow_job_templates/47/notification_templates_success/",
"notification_templates_error": "/api/v2/workflow_job_templates/47/notification_templates_error/",
"notification_templates_approvals": "/api/v2/workflow_job_templates/47/notification_templates_approvals/",
"access_list": "/api/v2/workflow_job_templates/47/access_list/",
"object_roles": "/api/v2/workflow_job_templates/47/object_roles/",
"survey_spec": "/api/v2/workflow_job_templates/47/survey_spec/",
"copy": "/api/v2/workflow_job_templates/47/copy/",
"organization": "/api/v2/organizations/3/",
"webhook_credential": "/api/v2/credentials/8/"
},
"summary_fields": {
"organization": {
"id": 3,
"name": "Mock Org",
"description": ""
},
"inventory": {
"id": 7,
"name": "Mock Smart Inv",
"description": "",
"has_active_failures": false,
"total_hosts": 1,
"hosts_with_active_failures": 0,
"total_groups": 0,
"has_inventory_sources": false,
"total_inventory_sources": 0,
"inventory_sources_with_failures": 0,
"organization_id": 1,
"kind": "smart"
},
"last_job": {
"id": 226,
"name": "abc",
"description": "From Tower bulk-data script",
"finished": "2020-04-08T21:30:44.282245Z",
"status": "failed",
"failed": true
},
"last_update": {
"id": 226,
"name": "abc",
"description": "From Tower bulk-data script",
"status": "failed",
"failed": true
},
"webhook_credential": {
"id": 8,
"name": "github",
"description": "",
"kind": "github_token",
"cloud": false,
"credential_type_id": 12
},
"created_by": {
"id": 8,
"username": "user-2",
"first_name": "",
"last_name": ""
},
"modified_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"object_roles": {
"admin_role": {
"description": "Can manage all aspects of the workflow job template",
"name": "Admin",
"id": 260
},
"execute_role": {
"description": "May run the workflow job template",
"name": "Execute",
"id": 261
},
"read_role": {
"description": "May view settings for the workflow job template",
"name": "Read",
"id": 262
},
"approval_role": {
"description": "Can approve or deny a workflow approval node",
"name": "Approve",
"id": 263
}
},
"user_capabilities": {
"edit": true,
"delete": true,
"start": true,
"schedule": true,
"copy": true
},
"labels": {
"count": 2,
"results": [
{
"id": 104,
"name": "L_10o0"
},
{
"id": 105,
"name": "L_20o0"
}
]
},
"recent_jobs": [
{
"id": 226,
"status": "failed",
"finished": "2020-04-08T21:30:44.282245Z",
"canceled_on": null,
"type": "workflow_job"
}
]
},
"created": "2020-04-07T16:38:02.856877Z",
"modified": "2020-04-13T20:53:53.761355Z",
"name": "Mock Workflow",
"description": "Mock WF Description",
"last_job_run": "2020-04-08T21:30:44.282245Z",
"last_job_failed": true,
"next_job_run": null,
"status": "failed",
"extra_vars": "---\nmock: data",
"organization": 3,
"survey_enabled": false,
"allow_simultaneous": true,
"ask_variables_on_launch": false,
"inventory": 7,
"limit": "hosts1,hosts2",
"scm_branch": "/bar/",
"ask_inventory_on_launch": true,
"ask_scm_branch_on_launch": true,
"ask_limit_on_launch": true,
"webhook_service": "github",
"webhook_credential": 8
}

View File

@@ -72,6 +72,7 @@ function NodeViewModal({ i18n }) {
} = useRequest(
useCallback(async () => {
let { data } = await nodeAPI?.readDetail(unifiedJobTemplate.id);
if (data?.type === 'job_template') {
const {
data: { results = [] },
@@ -79,6 +80,13 @@ function NodeViewModal({ i18n }) {
data = Object.assign(data, { instance_groups: results });
}
if (data?.related?.webhook_receiver) {
const {
data: { webhook_key },
} = await nodeAPI?.readWebhookKey(data.id);
data = Object.assign(data, { webhook_key });
}
return data;
}, [nodeAPI, unifiedJobTemplate.id]),
null

View File

@@ -11,9 +11,23 @@ import NodeViewModal from './NodeViewModal';
jest.mock('@api/models/JobTemplates');
jest.mock('@api/models/WorkflowJobTemplates');
WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({});
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({});
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: {
id: 1,
type: 'workflow_job_template',
related: {
webhook_receiver: '/api/v2/job_templates/7/gitlab/',
},
},
});
WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({
data: {
webhook_key: 'Pim3mRXT0',
},
});
JobTemplatesAPI.readLaunch.mockResolvedValue({});
JobTemplatesAPI.readInstanceGroups.mockResolvedValue({});
JobTemplatesAPI.readWebhookKey.mockResolvedValue({});
JobTemplatesAPI.readDetail.mockResolvedValue({
data: {
id: 1,
@@ -74,6 +88,7 @@ describe('NodeViewModal', () => {
expect(JobTemplatesAPI.readDetail).not.toHaveBeenCalled();
expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
expect(WorkflowJobTemplatesAPI.readWebhookKey).toHaveBeenCalledWith(1);
});
test('Close button dispatches as expected', () => {
@@ -125,6 +140,7 @@ describe('NodeViewModal', () => {
});
waitForLoaded(wrapper);
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
expect(JobTemplatesAPI.readWebhookKey).not.toHaveBeenCalledWith();
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
expect(JobTemplatesAPI.readDetail).toHaveBeenCalledWith(1);
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);