mirror of
https://github.com/ansible/awx.git
synced 2026-05-13 12:27:37 -02:30
Merge pull request #2352 from ansible/workflows_squared
[Feature] Allow use of workflows inside of workflows Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
@@ -25,7 +25,6 @@ from rest_framework.filters import BaseFilterBackend
|
||||
from awx.main.utils import get_type_for_model, to_python_boolean
|
||||
from awx.main.utils.db import get_all_field_names
|
||||
from awx.main.models.credential import CredentialType
|
||||
from awx.main.models.rbac import RoleAncestorEntry
|
||||
|
||||
|
||||
class V1CredentialFilterBackend(BaseFilterBackend):
|
||||
@@ -347,12 +346,12 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
else:
|
||||
args.append(Q(**{k:v}))
|
||||
for role_name in role_filters:
|
||||
if not hasattr(queryset.model, 'accessible_pk_qs'):
|
||||
raise ParseError(_(
|
||||
'Cannot apply role_level filter to this list because its model '
|
||||
'does not use roles for access control.'))
|
||||
args.append(
|
||||
Q(pk__in=RoleAncestorEntry.objects.filter(
|
||||
ancestor__in=request.user.roles.all(),
|
||||
content_type_id=ContentType.objects.get_for_model(queryset.model).id,
|
||||
role_field=role_name
|
||||
).values_list('object_id').distinct())
|
||||
Q(pk__in=queryset.model.accessible_pk_qs(request.user, role_name))
|
||||
)
|
||||
if or_filters:
|
||||
q = Q()
|
||||
|
||||
@@ -104,7 +104,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
|
||||
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
'vault_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed'),
|
||||
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'),
|
||||
'job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_job': DEFAULT_SUMMARY_FIELDS,
|
||||
@@ -3830,9 +3830,6 @@ class WorkflowJobTemplateNodeSerializer(LaunchConfigurationBaseSerializer):
|
||||
ujt_obj = attrs['unified_job_template']
|
||||
elif self.instance:
|
||||
ujt_obj = self.instance.unified_job_template
|
||||
if isinstance(ujt_obj, (WorkflowJobTemplate)):
|
||||
raise serializers.ValidationError({
|
||||
"unified_job_template": _("Cannot nest a %s inside a WorkflowJobTemplate") % ujt_obj.__class__.__name__})
|
||||
if 'credential' in deprecated_fields: # TODO: remove when v2 API is deprecated
|
||||
cred = deprecated_fields['credential']
|
||||
attrs['credential'] = cred
|
||||
|
||||
@@ -31,7 +31,7 @@ from awx.main.models.mixins import (
|
||||
SurveyJobMixin,
|
||||
RelatedJobsMixin,
|
||||
)
|
||||
from awx.main.models.jobs import LaunchTimeConfig
|
||||
from awx.main.models.jobs import LaunchTimeConfig, JobTemplate
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.redact import REPLACE_STR
|
||||
from awx.main.fields import JSONField
|
||||
@@ -199,7 +199,14 @@ class WorkflowJobNode(WorkflowNodeBase):
|
||||
data = {}
|
||||
ujt_obj = self.unified_job_template
|
||||
if ujt_obj is not None:
|
||||
accepted_fields, ignored_fields, errors = ujt_obj._accept_or_ignore_job_kwargs(**self.prompts_dict())
|
||||
# MERGE note: move this to prompts_dict method on node when merging
|
||||
# with the workflow inventory branch
|
||||
prompts_data = self.prompts_dict()
|
||||
if isinstance(ujt_obj, WorkflowJobTemplate):
|
||||
if self.workflow_job.extra_vars:
|
||||
prompts_data.setdefault('extra_vars', {})
|
||||
prompts_data['extra_vars'].update(self.workflow_job.extra_vars_dict)
|
||||
accepted_fields, ignored_fields, errors = ujt_obj._accept_or_ignore_job_kwargs(**prompts_data)
|
||||
if errors:
|
||||
logger.info(_('Bad launch configuration starting template {template_pk} as part of '
|
||||
'workflow {workflow_pk}. Errors:\n{error_text}').format(
|
||||
@@ -246,8 +253,9 @@ class WorkflowJobNode(WorkflowNodeBase):
|
||||
functional_aa_dict.pop('_ansible_no_log', None)
|
||||
extra_vars.update(functional_aa_dict)
|
||||
# Workflow Job extra_vars higher precedence than ancestor artifacts
|
||||
if self.workflow_job and self.workflow_job.extra_vars:
|
||||
extra_vars.update(self.workflow_job.extra_vars_dict)
|
||||
if ujt_obj and isinstance(ujt_obj, JobTemplate):
|
||||
if self.workflow_job and self.workflow_job.extra_vars:
|
||||
extra_vars.update(self.workflow_job.extra_vars_dict)
|
||||
if extra_vars:
|
||||
data['extra_vars'] = extra_vars
|
||||
# ensure that unified jobs created by WorkflowJobs are marked
|
||||
@@ -505,6 +513,24 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
|
||||
def task_impact(self):
|
||||
return 0
|
||||
|
||||
def get_ancestor_workflows(self):
|
||||
"""Returns a list of WFJTs that are indirect parents of this workflow job
|
||||
say WFJTs are set up to spawn in order of A->B->C, and this workflow job
|
||||
came from C, then C is the parent and [B, A] will be returned from this.
|
||||
"""
|
||||
ancestors = []
|
||||
wj_ids = set([self.pk])
|
||||
wj = self.get_workflow_job()
|
||||
while wj and wj.workflow_job_template_id:
|
||||
if wj.pk in wj_ids:
|
||||
logger.critical('Cycles detected in the workflow jobs graph, '
|
||||
'this is not normal and suggests task manager degeneracy.')
|
||||
break
|
||||
wj_ids.add(wj.pk)
|
||||
ancestors.append(wj.workflow_job_template)
|
||||
wj = wj.get_workflow_job()
|
||||
return ancestors
|
||||
|
||||
def get_notification_templates(self):
|
||||
return self.workflow_job_template.notification_templates
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from awx.main.models import (
|
||||
ProjectUpdate,
|
||||
SystemJob,
|
||||
WorkflowJob,
|
||||
WorkflowJobTemplate
|
||||
)
|
||||
from awx.main.scheduler.dag_workflow import WorkflowDAG
|
||||
from awx.main.utils.pglock import advisory_lock
|
||||
@@ -120,7 +121,26 @@ class TaskManager():
|
||||
spawn_node.job = job
|
||||
spawn_node.save()
|
||||
logger.info('Spawned %s in %s for node %s', job.log_format, workflow_job.log_format, spawn_node.pk)
|
||||
if job._resources_sufficient_for_launch():
|
||||
can_start = True
|
||||
if isinstance(spawn_node.unified_job_template, WorkflowJobTemplate):
|
||||
workflow_ancestors = job.get_ancestor_workflows()
|
||||
if spawn_node.unified_job_template in set(workflow_ancestors):
|
||||
can_start = False
|
||||
logger.info('Refusing to start recursive workflow-in-workflow id={}, wfjt={}, ancestors={}'.format(
|
||||
job.id, spawn_node.unified_job_template.pk, [wa.pk for wa in workflow_ancestors]))
|
||||
display_list = [spawn_node.unified_job_template] + workflow_ancestors
|
||||
job.job_explanation = _(
|
||||
"Workflow Job spawned from workflow could not start because it "
|
||||
"would result in recursion (spawn order, most recent first: {})"
|
||||
).format(six.text_type(', ').join([six.text_type('<{}>').format(tmp) for tmp in display_list]))
|
||||
else:
|
||||
logger.debug('Starting workflow-in-workflow id={}, wfjt={}, ancestors={}'.format(
|
||||
job.id, spawn_node.unified_job_template.pk, [wa.pk for wa in workflow_ancestors]))
|
||||
if not job._resources_sufficient_for_launch():
|
||||
can_start = False
|
||||
job.job_explanation = _("Job spawned from workflow could not start because it "
|
||||
"was missing a related resource such as project or inventory")
|
||||
if can_start:
|
||||
if workflow_job.start_args:
|
||||
start_args = json.loads(decrypt_field(workflow_job, 'start_args'))
|
||||
else:
|
||||
@@ -129,14 +149,10 @@ class TaskManager():
|
||||
if not can_start:
|
||||
job.job_explanation = _("Job spawned from workflow could not start because it "
|
||||
"was not in the right state or required manual credentials")
|
||||
else:
|
||||
can_start = False
|
||||
job.job_explanation = _("Job spawned from workflow could not start because it "
|
||||
"was missing a related resource such as project or inventory")
|
||||
if not can_start:
|
||||
job.status = 'failed'
|
||||
job.save(update_fields=['status', 'job_explanation'])
|
||||
connection.on_commit(lambda: job.websocket_emit_status('failed'))
|
||||
job.websocket_emit_status('failed')
|
||||
|
||||
# TODO: should we emit a status on the socket here similar to tasks.py awx_periodic_scheduler() ?
|
||||
#emit_websocket_notification('/socket.io/jobs', '', dict(id=))
|
||||
@@ -153,7 +169,7 @@ class TaskManager():
|
||||
workflow_job.status = 'canceled'
|
||||
workflow_job.start_args = '' # blank field to remove encrypted passwords
|
||||
workflow_job.save(update_fields=['status', 'start_args'])
|
||||
connection.on_commit(lambda: workflow_job.websocket_emit_status(workflow_job.status))
|
||||
workflow_job.websocket_emit_status(workflow_job.status)
|
||||
else:
|
||||
is_done, has_failed = dag.is_workflow_done()
|
||||
if not is_done:
|
||||
@@ -165,7 +181,7 @@ class TaskManager():
|
||||
workflow_job.status = new_status
|
||||
workflow_job.start_args = '' # blank field to remove encrypted passwords
|
||||
workflow_job.save(update_fields=['status', 'start_args'])
|
||||
connection.on_commit(lambda: workflow_job.websocket_emit_status(workflow_job.status))
|
||||
workflow_job.websocket_emit_status(workflow_job.status)
|
||||
return result
|
||||
|
||||
def get_dependent_jobs_for_inv_and_proj_update(self, job_obj):
|
||||
@@ -231,7 +247,6 @@ class TaskManager():
|
||||
self.consume_capacity(task, rampart_group.name)
|
||||
|
||||
def post_commit():
|
||||
task.websocket_emit_status(task.status)
|
||||
if task.status != 'failed' and type(task) is not WorkflowJob:
|
||||
task_cls = task._get_task_class()
|
||||
task_cls.apply_async(
|
||||
@@ -250,6 +265,7 @@ class TaskManager():
|
||||
}],
|
||||
)
|
||||
|
||||
task.websocket_emit_status(task.status) # adds to on_commit
|
||||
connection.on_commit(post_commit)
|
||||
|
||||
def process_running_tasks(self, running_tasks):
|
||||
|
||||
@@ -215,3 +215,55 @@ class TestWorkflowJobTemplate:
|
||||
wfjt2.validate_unique()
|
||||
wfjt2 = WorkflowJobTemplate(name='foo', organization=None)
|
||||
wfjt2.validate_unique()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_workflow_ancestors(organization):
|
||||
# Spawn order of templates grandparent -> parent -> child
|
||||
# create child WFJT and workflow job
|
||||
child = WorkflowJobTemplate.objects.create(organization=organization, name='child')
|
||||
child_job = WorkflowJob.objects.create(
|
||||
workflow_job_template=child,
|
||||
launch_type='workflow'
|
||||
)
|
||||
# create parent WFJT and workflow job, and link it up
|
||||
parent = WorkflowJobTemplate.objects.create(organization=organization, name='parent')
|
||||
parent_job = WorkflowJob.objects.create(
|
||||
workflow_job_template=parent,
|
||||
launch_type='workflow'
|
||||
)
|
||||
WorkflowJobNode.objects.create(
|
||||
workflow_job=parent_job,
|
||||
unified_job_template=child,
|
||||
job=child_job
|
||||
)
|
||||
# create grandparent WFJT and workflow job and link it up
|
||||
grandparent = WorkflowJobTemplate.objects.create(organization=organization, name='grandparent')
|
||||
grandparent_job = WorkflowJob.objects.create(
|
||||
workflow_job_template=grandparent,
|
||||
launch_type='schedule'
|
||||
)
|
||||
WorkflowJobNode.objects.create(
|
||||
workflow_job=grandparent_job,
|
||||
unified_job_template=parent,
|
||||
job=parent_job
|
||||
)
|
||||
# ancestors method gives a list of WFJT ids
|
||||
assert child_job.get_ancestor_workflows() == [parent, grandparent]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_workflow_ancestors_recursion_prevention(organization):
|
||||
# This is toxic database data, this tests that it doesn't create an infinite loop
|
||||
wfjt = WorkflowJobTemplate.objects.create(organization=organization, name='child')
|
||||
wfj = WorkflowJob.objects.create(
|
||||
workflow_job_template=wfjt,
|
||||
launch_type='workflow'
|
||||
)
|
||||
WorkflowJobNode.objects.create(
|
||||
workflow_job=wfj,
|
||||
unified_job_template=wfjt,
|
||||
job=wfj # well, this is a problem
|
||||
)
|
||||
# mostly, we just care that this assertion finishes in finite time
|
||||
assert wfj.get_ancestor_workflows() == []
|
||||
|
||||
@@ -334,7 +334,7 @@
|
||||
.JobResults-container {
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: minmax(300px, 1fr) minmax(500px, 2fr);
|
||||
grid-template-columns: minmax(400px, 1fr) minmax(500px, 2fr);
|
||||
grid-template-rows: minmax(500px, ~"calc(100vh - 130px)");
|
||||
|
||||
.at-Panel {
|
||||
@@ -457,5 +457,6 @@
|
||||
.JobResults-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +120,12 @@ function getSourceWorkflowJobDetails () {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = strings.get('labels.SOURCE_WORKFLOW_JOB');
|
||||
const value = sourceWorkflowJob.name;
|
||||
const link = `/#/workflows/${sourceWorkflowJob.id}`;
|
||||
const tooltip = strings.get('tooltips.SOURCE_WORKFLOW_JOB');
|
||||
|
||||
return { link, tooltip };
|
||||
return { label, value, link, tooltip };
|
||||
}
|
||||
|
||||
function getSliceJobDetails () {
|
||||
|
||||
@@ -281,6 +281,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SOURCE WORKFLOW JOB DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.sourceWorkflowJob">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.sourceWorkflowJob.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ vm.sourceWorkflowJob.link }}"
|
||||
aw-tool-tip="{{ vm.sourceWorkflowJob.tooltip }}"
|
||||
data-placement="top">
|
||||
{{ vm.sourceWorkflowJob.value }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- EXTRA VARIABLES DETAIL -->
|
||||
<at-code-mirror
|
||||
class="JobResults-resultRow"
|
||||
|
||||
@@ -74,6 +74,7 @@ function OutputStrings (BaseString) {
|
||||
SKIP_TAGS: t.s('Skip Tags'),
|
||||
SOURCE: t.s('Source'),
|
||||
SOURCE_CREDENTIAL: t.s('Source Credential'),
|
||||
SOURCE_WORKFLOW_JOB: t.s('Source Workflow'),
|
||||
STARTED: t.s('Started'),
|
||||
STATUS: t.s('Status'),
|
||||
VERBOSITY: t.s('Verbosity'),
|
||||
|
||||
@@ -102,6 +102,7 @@ function TemplatesStrings (BaseString) {
|
||||
ALWAYS: t.s('Always'),
|
||||
PROJECT_SYNC: t.s('Project Sync'),
|
||||
INVENTORY_SYNC: t.s('Inventory Sync'),
|
||||
WORKFLOW: t.s('Workflow'),
|
||||
WARNING: t.s('Warning'),
|
||||
TOTAL_TEMPLATES: t.s('TOTAL TEMPLATES'),
|
||||
ADD_A_TEMPLATE: t.s('ADD A TEMPLATE'),
|
||||
|
||||
@@ -493,21 +493,6 @@ table, tbody {
|
||||
}
|
||||
}
|
||||
|
||||
.List-infoCell--badge {
|
||||
height: 15px;
|
||||
color: @default-interface-txt;
|
||||
background-color: @default-list-header-bg;
|
||||
border-radius: 5px;
|
||||
font-size: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin-left: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 100;
|
||||
margin-top: 2.25px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.List-actionsInner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
padding: @at-padding-list-row-item-tag;
|
||||
line-height: @at-line-height-list-row-item-tag;
|
||||
word-break: keep-all;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.at-RowItem-tag--primary {
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
@at-highlight-left-border-margin-makeup: -5px;
|
||||
@at-z-index-nav: 1040;
|
||||
@at-z-index-side-nav: 1030;
|
||||
@at-line-height-list-row-item-tag: 22px;
|
||||
@at-line-height-list-row-item-tag: 14px;
|
||||
|
||||
// 6. Breakpoints ---------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@
|
||||
|
||||
@media screen and (max-width: @breakpoint-sm) {
|
||||
.BreadCrumb {
|
||||
padding-left: 50px;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<div class="List-infoCell">
|
||||
<span class="List-infoCell--badge" aw-pop-over="<dl><dt>{{ 'INVENTORY' | translate }}</dt><dd>{{(job_template.summary_fields.inventory.name | sanitize) || ('NONE SELECTED' | translate)}}</dd></dl><dl><dt>{{ 'PROJECT' | translate }}</dt><dd>{{job_template.summary_fields.project.name | sanitize}}</dd></dl><dl><dt>{{ 'PLAYBOOK' | translate }}</dt><dd>{{job_template.playbook| sanitize}}</dd></dl><dl><dt>{{ 'CREDENTIAL' | translate }}</dt> <dd>{{(job_template.summary_fields.credential.name | sanitize) || ('NONE SELECTED' | translate)}}</dd></dl>" data-popover-title="{{job_template.name| sanitize}}" translate>INFO</span>
|
||||
<div class="List-infoCell" ng-if="wf_maker_template.type === 'job_template'">
|
||||
<span class="Key-icon Key-icon--circle Key-icon--default" aw-pop-over="<dl><dt>{{ 'INVENTORY' | translate }}</dt><dd>{{(wf_maker_template.summary_fields.inventory.name | sanitize) || ('NONE SELECTED' | translate)}}</dd></dl><dl><dt>{{ 'PROJECT' | translate }}</dt><dd>{{wf_maker_template.summary_fields.project.name | sanitize}}</dd></dl><dl><dt>{{ 'PLAYBOOK' | translate }}</dt><dd>{{wf_maker_template.playbook| sanitize}}</dd></dl><dl><dt>{{ 'CREDENTIAL' | translate }}</dt> <dd>{{(wf_maker_template.summary_fields.credential.name | sanitize) || ('NONE SELECTED' | translate)}}</dd></dl>"
|
||||
data-popover-title="{{wf_maker_template.name| sanitize}}">?</span>
|
||||
</div>
|
||||
|
||||
@@ -696,6 +696,13 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
if (options.mode !== 'lookup' && field.badgeIcon && field.badgePlacement && field.badgePlacement !== 'left') {
|
||||
html += Badge(field);
|
||||
}
|
||||
|
||||
// Field Tag
|
||||
if (field.tag) {
|
||||
html += `<span class="at-RowItem-tag" ng-show="${field.showTag}">
|
||||
${field.tag}
|
||||
</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -409,34 +409,33 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
workflowMaker = {
|
||||
name: 'templates.editWorkflowJobTemplate.workflowMaker',
|
||||
url: '/workflow-maker',
|
||||
// ncyBreadcrumb: {
|
||||
// label: 'WORKFLOW MAKER'
|
||||
// },
|
||||
data: {
|
||||
formChildState: true
|
||||
},
|
||||
params: {
|
||||
job_template_search: {
|
||||
wf_maker_template_search: {
|
||||
value: {
|
||||
page_size: '5',
|
||||
order_by: 'name'
|
||||
order_by: 'name',
|
||||
page_size: '10',
|
||||
role_level: 'execute_role',
|
||||
type: 'workflow_job_template,job_template'
|
||||
},
|
||||
squash: false,
|
||||
dynamic: true
|
||||
},
|
||||
project_search: {
|
||||
wf_maker_project_search: {
|
||||
value: {
|
||||
page_size: '5',
|
||||
order_by: 'name'
|
||||
order_by: 'name',
|
||||
page_size: '10'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
},
|
||||
inventory_source_search: {
|
||||
wf_maker_inventory_source_search: {
|
||||
value: {
|
||||
page_size: '5',
|
||||
not__source: '',
|
||||
order_by: 'name'
|
||||
order_by: 'name',
|
||||
page_size: '10'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
@@ -467,14 +466,14 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
|
||||
$scope.$watch('job_templates', function(){
|
||||
$scope.$watch('wf_maker_templates', function(){
|
||||
if($scope.selectedTemplate){
|
||||
$scope.job_templates.forEach(function(row, i) {
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
if(row.id === $scope.selectedTemplate.id) {
|
||||
$scope.job_templates[i].checked = 1;
|
||||
$scope.wf_maker_templates[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.job_templates[i].checked = 0;
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -483,9 +482,9 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) {
|
||||
$scope.job_templates.forEach(function(row, i) {
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
if (row.id === selectedRow.id) {
|
||||
$scope.job_templates[i].checked = 1;
|
||||
$scope.wf_maker_templates[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
@@ -498,27 +497,27 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTemplate', () => {
|
||||
$scope.job_templates.forEach(function(row, i) {
|
||||
if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) {
|
||||
$scope.job_templates[i].checked = 1;
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
if(_.has($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) {
|
||||
$scope.wf_maker_templates[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.job_templates[i].checked = 0;
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('activeTab', () => {
|
||||
if(!$scope.activeTab || $scope.activeTab !== "jobs") {
|
||||
$scope.job_templates.forEach(function(row, i) {
|
||||
$scope.job_templates[i].checked = 0;
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('clearWorkflowLists', function() {
|
||||
$scope.job_templates.forEach(function(row, i) {
|
||||
$scope.job_templates[i].checked = 0;
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -544,14 +543,14 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
|
||||
$scope.$watch('workflow_inventory_sources', function(){
|
||||
$scope.$watch('wf_maker_inventory_sources', function(){
|
||||
if($scope.selectedTemplate){
|
||||
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
if(row.id === $scope.selectedTemplate.id) {
|
||||
$scope.workflow_inventory_sources[i].checked = 1;
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.workflow_inventory_sources[i].checked = 0;
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -560,9 +559,9 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) {
|
||||
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
if (row.id === selectedRow.id) {
|
||||
$scope.workflow_inventory_sources[i].checked = 1;
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
@@ -575,27 +574,27 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTemplate', () => {
|
||||
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) {
|
||||
$scope.workflow_inventory_sources[i].checked = 1;
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.workflow_inventory_sources[i].checked = 0;
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('activeTab', () => {
|
||||
if(!$scope.activeTab || $scope.activeTab !== "inventory_sync") {
|
||||
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||
$scope.workflow_inventory_sources[i].checked = 0;
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('clearWorkflowLists', function() {
|
||||
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||
$scope.workflow_inventory_sources[i].checked = 0;
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -621,14 +620,14 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
|
||||
$scope.$watch('projects', function(){
|
||||
$scope.$watch('wf_maker_projects', function(){
|
||||
if($scope.selectedTemplate){
|
||||
$scope.projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
if(row.id === $scope.selectedTemplate.id) {
|
||||
$scope.projects[i].checked = 1;
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.projects[i].checked = 0;
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -637,9 +636,9 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) {
|
||||
$scope.projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
if (row.id === selectedRow.id) {
|
||||
$scope.projects[i].checked = 1;
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
@@ -652,27 +651,27 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTemplate', () => {
|
||||
$scope.projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) {
|
||||
$scope.projects[i].checked = 1;
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.projects[i].checked = 0;
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('activeTab', () => {
|
||||
if(!$scope.activeTab || $scope.activeTab !== "project_sync") {
|
||||
$scope.projects.forEach(function(row, i) {
|
||||
$scope.projects[i].checked = 0;
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('clearWorkflowLists', function() {
|
||||
$scope.projects.forEach(function(row, i) {
|
||||
$scope.projects[i].checked = 0;
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -698,8 +697,8 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
],
|
||||
WorkflowMakerJobTemplateList: ['TemplateList',
|
||||
(TemplateList) => {
|
||||
WorkflowMakerJobTemplateList: ['TemplateList', 'i18n',
|
||||
(TemplateList, i18n) => {
|
||||
let list = _.cloneDeep(TemplateList);
|
||||
delete list.actions;
|
||||
delete list.fields.type;
|
||||
@@ -707,16 +706,19 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
delete list.fields.smart_status;
|
||||
delete list.fields.labels;
|
||||
delete list.fieldActions;
|
||||
list.name = 'wf_maker_templates';
|
||||
list.iterator = 'wf_maker_template';
|
||||
list.fields.name.columnClass = "col-md-8";
|
||||
list.fields.name.tag = i18n._('WORKFLOW');
|
||||
list.fields.name.showTag = "{{wf_maker_template.type === 'workflow_job_template'}}";
|
||||
list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}";
|
||||
list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit';
|
||||
list.iterator = 'job_template';
|
||||
list.name = 'job_templates';
|
||||
list.basePath = "job_templates";
|
||||
list.basePath = 'unified_job_templates';
|
||||
list.fields.info = {
|
||||
ngInclude: "'/static/partials/job-template-details.html'",
|
||||
type: 'template',
|
||||
columnClass: 'col-md-3',
|
||||
infoHeaderClass: 'col-md-3',
|
||||
label: '',
|
||||
nosort: true
|
||||
};
|
||||
@@ -732,6 +734,8 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
delete list.fields.status;
|
||||
delete list.fields.scm_type;
|
||||
delete list.fields.last_updated;
|
||||
list.name = 'wf_maker_projects';
|
||||
list.iterator = 'wf_maker_project';
|
||||
list.fields.name.columnClass = "col-md-11";
|
||||
list.maxVisiblePages = 5;
|
||||
list.searchBarFullWidth = true;
|
||||
@@ -744,6 +748,8 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
WorkflowInventorySourcesList: ['InventorySourcesList',
|
||||
(InventorySourcesList) => {
|
||||
let list = _.cloneDeep(InventorySourcesList);
|
||||
list.name = 'wf_maker_inventory_sources';
|
||||
list.iterator = 'wf_maker_inventory_source';
|
||||
list.maxVisiblePages = 5;
|
||||
list.searchBarFullWidth = true;
|
||||
list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
.WorkflowMaker-contentLeft {
|
||||
flex: 1 0 auto;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -152,6 +152,7 @@
|
||||
}
|
||||
.WorkflowMaker-chart {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.WorkflowMaker-totalJobs {
|
||||
margin-right: 5px;
|
||||
@@ -202,23 +203,17 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
.WorkflowLegend-details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding-left: 20px;
|
||||
margin-top:10px;
|
||||
border: 1px solid @d7grey;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
.WorkflowLegend-legendItem {
|
||||
display: flex;
|
||||
}
|
||||
.WorkflowLegend-legendItem:not(:last-child) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.WorkflowLegend-details--left {
|
||||
display: flex;
|
||||
display: block;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.WorkflowLegend-details--right {
|
||||
@@ -304,6 +299,7 @@
|
||||
height: 3px;
|
||||
width: 20px;
|
||||
margin: 9px 5px 9px 0px;
|
||||
outline: none;
|
||||
}
|
||||
.Key-heading {
|
||||
font-weight: 700;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
|
||||
'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel', 'WorkflowJobTemplateModel',
|
||||
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
|
||||
function ($scope, WorkflowService, TemplatesService,
|
||||
ProcessErrors, CreateSelect2, $q, JobTemplate,
|
||||
ProcessErrors, CreateSelect2, $q, JobTemplate, WorkflowJobTemplate,
|
||||
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
|
||||
|
||||
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
|
||||
@@ -110,7 +110,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
// Check to see if the user has provided any prompt values that are different
|
||||
// from the defaults in the job template
|
||||
|
||||
if (params.node.unifiedJobTemplate.type === "job_template" && params.node.promptData) {
|
||||
if ((params.node.unifiedJobTemplate.type === "job_template" || params.node.unifiedJobTemplate.type === "workflow_job_template") && params.node.promptData) {
|
||||
sendableNodeData = PromptService.bundlePromptDataForSaving({
|
||||
promptData: params.node.promptData,
|
||||
dataToSave: sendableNodeData
|
||||
@@ -188,7 +188,11 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
|
||||
params.node.isNew = false;
|
||||
continueRecursing(data.data.id);
|
||||
}, function ({ data, config, status }) {
|
||||
}, function ({
|
||||
data,
|
||||
config,
|
||||
status
|
||||
}) {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER'),
|
||||
msg: $scope.strings.get('error.CALL', {
|
||||
@@ -339,7 +343,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
let missingPromptValue = false;
|
||||
if ($scope.missingSurveyValue) {
|
||||
missingPromptValue = true;
|
||||
} else if (!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) {
|
||||
} else if ($scope.selectedTemplate.type === 'job_template' && (!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id)) {
|
||||
missingPromptValue = true;
|
||||
}
|
||||
$scope.promptModalMissingReqFields = missingPromptValue;
|
||||
@@ -481,7 +485,8 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
|
||||
$scope.placeholderNode.unifiedJobTemplate = $scope.selectedTemplate;
|
||||
$scope.placeholderNode.edgeType = $scope.edgeType.value;
|
||||
if ($scope.placeholderNode.unifiedJobTemplate.type === 'job_template') {
|
||||
if ($scope.placeholderNode.unifiedJobTemplate.type === 'job_template' ||
|
||||
$scope.placeholderNode.unifiedJobTemplate.type === 'workflow_job_template') {
|
||||
$scope.placeholderNode.promptData = _.cloneDeep($scope.promptData);
|
||||
}
|
||||
$scope.placeholderNode.canEdit = true;
|
||||
@@ -498,8 +503,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) {
|
||||
$scope.nodeBeingEdited.unifiedJobTemplate = $scope.selectedTemplate;
|
||||
$scope.nodeBeingEdited.edgeType = $scope.edgeType.value;
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template') {
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template' || $scope.nodeBeingEdited.unifiedJobTemplate.type === 'workflow_job_template') {
|
||||
$scope.nodeBeingEdited.promptData = _.cloneDeep($scope.promptData);
|
||||
}
|
||||
|
||||
@@ -591,9 +595,8 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
$scope.nodeBeingEdited.isActiveEdit = true;
|
||||
|
||||
let finishConfiguringEdit = function () {
|
||||
|
||||
let jobTemplate = new JobTemplate();
|
||||
|
||||
let templateType = $scope.nodeBeingEdited.unifiedJobTemplate.type;
|
||||
let jobTemplate = templateType === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
|
||||
if (!_.isEmpty($scope.nodeBeingEdited.promptData)) {
|
||||
$scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData);
|
||||
const launchConf = $scope.promptData.launchConf;
|
||||
@@ -615,15 +618,17 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) {
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template' && launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
} else {
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job_template' ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template'
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job' ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template' ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'workflow_job' ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'workflow_job_template'
|
||||
) {
|
||||
let promises = [jobTemplate.optionsLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id)];
|
||||
|
||||
@@ -674,8 +679,12 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
|
||||
prompts.credentials.value = workflowNodeCredentials.concat(defaultCredsWithoutOverrides);
|
||||
|
||||
if ((!$scope.nodeBeingEdited.unifiedJobTemplate.inventory && !launchConf.ask_inventory_on_launch) || !$scope.nodeBeingEdited.unifiedJobTemplate.project) {
|
||||
$scope.selectedTemplateInvalid = true;
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.unified_job_template === 'job') {
|
||||
if ((!$scope.nodeBeingEdited.unifiedJobTemplate.inventory && !launchConf.ask_inventory_on_launch) || !$scope.nodeBeingEdited.unifiedJobTemplate.project) {
|
||||
$scope.selectedTemplateInvalid = true;
|
||||
} else {
|
||||
$scope.selectedTemplateInvalid = false;
|
||||
}
|
||||
} else {
|
||||
$scope.selectedTemplateInvalid = false;
|
||||
}
|
||||
@@ -774,7 +783,9 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
}
|
||||
|
||||
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) {
|
||||
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === "job_template") {
|
||||
|
||||
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === "job_template" ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === "workflow_job_template") {
|
||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||
}
|
||||
|
||||
@@ -783,6 +794,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
if ($scope.selectedTemplate.unified_job_type) {
|
||||
switch ($scope.selectedTemplate.unified_job_type) {
|
||||
case "job":
|
||||
case "workflow_job":
|
||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||
break;
|
||||
case "project_update":
|
||||
@@ -795,6 +807,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
} else if ($scope.selectedTemplate.type) {
|
||||
switch ($scope.selectedTemplate.type) {
|
||||
case "job_template":
|
||||
case "workflow_job_template":
|
||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||
break;
|
||||
case "project":
|
||||
@@ -843,8 +856,9 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
|
||||
// Determine whether or not we need to go out and GET this nodes unified job template
|
||||
// in order to determine whether or not prompt fields are needed
|
||||
|
||||
if (!$scope.nodeBeingEdited.isNew && !$scope.nodeBeingEdited.edited && $scope.nodeBeingEdited.unifiedJobTemplate && $scope.nodeBeingEdited.unifiedJobTemplate.unified_job_type && $scope.nodeBeingEdited.unifiedJobTemplate.unified_job_type === 'job') {
|
||||
if (!$scope.nodeBeingEdited.isNew && !$scope.nodeBeingEdited.edited &&
|
||||
(_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job' ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'workflow_job')) {
|
||||
// This is a node that we got back from the api with an incomplete
|
||||
// unified job template so we're going to pull down the whole object
|
||||
|
||||
@@ -852,15 +866,19 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
.then(function (data) {
|
||||
$scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]);
|
||||
finishConfiguringEdit();
|
||||
}, function ({ data, status, config }) {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER'),
|
||||
msg: $scope.strings.get('error.CALL', {
|
||||
path: `${config.url}`,
|
||||
action: `${config.method}`,
|
||||
status
|
||||
})
|
||||
});
|
||||
}, function ({
|
||||
data,
|
||||
status,
|
||||
config
|
||||
}) {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER'),
|
||||
msg: $scope.strings.get('error.CALL', {
|
||||
path: `${config.url}`,
|
||||
action: `${config.method}`,
|
||||
status
|
||||
})
|
||||
});
|
||||
});
|
||||
} else {
|
||||
finishConfiguringEdit();
|
||||
@@ -982,24 +1000,24 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
}
|
||||
|
||||
$scope.promptData = null;
|
||||
|
||||
if (selectedTemplate.type === "job_template") {
|
||||
let jobTemplate = new JobTemplate();
|
||||
if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") {
|
||||
let jobTemplate = selectedTemplate.type === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
|
||||
|
||||
$q.all([jobTemplate.optionsLaunch(selectedTemplate.id), jobTemplate.getLaunch(selectedTemplate.id)])
|
||||
.then((responses) => {
|
||||
let launchConf = responses[1].data;
|
||||
if (selectedTemplate.type === 'job_template') {
|
||||
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
|
||||
$scope.selectedTemplateInvalid = true;
|
||||
} else {
|
||||
$scope.selectedTemplateInvalid = false;
|
||||
}
|
||||
|
||||
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
|
||||
$scope.selectedTemplateInvalid = true;
|
||||
} else {
|
||||
$scope.selectedTemplateInvalid = false;
|
||||
}
|
||||
|
||||
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
|
||||
$scope.credentialRequiresPassword = true;
|
||||
} else {
|
||||
$scope.credentialRequiresPassword = false;
|
||||
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
|
||||
$scope.credentialRequiresPassword = true;
|
||||
} else {
|
||||
$scope.credentialRequiresPassword = false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||
@@ -1021,8 +1039,12 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
if (selectedTemplate.type === 'job_template') {
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
} else {
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
}
|
||||
} else {
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
}
|
||||
@@ -1156,7 +1178,11 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
|
||||
// This is the last page
|
||||
buildTreeFromNodes();
|
||||
}
|
||||
}, function ({ data, status, config }) {
|
||||
}, function ({
|
||||
data,
|
||||
status,
|
||||
config
|
||||
}) {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER'),
|
||||
msg: $scope.strings.get('error.CALL', {
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
<div class="Key-icon Key-icon--circle Key-icon--default">I</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--circle Key-icon--default">W</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{strings.get('workflow_maker.WORKFLOW')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--circle Key-icon--warning">!</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{strings.get('workflow_maker.WARNING')}}</p>
|
||||
@@ -94,8 +98,10 @@
|
||||
<div id="workflow-inventory-sync-list" ui-view="inventorySyncList" ng-show="workflowMakerFormConfig.activeTab === 'inventory_sync'"></div>
|
||||
</div>
|
||||
<span ng-show="selectedTemplate &&
|
||||
((selectedTemplate.type === 'job_template' && workflowMakerFormConfig.activeTab === 'jobs') ||
|
||||
((selectedTemplate.type === 'job_template' || selectedTemplate.type === 'workflow_job_template' && workflowMakerFormConfig.activeTab === 'jobs') ||
|
||||
(selectedTemplate.unified_job_type === 'job' || selectedTemplate.unified_job_type === 'workflow_job' && workflowMakerFormConfig.activeTab === 'jobs') ||
|
||||
(selectedTemplate.type === 'project' && workflowMakerFormConfig.activeTab === 'project_sync') ||
|
||||
(selectedTemplate.unified_job_type === 'inventory_update' && workflowMakerFormConfig.activeTab === 'inventory_sync') ||
|
||||
(selectedTemplate.type === 'inventory_source' && workflowMakerFormConfig.activeTab === 'inventory_sync'))">
|
||||
<div ng-if="selectedTemplate && selectedTemplateInvalid">
|
||||
<div class="WorkflowMaker-invalidJobTemplateWarning">
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
display: block;
|
||||
min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.WorkflowResults-leftSide {
|
||||
@@ -26,6 +31,7 @@
|
||||
.OnePlusTwo-right--panel(100%, @breakpoint-md);
|
||||
height: ~"calc(100vh - 177px)";
|
||||
min-height: 350px;
|
||||
min-width: 0;
|
||||
|
||||
@media (max-width: @breakpoint-md - 1px) {
|
||||
padding-right: 15px;
|
||||
@@ -74,7 +80,7 @@
|
||||
.WorkflowResults-resultRowLabel {
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: normal!important;
|
||||
width: 30%;
|
||||
margin-right: 20px;
|
||||
@@ -141,3 +147,13 @@
|
||||
.WorkflowResults-extraVarsLabel {
|
||||
font-size:14px!important;
|
||||
}
|
||||
|
||||
.WorkflowResults-seeMoreLess {
|
||||
color: #337AB7;
|
||||
margin: 4px 0px;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 0px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
'jobLabels', 'workflowNodes', '$scope', 'ParseTypeChange',
|
||||
'ParseVariableString', 'WorkflowService', 'count', '$state', 'i18n',
|
||||
'moment', function(workflowData, workflowResultsService,
|
||||
'moment', '$filter', function(workflowData, workflowResultsService,
|
||||
workflowDataOptions, jobLabels, workflowNodes, $scope, ParseTypeChange,
|
||||
ParseVariableString, WorkflowService, count, $state, i18n, moment) {
|
||||
ParseVariableString, WorkflowService, count, $state, i18n, moment, $filter) {
|
||||
var runTimeElapsedTimer = null;
|
||||
|
||||
var getLinks = function() {
|
||||
@@ -41,6 +41,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
EDIT_WORKFLOW: i18n._('Edit the workflow job template'),
|
||||
EDIT_SLICE_TEMPLATE: i18n._('Edit the slice job template'),
|
||||
EDIT_SCHEDULE: i18n._('Edit the schedule'),
|
||||
SOURCE_WORKFLOW_JOB: i18n._('View the source Workflow Job'),
|
||||
TOGGLE_STDOUT_FULLSCREEN: i18n._('Expand Output'),
|
||||
STATUS: '' // re-assigned elsewhere
|
||||
},
|
||||
@@ -50,13 +51,17 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
STARTED: i18n._('Started'),
|
||||
FINISHED: i18n._('Finished'),
|
||||
LABELS: i18n._('Labels'),
|
||||
STATUS: i18n._('Status'),
|
||||
SLICE_TEMPLATE: i18n._('Slice Job Template'),
|
||||
STATUS: i18n._('Status')
|
||||
JOB_EXPLANATION: i18n._('Explanation'),
|
||||
SOURCE_WORKFLOW_JOB: i18n._('Source Workflow')
|
||||
},
|
||||
details: {
|
||||
HEADER: i18n._('DETAILS'),
|
||||
NOT_FINISHED: i18n._('Not Finished'),
|
||||
NOT_STARTED: i18n._('Not Started'),
|
||||
SHOW_LESS: i18n._('Show Less'),
|
||||
SHOW_MORE: i18n._('Show More'),
|
||||
},
|
||||
results: {
|
||||
TOTAL_JOBS: i18n._('Total Jobs'),
|
||||
@@ -64,10 +69,11 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
},
|
||||
legend: {
|
||||
ON_SUCCESS: i18n._('On Success'),
|
||||
ON_FAIL: i18n._('On Fail'),
|
||||
ON_FAILURE: i18n._('On Failure'),
|
||||
ALWAYS: i18n._('Always'),
|
||||
PROJECT_SYNC: i18n._('Project Sync'),
|
||||
INVENTORY_SYNC: i18n._('Inventory Sync'),
|
||||
WORKFLOW: i18n._('Workflow'),
|
||||
KEY: i18n._('KEY'),
|
||||
}
|
||||
};
|
||||
@@ -100,6 +106,9 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
$scope.labels = jobLabels;
|
||||
$scope.count = count.val;
|
||||
$scope.showManualControls = false;
|
||||
$scope.showKey = false;
|
||||
$scope.toggleKey = () => $scope.showKey = !$scope.showKey;
|
||||
$scope.keyClassList = `{ 'Key-menuIcon--active': showKey }`;
|
||||
|
||||
// Start elapsed time updater for job known to be running
|
||||
if ($scope.workflow.started !== null && $scope.workflow.status === 'running') {
|
||||
@@ -116,6 +125,27 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
$scope.slice_job_template_link = `/#/templates/job_template/${$scope.workflow.summary_fields.job_template.id}`;
|
||||
}
|
||||
|
||||
if (_.get(workflowData, 'summary_fields.source_workflow_job.id')) {
|
||||
$scope.source_workflow_job_link = `/#/workflows/${workflowData.summary_fields.source_workflow_job.id}`;
|
||||
}
|
||||
|
||||
if (workflowData.job_explanation) {
|
||||
const limit = 150;
|
||||
const more = workflowData.job_explanation;
|
||||
const less = $filter('limitTo')(more, limit);
|
||||
const showMore = false;
|
||||
const hasMoreToShow = more.length > limit;
|
||||
|
||||
const job_explanation = {
|
||||
more: more,
|
||||
less: less,
|
||||
showMore: showMore,
|
||||
hasMoreToShow: hasMoreToShow
|
||||
};
|
||||
|
||||
$scope.job_explanation = job_explanation;
|
||||
}
|
||||
|
||||
// turn related api browser routes into front end routes
|
||||
getLinks();
|
||||
|
||||
|
||||
@@ -75,6 +75,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXPLANATION DETAIL -->
|
||||
<div class="WorkflowResults-resultRow" ng-show="workflow.job_explanation">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
{{ strings.labels.JOB_EXPLANATION }}
|
||||
</label>
|
||||
|
||||
<div class="WorkflowResults-resultRowText"
|
||||
ng-show="!job_explanation.showMore">
|
||||
{{ job_explanation.less }}
|
||||
<span ng-show="job_explanation.hasMoreToShow">...</span>
|
||||
<span ng-show="job_explanation.hasMoreToShow"
|
||||
class="WorkflowResults-seeMoreLess"
|
||||
ng-click="job_explanation.showMore = true">
|
||||
{{ strings.details.SHOW_MORE }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="WorkflowResults-resultRowText"
|
||||
ng-show="job_explanation.showMore">
|
||||
{{ job_explanation.more }}
|
||||
<span class="WorkflowResults-seeMoreLess"
|
||||
ng-click="job_explanation.showMore = false">
|
||||
{{ strings.details.SHOW_LESS }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- START TIME DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.started">
|
||||
@@ -144,18 +171,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SLIIIIIICE -->
|
||||
<!-- SLIIIIIICE -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.summary_fields.job_template.name">
|
||||
<label
|
||||
class="WorkflowResults-resultRowLabel">
|
||||
{{ strings.labels.SLICE_TEMPLATE }}
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
<a href="{{ slice_job_template_link }}"
|
||||
aw-tool-tip="{{ strings.tooltips.EDIT_SLICE_TEMPLATE }}"
|
||||
data-placement="top">
|
||||
{{ workflow.summary_fields.job_template.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SOURCE WORKFLOW JOB DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.summary_fields.job_template.name">
|
||||
<label
|
||||
class="WorkflowResults-resultRowLabel">
|
||||
{{ strings.labels.SLICE_TEMPLATE }}
|
||||
ng-if="workflow.summary_fields.source_workflow_job">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
{{ strings.labels.SOURCE_WORKFLOW_JOB }}
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
<a href="{{ slice_job_template_link }}"
|
||||
aw-tool-tip="{{ strings.tooltips.EDIT_SLICE_TEMPLATE }}"
|
||||
data-placement="top">
|
||||
{{ workflow.summary_fields.job_template.name }}
|
||||
<a href="{{ source_workflow_job_link }}"
|
||||
aw-tool-tip="{{ strings.tooltips.SOURCE_WORKFLOW_JOB }}"
|
||||
data-placement="top">
|
||||
{{ workflow.summary_fields.source_workflow_job.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,27 +310,36 @@
|
||||
<workflow-status-bar></workflow-status-bar>
|
||||
<div class="WorkflowLegend-details">
|
||||
<div class="WorkflowLegend-details--left">
|
||||
<div class="WorkflowLegend-legendItem">{{ strings.legend.KEY }}:</div>
|
||||
<div class="WorkflowLegend-legendItem">
|
||||
<div class="WorkflowLegend-onSuccessLegend"></div>
|
||||
<div>{{ strings.legend.ON_SUCCESS }}</div>
|
||||
</div>
|
||||
<div class="WorkflowLegend-legendItem">
|
||||
<div class="WorkflowLegend-onFailLegend"></div>
|
||||
<div>{{ strings.legend.ON_FAIL }}</div>
|
||||
</div>
|
||||
<div class="WorkflowLegend-legendItem">
|
||||
<div class="WorkflowLegend-alwaysLegend"></div>
|
||||
<div>{{ strings.legend.ALWAYS }}</div>
|
||||
</div>
|
||||
<div class="WorkflowLegend-legendItem">
|
||||
<div class="WorkflowLegend-letterCircle">P</div>
|
||||
<div>{{ strings.legend.PROJECT_SYNC }}</div>
|
||||
</div>
|
||||
<div class="WorkflowLegend-legendItem">
|
||||
<div class="WorkflowLegend-letterCircle">I</div>
|
||||
<div>{{ strings.legend.INVENTORY_SYNC }}</div>
|
||||
</div>
|
||||
<i ng-class="{{ keyClassList }}" class="fa fa-key Key-menuIcon" ng-click="toggleKey()"></i>
|
||||
<ul ng-show="showKey" class="Key-list noselect">
|
||||
<li class="Key-listItem">
|
||||
<p class="Key-heading">{{strings.legend.KEY}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--success"></div>
|
||||
<p class="Key-listItemContent">{{strings.legend.ON_SUCCESS}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--fail"></div>
|
||||
<p class="Key-listItemContent">{{strings.legend.ON_FAILURE}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--always"></div>
|
||||
<p class="Key-listItemContent">{{strings.legend.ALWAYS}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--circle Key-icon--default">P</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{strings.legend.PROJECT_SYNC}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--circle Key-icon--default">I</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{strings.legend.INVENTORY_SYNC}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--circle Key-icon--default">W</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{strings.legend.WORKFLOW}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="WorkflowLegend-details--right">
|
||||
<i ng-class="{'WorkflowMaker-manualControlsIcon--active': showManualControls}" class="fa fa-cog WorkflowMaker-manualControlsIcon" aria-hidden="true" alt="Controls" ng-click="toggleManualControls()"></i>
|
||||
|
||||
Reference in New Issue
Block a user