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:
softwarefactory-project-zuul[bot]
2018-11-08 16:28:37 +00:00
committed by GitHub
25 changed files with 1525 additions and 1111 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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() == []

View File

@@ -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;
}
}

View File

@@ -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 () {

View File

@@ -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"

View File

@@ -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'),

View File

@@ -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'),

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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 ---------------------------------------------------------------------------------

View File

@@ -99,7 +99,6 @@
@media screen and (max-width: @breakpoint-sm) {
.BreadCrumb {
padding-left: 50px;
position: fixed;
z-index: 2;
}

View File

@@ -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>

View File

@@ -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>`;
}
}
}

View File

@@ -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 }}";

View File

@@ -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;

View File

@@ -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', {

View File

@@ -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">

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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>