mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
Merge pull request #4264 from beeankha/workflow_pause_approve
Workflow Approval Nodes
Reviewed-by: Ryan Petrello
https://github.com/ryanpetrello
This commit is contained in:
commit
2918b6c927
@ -34,7 +34,8 @@ from rest_framework.negotiation import DefaultContentNegotiation
|
||||
# AWX
|
||||
from awx.api.filters import FieldLookupBackend
|
||||
from awx.main.models import (
|
||||
UnifiedJob, UnifiedJobTemplate, User, Role, Credential
|
||||
UnifiedJob, UnifiedJobTemplate, User, Role, Credential,
|
||||
WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
||||
)
|
||||
from awx.main.access import access_registry
|
||||
from awx.main.utils import (
|
||||
@ -882,6 +883,21 @@ class CopyAPIView(GenericAPIView):
|
||||
create_kwargs[field.name] = CopyAPIView._decrypt_model_field_if_needed(
|
||||
obj, field.name, field_val
|
||||
)
|
||||
|
||||
# WorkflowJobTemplateNodes that represent an approval are *special*;
|
||||
# when we copy them, we actually want to *copy* the UJT they point at
|
||||
# rather than share the template reference between nodes in disparate
|
||||
# workflows
|
||||
if (
|
||||
isinstance(obj, WorkflowJobTemplateNode) and
|
||||
isinstance(getattr(obj, 'unified_job_template'), WorkflowApprovalTemplate)
|
||||
):
|
||||
new_approval_template, sub_objs = CopyAPIView.copy_model_obj(
|
||||
None, None, WorkflowApprovalTemplate,
|
||||
obj.unified_job_template, creater
|
||||
)
|
||||
create_kwargs['unified_job_template'] = new_approval_template
|
||||
|
||||
new_obj = model.objects.create(**create_kwargs)
|
||||
logger.debug('Deep copy: Created new object {}({})'.format(
|
||||
new_obj, model
|
||||
|
||||
@ -17,7 +17,7 @@ logger = logging.getLogger('awx.api.permissions')
|
||||
|
||||
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', 'VariableDataPermission',
|
||||
'TaskPermission', 'ProjectUpdatePermission', 'InventoryInventorySourcesUpdatePermission',
|
||||
'UserPermission', 'IsSuperUser', 'InstanceGroupTowerPermission',]
|
||||
'UserPermission', 'IsSuperUser', 'InstanceGroupTowerPermission', 'WorkflowApprovalPermission']
|
||||
|
||||
|
||||
class ModelAccessPermission(permissions.BasePermission):
|
||||
@ -196,6 +196,17 @@ class TaskPermission(ModelAccessPermission):
|
||||
return False
|
||||
|
||||
|
||||
class WorkflowApprovalPermission(ModelAccessPermission):
|
||||
'''
|
||||
Permission check used by workflow `approval` and `deny` views to determine
|
||||
who has access to approve and deny paused workflow nodes
|
||||
'''
|
||||
|
||||
def check_post_permissions(self, request, view, obj=None):
|
||||
approval = get_object_or_400(view.model, pk=view.kwargs['pk'])
|
||||
return check_user_access(request.user, view.model, 'approve_or_deny', approval)
|
||||
|
||||
|
||||
class ProjectUpdatePermission(ModelAccessPermission):
|
||||
'''
|
||||
Permission check used by ProjectUpdateView to determine who can update projects
|
||||
@ -238,4 +249,3 @@ class InstanceGroupTowerPermission(ModelAccessPermission):
|
||||
if request.method == 'DELETE' and obj.name == "tower":
|
||||
return False
|
||||
return super(InstanceGroupTowerPermission, self).has_object_permission(request, view, obj)
|
||||
|
||||
|
||||
@ -50,16 +50,16 @@ from awx.main.constants import (
|
||||
CENSOR_VALUE,
|
||||
)
|
||||
from awx.main.models import (
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential,
|
||||
CredentialInputSource, CredentialType, CustomInventoryScript,
|
||||
Group, Host, Instance, InstanceGroup, Inventory, InventorySource,
|
||||
InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, JobHostSummary,
|
||||
JobLaunchConfig, JobNotificationMixin, JobTemplate, Label, Notification,
|
||||
NotificationTemplate, OAuth2AccessToken, OAuth2Application, Organization,
|
||||
Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
||||
StdoutMaxBytesExceeded, SystemJob, SystemJobEvent, SystemJobTemplate,
|
||||
Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
|
||||
WorkflowJobTemplate, WorkflowJobTemplateNode
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource,
|
||||
CredentialType, CustomInventoryScript, Group, Host, Instance,
|
||||
InstanceGroup, Inventory, InventorySource, InventoryUpdate,
|
||||
InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig,
|
||||
JobNotificationMixin, JobTemplate, Label, Notification, NotificationTemplate,
|
||||
OAuth2AccessToken, OAuth2Application, Organization, Project,
|
||||
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
||||
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
|
||||
UnifiedJobTemplate, WorkflowApproval, WorkflowApprovalTemplate, WorkflowJob,
|
||||
WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
|
||||
)
|
||||
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
||||
from awx.main.models.rbac import (
|
||||
@ -121,6 +121,8 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_job': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_approval_template': DEFAULT_SUMMARY_FIELDS + ('timeout',),
|
||||
'workflow_approval': DEFAULT_SUMMARY_FIELDS + ('timeout',),
|
||||
'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',),
|
||||
'unified_job_template': DEFAULT_SUMMARY_FIELDS + ('unified_job_type',),
|
||||
'last_job': DEFAULT_SUMMARY_FIELDS + ('finished', 'status', 'failed', 'license_error'),
|
||||
@ -681,6 +683,8 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
serializer_class = SystemJobTemplateSerializer
|
||||
elif isinstance(obj, WorkflowJobTemplate):
|
||||
serializer_class = WorkflowJobTemplateSerializer
|
||||
elif isinstance(obj, WorkflowApprovalTemplate):
|
||||
serializer_class = WorkflowApprovalTemplateSerializer
|
||||
return serializer_class
|
||||
|
||||
def to_representation(self, obj):
|
||||
@ -782,6 +786,8 @@ class UnifiedJobSerializer(BaseSerializer):
|
||||
serializer_class = SystemJobSerializer
|
||||
elif isinstance(obj, WorkflowJob):
|
||||
serializer_class = WorkflowJobSerializer
|
||||
elif isinstance(obj, WorkflowApproval):
|
||||
serializer_class = WorkflowApprovalSerializer
|
||||
return serializer_class
|
||||
|
||||
def to_representation(self, obj):
|
||||
@ -838,6 +844,8 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
||||
serializer_class = SystemJobListSerializer
|
||||
elif isinstance(obj, WorkflowJob):
|
||||
serializer_class = WorkflowJobListSerializer
|
||||
elif isinstance(obj, WorkflowApproval):
|
||||
serializer_class = WorkflowApprovalListSerializer
|
||||
return serializer_class
|
||||
|
||||
def to_representation(self, obj):
|
||||
@ -3395,6 +3403,76 @@ class WorkflowJobCancelSerializer(WorkflowJobSerializer):
|
||||
fields = ('can_cancel',)
|
||||
|
||||
|
||||
class WorkflowApprovalViewSerializer(UnifiedJobSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WorkflowApproval
|
||||
fields = []
|
||||
|
||||
|
||||
class WorkflowApprovalSerializer(UnifiedJobSerializer):
|
||||
|
||||
can_approve_or_deny = serializers.SerializerMethodField()
|
||||
approval_expiration = serializers.SerializerMethodField()
|
||||
timed_out = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = WorkflowApproval
|
||||
fields = ('*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration', 'timed_out',)
|
||||
|
||||
def get_approval_expiration(self, obj):
|
||||
if obj.status != 'pending' or obj.timeout == 0:
|
||||
return None
|
||||
return obj.created + timedelta(seconds=obj.timeout)
|
||||
|
||||
def get_can_approve_or_deny(self, obj):
|
||||
request = self.context.get('request', None)
|
||||
allowed = request.user.can_access(WorkflowApproval, 'approve_or_deny', obj)
|
||||
return allowed is True and obj.status == 'pending'
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(WorkflowApprovalSerializer, self).get_related(obj)
|
||||
|
||||
if obj.workflow_approval_template:
|
||||
res['workflow_approval_template'] = self.reverse('api:workflow_approval_template_detail',
|
||||
kwargs={'pk': obj.workflow_approval_template.pk})
|
||||
res['approve'] = self.reverse('api:workflow_approval_approve', kwargs={'pk': obj.pk})
|
||||
res['deny'] = self.reverse('api:workflow_approval_deny', kwargs={'pk': obj.pk})
|
||||
return res
|
||||
|
||||
|
||||
class WorkflowApprovalActivityStreamSerializer(WorkflowApprovalSerializer):
|
||||
"""
|
||||
timed_out and status are usually read-only fields
|
||||
However, when we generate an activity stream record, we *want* to record
|
||||
these types of changes. This serializer allows us to do so.
|
||||
"""
|
||||
status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES)
|
||||
timed_out = serializers.BooleanField()
|
||||
|
||||
|
||||
|
||||
class WorkflowApprovalListSerializer(WorkflowApprovalSerializer, UnifiedJobListSerializer):
|
||||
|
||||
class Meta:
|
||||
fields = ('*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration', 'timed_out',)
|
||||
|
||||
|
||||
class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WorkflowApprovalTemplate
|
||||
fields = ('*', 'timeout', 'name',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(WorkflowApprovalTemplateSerializer, self).get_related(obj)
|
||||
if 'last_job' in res:
|
||||
del res['last_job']
|
||||
|
||||
res.update(dict(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}),))
|
||||
return res
|
||||
|
||||
|
||||
class LaunchConfigurationBaseSerializer(BaseSerializer):
|
||||
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
||||
job_type = serializers.ChoiceField(allow_blank=True, allow_null=True, required=False, default=None,
|
||||
@ -3453,6 +3531,10 @@ class LaunchConfigurationBaseSerializer(BaseSerializer):
|
||||
ujt = attrs['unified_job_template']
|
||||
elif self.instance:
|
||||
ujt = self.instance.unified_job_template
|
||||
if ujt is None:
|
||||
if 'workflow_job_template' in attrs:
|
||||
return {'workflow_job_template': attrs['workflow_job_template']}
|
||||
return {}
|
||||
|
||||
# build additional field survey_passwords to track redacted variables
|
||||
password_dict = {}
|
||||
@ -3534,6 +3616,7 @@ class WorkflowJobTemplateNodeSerializer(LaunchConfigurationBaseSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(WorkflowJobTemplateNodeSerializer, self).get_related(obj)
|
||||
res['create_approval_template'] = self.reverse('api:workflow_job_template_node_create_approval', kwargs={'pk': obj.pk})
|
||||
res['success_nodes'] = self.reverse('api:workflow_job_template_node_success_nodes_list', kwargs={'pk': obj.pk})
|
||||
res['failure_nodes'] = self.reverse('api:workflow_job_template_node_failure_nodes_list', kwargs={'pk': obj.pk})
|
||||
res['always_nodes'] = self.reverse('api:workflow_job_template_node_always_nodes_list', kwargs={'pk': obj.pk})
|
||||
@ -3553,6 +3636,12 @@ class WorkflowJobTemplateNodeSerializer(LaunchConfigurationBaseSerializer):
|
||||
field_kwargs.pop('queryset', None)
|
||||
return field_class, field_kwargs
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
summary_fields = super(WorkflowJobTemplateNodeSerializer, self).get_summary_fields(obj)
|
||||
if isinstance(obj.unified_job_template, WorkflowApprovalTemplate):
|
||||
summary_fields['unified_job_template']['timeout'] = obj.unified_job_template.timeout
|
||||
return summary_fields
|
||||
|
||||
|
||||
class WorkflowJobNodeSerializer(LaunchConfigurationBaseSerializer):
|
||||
success_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
|
||||
@ -3578,6 +3667,12 @@ class WorkflowJobNodeSerializer(LaunchConfigurationBaseSerializer):
|
||||
res['workflow_job'] = self.reverse('api:workflow_job_detail', kwargs={'pk': obj.workflow_job.pk})
|
||||
return res
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
summary_fields = super(WorkflowJobNodeSerializer, self).get_summary_fields(obj)
|
||||
if isinstance(obj.job, WorkflowApproval):
|
||||
summary_fields['job']['timed_out'] = obj.job.timed_out
|
||||
return summary_fields
|
||||
|
||||
|
||||
class WorkflowJobNodeListSerializer(WorkflowJobNodeSerializer):
|
||||
pass
|
||||
@ -3603,6 +3698,16 @@ class WorkflowJobTemplateNodeDetailSerializer(WorkflowJobTemplateNodeSerializer)
|
||||
return field_class, field_kwargs
|
||||
|
||||
|
||||
class WorkflowJobTemplateNodeCreateApprovalSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WorkflowApprovalTemplate
|
||||
fields = ('timeout', 'name', 'description',)
|
||||
|
||||
def to_representation(self, obj):
|
||||
return {}
|
||||
|
||||
|
||||
class JobListSerializer(JobSerializer, UnifiedJobListSerializer):
|
||||
pass
|
||||
|
||||
@ -4663,7 +4768,8 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
('o_auth2_access_token', ('id', 'user_id', 'description', 'application_id', 'scope')),
|
||||
('o_auth2_application', ('id', 'name', 'description')),
|
||||
('credential_type', ('id', 'name', 'description', 'kind', 'managed_by_tower')),
|
||||
('ad_hoc_command', ('id', 'name', 'status', 'limit'))
|
||||
('ad_hoc_command', ('id', 'name', 'status', 'limit')),
|
||||
('workflow_approval', ('id', 'name', 'unified_job_id')),
|
||||
]
|
||||
return field_list
|
||||
|
||||
@ -4772,6 +4878,8 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
def _summarize_parent_ujt(self, obj, fk, summary_fields):
|
||||
summary_keys = {'job': 'job_template',
|
||||
'workflow_job_template_node': 'workflow_job_template',
|
||||
'workflow_approval_template': 'workflow_job_template',
|
||||
'workflow_approval': 'workflow_job',
|
||||
'schedule': 'unified_job_template'}
|
||||
if fk not in summary_keys:
|
||||
return
|
||||
|
||||
@ -71,6 +71,8 @@ from .instance import urls as instance_urls
|
||||
from .instance_group import urls as instance_group_urls
|
||||
from .oauth2 import urls as oauth2_urls
|
||||
from .oauth2_root import urls as oauth2_root_urls
|
||||
from .workflow_approval_template import urls as workflow_approval_template_urls
|
||||
from .workflow_approval import urls as workflow_approval_urls
|
||||
|
||||
|
||||
v2_urls = [
|
||||
@ -131,8 +133,11 @@ v2_urls = [
|
||||
url(r'^unified_job_templates/$', UnifiedJobTemplateList.as_view(), name='unified_job_template_list'),
|
||||
url(r'^unified_jobs/$', UnifiedJobList.as_view(), name='unified_job_list'),
|
||||
url(r'^activity_stream/', include(activity_stream_urls)),
|
||||
url(r'^workflow_approval_templates/', include(workflow_approval_template_urls)),
|
||||
url(r'^workflow_approvals/', include(workflow_approval_urls)),
|
||||
]
|
||||
|
||||
|
||||
app_name = 'api'
|
||||
urlpatterns = [
|
||||
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
||||
|
||||
21
awx/api/urls/workflow_approval.py
Normal file
21
awx/api/urls/workflow_approval.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2017 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from awx.api.views import (
|
||||
WorkflowApprovalList,
|
||||
WorkflowApprovalDetail,
|
||||
WorkflowApprovalApprove,
|
||||
WorkflowApprovalDeny,
|
||||
)
|
||||
|
||||
|
||||
urls = [
|
||||
url(r'^$', WorkflowApprovalList.as_view(), name='workflow_approval_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', WorkflowApprovalDetail.as_view(), name='workflow_approval_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/approve/$', WorkflowApprovalApprove.as_view(), name='workflow_approval_approve'),
|
||||
url(r'^(?P<pk>[0-9]+)/deny/$', WorkflowApprovalDeny.as_view(), name='workflow_approval_deny'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
17
awx/api/urls/workflow_approval_template.py
Normal file
17
awx/api/urls/workflow_approval_template.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2017 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from awx.api.views import (
|
||||
WorkflowApprovalTemplateDetail,
|
||||
WorkflowApprovalTemplateJobsList,
|
||||
)
|
||||
|
||||
|
||||
urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/$', WorkflowApprovalTemplateDetail.as_view(), name='workflow_approval_template_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/approvals/$', WorkflowApprovalTemplateJobsList.as_view(), name='workflow_approval_template_jobs_list'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
@ -10,6 +10,7 @@ from awx.api.views import (
|
||||
WorkflowJobTemplateNodeFailureNodesList,
|
||||
WorkflowJobTemplateNodeAlwaysNodesList,
|
||||
WorkflowJobTemplateNodeCredentialsList,
|
||||
WorkflowJobTemplateNodeCreateApproval,
|
||||
)
|
||||
|
||||
|
||||
@ -20,6 +21,7 @@ urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/failure_nodes/$', WorkflowJobTemplateNodeFailureNodesList.as_view(), name='workflow_job_template_node_failure_nodes_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/always_nodes/$', WorkflowJobTemplateNodeAlwaysNodesList.as_view(), name='workflow_job_template_node_always_nodes_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/credentials/$', WorkflowJobTemplateNodeCredentialsList.as_view(), name='workflow_job_template_node_credentials_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/create_approval_template/$', WorkflowJobTemplateNodeCreateApproval.as_view(), name='workflow_job_template_node_create_approval'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
|
||||
@ -91,7 +91,8 @@ from awx.main.redact import UriCleaner
|
||||
from awx.api.permissions import (
|
||||
JobTemplateCallbackPermission, TaskPermission, ProjectUpdatePermission,
|
||||
InventoryInventorySourcesUpdatePermission, UserPermission,
|
||||
InstanceGroupTowerPermission, VariableDataPermission
|
||||
InstanceGroupTowerPermission, VariableDataPermission,
|
||||
WorkflowApprovalPermission
|
||||
)
|
||||
from awx.api import renderers
|
||||
from awx.api import serializers
|
||||
@ -839,8 +840,6 @@ class SystemJobEventsList(SubListAPIView):
|
||||
return super(SystemJobEventsList, self).finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
class ProjectUpdateCancel(RetrieveAPIView):
|
||||
|
||||
model = models.ProjectUpdate
|
||||
@ -3013,6 +3012,34 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
|
||||
return None
|
||||
|
||||
|
||||
class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView):
|
||||
|
||||
model = models.WorkflowJobTemplateNode
|
||||
serializer_class = serializers.WorkflowJobTemplateNodeCreateApprovalSerializer
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
serializer = self.get_serializer(instance=obj, data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
approval_template = obj.create_approval_template(**serializer.validated_data)
|
||||
data = serializers.WorkflowApprovalTemplateSerializer(
|
||||
approval_template,
|
||||
context=self.get_serializer_context()
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
def check_permissions(self, request):
|
||||
obj = self.get_object().workflow_job_template
|
||||
if request.method == 'POST':
|
||||
if not request.user.can_access(models.WorkflowJobTemplate, 'change', obj, request.data):
|
||||
self.permission_denied(request)
|
||||
else:
|
||||
if not request.user.can_access(models.WorkflowJobTemplate, 'read', obj):
|
||||
self.permission_denied(request)
|
||||
|
||||
|
||||
class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
||||
relationship = 'success_nodes'
|
||||
|
||||
@ -4405,3 +4432,63 @@ for attr, value in list(locals().items()):
|
||||
name = camelcase_to_underscore(attr)
|
||||
view = value.as_view()
|
||||
setattr(this_module, name, view)
|
||||
|
||||
|
||||
class WorkflowApprovalTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = models.WorkflowApprovalTemplate
|
||||
serializer_class = serializers.WorkflowApprovalTemplateSerializer
|
||||
|
||||
|
||||
class WorkflowApprovalTemplateJobsList(SubListAPIView):
|
||||
|
||||
model = models.WorkflowApproval
|
||||
serializer_class = serializers.WorkflowApprovalListSerializer
|
||||
parent_model = models.WorkflowApprovalTemplate
|
||||
relationship = 'approvals'
|
||||
parent_key = 'workflow_approval_template'
|
||||
|
||||
|
||||
class WorkflowApprovalList(ListCreateAPIView):
|
||||
|
||||
model = models.WorkflowApproval
|
||||
serializer_class = serializers.WorkflowApprovalListSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super(WorkflowApprovalList, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class WorkflowApprovalDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
||||
|
||||
model = models.WorkflowApproval
|
||||
serializer_class = serializers.WorkflowApprovalSerializer
|
||||
|
||||
|
||||
class WorkflowApprovalApprove(RetrieveAPIView):
|
||||
model = models.WorkflowApproval
|
||||
serializer_class = serializers.WorkflowApprovalViewSerializer
|
||||
permission_classes = (WorkflowApprovalPermission,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj):
|
||||
raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow."))
|
||||
if obj.status != 'pending':
|
||||
return Response({"error": _("This workflow step has already been approved or denied.")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
obj.approve(request)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class WorkflowApprovalDeny(RetrieveAPIView):
|
||||
model = models.WorkflowApproval
|
||||
serializer_class = serializers.WorkflowApprovalViewSerializer
|
||||
permission_classes = (WorkflowApprovalPermission,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj):
|
||||
raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow."))
|
||||
if obj.status != 'pending':
|
||||
return Response({"error": _("This workflow step has already been approved or denied.")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
obj.deny(request)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -124,6 +124,7 @@ class ApiVersionRootView(APIView):
|
||||
data['activity_stream'] = reverse('api:activity_stream_list', request=request)
|
||||
data['workflow_job_templates'] = reverse('api:workflow_job_template_list', request=request)
|
||||
data['workflow_jobs'] = reverse('api:workflow_job_list', request=request)
|
||||
data['workflow_approvals'] = reverse('api:workflow_approval_list', request=request)
|
||||
data['workflow_job_template_nodes'] = reverse('api:workflow_job_template_node_list', request=request)
|
||||
data['workflow_job_nodes'] = reverse('api:workflow_job_node_list', request=request)
|
||||
return Response(data)
|
||||
|
||||
@ -37,6 +37,7 @@ from awx.main.models import (
|
||||
ProjectUpdateEvent, Role, Schedule, SystemJob, SystemJobEvent,
|
||||
SystemJobTemplate, Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob,
|
||||
WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode,
|
||||
WorkflowApproval, WorkflowApprovalTemplate,
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
|
||||
)
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
@ -2377,13 +2378,18 @@ class UnifiedJobTemplateAccess(BaseAccess):
|
||||
return self.model.objects.filter(
|
||||
Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')) |
|
||||
Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs(
|
||||
Inventory, self.user, 'read_role')))
|
||||
Inventory, self.user, 'read_role'))
|
||||
)
|
||||
|
||||
def can_start(self, obj, validate_license=True):
|
||||
access_class = access_registry[obj.__class__]
|
||||
access_instance = access_class(self.user)
|
||||
return access_instance.can_start(obj, validate_license=validate_license)
|
||||
|
||||
def get_queryset(self):
|
||||
return super(UnifiedJobTemplateAccess, self).get_queryset().filter(
|
||||
workflowapprovaltemplate__isnull=True)
|
||||
|
||||
|
||||
class UnifiedJobAccess(BaseAccess):
|
||||
'''
|
||||
@ -2430,6 +2436,10 @@ class UnifiedJobAccess(BaseAccess):
|
||||
)
|
||||
return qs
|
||||
|
||||
def get_queryset(self):
|
||||
return super(UnifiedJobAccess, self).get_queryset().filter(
|
||||
workflowapproval__isnull=True)
|
||||
|
||||
|
||||
class ScheduleAccess(BaseAccess):
|
||||
'''
|
||||
@ -2769,5 +2779,80 @@ class RoleAccess(BaseAccess):
|
||||
return False
|
||||
|
||||
|
||||
class WorkflowApprovalAccess(BaseAccess):
|
||||
'''
|
||||
A user can create a workflow approval if they are a superuser, an org admin
|
||||
of the org connected to the workflow, or if they are assigned as admins to
|
||||
the workflow.
|
||||
|
||||
A user can approve a workflow when they are:
|
||||
- a superuser
|
||||
- a workflow admin
|
||||
- an organization admin
|
||||
- any user who has explicitly been assigned the "approver" role
|
||||
|
||||
A user can see approvals if they have read access to the associated WorkflowJobTemplate.
|
||||
'''
|
||||
|
||||
model = WorkflowApproval
|
||||
prefetch_related = ('created_by', 'modified_by',)
|
||||
|
||||
def can_use(self, obj):
|
||||
return True
|
||||
|
||||
def can_start(self, obj, validate_license=True):
|
||||
return True
|
||||
|
||||
def filtered_queryset(self):
|
||||
return self.model.objects.filter(
|
||||
unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.accessible_pk_qs(
|
||||
self.user, 'read_role'))
|
||||
|
||||
def can_approve_or_deny(self, obj):
|
||||
if (
|
||||
(obj.workflow_job_template and self.user in obj.workflow_job_template.approval_role) or
|
||||
self.user.is_superuser
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
class WorkflowApprovalTemplateAccess(BaseAccess):
|
||||
'''
|
||||
A user can create a workflow approval if they are a superuser, an org admin
|
||||
of the org connected to the workflow, or if they are assigned as admins to
|
||||
the workflow.
|
||||
|
||||
A user can approve a workflow when they are:
|
||||
- a superuser
|
||||
- a workflow admin
|
||||
- an organization admin
|
||||
- any user who has explicitly been assigned the "approver" role at the workflow or organization level
|
||||
|
||||
A user can see approval templates if they have read access to the associated WorkflowJobTemplate.
|
||||
'''
|
||||
|
||||
model = WorkflowApprovalTemplate
|
||||
prefetch_related = ('created_by', 'modified_by',)
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
if data is None: # Hide direct creation in API browser
|
||||
return False
|
||||
else:
|
||||
return (self.check_related('workflow_approval_template', UnifiedJobTemplate, role_field='admin_role'))
|
||||
|
||||
def can_start(self, obj, validate_license=False):
|
||||
# for copying WFJTs that contain approval nodes
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
|
||||
return self.user in obj.workflow_job_template.execute_role
|
||||
|
||||
def filtered_queryset(self):
|
||||
return self.model.objects.filter(
|
||||
workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.accessible_pk_qs(
|
||||
self.user, 'read_role'))
|
||||
|
||||
|
||||
for cls in BaseAccess.__subclasses__():
|
||||
access_registry[cls.model] = cls
|
||||
|
||||
@ -164,7 +164,7 @@ def is_implicit_parent(parent_role, child_role):
|
||||
# The only singleton implicit parent is the system admin being
|
||||
# a parent of the system auditor role
|
||||
return bool(
|
||||
child_role.singleton_name == ROLE_SINGLETON_SYSTEM_AUDITOR and
|
||||
child_role.singleton_name == ROLE_SINGLETON_SYSTEM_AUDITOR and
|
||||
parent_role.singleton_name == ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
||||
)
|
||||
# Get the list of implicit parents that were defined at the class level.
|
||||
|
||||
83
awx/main/migrations/0086_v360_workflow_approval.py
Normal file
83
awx/main/migrations/0086_v360_workflow_approval.py
Normal file
@ -0,0 +1,83 @@
|
||||
# Generated by Django 2.2.4 on 2019-08-02 17:51
|
||||
|
||||
import awx.main.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0085_v360_add_notificationtemplate_messages'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WorkflowApprovalTemplate',
|
||||
fields=[
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('timeout', models.IntegerField(blank=True, default=0, help_text='The amount of time (in seconds) before the approval node expires and fails.')),
|
||||
],
|
||||
bases=('main.unifiedjobtemplate',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='approval_role',
|
||||
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role='admin_role', related_name='+', to='main.Role'),
|
||||
preserve_default='True',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='approval_role',
|
||||
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['organization.approval_role', 'admin_role'], related_name='+', to='main.Role'),
|
||||
preserve_default='True',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobnode',
|
||||
name='unified_job_template',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowjobnodes', to='main.UnifiedJobTemplate'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
name='unified_job_template',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowjobtemplatenodes', to='main.UnifiedJobTemplate'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkflowApproval',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('workflow_approval_template', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approvals', to='main.WorkflowApprovalTemplate')),
|
||||
],
|
||||
bases=('main.unifiedjob',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
name='workflow_approval',
|
||||
field=models.ManyToManyField(blank=True, to='main.WorkflowApproval'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
name='workflow_approval_template',
|
||||
field=models.ManyToManyField(blank=True, to='main.WorkflowApprovalTemplate'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organization',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['member_role', 'auditor_role', 'execute_role', 'project_admin_role', 'inventory_admin_role', 'workflow_admin_role', 'notification_admin_role', 'credential_admin_role', 'job_template_admin_role', 'approval_role'], related_name='+', to='main.Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='read_role',
|
||||
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['singleton:system_auditor', 'organization.auditor_role', 'execute_role', 'admin_role', 'approval_role'], related_name='+', to='main.Role'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowapproval',
|
||||
name='timeout',
|
||||
field=models.IntegerField(blank=True, default=0, help_text='The amount of time (in seconds) before the approval node expires and fails.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowapproval',
|
||||
name='timed_out',
|
||||
field=models.BooleanField(default=False, help_text='Shows when an approval node (with a timeout assigned to it) has timed out.'),
|
||||
),
|
||||
]
|
||||
@ -56,7 +56,7 @@ from awx.main.models.notifications import ( # noqa
|
||||
from awx.main.models.label import Label # noqa
|
||||
from awx.main.models.workflow import ( # noqa
|
||||
WorkflowJob, WorkflowJobNode, WorkflowJobOptions, WorkflowJobTemplate,
|
||||
WorkflowJobTemplateNode,
|
||||
WorkflowJobTemplateNode, WorkflowApproval, WorkflowApprovalTemplate,
|
||||
)
|
||||
from awx.main.models.channels import ChannelGroup # noqa
|
||||
from awx.api.versioning import reverse
|
||||
@ -203,6 +203,8 @@ activity_stream_registrar.connect(User)
|
||||
activity_stream_registrar.connect(WorkflowJobTemplate)
|
||||
activity_stream_registrar.connect(WorkflowJobTemplateNode)
|
||||
activity_stream_registrar.connect(WorkflowJob)
|
||||
activity_stream_registrar.connect(WorkflowApproval)
|
||||
activity_stream_registrar.connect(WorkflowApprovalTemplate)
|
||||
activity_stream_registrar.connect(OAuth2Application)
|
||||
activity_stream_registrar.connect(OAuth2AccessToken)
|
||||
|
||||
@ -213,4 +215,3 @@ prevent_search(RefreshToken._meta.get_field('token'))
|
||||
prevent_search(OAuth2Application._meta.get_field('client_secret'))
|
||||
prevent_search(OAuth2Application._meta.get_field('client_id'))
|
||||
prevent_search(Grant._meta.get_field('code'))
|
||||
|
||||
|
||||
@ -67,6 +67,8 @@ class ActivityStream(models.Model):
|
||||
workflow_job_node = models.ManyToManyField("WorkflowJobNode", blank=True)
|
||||
workflow_job_template = models.ManyToManyField("WorkflowJobTemplate", blank=True)
|
||||
workflow_job = models.ManyToManyField("WorkflowJob", blank=True)
|
||||
workflow_approval_template = models.ManyToManyField("WorkflowApprovalTemplate", blank=True)
|
||||
workflow_approval = models.ManyToManyField("WorkflowApproval", blank=True)
|
||||
unified_job_template = models.ManyToManyField("UnifiedJobTemplate", blank=True, related_name='activity_stream_as_unified_job_template+')
|
||||
unified_job = models.ManyToManyField("UnifiedJob", blank=True, related_name='activity_stream_as_unified_job+')
|
||||
ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True)
|
||||
|
||||
@ -483,4 +483,3 @@ class RelatedJobsMixin(object):
|
||||
raise RuntimeError("Programmer error. Expected _get_active_jobs() to return a QuerySet.")
|
||||
|
||||
return [dict(id=t[0], type=mapping[t[1]]) for t in jobs.values_list('id', 'polymorphic_ctype_id')]
|
||||
|
||||
|
||||
@ -465,7 +465,6 @@ class JobNotificationMixin(object):
|
||||
from awx.main.tasks import send_notifications # avoid circular import
|
||||
if status not in ['running', 'succeeded', 'failed']:
|
||||
raise ValueError(_("status must be either running, succeeded or failed"))
|
||||
|
||||
try:
|
||||
notification_templates = self.get_notification_templates()
|
||||
except Exception:
|
||||
|
||||
@ -87,7 +87,10 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
|
||||
'execute_role', 'project_admin_role',
|
||||
'inventory_admin_role', 'workflow_admin_role',
|
||||
'notification_admin_role', 'credential_admin_role',
|
||||
'job_template_admin_role',],
|
||||
'job_template_admin_role', 'approval_role',],
|
||||
)
|
||||
approval_role = ImplicitRoleField(
|
||||
parent_role='admin_role',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ role_names = {
|
||||
'read_role': _('Read'),
|
||||
'update_role': _('Update'),
|
||||
'use_role': _('Use'),
|
||||
'approval_role': _('Approve'),
|
||||
}
|
||||
|
||||
role_descriptions = {
|
||||
@ -70,6 +71,7 @@ role_descriptions = {
|
||||
'read_role': _('May view settings for the %s'),
|
||||
'update_role': _('May update the %s'),
|
||||
'use_role': _('Can use the %s in a job template'),
|
||||
'approval_role': _('Can approve or deny a workflow approval node'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1173,7 +1173,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
def websocket_emit_data(self):
|
||||
''' Return extra data that should be included when submitting data to the browser over the websocket connection '''
|
||||
websocket_data = dict()
|
||||
websocket_data = dict(type=self.get_real_instance_class()._meta.verbose_name.replace(' ', '_'))
|
||||
if self.spawned_by_workflow:
|
||||
websocket_data.update(dict(workflow_job_id=self.workflow_job_id,
|
||||
workflow_node_id=self.workflow_node_id))
|
||||
|
||||
@ -35,11 +35,14 @@ from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemp
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.redact import REPLACE_STR
|
||||
from awx.main.fields import JSONField
|
||||
from awx.main.utils import schedule_task_manager
|
||||
|
||||
|
||||
from copy import copy
|
||||
from urllib.parse import urljoin
|
||||
|
||||
__all__ = ['WorkflowJobTemplate', 'WorkflowJob', 'WorkflowJobOptions', 'WorkflowJobNode', 'WorkflowJobTemplateNode',]
|
||||
__all__ = ['WorkflowJobTemplate', 'WorkflowJob', 'WorkflowJobOptions', 'WorkflowJobNode',
|
||||
'WorkflowJobTemplateNode', 'WorkflowApprovalTemplate', 'WorkflowApproval']
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.models.workflow')
|
||||
@ -71,7 +74,7 @@ class WorkflowNodeBase(CreatedModifiedModel, LaunchTimeConfig):
|
||||
unified_job_template = models.ForeignKey(
|
||||
'UnifiedJobTemplate',
|
||||
related_name='%(class)ss',
|
||||
blank=False,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
@ -161,6 +164,13 @@ class WorkflowJobTemplateNode(WorkflowNodeBase):
|
||||
new_node.credentials.add(cred)
|
||||
return new_node
|
||||
|
||||
def create_approval_template(self, **kwargs):
|
||||
approval_template = WorkflowApprovalTemplate(**kwargs)
|
||||
approval_template.save()
|
||||
self.unified_job_template = approval_template
|
||||
self.save()
|
||||
return approval_template
|
||||
|
||||
|
||||
class WorkflowJobNode(WorkflowNodeBase):
|
||||
job = models.OneToOneField(
|
||||
@ -385,7 +395,11 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
||||
])
|
||||
read_role = ImplicitRoleField(parent_role=[
|
||||
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
'organization.auditor_role', 'execute_role', 'admin_role'
|
||||
'organization.auditor_role', 'execute_role', 'admin_role',
|
||||
'approval_role',
|
||||
])
|
||||
approval_role = ImplicitRoleField(parent_role=[
|
||||
'organization.approval_role', 'admin_role',
|
||||
])
|
||||
|
||||
@property
|
||||
@ -601,3 +615,92 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
|
||||
# WorkflowJobs don't _actually_ run anything in the dispatcher, so
|
||||
# there's no point in asking the dispatcher if it knows about this task
|
||||
return self.status == 'running'
|
||||
|
||||
|
||||
class WorkflowApprovalTemplate(UnifiedJobTemplate):
|
||||
|
||||
FIELDS_TO_PRESERVE_AT_COPY = ['description', 'timeout',]
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
|
||||
timeout = models.IntegerField(
|
||||
blank=True,
|
||||
default=0,
|
||||
help_text=_("The amount of time (in seconds) before the approval node expires and fails."),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_unified_job_class(cls):
|
||||
return WorkflowApproval
|
||||
|
||||
@classmethod
|
||||
def _get_unified_job_field_names(cls):
|
||||
return ['name', 'description', 'timeout']
|
||||
|
||||
def get_absolute_url(self, request=None):
|
||||
return reverse('api:workflow_approval_template_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
@property
|
||||
def workflow_job_template(self):
|
||||
return self.workflowjobtemplatenodes.first().workflow_job_template
|
||||
|
||||
|
||||
class WorkflowApproval(UnifiedJob):
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
|
||||
workflow_approval_template = models.ForeignKey(
|
||||
'WorkflowApprovalTemplate',
|
||||
related_name='approvals',
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
timeout = models.IntegerField(
|
||||
blank=True,
|
||||
default=0,
|
||||
help_text=_("The amount of time (in seconds) before the approval node expires and fails."),
|
||||
)
|
||||
timed_out = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("Shows when an approval node (with a timeout assigned to it) has timed out.")
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _get_unified_job_template_class(cls):
|
||||
return WorkflowApprovalTemplate
|
||||
|
||||
def get_absolute_url(self, request=None):
|
||||
return reverse('api:workflow_approval_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
@property
|
||||
def event_class(self):
|
||||
return None
|
||||
|
||||
def _get_parent_field_name(self):
|
||||
return 'workflow_approval_template'
|
||||
|
||||
def approve(self, request=None):
|
||||
self.status = 'successful'
|
||||
self.save()
|
||||
self.websocket_emit_status(self.status)
|
||||
schedule_task_manager()
|
||||
return reverse('api:workflow_approval_approve', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def deny(self, request=None):
|
||||
self.status = 'failed'
|
||||
self.save()
|
||||
self.websocket_emit_status(self.status)
|
||||
schedule_task_manager()
|
||||
return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
@property
|
||||
def workflow_job_template(self):
|
||||
return self.unified_job_node.workflow_job.unified_job_template
|
||||
|
||||
@property
|
||||
def workflow_job(self):
|
||||
return self.unified_job_node.workflow_job
|
||||
|
||||
@ -23,6 +23,7 @@ from awx.main.models import (
|
||||
Project,
|
||||
ProjectUpdate,
|
||||
SystemJob,
|
||||
WorkflowApproval,
|
||||
WorkflowJob,
|
||||
WorkflowJobTemplate
|
||||
)
|
||||
@ -518,6 +519,21 @@ class TaskManager():
|
||||
if not found_acceptable_queue:
|
||||
logger.debug("{} couldn't be scheduled on graph, waiting for next cycle".format(task.log_format))
|
||||
|
||||
def timeout_approval_node(self):
|
||||
workflow_approvals = WorkflowApproval.objects.filter(status='pending')
|
||||
now = tz_now()
|
||||
for task in workflow_approvals:
|
||||
approval_timeout_seconds = timedelta(seconds=task.timeout)
|
||||
if task.timeout == 0:
|
||||
continue
|
||||
if (now - task.created) >= approval_timeout_seconds:
|
||||
timeout_message = "The approval node {} ({}) has expired after {} seconds.".format(task.name, task.pk, task.timeout)
|
||||
logger.warn(timeout_message)
|
||||
task.timed_out = True
|
||||
task.status = 'failed'
|
||||
task.job_explanation = _(timeout_message)
|
||||
task.save(update_fields=['status', 'job_explanation', 'timed_out'])
|
||||
|
||||
def calculate_capacity_consumed(self, tasks):
|
||||
self.graph = InstanceGroup.objects.capacity_values(tasks=tasks, graph=self.graph)
|
||||
|
||||
@ -573,6 +589,8 @@ class TaskManager():
|
||||
|
||||
self.spawn_workflow_graph_jobs(running_workflow_tasks)
|
||||
|
||||
self.timeout_approval_node()
|
||||
|
||||
self.process_tasks(all_sorted_tasks)
|
||||
return finished_wfjs
|
||||
|
||||
|
||||
@ -34,8 +34,8 @@ from awx.main.models import (
|
||||
InventorySource, InventoryUpdateEvent, Job, JobEvent, JobHostSummary,
|
||||
JobTemplate, OAuth2AccessToken, Organization, Project, ProjectUpdateEvent,
|
||||
Role, SystemJob, SystemJobEvent, SystemJobTemplate, UnifiedJob,
|
||||
UnifiedJobTemplate, User, UserSessionMembership,
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
||||
UnifiedJobTemplate, User, UserSessionMembership, WorkflowJobTemplateNode,
|
||||
WorkflowApproval, WorkflowApprovalTemplate, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
||||
)
|
||||
from awx.main.constants import CENSOR_VALUE
|
||||
from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, get_current_apps
|
||||
@ -355,6 +355,7 @@ def update_host_last_job_after_job_deleted(sender, **kwargs):
|
||||
for host in Host.objects.filter(pk__in=hosts_pks):
|
||||
_update_host_last_jhs(host)
|
||||
|
||||
|
||||
# Set via ActivityStreamRegistrar to record activity stream events
|
||||
|
||||
|
||||
@ -429,6 +430,8 @@ def model_serializer_mapping():
|
||||
models.Label: serializers.LabelSerializer,
|
||||
models.WorkflowJobTemplate: serializers.WorkflowJobTemplateWithSpecSerializer,
|
||||
models.WorkflowJobTemplateNode: serializers.WorkflowJobTemplateNodeSerializer,
|
||||
models.WorkflowApproval: serializers.WorkflowApprovalActivityStreamSerializer,
|
||||
models.WorkflowApprovalTemplate: serializers.WorkflowApprovalTemplateSerializer,
|
||||
models.WorkflowJob: serializers.WorkflowJobSerializer,
|
||||
models.OAuth2AccessToken: serializers.OAuth2TokenSerializer,
|
||||
models.OAuth2Application: serializers.OAuth2ApplicationSerializer,
|
||||
@ -637,6 +640,30 @@ def delete_inventory_for_org(sender, instance, **kwargs):
|
||||
logger.debug(e)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=WorkflowJobTemplateNode)
|
||||
def delete_approval_templates(sender, instance, **kwargs):
|
||||
if type(instance.unified_job_template) is WorkflowApprovalTemplate:
|
||||
instance.unified_job_template.delete()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=WorkflowJobTemplateNode)
|
||||
def delete_approval_node_type_change(sender, instance, **kwargs):
|
||||
try:
|
||||
old = WorkflowJobTemplateNode.objects.get(id=instance.id)
|
||||
except sender.DoesNotExist:
|
||||
return
|
||||
if old.unified_job_template == instance.unified_job_template:
|
||||
return
|
||||
if type(old.unified_job_template) is WorkflowApprovalTemplate:
|
||||
old.unified_job_template.delete()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=WorkflowApprovalTemplate)
|
||||
def deny_orphaned_approvals(sender, instance, **kwargs):
|
||||
for approval in WorkflowApproval.objects.filter(workflow_approval_template=instance, status='pending'):
|
||||
approval.deny()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Session)
|
||||
def save_user_session_membership(sender, **kwargs):
|
||||
session = kwargs.get('instance', None)
|
||||
|
||||
@ -3,9 +3,17 @@ import json
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
|
||||
from awx.main.models.activity_stream import ActivityStream
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.workflow import WorkflowJobTemplateNode
|
||||
from awx.main.models.workflow import (
|
||||
WorkflowApproval,
|
||||
WorkflowApprovalTemplate,
|
||||
WorkflowJob,
|
||||
WorkflowJobTemplate,
|
||||
WorkflowJobTemplateNode,
|
||||
)
|
||||
from awx.main.models.credential import Credential
|
||||
from awx.main.scheduler import TaskManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -19,31 +27,18 @@ def job_template(inventory, project):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def node(workflow_job_template, post, admin_user, job_template):
|
||||
def node(workflow_job_template, admin_user, job_template):
|
||||
return WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_blank_UJT_unallowed(workflow_job_template, post, admin_user):
|
||||
url = reverse('api:workflow_job_template_workflow_nodes_list',
|
||||
kwargs={'pk': workflow_job_template.pk})
|
||||
r = post(url, {}, user=admin_user, expect=400)
|
||||
assert 'unified_job_template' in r.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cannot_remove_UJT(node, patch, admin_user):
|
||||
r = patch(
|
||||
node.get_absolute_url(),
|
||||
data={'unified_job_template': None},
|
||||
user=admin_user,
|
||||
expect=400
|
||||
@pytest.fixture
|
||||
def approval_node(workflow_job_template, admin_user):
|
||||
return WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template
|
||||
)
|
||||
assert 'unified_job_template' in r.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -76,6 +71,191 @@ def test_node_accepts_prompted_fields(inventory, project, workflow_job_template,
|
||||
user=admin_user, expect=201)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestApprovalNodes():
|
||||
def test_approval_node_creation(self, post, approval_node, admin_user):
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'Test', 'description': 'Approval Node', 'timeout': 0},
|
||||
user=admin_user, expect=200)
|
||||
|
||||
approval_node = WorkflowJobTemplateNode.objects.get(pk=approval_node.pk)
|
||||
assert isinstance(approval_node.unified_job_template, WorkflowApprovalTemplate)
|
||||
assert approval_node.unified_job_template.name=='Test'
|
||||
assert approval_node.unified_job_template.description=='Approval Node'
|
||||
assert approval_node.unified_job_template.timeout==0
|
||||
|
||||
def test_approval_node_creation_failure(self, post, approval_node, admin_user):
|
||||
# This test leaves off a required param to assert that user will get a 400.
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
r = post(url, {'name': '', 'description': 'Approval Node', 'timeout': 0},
|
||||
user=admin_user, expect=400)
|
||||
approval_node = WorkflowJobTemplateNode.objects.get(pk=approval_node.pk)
|
||||
assert isinstance(approval_node.unified_job_template, WorkflowApprovalTemplate) is False
|
||||
assert {'name': ['This field may not be blank.']} == json.loads(r.content)
|
||||
|
||||
@pytest.mark.parametrize("is_admin, is_org_admin, status", [
|
||||
[True, False, 200], # if they're a WFJT admin, they get a 200
|
||||
[False, False, 403], # if they're not a WFJT *nor* org admin, they get a 403
|
||||
[False, True, 200], # if they're an organization admin, they get a 200
|
||||
])
|
||||
def test_approval_node_creation_rbac(self, post, approval_node, alice, is_admin, is_org_admin, status):
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
if is_admin is True:
|
||||
approval_node.workflow_job_template.admin_role.members.add(alice)
|
||||
if is_org_admin is True:
|
||||
approval_node.workflow_job_template.organization.admin_role.members.add(alice)
|
||||
post(url, {'name': 'Test', 'description': 'Approval Node', 'timeout': 0},
|
||||
user=alice, expect=status)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_approval_node_exists(self, post, admin_user, get):
|
||||
workflow_job_template = WorkflowJobTemplate.objects.create()
|
||||
approval_node = WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template
|
||||
)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'URL Test', 'description': 'An approval', 'timeout': 0},
|
||||
user=admin_user)
|
||||
get(url, admin_user, expect=200)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_activity_stream_create_wf_approval(self, post, admin_user, workflow_job_template):
|
||||
wfjn = WorkflowJobTemplateNode.objects.create(workflow_job_template=workflow_job_template)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': wfjn.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'Activity Stream Test', 'description': 'Approval Node', 'timeout': 0},
|
||||
user=admin_user)
|
||||
|
||||
qs1 = ActivityStream.objects.filter(organization__isnull=False)
|
||||
assert qs1.count() == 1
|
||||
assert qs1[0].operation == 'create'
|
||||
|
||||
qs2 = ActivityStream.objects.filter(organization__isnull=True)
|
||||
assert qs2.count() == 5
|
||||
assert list(qs2.values_list('operation', 'object1')) == [('create', 'user'),
|
||||
('create', 'workflow_job_template'),
|
||||
('create', 'workflow_job_template_node'),
|
||||
('create', 'workflow_approval_template'),
|
||||
('update', 'workflow_job_template_node'),
|
||||
]
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_approval_node_approve(self, post, admin_user, job_template):
|
||||
# This test ensures that a user (with permissions to do so) can APPROVE
|
||||
# workflow approvals. Also asserts that trying to APPROVE approvals
|
||||
# that have already been dealt with will throw an error.
|
||||
wfjt = WorkflowJobTemplate.objects.create(name='foobar')
|
||||
node = wfjt.workflow_nodes.create(unified_job_template=job_template)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': node.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'Approve Test', 'description': '', 'timeout': 0},
|
||||
user=admin_user, expect=200)
|
||||
post(reverse('api:workflow_job_template_launch', kwargs={'pk': wfjt.pk}),
|
||||
user=admin_user, expect=201)
|
||||
wf_job = WorkflowJob.objects.first()
|
||||
TaskManager().schedule()
|
||||
TaskManager().schedule()
|
||||
wfj_node = wf_job.workflow_nodes.first()
|
||||
approval = wfj_node.job
|
||||
assert approval.name == 'Approve Test'
|
||||
post(reverse('api:workflow_approval_approve', kwargs={'pk': approval.pk}),
|
||||
user=admin_user, expect=204)
|
||||
# Test that there is an activity stream entry that was created for the "approve" action.
|
||||
qs = ActivityStream.objects.order_by('-timestamp').first()
|
||||
assert qs.object1 == 'workflow_approval'
|
||||
assert qs.changes == '{"status": ["pending", "successful"]}'
|
||||
assert WorkflowApproval.objects.get(pk=approval.pk).status == 'successful'
|
||||
assert qs.operation == 'update'
|
||||
post(reverse('api:workflow_approval_approve', kwargs={'pk': approval.pk}),
|
||||
user=admin_user, expect=400)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_approval_node_deny(self, post, admin_user, job_template):
|
||||
# This test ensures that a user (with permissions to do so) can DENY
|
||||
# workflow approvals. Also asserts that trying to DENY approvals
|
||||
# that have already been dealt with will throw an error.
|
||||
wfjt = WorkflowJobTemplate.objects.create(name='foobar')
|
||||
node = wfjt.workflow_nodes.create(unified_job_template=job_template)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': node.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'Deny Test', 'description': '', 'timeout': 0},
|
||||
user=admin_user, expect=200)
|
||||
post(reverse('api:workflow_job_template_launch', kwargs={'pk': wfjt.pk}),
|
||||
user=admin_user, expect=201)
|
||||
wf_job = WorkflowJob.objects.first()
|
||||
TaskManager().schedule()
|
||||
TaskManager().schedule()
|
||||
wfj_node = wf_job.workflow_nodes.first()
|
||||
approval = wfj_node.job
|
||||
assert approval.name == 'Deny Test'
|
||||
post(reverse('api:workflow_approval_deny', kwargs={'pk': approval.pk}),
|
||||
user=admin_user, expect=204)
|
||||
# Test that there is an activity stream entry that was created for the "deny" action.
|
||||
qs = ActivityStream.objects.order_by('-timestamp').first()
|
||||
assert qs.object1 == 'workflow_approval'
|
||||
assert qs.changes == '{"status": ["pending", "failed"]}'
|
||||
assert WorkflowApproval.objects.get(pk=approval.pk).status == 'failed'
|
||||
assert qs.operation == 'update'
|
||||
post(reverse('api:workflow_approval_deny', kwargs={'pk': approval.pk}),
|
||||
user=admin_user, expect=400)
|
||||
|
||||
def test_approval_node_cleanup(self, post, approval_node, admin_user, get):
|
||||
workflow_job_template = WorkflowJobTemplate.objects.create()
|
||||
approval_node = WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template
|
||||
)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
|
||||
post(url, {'name': 'URL Test', 'description': 'An approval', 'timeout': 0},
|
||||
user=admin_user)
|
||||
assert WorkflowApprovalTemplate.objects.count() == 1
|
||||
workflow_job_template.delete()
|
||||
assert WorkflowApprovalTemplate.objects.count() == 0
|
||||
get(url, admin_user, expect=404)
|
||||
|
||||
def test_changed_approval_deletion(self, post, approval_node, admin_user, workflow_job_template, job_template):
|
||||
# This test verifies that when an approval node changes into something else
|
||||
# (in this case, a job template), then the previously-set WorkflowApprovalTemplate
|
||||
# is automatically deleted.
|
||||
workflow_job_template = WorkflowJobTemplate.objects.create()
|
||||
approval_node = WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template
|
||||
)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'URL Test', 'description': 'An approval', 'timeout': 0},
|
||||
user=admin_user)
|
||||
assert WorkflowApprovalTemplate.objects.count() == 1
|
||||
approval_node.unified_job_template = job_template
|
||||
approval_node.save()
|
||||
assert WorkflowApprovalTemplate.objects.count() == 0
|
||||
|
||||
def test_deleted_approval_denial(self, post, approval_node, admin_user, workflow_job_template):
|
||||
# Verifying that when a WorkflowApprovalTemplate is deleted, any/all of
|
||||
# its pending approvals are auto-denied (vs left in 'pending' state).
|
||||
workflow_job_template = WorkflowJobTemplate.objects.create()
|
||||
approval_node = WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template
|
||||
)
|
||||
url = reverse('api:workflow_job_template_node_create_approval',
|
||||
kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||
post(url, {'name': 'URL Test', 'description': 'An approval', 'timeout': 0},
|
||||
user=admin_user)
|
||||
assert WorkflowApprovalTemplate.objects.count() == 1
|
||||
approval_template = WorkflowApprovalTemplate.objects.first()
|
||||
approval = approval_template.create_unified_job()
|
||||
approval.status = 'pending'
|
||||
approval.save()
|
||||
approval_template.delete()
|
||||
approval.refresh_from_db()
|
||||
assert approval.status == 'failed'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestExclusiveRelationshipEnforcement():
|
||||
@pytest.fixture
|
||||
|
||||
@ -10,7 +10,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
# AWX
|
||||
from awx.main.models import (
|
||||
UnifiedJobTemplate, Job, JobTemplate, WorkflowJobTemplate,
|
||||
Project, WorkflowJob, Schedule,
|
||||
WorkflowApprovalTemplate, Project, WorkflowJob, Schedule,
|
||||
Credential
|
||||
)
|
||||
|
||||
@ -20,7 +20,9 @@ def test_subclass_types(rando):
|
||||
assert set(UnifiedJobTemplate._submodels_with_roles()) == set([
|
||||
ContentType.objects.get_for_model(JobTemplate).id,
|
||||
ContentType.objects.get_for_model(Project).id,
|
||||
ContentType.objects.get_for_model(WorkflowJobTemplate).id
|
||||
ContentType.objects.get_for_model(WorkflowJobTemplate).id,
|
||||
ContentType.objects.get_for_model(WorkflowApprovalTemplate).id
|
||||
|
||||
])
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,9 @@ from unittest import mock
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.utils import decrypt_field
|
||||
from awx.main.models.workflow import WorkflowJobTemplateNode
|
||||
from awx.main.models.workflow import (
|
||||
WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
||||
)
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.tasks import deep_copy_model_obj
|
||||
|
||||
@ -175,6 +177,76 @@ def test_workflow_job_template_copy(workflow_job_template, post, get, admin, org
|
||||
assert copied_node_list[4] in copied_node_list[3].failure_nodes.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_workflow_approval_node_copy(workflow_job_template, post, get, admin, organization):
|
||||
workflow_job_template.organization = organization
|
||||
workflow_job_template.save()
|
||||
ajts = [
|
||||
WorkflowApprovalTemplate.objects.create(
|
||||
name='test-approval-{}'.format(i),
|
||||
description='description-{}'.format(i),
|
||||
timeout=30
|
||||
)
|
||||
for i in range(0, 5)
|
||||
]
|
||||
nodes = [
|
||||
WorkflowJobTemplateNode.objects.create(
|
||||
workflow_job_template=workflow_job_template, unified_job_template=ajts[i]
|
||||
) for i in range(0, 5)
|
||||
]
|
||||
nodes[0].success_nodes.add(nodes[1])
|
||||
nodes[1].success_nodes.add(nodes[2])
|
||||
nodes[0].failure_nodes.add(nodes[3])
|
||||
nodes[3].failure_nodes.add(nodes[4])
|
||||
assert WorkflowJobTemplate.objects.count() == 1
|
||||
assert WorkflowJobTemplateNode.objects.count() == 5
|
||||
assert WorkflowApprovalTemplate.objects.count() == 5
|
||||
|
||||
with mock.patch('awx.api.generics.trigger_delayed_deep_copy') as deep_copy_mock:
|
||||
wfjt_copy_id = post(
|
||||
reverse('api:workflow_job_template_copy', kwargs={'pk': workflow_job_template.pk}),
|
||||
{'name': 'new wfjt name'}, admin, expect=201
|
||||
).data['id']
|
||||
wfjt_copy = type(workflow_job_template).objects.get(pk=wfjt_copy_id)
|
||||
args, kwargs = deep_copy_mock.call_args
|
||||
deep_copy_model_obj(*args, **kwargs)
|
||||
assert wfjt_copy.organization == organization
|
||||
assert wfjt_copy.created_by == admin
|
||||
assert wfjt_copy.name == 'new wfjt name'
|
||||
|
||||
assert WorkflowJobTemplate.objects.count() == 2
|
||||
assert WorkflowJobTemplateNode.objects.count() == 10
|
||||
assert WorkflowApprovalTemplate.objects.count() == 10
|
||||
original_templates = [
|
||||
x.unified_job_template for x in workflow_job_template.workflow_job_template_nodes.all()
|
||||
]
|
||||
copied_templates = [
|
||||
x.unified_job_template for x in wfjt_copy.workflow_job_template_nodes.all()
|
||||
]
|
||||
|
||||
# make sure shallow fields like `timeout` are copied properly
|
||||
for i, t in enumerate(original_templates):
|
||||
assert t.timeout == 30
|
||||
assert t.description == 'description-{}'.format(i)
|
||||
|
||||
for i, t in enumerate(copied_templates):
|
||||
assert t.timeout == 30
|
||||
assert t.description == 'description-{}'.format(i)
|
||||
|
||||
# the Approval Template IDs on the *original* WFJT should not match *any*
|
||||
# of the Approval Template IDs on the *copied* WFJT
|
||||
assert not set([x.id for x in original_templates]).intersection(
|
||||
set([x.id for x in copied_templates])
|
||||
)
|
||||
|
||||
# if you remove the " copy" suffix from the copied template names, they
|
||||
# should match the original templates
|
||||
assert (
|
||||
set([x.name for x in original_templates]) ==
|
||||
set([x.name.replace(' copy', '') for x in copied_templates])
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_credential_copy(post, get, machine_credential, credentialtype_ssh, admin):
|
||||
assert get(
|
||||
|
||||
@ -10,12 +10,7 @@ export default {
|
||||
name: 'organizations.job_templates',
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'template',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
activityStreamTarget: 'template'
|
||||
},
|
||||
params: {
|
||||
template_search: {
|
||||
|
||||
@ -13,12 +13,7 @@ export default {
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'template',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
activityStreamTarget: 'template'
|
||||
},
|
||||
params: {
|
||||
template_search: {
|
||||
|
||||
@ -110,14 +110,17 @@ function TemplatesStrings (BaseString) {
|
||||
ON_SUCCESS: t.s('On Success'),
|
||||
ON_FAILURE: t.s('On Failure'),
|
||||
ALWAYS: t.s('Always'),
|
||||
PAUSE: t.s('Wait For Approval'),
|
||||
JOB_TEMPLATE: t.s('Job Template'),
|
||||
PROJECT_SYNC: t.s('Project Sync'),
|
||||
INVENTORY_SYNC: t.s('Inventory Sync'),
|
||||
WORKFLOW: t.s('Workflow'),
|
||||
TEMPLATE: t.s('Template'),
|
||||
WARNING: t.s('Warning'),
|
||||
TOTAL_NODES: t.s('TOTAL NODES'),
|
||||
ADD_A_NODE: t.s('ADD A NODE'),
|
||||
EDIT_TEMPLATE: t.s('EDIT TEMPLATE'),
|
||||
JOBS: t.s('JOBS'),
|
||||
JOBS: t.s('Jobs'),
|
||||
PLEASE_CLICK_THE_START_BUTTON: t.s('Please click the start button to build your workflow.'),
|
||||
PLEASE_HOVER_OVER_A_TEMPLATE: t.s('Please hover over a template for additional options.'),
|
||||
EDIT_LINK_TOOLTIP: t.s('Click to edit link'),
|
||||
@ -144,7 +147,13 @@ function TemplatesStrings (BaseString) {
|
||||
UNSAVED_CHANGES_PROMPT_TEXT: t.s('Are you sure you want to exit the Workflow Creator without saving your changes?'),
|
||||
EXIT: t.s('EXIT'),
|
||||
CANCEL: t.s('CANCEL'),
|
||||
SAVE_AND_EXIT: t.s('SAVE & EXIT')
|
||||
SAVE_AND_EXIT: t.s('SAVE & EXIT'),
|
||||
APPROVAL: t.s('Approval'),
|
||||
TIMEOUT_POPOVER: t.s('The amount of time to wait before this approval step is automatically denied. Defaults to 0 for no timeout.'),
|
||||
TIMED_OUT: t.s('APPROVAL TIMED OUT'),
|
||||
TIMEOUT: t.s('Timeout'),
|
||||
APPROVED: t.s('APPROVED'),
|
||||
DENIED: t.s('DENIED')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1981,11 +1981,6 @@ tr td button i {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.select2-container {
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.form-control + .select2-container--disabled .select2-selection {
|
||||
background-color: @ebgrey !important;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@import 'action/_index';
|
||||
@import 'approvalsDrawer/_index';
|
||||
@import 'dialog/_index';
|
||||
@import 'input/_index';
|
||||
@import 'launchTemplateButton/_index';
|
||||
|
||||
68
awx/ui/client/lib/components/approvalsDrawer/_index.less
Normal file
68
awx/ui/client/lib/components/approvalsDrawer/_index.less
Normal file
@ -0,0 +1,68 @@
|
||||
.at-ApprovalsDrawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
// z-index of the nav header is 1040
|
||||
z-index: 1041;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
|
||||
&--drawer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 540px;
|
||||
background-color: @default-bg;
|
||||
padding: 20px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
&--header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&--title {
|
||||
flex: 1 0 auto;
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--actionRow {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
line-height: 30px;
|
||||
|
||||
button {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&--exit {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
color: @d7grey;
|
||||
line-height: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button:hover{
|
||||
color: @default-icon;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--expires {
|
||||
color: @default-err;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
const templateUrl = require('~components/approvalsDrawer/approvalsDrawer.partial.html');
|
||||
|
||||
function AtApprovalsDrawerController (strings, Rest, GetBasePath, $rootScope) {
|
||||
const vm = this || {};
|
||||
|
||||
const toolbarSortDefault = {
|
||||
label: `${strings.get('sort.CREATED_ASCENDING')}`,
|
||||
value: 'created'
|
||||
};
|
||||
|
||||
vm.strings = strings;
|
||||
vm.toolbarSortValue = toolbarSortDefault;
|
||||
vm.queryset = {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'created',
|
||||
status: 'pending'
|
||||
};
|
||||
vm.emptyListReason = vm.strings.get('approvals.NONE');
|
||||
|
||||
vm.toolbarSortOptions = [
|
||||
toolbarSortDefault,
|
||||
{ label: `${vm.strings.get('sort.CREATED_DESCENDING')}`, value: '-created' }
|
||||
];
|
||||
|
||||
const loadTheList = () => {
|
||||
const queryParams = Object.keys(vm.queryset).map(key => `${key}=${vm.queryset[key]}`).join('&');
|
||||
Rest.setUrl(`${GetBasePath('workflow_approvals')}?${queryParams}`);
|
||||
return Rest.get()
|
||||
.then(({ data }) => {
|
||||
vm.dataset = data;
|
||||
vm.approvals = data.results;
|
||||
vm.count = data.count;
|
||||
$rootScope.pendingApprovalCount = data.count;
|
||||
});
|
||||
};
|
||||
|
||||
loadTheList()
|
||||
.then(() => { vm.listLoaded = true; });
|
||||
|
||||
vm.approve = (approval) => {
|
||||
Rest.setUrl(`${GetBasePath('workflow_approvals')}${approval.id}/approve`);
|
||||
Rest.post()
|
||||
.then(() => loadTheList());
|
||||
};
|
||||
|
||||
vm.deny = (approval) => {
|
||||
Rest.setUrl(`${GetBasePath('workflow_approvals')}${approval.id}/deny`);
|
||||
Rest.post()
|
||||
.then(() => loadTheList());
|
||||
};
|
||||
|
||||
vm.onToolbarSort = (sort) => {
|
||||
vm.toolbarSortValue = sort;
|
||||
vm.queryset.page = 1;
|
||||
vm.queryset.order_by = sort.value;
|
||||
loadTheList();
|
||||
};
|
||||
}
|
||||
|
||||
AtApprovalsDrawerController.$inject = ['ComponentsStrings', 'Rest', 'GetBasePath', '$rootScope'];
|
||||
|
||||
function atApprovalsDrawer () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
templateUrl,
|
||||
controller: AtApprovalsDrawerController,
|
||||
controllerAs: 'vm',
|
||||
scope: {
|
||||
closeApprovals: '&'
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default atApprovalsDrawer;
|
||||
@ -0,0 +1,88 @@
|
||||
<div class="at-ApprovalsDrawer">
|
||||
<div class="at-ApprovalsDrawer--drawer" ng-if="vm.listLoaded">
|
||||
<div class="at-ApprovalsDrawer--header">
|
||||
<div class="at-ApprovalsDrawer--title">
|
||||
<span>
|
||||
{{:: vm.strings.get('approvals.NOTIFICATIONS') }}
|
||||
</span>
|
||||
<span class="at-Panel-headingTitleBadge">
|
||||
{{vm.count}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="at-ApprovalsDrawer--exit">
|
||||
<button class="close" ng-click="closeApprovals()">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<at-list-toolbar
|
||||
ng-if="vm.approvals.length > 0"
|
||||
sort-only="true"
|
||||
sort-value="vm.toolbarSortValue"
|
||||
sort-options="vm.toolbarSortOptions"
|
||||
on-sort="vm.onToolbarSort">
|
||||
</at-list-toolbar>
|
||||
<at-list results="vm.approvals" id="approvals_list" empty-list-reason="{{ vm.emptyListReason }}">
|
||||
<at-row ng-repeat="approval in vm.approvals"
|
||||
id="approval-row-{{ approval.id }}">
|
||||
<div class="at-Row-items">
|
||||
<div class="at-Row-container">
|
||||
<div class="at-Row-container">
|
||||
<at-row-item
|
||||
header-value="{{ approval.summary_fields.source_workflow_job.name }}"
|
||||
header-state="workflowResults({id: {{approval.summary_fields.source_workflow_job.id}}})">
|
||||
</at-row-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="at-Row-container--wrapped">
|
||||
<at-row-item
|
||||
value-bind-html="<b>{{:: vm.strings.get('approvals.APPROVAL') }}</b>">
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
value-bind-html="{{ approval.name }}">
|
||||
</at-row-item>
|
||||
</div>
|
||||
<div class="at-Row-container--wrapped">
|
||||
<at-row-item
|
||||
value-bind-html="{{ approval.created | longDate }}">
|
||||
</at-row-item>
|
||||
<!-- todo: translate strings -->
|
||||
<at-row-item
|
||||
ng-if="approval.approval_expiration"
|
||||
class="at-ApprovalsDrawer--expires"
|
||||
value-bind-html="{{:: vm.strings.get('approvals.EXPIRES') }} {{ approval.approval_expiration | longDate }}">
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
ng-if="!approval.approval_expiration"
|
||||
class="at-ApprovalsDrawer--expires"
|
||||
value-bind-html="{{:: vm.strings.get('approvals.EXPIRES_NEVER') }}">
|
||||
</at-row-item>
|
||||
</div>
|
||||
<div class="at-Row-container--wrapped" ng-if="approval.can_approve_or_deny">
|
||||
<div class="at-ApprovalsDrawer--actionRow">
|
||||
<div>{{:: vm.strings.get('approvals.CONTINUE') }}</div>
|
||||
<button class="btn at-Button--success"
|
||||
ng-click="vm.approve(approval)"
|
||||
type="button">
|
||||
{{:: vm.strings.get('approvals.APPROVE') }}
|
||||
</button>
|
||||
<button class="btn at-Button--error"
|
||||
ng-click="vm.deny(approval)"
|
||||
type="button">
|
||||
{{:: vm.strings.get('approvals.DENY') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</at-row>
|
||||
</at-list>
|
||||
<paginate
|
||||
collection="vm.approvals"
|
||||
dataset="vm.dataset"
|
||||
iterator="template"
|
||||
base-path="unified_job_templates"
|
||||
query-set="vm.queryset"
|
||||
hide-view-per-page="true">
|
||||
</paginate>
|
||||
</div>
|
||||
</div>
|
||||
@ -119,6 +119,18 @@ function ComponentsStrings (BaseString) {
|
||||
EXPANDED: t.s('Expanded'),
|
||||
SORT_BY: t.s('SORT BY')
|
||||
};
|
||||
|
||||
ns.approvals = {
|
||||
APPROVAL: t.s('APPROVAL'),
|
||||
NONE: t.s('There are no jobs awaiting approval'),
|
||||
APPROVE: t.s('APPROVE'),
|
||||
DENY: t.s('DENY'),
|
||||
CONTINUE: t.s('Continue workflow job?'),
|
||||
NOTIFICATIONS: t.s('NOTIFICATIONS'),
|
||||
WORKFLOW_TEMPLATE: t.s('Workflow Template'),
|
||||
EXPIRES: t.s('Expires:'),
|
||||
EXPIRES_NEVER: t.s('Expires: Never')
|
||||
};
|
||||
}
|
||||
|
||||
ComponentsStrings.$inject = ['BaseStringService'];
|
||||
|
||||
@ -2,6 +2,7 @@ import atLibServices from '~services';
|
||||
|
||||
import actionGroup from '~components/action/action-group.directive';
|
||||
import actionButton from '~components/action/action-button.directive';
|
||||
import approvalsDrawer from '~components/approvalsDrawer/approvalsDrawer.directive';
|
||||
import dialog from '~components/dialog/dialog.component';
|
||||
import divider from '~components/utility/divider.directive';
|
||||
import dynamicSelect from '~components/input/dynamic-select.directive';
|
||||
@ -60,6 +61,7 @@ angular
|
||||
])
|
||||
.directive('atActionGroup', actionGroup)
|
||||
.directive('atActionButton', actionButton)
|
||||
.directive('atApprovalsDrawer', approvalsDrawer)
|
||||
.component('atDialog', dialog)
|
||||
.directive('atDivider', divider)
|
||||
.directive('atDynamicSelect', dynamicSelect)
|
||||
|
||||
@ -81,6 +81,30 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.at-Layout-Approvals {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.at-Layout-ApprovalsBadge {
|
||||
margin-left: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: @at-gray-646972;
|
||||
color: @at-white;
|
||||
height: 16px;
|
||||
font-size: 11px;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.at-Layout-ApprovalsBadgeActive {
|
||||
background-color: @at-red-bright;
|
||||
}
|
||||
}
|
||||
|
||||
&-sideContainer {
|
||||
|
||||
@ -25,6 +25,10 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('$root.pendingApprovalCount', () => {
|
||||
vm.approvalsCount = _.get($scope, '$root.pendingApprovalCount') || 0;
|
||||
});
|
||||
|
||||
$scope.$watch('$root.socketStatus', (newStatus) => {
|
||||
vm.socketState = newStatus;
|
||||
vm.socketIconClass = `icon-socket-${vm.socketState}`;
|
||||
@ -42,6 +46,14 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions
|
||||
}
|
||||
};
|
||||
|
||||
vm.openApprovals = () => {
|
||||
vm.showApprovals = true;
|
||||
};
|
||||
|
||||
vm.closeApprovals = () => {
|
||||
vm.showApprovals = false;
|
||||
};
|
||||
|
||||
function checkOrgAdmin () {
|
||||
const usersPath = `/api/v2/users/${vm.currentUserId}/admin_of_organizations/`;
|
||||
$http.get(usersPath)
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
<span>{{ $parent.layoutVm.currentUsername }}</span>
|
||||
</a>
|
||||
</at-top-nav-item>
|
||||
<at-top-nav-item ng-click="vm.openApprovals()">
|
||||
<div class="at-Layout-Approvals">
|
||||
<i class="fa fa-bell" alt="{{ vm.getString('NOTIFICATIONS') }}"></i>
|
||||
<span class="at-Layout-ApprovalsBadge" ng-class="{'at-Layout-ApprovalsBadgeActive': vm.approvalsCount && vm.approvalsCount > 0}">{{vm.approvalsCount}}</span>
|
||||
</div>
|
||||
</at-top-nav-item>
|
||||
<at-top-nav-item>
|
||||
<a ui-sref="about">
|
||||
<i class="fa fa-info-circle" alt="{{ vm.getString('ABOUT') }}"></i>
|
||||
@ -104,4 +110,5 @@
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
</div>
|
||||
<at-approvals-drawer ng-if="vm.isLoggedIn && vm.showApprovals" close-approvals="vm.closeApprovals()"></at-approvals-drawer>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import atLibServices from '~services';
|
||||
|
||||
import Application from '~models/Application';
|
||||
import AdHocCommand from '~models/AdHocCommand';
|
||||
import Application from '~models/Application';
|
||||
import Base from '~models/Base';
|
||||
import Config from '~models/Config';
|
||||
import Credential from '~models/Credential';
|
||||
@ -19,16 +19,16 @@ import Me from '~models/Me';
|
||||
import NotificationTemplate from '~models/NotificationTemplate';
|
||||
import Organization from '~models/Organization';
|
||||
import Project from '~models/Project';
|
||||
import Schedule from '~models/Schedule';
|
||||
import ProjectUpdate from '~models/ProjectUpdate';
|
||||
import Schedule from '~models/Schedule';
|
||||
import SystemJob from '~models/SystemJob';
|
||||
import Token from '~models/Token';
|
||||
import UnifiedJob from '~models/UnifiedJob';
|
||||
import UnifiedJobTemplate from '~models/UnifiedJobTemplate';
|
||||
import User from '~models/User';
|
||||
import WorkflowJob from '~models/WorkflowJob';
|
||||
import WorkflowJobTemplate from '~models/WorkflowJobTemplate';
|
||||
import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode';
|
||||
import UnifiedJob from '~models/UnifiedJob';
|
||||
import User from '~models/User';
|
||||
|
||||
import ModelsStrings from '~models/models.strings';
|
||||
|
||||
@ -38,8 +38,8 @@ angular
|
||||
.module(MODULE_NAME, [
|
||||
atLibServices
|
||||
])
|
||||
.service('ApplicationModel', Application)
|
||||
.service('AdHocCommandModel', AdHocCommand)
|
||||
.service('ApplicationModel', Application)
|
||||
.service('BaseModel', Base)
|
||||
.service('ConfigModel', Config)
|
||||
.service('CredentialModel', Credential)
|
||||
@ -54,19 +54,19 @@ angular
|
||||
.service('JobModel', Job)
|
||||
.service('JobTemplateModel', JobTemplate)
|
||||
.service('MeModel', Me)
|
||||
.service('ModelsStrings', ModelsStrings)
|
||||
.service('NotificationTemplate', NotificationTemplate)
|
||||
.service('OrganizationModel', Organization)
|
||||
.service('ProjectModel', Project)
|
||||
.service('ScheduleModel', Schedule)
|
||||
.service('UnifiedJobModel', UnifiedJob)
|
||||
.service('ProjectUpdateModel', ProjectUpdate)
|
||||
.service('ScheduleModel', Schedule)
|
||||
.service('SystemJobModel', SystemJob)
|
||||
.service('TokenModel', Token)
|
||||
.service('UnifiedJobModel', UnifiedJob)
|
||||
.service('UnifiedJobTemplateModel', UnifiedJobTemplate)
|
||||
.service('UserModel', User)
|
||||
.service('WorkflowJobModel', WorkflowJob)
|
||||
.service('WorkflowJobTemplateModel', WorkflowJobTemplate)
|
||||
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode)
|
||||
.service('UserModel', User)
|
||||
.service('ModelsStrings', ModelsStrings);
|
||||
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode);
|
||||
|
||||
export default MODULE_NAME;
|
||||
|
||||
@ -93,6 +93,14 @@ export default function BuildAnchor($log, $filter) {
|
||||
case 'o_auth2_application':
|
||||
url += `applications/${obj.id}`;
|
||||
break;
|
||||
case 'workflow_approval':
|
||||
url += `workflows/${activity.summary_fields.workflow_job[0].id}`;
|
||||
name = activity.summary_fields.workflow_job[0].name + ' | ' + activity.summary_fields.workflow_approval[0].name;
|
||||
break;
|
||||
case 'workflow_approval_template':
|
||||
url += `templates/workflow_job_template/${activity.summary_fields.workflow_job_template[0].id}/workflow-maker`;
|
||||
name = activity.summary_fields.workflow_job_template[0].name + ' | ' + activity.summary_fields.workflow_approval_template[0].name;
|
||||
break;
|
||||
default:
|
||||
url += resource + 's/' + obj.id + '/';
|
||||
}
|
||||
|
||||
@ -124,7 +124,26 @@ export default function BuildDescription(BuildAnchor, $log, i18n) {
|
||||
break;
|
||||
// expected outcome: "operation <object1>"
|
||||
case 'update':
|
||||
activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||
if (activity.object1 === 'workflow_approval' &&
|
||||
_.has(activity, 'changes.status') &&
|
||||
activity.changes.status.length === 2
|
||||
) {
|
||||
let operationText = '';
|
||||
if (activity.changes.status[1] === 'successful') {
|
||||
operationText = i18n._('approved');
|
||||
} else if (activity.changes.status[1] === 'failed') {
|
||||
if (activity.changes.timed_out && activity.changes.timed_out[1] === true) {
|
||||
operationText = i18n._('timed out');
|
||||
} else {
|
||||
operationText = i18n._('denied');
|
||||
}
|
||||
} else {
|
||||
operationText = i18n._('updated');
|
||||
}
|
||||
activity.description = `${operationText} ${activity.object1} ${BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity)}`;
|
||||
} else {
|
||||
activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||
}
|
||||
break;
|
||||
case 'create':
|
||||
activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity);
|
||||
|
||||
@ -50,11 +50,22 @@ export default ['templateUrl', 'i18n', function(templateUrl, i18n) {
|
||||
}
|
||||
else {
|
||||
let search = {
|
||||
or__object1__in: $scope.streamTarget && $scope.streamTarget === 'template' ? 'job_template,workflow_job_template' : $scope.streamTarget,
|
||||
or__object2__in: $scope.streamTarget && $scope.streamTarget === 'template' ? 'job_template,workflow_job_template' : $scope.streamTarget,
|
||||
or__object1__in: $scope.streamTarget,
|
||||
or__object2__in: $scope.streamTarget,
|
||||
page_size: '20',
|
||||
order_by: '-timestamp'
|
||||
};
|
||||
|
||||
if ($scope.streamTarget && $scope.streamTarget === 'template') {
|
||||
search.or__object1__in = 'job_template,workflow_job_template';
|
||||
search.or__object2__in = 'job_template,workflow_job_template';
|
||||
}
|
||||
|
||||
if ($scope.streamTarget && $scope.streamTarget === 'job') {
|
||||
search.or__object1__in = 'job,workflow_approval';
|
||||
search.or__object2__in = 'job,workflow_approval';
|
||||
}
|
||||
|
||||
// Attach the taget to the query parameters
|
||||
$state.go('activityStream', {target: $scope.streamTarget, id: null, activity_search: search});
|
||||
}
|
||||
|
||||
@ -161,16 +161,16 @@ angular
|
||||
// })
|
||||
}
|
||||
])
|
||||
.run(['$stateExtender', '$q', '$compile', '$cookies', '$rootScope', '$log', '$stateParams',
|
||||
.run(['$q', '$cookies', '$rootScope', '$log', '$stateParams',
|
||||
'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer',
|
||||
'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest',
|
||||
'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
|
||||
'$filter', 'SocketService', 'AppStrings', '$transitions',
|
||||
function($stateExtender, $q, $compile, $cookies, $rootScope, $log, $stateParams,
|
||||
'LoadConfig', 'Store', 'pendoService', 'Rest',
|
||||
'$state', 'GetBasePath', 'ConfigService', 'ProcessErrors',
|
||||
'SocketService', 'AppStrings', '$transitions', 'i18n',
|
||||
function($q, $cookies, $rootScope, $log, $stateParams,
|
||||
CheckLicense, $location, Authorization, LoadBasePaths, Timer,
|
||||
LoadConfig, Store, pendoService, Prompt, Rest, Wait,
|
||||
ProcessErrors, $state, GetBasePath, ConfigService,
|
||||
$filter, SocketService, AppStrings, $transitions) {
|
||||
LoadConfig, Store, pendoService, Rest,
|
||||
$state, GetBasePath, ConfigService, ProcessErrors,
|
||||
SocketService, AppStrings, $transitions, i18n) {
|
||||
|
||||
$rootScope.$state = $state;
|
||||
$rootScope.$state.matches = function(stateName) {
|
||||
@ -198,6 +198,10 @@ angular
|
||||
document.title = `Ansible ${$rootScope.BRAND_NAME} ${title}`;
|
||||
});
|
||||
|
||||
$rootScope.$on('ws-approval', () => {
|
||||
fetchApprovalsCount();
|
||||
});
|
||||
|
||||
function activateTab() {
|
||||
// Make the correct tab active
|
||||
var base = $location.path().replace(/^\//, '').split('/')[0];
|
||||
@ -210,6 +214,20 @@ angular
|
||||
}
|
||||
}
|
||||
|
||||
function fetchApprovalsCount() {
|
||||
Rest.setUrl(`${GetBasePath('workflow_approvals')}?status=pending&page_size=1`);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
$rootScope.pendingApprovalCount = data.count;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors({}, data, status, null, {
|
||||
hdr: i18n._('Error!'),
|
||||
msg: i18n._('Failed to get workflow jobs pending approval. GET returned status: ') + status
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($rootScope.removeConfigReady) {
|
||||
$rootScope.removeConfigReady();
|
||||
}
|
||||
@ -387,6 +405,7 @@ angular
|
||||
}
|
||||
});
|
||||
});
|
||||
fetchApprovalsCount();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -75,11 +75,22 @@ export default
|
||||
stateGoParams.target = streamConfig.activityStreamTarget;
|
||||
let isTemplateTarget = _.includes(['template', 'job_template', 'workflow_job_template'], streamConfig.activityStreamTarget);
|
||||
stateGoParams.activity_search = {
|
||||
or__object1__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
||||
or__object2__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
||||
or__object1__in: streamConfig.activityStreamTarget,
|
||||
or__object2__in: streamConfig.activityStreamTarget,
|
||||
order_by: '-timestamp',
|
||||
page_size: '20',
|
||||
};
|
||||
|
||||
if (isTemplateTarget) {
|
||||
stateGoParams.activity_search.or__object1__in = 'job_template,workflow_job_template';
|
||||
stateGoParams.activity_search.or__object2__in = 'job_template,workflow_job_template';
|
||||
}
|
||||
|
||||
if (streamConfig.activityStreamTarget === 'job') {
|
||||
stateGoParams.activity_search.or__object1__in = 'job,workflow_approval';
|
||||
stateGoParams.activity_search.or__object2__in = 'job,workflow_approval';
|
||||
}
|
||||
|
||||
if (streamConfig.activityStreamTarget && streamConfig.activityStreamId && !streamConfig.noActivityStreamID) {
|
||||
stateGoParams.activity_search[streamConfig.activityStreamTarget] = $state.params[streamConfig.activityStreamId];
|
||||
}
|
||||
|
||||
@ -94,9 +94,12 @@
|
||||
<div class="DashboardGraphs-graphContainer" auto-size-module
|
||||
graph-type="jobsStatus"
|
||||
ng-class="{'is-selected': jobStatusSelected }">
|
||||
<job-status-graph class="DashboardGraphs-graph
|
||||
DashboardGraphs-graph--jobStatusGraph"
|
||||
data="graphData.jobStatus" period="month" job-type="all">
|
||||
<job-status-graph
|
||||
class="DashboardGraphs-graph DashboardGraphs-graph--jobStatusGraph"
|
||||
data="graphData.jobStatus"
|
||||
period="graphData.period"
|
||||
job-type="graphData.jobType"
|
||||
status="graphData.status">
|
||||
</job-status-graph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,19 +18,17 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, moment, gra
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
data: '='
|
||||
data: '=',
|
||||
period: '=',
|
||||
jobType: '=',
|
||||
status: '='
|
||||
},
|
||||
templateUrl: templateUrl('home/dashboard/graphs/job-status/job_status_graph'),
|
||||
link: link
|
||||
};
|
||||
|
||||
function link(scope, element) {
|
||||
var job_type,
|
||||
job_status_chart = nv.models.lineChart();
|
||||
|
||||
scope.period="month";
|
||||
scope.jobType="all";
|
||||
scope.status="both";
|
||||
var job_status_chart = nv.models.lineChart();
|
||||
|
||||
scope.$watchCollection('data', function(value) {
|
||||
if (value) {
|
||||
@ -129,8 +127,6 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, moment, gra
|
||||
|
||||
// when the Period drop down filter is used, create a new graph based on the
|
||||
$('.n').off('click').on("click", function(){
|
||||
period = this.getAttribute("id");
|
||||
|
||||
$('#period-dropdown-display')
|
||||
.html(`
|
||||
<span>${this.text}</span>
|
||||
@ -139,13 +135,11 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, moment, gra
|
||||
|
||||
scope.$parent.isFailed = true;
|
||||
scope.$parent.isSuccessful = true;
|
||||
recreateGraph(period, job_type);
|
||||
recreateGraph(this.getAttribute("id"), scope.jobType, scope.status);
|
||||
});
|
||||
|
||||
//On click, update with new data
|
||||
$('.m').off('click').on("click", function(){
|
||||
job_type = this.getAttribute("id");
|
||||
|
||||
$('#type-dropdown-display')
|
||||
.html(`
|
||||
<span>${this.text}</span>
|
||||
@ -154,19 +148,17 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, moment, gra
|
||||
|
||||
scope.$parent.isFailed = true;
|
||||
scope.$parent.isSuccessful = true;
|
||||
recreateGraph(period, job_type);
|
||||
recreateGraph(scope.period, this.getAttribute("id"), scope.status);
|
||||
});
|
||||
|
||||
$('.o').off('click').on('click', function() {
|
||||
var job_status = this.getAttribute('id');
|
||||
|
||||
$('#status-dropdown-display')
|
||||
.html(`
|
||||
<span>${this.text}</span>
|
||||
<i class="fa fa-angle-down DashboardGraphs-filterIcon"></i>
|
||||
`);
|
||||
|
||||
recreateGraph(scope.period, scope.jobType, job_status);
|
||||
recreateGraph(scope.period, scope.jobType, this.getAttribute("id"));
|
||||
});
|
||||
|
||||
adjustGraphSize(job_status_chart, element);
|
||||
|
||||
@ -4,95 +4,44 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
["Rest",
|
||||
"GetBasePath",
|
||||
"ProcessErrors",
|
||||
"$rootScope",
|
||||
"$q",
|
||||
"$timeout",
|
||||
JobStatusGraphData];
|
||||
|
||||
function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope, $q, $timeout) {
|
||||
|
||||
function getData(period, jobType, status) {
|
||||
var url, dash_path = getBasePath('dashboard');
|
||||
if(dash_path === '' ){
|
||||
processErrors(null,
|
||||
null,
|
||||
null,
|
||||
null, {
|
||||
hdr: 'Error!',
|
||||
msg: "There was an error. Please try again."
|
||||
});
|
||||
return;
|
||||
}
|
||||
url = dash_path + 'graphs/jobs/?period='+period+'&job_type='+jobType;
|
||||
Rest.setHeader({'X-WS-Session-Quiet': true});
|
||||
Rest.setUrl(url);
|
||||
return Rest.get()
|
||||
.then(function(value) {
|
||||
if(status === "successful" || status === "failed"){
|
||||
delete value.data.jobs[status];
|
||||
}
|
||||
return value.data;
|
||||
})
|
||||
.catch(function(response) {
|
||||
var errorMessage = 'Failed to get: ' + response.url + ' GET returned: ' + response.status;
|
||||
|
||||
processErrors(null,
|
||||
response.data,
|
||||
response.status,
|
||||
null, {
|
||||
hdr: 'Error!',
|
||||
msg: errorMessage
|
||||
});
|
||||
return $q.reject(response);
|
||||
});
|
||||
}
|
||||
export default ["Rest", "GetBasePath", "ProcessErrors", "$q", JobStatusGraphData];
|
||||
|
||||
function JobStatusGraphData(Rest, getBasePath, processErrors, $q) {
|
||||
return {
|
||||
pendingRefresh: false,
|
||||
refreshTimerRunning: false,
|
||||
refreshTimer: angular.noop,
|
||||
destroyWatcher: angular.noop,
|
||||
setupWatcher: function(period, jobType) {
|
||||
const that = this;
|
||||
that.destroyWatcher =
|
||||
$rootScope.$on('ws-jobs', function() {
|
||||
if (!that.refreshTimerRunning) {
|
||||
that.timebandGetData(period, jobType);
|
||||
} else {
|
||||
that.pendingRefresh = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
timebandGetData: function(period, jobType) {
|
||||
getData(period, jobType).then(function(result) {
|
||||
$rootScope.
|
||||
$broadcast('DataReceived:JobStatusGraph',
|
||||
result);
|
||||
return result;
|
||||
});
|
||||
this.pendingRefresh = false;
|
||||
this.refreshTimerRunning = true;
|
||||
this.refreshTimer = $timeout(() => {
|
||||
if (this.pendingRefresh) {
|
||||
this.timebandGetData(period, jobType);
|
||||
} else {
|
||||
this.refreshTimerRunning = false;
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
get: function(period, jobType, status) {
|
||||
|
||||
this.destroyWatcher();
|
||||
$timeout.cancel(this.refreshTimer);
|
||||
this.refreshTimerRunning = false;
|
||||
this.pendingRefresh = false;
|
||||
this.setupWatcher(period, jobType);
|
||||
|
||||
return getData(period, jobType, status);
|
||||
var url, dash_path = getBasePath('dashboard');
|
||||
if(dash_path === '' ){
|
||||
processErrors(null,
|
||||
null,
|
||||
null,
|
||||
null, {
|
||||
hdr: 'Error!',
|
||||
msg: "There was an error. Please try again."
|
||||
});
|
||||
return;
|
||||
}
|
||||
url = dash_path + 'graphs/jobs/?period='+period+'&job_type='+jobType;
|
||||
Rest.setHeader({'X-WS-Session-Quiet': true});
|
||||
Rest.setUrl(url);
|
||||
return Rest.get()
|
||||
.then(function(value) {
|
||||
if(status === "successful" || status === "failed"){
|
||||
delete value.data.jobs[status];
|
||||
}
|
||||
return value.data;
|
||||
})
|
||||
.catch(function(response) {
|
||||
var errorMessage = 'Failed to get: ' + response.url + ' GET returned: ' + response.status;
|
||||
|
||||
processErrors(null,
|
||||
response.data,
|
||||
response.status,
|
||||
null, {
|
||||
hdr: 'Error!',
|
||||
msg: errorMessage
|
||||
});
|
||||
return $q.reject(response);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
export default ['$scope','Wait', '$timeout', 'i18n',
|
||||
'Rest', 'GetBasePath', 'ProcessErrors', 'graphData',
|
||||
function($scope, $rootScope, Wait, $timeout,
|
||||
function($scope, Wait, $timeout, i18n,
|
||||
Rest, GetBasePath, ProcessErrors, graphData) {
|
||||
|
||||
var dataCount = 0;
|
||||
@ -53,16 +53,10 @@ export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
$scope.removeDashboardReady = $scope.$on('dashboardReady', function (e, data) {
|
||||
$scope.dashboardCountsData = data;
|
||||
$scope.graphData = graphData;
|
||||
$scope.graphData.period = "month";
|
||||
$scope.graphData.jobType = "all";
|
||||
$scope.graphData.status = "both";
|
||||
$scope.$emit('dashboardDataLoadComplete');
|
||||
|
||||
var cleanupJobListener =
|
||||
$rootScope.$on('DataReceived:JobStatusGraph', function(e, data) {
|
||||
$scope.graphData.jobStatus = data;
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
cleanupJobListener();
|
||||
});
|
||||
});
|
||||
|
||||
if ($scope.removeDashboardJobsListReady) {
|
||||
@ -81,40 +75,6 @@ export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
$scope.$emit('dashboardDataLoadComplete');
|
||||
});
|
||||
|
||||
$scope.refresh = function () {
|
||||
Wait('start');
|
||||
Rest.setUrl(GetBasePath('dashboard'));
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
$scope.dashboardData = data;
|
||||
$scope.$emit('dashboardReady', data);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard: ' + status });
|
||||
});
|
||||
Rest.setUrl(GetBasePath("unified_jobs") + "?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job");
|
||||
Rest.setHeader({'X-WS-Session-Quiet': true});
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
data = data.results;
|
||||
$scope.$emit('dashboardJobsListReady', data);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status });
|
||||
});
|
||||
Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template");
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
data = data.results;
|
||||
$scope.$emit('dashboardJobTemplatesListReady', data);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard job templates list: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
function refreshLists () {
|
||||
Rest.setUrl(GetBasePath('dashboard'));
|
||||
Rest.get()
|
||||
@ -122,7 +82,7 @@ export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
$scope.dashboardData = data;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard host graph data: ' + status });
|
||||
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard host graph data: ${status}`) });
|
||||
});
|
||||
|
||||
Rest.setUrl(GetBasePath("unified_jobs") + "?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job");
|
||||
@ -132,7 +92,7 @@ export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
$scope.dashboardJobsListData = data.results;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status });
|
||||
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard jobs list: ${status}`) });
|
||||
});
|
||||
|
||||
Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template");
|
||||
@ -141,8 +101,24 @@ export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
$scope.dashboardJobTemplatesListData = data.results;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status });
|
||||
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard jobs list: ${status}`) });
|
||||
});
|
||||
|
||||
if ($scope.graphData) {
|
||||
Rest.setUrl(`${GetBasePath('dashboard')}graphs/jobs/?period=${$scope.graphData.period}&job_type=${$scope.graphData.jobType}`);
|
||||
Rest.setHeader({'X-WS-Session-Quiet': true});
|
||||
Rest.get()
|
||||
.then(function(value) {
|
||||
if($scope.graphData.status === "successful" || $scope.graphData.status === "failed"){
|
||||
delete value.data.jobs[$scope.graphData.status];
|
||||
}
|
||||
$scope.graphData.jobStatus = value.data;
|
||||
})
|
||||
.catch(function({data, status}) {
|
||||
ProcessErrors(null, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard graph data: ${status}`)});
|
||||
});
|
||||
}
|
||||
|
||||
pendingRefresh = false;
|
||||
refreshTimerRunning = true;
|
||||
$timeout(() => {
|
||||
@ -154,5 +130,35 @@ export default ['$scope', '$rootScope','Wait', '$timeout',
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
Wait('start');
|
||||
Rest.setUrl(GetBasePath('dashboard'));
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
$scope.dashboardData = data;
|
||||
$scope.$emit('dashboardReady', data);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard: ${status}`) });
|
||||
});
|
||||
Rest.setUrl(GetBasePath("unified_jobs") + "?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job");
|
||||
Rest.setHeader({'X-WS-Session-Quiet': true});
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
data = data.results;
|
||||
$scope.$emit('dashboardJobsListReady', data);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard jobs list: ${status}`) });
|
||||
});
|
||||
Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template");
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
data = data.results;
|
||||
$scope.$emit('dashboardJobTemplatesListReady', data);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n._(`Failed to get dashboard job templates list: ${status}`) });
|
||||
});
|
||||
|
||||
}
|
||||
];
|
||||
|
||||
@ -10,12 +10,7 @@ export default {
|
||||
params: { licenseMissing: null },
|
||||
data: {
|
||||
activityStream: true,
|
||||
refreshButton: true,
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
refreshButton: true
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: N_("DASHBOARD")
|
||||
|
||||
@ -112,6 +112,7 @@ export default
|
||||
$rootScope.login_username = null;
|
||||
$rootScope.login_password = null;
|
||||
$rootScope.userLoggedOut = true;
|
||||
$rootScope.pendingApprovalCount = 0;
|
||||
if ($rootScope.sessionTimer) {
|
||||
$rootScope.sessionTimer.clearTimers();
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ export default
|
||||
timeout: null,
|
||||
|
||||
getSessionTime: function () {
|
||||
if(Store('sessionTime')){
|
||||
if(Store('sessionTime') && Store('sessionTime')[$rootScope.current_user.id]){
|
||||
return Store('sessionTime')[$rootScope.current_user.id].time;
|
||||
}
|
||||
else {
|
||||
|
||||
@ -39,14 +39,14 @@
|
||||
* This is usage information.
|
||||
*/
|
||||
|
||||
export default ['$log', '$cookies', '$compile', '$rootScope',
|
||||
export default ['$log', '$cookies', '$rootScope', 'ProcessErrors',
|
||||
'$location', 'Authorization', 'Alert', 'Wait', 'Timer',
|
||||
'Empty', '$scope', 'pendoService', 'ConfigService',
|
||||
'CheckLicense', 'SocketService',
|
||||
function ($log, $cookies, $compile, $rootScope, $location,
|
||||
Authorization, Alert, Wait, Timer, Empty,
|
||||
scope, pendoService, ConfigService, CheckLicense,
|
||||
SocketService) {
|
||||
'CheckLicense', 'SocketService', 'Rest', 'GetBasePath', 'i18n',
|
||||
function ($log, $cookies, $rootScope, ProcessErrors,
|
||||
$location, Authorization, Alert, Wait, Timer,
|
||||
Empty, scope, pendoService, ConfigService,
|
||||
CheckLicense, SocketService, Rest, GetBasePath, i18n) {
|
||||
var lastPath, lastUser, sessionExpired, loginAgain, preAuthUrl;
|
||||
|
||||
loginAgain = function() {
|
||||
@ -132,6 +132,18 @@ export default ['$log', '$cookies', '$compile', '$rootScope',
|
||||
$rootScope.user_is_system_auditor = data.results[0].is_system_auditor;
|
||||
scope.$emit('AuthorizationGetLicense');
|
||||
});
|
||||
|
||||
Rest.setUrl(`${GetBasePath('workflow_approvals')}?status=pending&page_size=1`);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
$rootScope.pendingApprovalCount = data.count;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors({}, data, status, null, {
|
||||
hdr: i18n._('Error!'),
|
||||
msg: i18n._('Failed to get workflow jobs pending approval. GET returned status: ') + status
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
Authorization.logout().then( () => {
|
||||
|
||||
@ -175,12 +175,7 @@ let lists = [{
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'organization',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "organizations.edit",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'QuerySet', '$interpolate',
|
||||
function($scope, $stateParams, $state, $filter, GetBasePath, qs, $interpolate) {
|
||||
export default ['$scope', '$stateParams', '$state', 'GetBasePath', 'QuerySet', '$interpolate',
|
||||
function($scope, $stateParams, $state, GetBasePath, qs, $interpolate) {
|
||||
|
||||
let pageSize = $scope.querySet ? $scope.querySet.page_size || 20 : $stateParams[`${$scope.iterator}_search`].page_size || 20,
|
||||
queryset, path;
|
||||
|
||||
@ -96,6 +96,14 @@ export default
|
||||
$log.debug('Received From Server: ' + e.data);
|
||||
|
||||
var data = JSON.parse(e.data), str = "";
|
||||
|
||||
if (data.group_name === 'jobs' &&
|
||||
'type' in data &&
|
||||
data.type === 'workflow_approval'
|
||||
) {
|
||||
$rootScope.$broadcast('ws-approval');
|
||||
}
|
||||
|
||||
if(!window.liveUpdates && data.group_name !== "control" && $state.current.name !== "output"){
|
||||
$log.debug('Message from server dropped: ' + e.data);
|
||||
needsRefreshAfterBlur = true;
|
||||
@ -254,21 +262,23 @@ export default
|
||||
// requires a subscribe or an unsubscribe
|
||||
var self = this;
|
||||
return socketPromise.promise.then(function(){
|
||||
if(!state.data || !state.data.socket){
|
||||
_.merge(state.data, {socket: {groups: {}}});
|
||||
self.unsubscribe(state);
|
||||
if (_.get(state, 'data.socket.groups.jobs')) {
|
||||
if (!state.data.socket.groups.jobs.includes("status_changed")) {
|
||||
state.data.socket.groups.jobs.push("status_changed");
|
||||
}
|
||||
}
|
||||
else{
|
||||
["job_events", "ad_hoc_command_events", "workflow_events",
|
||||
else if(!state.data || !state.data.socket){
|
||||
_.merge(state.data, {socket: {groups: {jobs: ["status_changed"]}}});
|
||||
}
|
||||
["job_events", "ad_hoc_command_events", "workflow_events",
|
||||
"project_update_events", "inventory_update_events",
|
||||
"system_job_events"
|
||||
].forEach(function(group) {
|
||||
if(state.data && state.data.socket && state.data.socket.groups.hasOwnProperty(group)){
|
||||
state.data.socket.groups[group] = [id];
|
||||
}
|
||||
});
|
||||
self.subscribe(state);
|
||||
}
|
||||
].forEach(function(group) {
|
||||
if(state.data && state.data.socket && state.data.socket.groups.hasOwnProperty(group)){
|
||||
state.data.socket.groups[group] = [id];
|
||||
}
|
||||
});
|
||||
self.subscribe(state);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import workflowEdit from './workflows/edit-workflow/main';
|
||||
import labels from './labels/main';
|
||||
import prompt from './prompt/main';
|
||||
import workflowChart from './workflows/workflow-chart/main';
|
||||
import workflowKey from './workflows/workflow-key/main';
|
||||
import workflowMaker from './workflows/workflow-maker/main';
|
||||
import workflowControls from './workflows/workflow-controls/main';
|
||||
import WorkflowForm from './workflows.form';
|
||||
@ -31,7 +32,7 @@ import {
|
||||
|
||||
export default
|
||||
angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, prompt.name, workflowAdd.name, workflowEdit.name,
|
||||
workflowChart.name, workflowMaker.name, workflowControls.name
|
||||
workflowChart.name, workflowKey.name, workflowMaker.name, workflowControls.name
|
||||
])
|
||||
.service('TemplatesService', templatesService)
|
||||
.factory('WorkflowForm', WorkflowForm)
|
||||
@ -499,320 +500,7 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
||||
views: {
|
||||
'modal': {
|
||||
template: `<workflow-maker ng-if="includeWorkflowMaker" workflow-job-template-obj="workflow_job_template_obj" can-add-or-edit="canAddOrEdit"></workflow-maker>`
|
||||
},
|
||||
'jobTemplateList@templates.editWorkflowJobTemplate.workflowMaker': {
|
||||
templateProvider: function(WorkflowMakerJobTemplateList, generateList) {
|
||||
|
||||
let html = generateList.build({
|
||||
list: WorkflowMakerJobTemplateList,
|
||||
input_type: 'radio',
|
||||
mode: 'lookup'
|
||||
});
|
||||
return html;
|
||||
},
|
||||
// $scope encapsulated in this controller will be a initialized as child of 'modal' $scope, because of element hierarchy
|
||||
controller: ['$scope', 'WorkflowMakerJobTemplateList', 'JobTemplateDataset',
|
||||
function($scope, list, Dataset) {
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.list = list;
|
||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
|
||||
$scope.$watch('wf_maker_templates', function(){
|
||||
if($scope.selectedTemplate){
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
if(row.id === $scope.selectedTemplate.id) {
|
||||
$scope.wf_maker_templates[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) {
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
if (row.id === selectedRow.id) {
|
||||
$scope.wf_maker_templates[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
$scope.templateManuallySelected(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTemplate', () => {
|
||||
$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.wf_maker_templates[i].checked = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('activeTab', () => {
|
||||
if(!$scope.activeTab || $scope.activeTab !== "jobs") {
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('clearWorkflowLists', function() {
|
||||
$scope.wf_maker_templates.forEach(function(row, i) {
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
]
|
||||
},
|
||||
'inventorySyncList@templates.editWorkflowJobTemplate.workflowMaker': {
|
||||
templateProvider: function(WorkflowInventorySourcesList, generateList) {
|
||||
let html = generateList.build({
|
||||
list: WorkflowInventorySourcesList,
|
||||
input_type: 'radio',
|
||||
mode: 'lookup'
|
||||
});
|
||||
return html;
|
||||
},
|
||||
// encapsulated $scope in this controller will be a initialized as child of 'modal' $scope, because of element hierarchy
|
||||
controller: ['$scope', 'WorkflowInventorySourcesList', 'InventorySourcesDataset',
|
||||
function($scope, list, Dataset) {
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.list = list;
|
||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
|
||||
$scope.$watch('wf_maker_inventory_sources', function(){
|
||||
if($scope.selectedTemplate){
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
if(row.id === $scope.selectedTemplate.id) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
if (row.id === selectedRow.id) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
$scope.templateManuallySelected(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTemplate', () => {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('activeTab', () => {
|
||||
if(!$scope.activeTab || $scope.activeTab !== "inventory_sync") {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('clearWorkflowLists', function() {
|
||||
$scope.wf_maker_inventory_sources.forEach(function(row, i) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
]
|
||||
},
|
||||
'projectSyncList@templates.editWorkflowJobTemplate.workflowMaker': {
|
||||
templateProvider: function(WorkflowProjectList, generateList) {
|
||||
let html = generateList.build({
|
||||
list: WorkflowProjectList,
|
||||
input_type: 'radio',
|
||||
mode: 'lookup'
|
||||
});
|
||||
return html;
|
||||
},
|
||||
// encapsulated $scope in this controller will be a initialized as child of 'modal' $scope, because of element hierarchy
|
||||
controller: ['$scope', 'WorkflowProjectList', 'ProjectDataset',
|
||||
function($scope, list, Dataset) {
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.list = list;
|
||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
|
||||
$scope.$watch('wf_maker_projects', function(){
|
||||
if($scope.selectedTemplate){
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
if(row.id === $scope.selectedTemplate.id) {
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
if (row.id === selectedRow.id) {
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
$scope.templateManuallySelected(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTemplate', () => {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) {
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('activeTab', () => {
|
||||
if(!$scope.activeTab || $scope.activeTab !== "project_sync") {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('clearWorkflowLists', function() {
|
||||
$scope.wf_maker_projects.forEach(function(row, i) {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
JobTemplateDataset: ['WorkflowMakerJobTemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.basePath);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
],
|
||||
ProjectDataset: ['WorkflowProjectList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.basePath);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
],
|
||||
InventorySourcesDataset: ['InventorySourcesList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.basePath);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
],
|
||||
WorkflowMakerJobTemplateList: ['TemplateList', 'i18n',
|
||||
(TemplateList, i18n) => {
|
||||
let list = _.cloneDeep(TemplateList);
|
||||
delete list.actions;
|
||||
delete list.fields.type;
|
||||
delete list.fields.description;
|
||||
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.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
|
||||
};
|
||||
list.maxVisiblePages = 5;
|
||||
list.searchBarFullWidth = true;
|
||||
|
||||
return list;
|
||||
}
|
||||
],
|
||||
WorkflowProjectList: ['ProjectList',
|
||||
(ProjectList) => {
|
||||
let list = _.cloneDeep(ProjectList);
|
||||
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;
|
||||
list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}";
|
||||
list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit';
|
||||
|
||||
return list;
|
||||
}
|
||||
],
|
||||
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 }}";
|
||||
list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit';
|
||||
|
||||
return list;
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -218,6 +218,7 @@ export default [ 'ProcessErrors', 'CredentialTypeModel', 'TemplatesStrings', '$f
|
||||
if(vm.steps[step].tab) {
|
||||
if(vm.steps[step].tab.order === currentTab.order) {
|
||||
vm.steps[step].tab._active = false;
|
||||
vm.steps[step].tab._disabled = true;
|
||||
} else if(vm.steps[step].tab.order === currentTab.order + 1) {
|
||||
activeTab = currentTab;
|
||||
vm.steps[step].tab._active = true;
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
<div class="Prompt-footer">
|
||||
<button id="prompt_cancel" class="Prompt-defaultButton" ng-click="vm.cancel()" ng-show="!vm.readOnlyPrompts">{{:: vm.strings.get('CANCEL') }}</button>
|
||||
<button id="prompt_close" class="Prompt-defaultButton" ng-click="vm.cancel()" ng-show="vm.readOnlyPrompts">{{:: vm.strings.get('CLOSE') }}</button>
|
||||
<button id="prompt_inventory_next" class="Prompt-actionButton" ng-show="vm.steps.inventory.tab._active" ng-click="vm.next(vm.steps.inventory.tab)" ng-disabled="vm.promptData.templateType === 'workflow_job_template' && !vm.promptDataClone.prompts.inventory.value.id && vm.promptDataClone.launchConf.defaults.inventory.id && !vm.readOnlyPrompts">{{:: vm.strings.get('NEXT') }}</button>
|
||||
<button id="prompt_inventory_next" class="Prompt-actionButton" ng-show="vm.steps.inventory.tab._active" ng-click="vm.next(vm.steps.inventory.tab)" ng-disabled="((vm.promptData.templateType === 'job_template' && !vm.promptDataClone.prompts.inventory.value.id) || (vm.promptData.templateType === 'workflow_job_template' && !vm.promptDataClone.prompts.inventory.value.id && vm.promptDataClone.launchConf.defaults.inventory.id)) && !vm.readOnlyPrompts">{{:: vm.strings.get('NEXT') }}</button>
|
||||
<button id="prompt_credential_next" class="Prompt-actionButton"
|
||||
ng-show="vm.steps.credential.tab._active"
|
||||
ng-click="vm.next(vm.steps.credential.tab)"
|
||||
|
||||
@ -75,221 +75,221 @@ export default ['Rest', 'GetBasePath', '$q', 'NextPage', function(Rest, GetBaseP
|
||||
}).catch(function(response){
|
||||
return $q.reject( response );
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
getAllWorkflowJobTemplateLabels: function(id) {
|
||||
Rest.setUrl(GetBasePath('workflow_job_templates') + id + "/labels?page_size=200");
|
||||
return Rest.get()
|
||||
.then(function(res) {
|
||||
if (res.data.next) {
|
||||
return NextPage({
|
||||
url: res.data.next,
|
||||
arrayOfValues: res.data.results
|
||||
}).then(function(labels) {
|
||||
return labels;
|
||||
}).catch(function(response){
|
||||
return $q.reject( response );
|
||||
});
|
||||
}
|
||||
else {
|
||||
return $q.resolve( res.data.results );
|
||||
}
|
||||
}).catch(function(response){
|
||||
return $q.reject( response );
|
||||
});
|
||||
},
|
||||
getJobTemplate: function(id) {
|
||||
var url = GetBasePath('job_templates');
|
||||
getAllWorkflowJobTemplateLabels: function(id) {
|
||||
Rest.setUrl(GetBasePath('workflow_job_templates') + id + "/labels?page_size=200");
|
||||
return Rest.get()
|
||||
.then(function(res) {
|
||||
if (res.data.next) {
|
||||
return NextPage({
|
||||
url: res.data.next,
|
||||
arrayOfValues: res.data.results
|
||||
}).then(function(labels) {
|
||||
return labels;
|
||||
}).catch(function(response){
|
||||
return $q.reject( response );
|
||||
});
|
||||
}
|
||||
else {
|
||||
return $q.resolve( res.data.results );
|
||||
}
|
||||
}).catch(function(response){
|
||||
return $q.reject( response );
|
||||
});
|
||||
},
|
||||
getJobTemplate: function(id) {
|
||||
var url = GetBasePath('job_templates');
|
||||
|
||||
url = url + id;
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
addWorkflowNode: function(params) {
|
||||
// params.url
|
||||
// params.data
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
addWorkflowNode: function(params) {
|
||||
// params.url
|
||||
// params.data
|
||||
|
||||
Rest.setUrl(params.url);
|
||||
return Rest.post(params.data);
|
||||
},
|
||||
editWorkflowNode: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
Rest.setUrl(params.url);
|
||||
return Rest.post(params.data);
|
||||
},
|
||||
editWorkflowNode: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.id;
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.put(params.data);
|
||||
},
|
||||
getJobTemplateLaunchInfo: function(id) {
|
||||
var url = GetBasePath('job_templates');
|
||||
Rest.setUrl(url);
|
||||
return Rest.put(params.data);
|
||||
},
|
||||
getJobTemplateLaunchInfo: function(id) {
|
||||
var url = GetBasePath('job_templates');
|
||||
|
||||
url = url + id + '/launch';
|
||||
url = url + id + '/launch';
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getWorkflowJobTemplateNodes: function(id, page) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getWorkflowJobTemplateNodes: function(id, page) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + id + '/workflow_nodes?page_size=200';
|
||||
url = url + id + '/workflow_nodes?page_size=200';
|
||||
|
||||
if(page) {
|
||||
url += '&page=' + page;
|
||||
}
|
||||
if(page) {
|
||||
url += '&page=' + page;
|
||||
}
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
updateWorkflowJobTemplate: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
updateWorkflowJobTemplate: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + params.id;
|
||||
url = url + params.id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.patch(params.data);
|
||||
},
|
||||
getWorkflowJobTemplate: function(id) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
Rest.setUrl(url);
|
||||
return Rest.patch(params.data);
|
||||
},
|
||||
getWorkflowJobTemplate: function(id) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + id;
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
deleteWorkflowJobTemplateNode: function(id) {
|
||||
var url = GetBasePath('workflow_job_template_nodes') + id;
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
deleteWorkflowJobTemplateNode: function(id) {
|
||||
var url = GetBasePath('workflow_job_template_nodes') + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.destroy();
|
||||
},
|
||||
disassociateWorkflowNode: function(params) {
|
||||
//params.parentId
|
||||
//params.nodeId
|
||||
//params.edge
|
||||
Rest.setUrl(url);
|
||||
return Rest.destroy();
|
||||
},
|
||||
disassociateWorkflowNode: function(params) {
|
||||
//params.parentId
|
||||
//params.nodeId
|
||||
//params.edge
|
||||
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.parentId;
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.parentId;
|
||||
|
||||
if(params.edge === 'success') {
|
||||
url = url + '/success_nodes';
|
||||
}
|
||||
else if(params.edge === 'failure') {
|
||||
url = url + '/failure_nodes';
|
||||
}
|
||||
else if(params.edge === 'always') {
|
||||
url = url + '/always_nodes';
|
||||
}
|
||||
if(params.edge === 'success') {
|
||||
url = url + '/success_nodes';
|
||||
}
|
||||
else if(params.edge === 'failure') {
|
||||
url = url + '/failure_nodes';
|
||||
}
|
||||
else if(params.edge === 'always') {
|
||||
url = url + '/always_nodes';
|
||||
}
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.post({
|
||||
"id": params.nodeId,
|
||||
"disassociate": true
|
||||
});
|
||||
},
|
||||
associateWorkflowNode: function(params) {
|
||||
//params.parentId
|
||||
//params.nodeId
|
||||
//params.edge
|
||||
Rest.setUrl(url);
|
||||
return Rest.post({
|
||||
"id": params.nodeId,
|
||||
"disassociate": true
|
||||
});
|
||||
},
|
||||
associateWorkflowNode: function(params) {
|
||||
//params.parentId
|
||||
//params.nodeId
|
||||
//params.edge
|
||||
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.parentId;
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.parentId;
|
||||
|
||||
if(params.edge === 'success') {
|
||||
url = url + '/success_nodes';
|
||||
}
|
||||
else if(params.edge === 'failure') {
|
||||
url = url + '/failure_nodes';
|
||||
}
|
||||
else if(params.edge === 'always') {
|
||||
url = url + '/always_nodes';
|
||||
}
|
||||
if(params.edge === 'success') {
|
||||
url = url + '/success_nodes';
|
||||
}
|
||||
else if(params.edge === 'failure') {
|
||||
url = url + '/failure_nodes';
|
||||
}
|
||||
else if(params.edge === 'always') {
|
||||
url = url + '/always_nodes';
|
||||
}
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.post({
|
||||
id: params.nodeId
|
||||
});
|
||||
},
|
||||
getUnifiedJobTemplate: function(id) {
|
||||
var url = GetBasePath('unified_job_templates');
|
||||
Rest.setUrl(url);
|
||||
return Rest.post({
|
||||
id: params.nodeId
|
||||
});
|
||||
},
|
||||
getUnifiedJobTemplate: function(id) {
|
||||
var url = GetBasePath('unified_job_templates');
|
||||
|
||||
url = url + "?id=" + id;
|
||||
url = url + "?id=" + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getCredential: function(id) {
|
||||
var url = GetBasePath('credentials');
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getCredential: function(id) {
|
||||
var url = GetBasePath('credentials');
|
||||
|
||||
url = url + id;
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getInventory: function(id) {
|
||||
var url = GetBasePath('inventory');
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getInventory: function(id) {
|
||||
var url = GetBasePath('inventory');
|
||||
|
||||
url = url + id;
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getWorkflowCopy: function(id) {
|
||||
let url = GetBasePath('workflow_job_templates');
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getWorkflowCopy: function(id) {
|
||||
let url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + id + '/copy';
|
||||
url = url + id + '/copy';
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
copyWorkflow: function(id) {
|
||||
let url = GetBasePath('workflow_job_templates');
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
copyWorkflow: function(id) {
|
||||
let url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + id + '/copy';
|
||||
url = url + id + '/copy';
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.post();
|
||||
},
|
||||
getWorkflowJobTemplateOptions: function() {
|
||||
var deferred = $q.defer();
|
||||
Rest.setUrl(url);
|
||||
return Rest.post();
|
||||
},
|
||||
getWorkflowJobTemplateOptions: function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
let url = GetBasePath('workflow_job_templates');
|
||||
let url = GetBasePath('workflow_job_templates');
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.options()
|
||||
.then(({data}) => {
|
||||
deferred.resolve(data);
|
||||
}).catch(({msg, code}) => {
|
||||
deferred.reject(msg, code);
|
||||
});
|
||||
Rest.setUrl(url);
|
||||
Rest.options()
|
||||
.then(({data}) => {
|
||||
deferred.resolve(data);
|
||||
}).catch(({msg, code}) => {
|
||||
deferred.reject(msg, code);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
getJobTemplateOptions: function() {
|
||||
var deferred = $q.defer();
|
||||
return deferred.promise;
|
||||
},
|
||||
getJobTemplateOptions: function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
let url = GetBasePath('job_templates');
|
||||
let url = GetBasePath('job_templates');
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.options()
|
||||
.then(({data}) => {
|
||||
deferred.resolve(data);
|
||||
}).catch(({msg, code}) => {
|
||||
deferred.reject(msg, code);
|
||||
});
|
||||
Rest.setUrl(url);
|
||||
Rest.options()
|
||||
.then(({data}) => {
|
||||
deferred.resolve(data);
|
||||
}).catch(({msg, code}) => {
|
||||
deferred.reject(msg, code);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
postWorkflowNodeCredential: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
return deferred.promise;
|
||||
},
|
||||
postWorkflowNodeCredential: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.id + '/credentials';
|
||||
var url = GetBasePath('workflow_job_template_nodes') + params.id + '/credentials';
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.post(params.data);
|
||||
}
|
||||
Rest.setUrl(url);
|
||||
return Rest.post(params.data);
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
@ -117,6 +117,7 @@
|
||||
|
||||
.WorkflowChart-nodeTypeLetter {
|
||||
fill: @default-bg;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.WorkflowChart-nodeStatus--running {
|
||||
@ -142,9 +143,12 @@
|
||||
}
|
||||
|
||||
.WorkflowChart-deletedText {
|
||||
width: 90px;
|
||||
width: 180px;
|
||||
height: 14px;
|
||||
color: @default-interface-txt;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.WorkflowChart-activeNode {
|
||||
fill: @default-link;
|
||||
}
|
||||
@ -159,7 +163,25 @@
|
||||
}
|
||||
|
||||
.WorkflowChart-nameText {
|
||||
width: 180px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.WorkflowChart-timedOutText, .WorkflowChart-deniedText {
|
||||
width: 180px;
|
||||
height: 14px;
|
||||
color: @default-err;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.WorkflowChart-approvedText {
|
||||
width: 180px;
|
||||
height: 14px;
|
||||
color: @default-succ;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.WorkflowChart-tooltip {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
11
awx/ui/client/src/templates/workflows/workflow-key/main.js
Normal file
11
awx/ui/client/src/templates/workflows/workflow-key/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2019 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import workflowKey from './workflow-key.directive';
|
||||
|
||||
export default
|
||||
angular.module('workflowKey', [])
|
||||
.directive('workflowKey', workflowKey);
|
||||
@ -0,0 +1,18 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['templateUrl', 'TemplatesStrings',
|
||||
function(templateUrl, TemplatesStrings) {
|
||||
return {
|
||||
scope: {},
|
||||
templateUrl: templateUrl('templates/workflows/workflow-key/workflow-key'),
|
||||
restrict: 'E',
|
||||
link: function(scope) {
|
||||
scope.strings = TemplatesStrings;
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,43 @@
|
||||
<ul class="Key-list noselect">
|
||||
<li class="Key-listItem">
|
||||
<p class="Key-heading">{{:: strings.get('workflow_maker.KEY')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--success"></div>
|
||||
<p class="Key-listItemContent">{{:: strings.get('workflow_maker.ON_SUCCESS')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--fail"></div>
|
||||
<p class="Key-listItemContent">{{:: strings.get('workflow_maker.ON_FAILURE')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--always"></div>
|
||||
<p class="Key-listItemContent">{{:: strings.get('workflow_maker.ALWAYS')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--circle Key-icon--default">JT</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{:: strings.get('workflow_maker.JOB_TEMPLATE')}}</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.get('workflow_maker.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.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--default">
|
||||
<span class="fa fa-pause"></span>
|
||||
</div>
|
||||
<p class="Key-listItemContent Key-listItemContent--circle">{{:: strings.get('workflow_maker.PAUSE')}}</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>
|
||||
</li>
|
||||
</ul>
|
||||
@ -1,7 +1,9 @@
|
||||
import workflowLinkForm from './workflow-link-form.directive';
|
||||
import workflowNodeForm from './workflow-node-form.directive';
|
||||
import workflowNodeFormService from './workflow-node-form.service';
|
||||
|
||||
export default
|
||||
angular.module('templates.workflowMaker.forms', [])
|
||||
.directive('workflowLinkForm', workflowLinkForm)
|
||||
.directive('workflowNodeForm', workflowNodeForm);
|
||||
.directive('workflowNodeForm', workflowNodeForm)
|
||||
.service('WorkflowNodeFormService', workflowNodeFormService);
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
|
||||
export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService', 'Rest', '$q',
|
||||
'TemplatesStrings', 'CreateSelect2', 'Empty', 'QuerySet', '$filter',
|
||||
'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList', 'ProcessErrors',
|
||||
'GetBasePath', 'WorkflowNodeFormService', 'ProcessErrors',
|
||||
'i18n', 'ParseTypeChange', 'WorkflowJobTemplateModel',
|
||||
function($scope, TemplatesService, JobTemplate, PromptService, Rest, $q,
|
||||
TemplatesStrings, CreateSelect2, Empty, qs, $filter,
|
||||
GetBasePath, TemplateList, ProjectList, InventorySourcesList, ProcessErrors,
|
||||
GetBasePath, WorkflowNodeFormService, ProcessErrors,
|
||||
i18n, ParseTypeChange, WorkflowJobTemplate
|
||||
) {
|
||||
|
||||
@ -33,58 +33,13 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
$scope.strings = TemplatesStrings;
|
||||
$scope.editNodeHelpMessage = null;
|
||||
|
||||
let templateList = _.cloneDeep(TemplateList);
|
||||
delete templateList.actions;
|
||||
delete templateList.fields.type;
|
||||
delete templateList.fields.description;
|
||||
delete templateList.fields.smart_status;
|
||||
delete templateList.fields.labels;
|
||||
delete templateList.fieldActions;
|
||||
templateList.name = 'wf_maker_templates';
|
||||
templateList.iterator = 'wf_maker_template';
|
||||
templateList.fields.name.columnClass = "col-md-8";
|
||||
templateList.fields.name.tag = i18n._('WORKFLOW');
|
||||
templateList.fields.name.showTag = "{{wf_maker_template.type === 'workflow_job_template'}}";
|
||||
templateList.disableRow = "{{ readOnly }}";
|
||||
templateList.disableRowValue = 'readOnly';
|
||||
templateList.basePath = 'unified_job_templates';
|
||||
templateList.fields.info = {
|
||||
ngInclude: "'/static/partials/job-template-details.html'",
|
||||
type: 'template',
|
||||
columnClass: 'col-md-3',
|
||||
infoHeaderClass: 'col-md-3',
|
||||
label: '',
|
||||
nosort: true
|
||||
};
|
||||
templateList.maxVisiblePages = 5;
|
||||
templateList.searchBarFullWidth = true;
|
||||
$scope.templateList = templateList;
|
||||
|
||||
let inventorySourceList = _.cloneDeep(InventorySourcesList);
|
||||
inventorySourceList.name = 'wf_maker_inventory_sources';
|
||||
inventorySourceList.iterator = 'wf_maker_inventory_source';
|
||||
inventorySourceList.maxVisiblePages = 5;
|
||||
inventorySourceList.searchBarFullWidth = true;
|
||||
inventorySourceList.disableRow = "{{ readOnly }}";
|
||||
inventorySourceList.disableRowValue = 'readOnly';
|
||||
$scope.inventorySourceList = inventorySourceList;
|
||||
|
||||
let projectList = _.cloneDeep(ProjectList);
|
||||
delete projectList.fields.status;
|
||||
delete projectList.fields.scm_type;
|
||||
delete projectList.fields.last_updated;
|
||||
projectList.name = 'wf_maker_projects';
|
||||
projectList.iterator = 'wf_maker_project';
|
||||
projectList.fields.name.columnClass = "col-md-11";
|
||||
projectList.maxVisiblePages = 5;
|
||||
projectList.searchBarFullWidth = true;
|
||||
projectList.disableRow = "{{ readOnly }}";
|
||||
projectList.disableRowValue = 'readOnly';
|
||||
$scope.projectList = projectList;
|
||||
$scope.templateList = WorkflowNodeFormService.templateListDefinition();
|
||||
$scope.inventorySourceList = WorkflowNodeFormService.inventorySourceListDefinition();
|
||||
$scope.projectList = WorkflowNodeFormService.projectListDefinition();
|
||||
|
||||
const checkCredentialsForRequiredPasswords = () => {
|
||||
let credentialRequiresPassword = false;
|
||||
$scope.promptData.prompts.credentials.value.forEach((credential) => {
|
||||
$scope.jobNodeState.promptData.prompts.credentials.value.forEach((credential) => {
|
||||
if ((credential.passwords_needed &&
|
||||
credential.passwords_needed.length > 0) ||
|
||||
(_.has(credential, 'inputs.vault_password') &&
|
||||
@ -94,86 +49,143 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
}
|
||||
});
|
||||
|
||||
$scope.credentialRequiresPassword = credentialRequiresPassword;
|
||||
$scope.jobNodeState.credentialRequiresPassword = credentialRequiresPassword;
|
||||
};
|
||||
|
||||
const watchForPromptChanges = () => {
|
||||
let promptDataToWatch = [
|
||||
'promptData.prompts.inventory.value',
|
||||
'promptData.prompts.verbosity.value',
|
||||
'missingSurveyValue'
|
||||
'jobNodeState.promptData.prompts.inventory.value',
|
||||
'jobNodeState.promptData.prompts.verbosity.value',
|
||||
'jobNodeState.missingSurveyValue'
|
||||
];
|
||||
|
||||
promptWatcher = $scope.$watchGroup(promptDataToWatch, () => {
|
||||
const templateType = _.get($scope, 'promptData.templateType');
|
||||
const templateType = _.get($scope, 'jobNodeState.promptData.templateType');
|
||||
let missingPromptValue = false;
|
||||
|
||||
if ($scope.missingSurveyValue) {
|
||||
if ($scope.jobNodeState.missingSurveyValue) {
|
||||
missingPromptValue = true;
|
||||
}
|
||||
|
||||
if (templateType !== "workflow_job_template") {
|
||||
if (!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) {
|
||||
if (!$scope.jobNodeState.promptData.prompts.inventory.value || !$scope.jobNodeState.promptData.prompts.inventory.value.id) {
|
||||
missingPromptValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.promptModalMissingReqFields = missingPromptValue;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = missingPromptValue;
|
||||
});
|
||||
|
||||
if ($scope.promptData.launchConf.ask_credential_on_launch && $scope.credentialRequiresPassword) {
|
||||
credentialsWatcher = $scope.$watch('promptData.prompts.credentials', () => {
|
||||
if ($scope.jobNodeState.promptData.launchConf.ask_credential_on_launch && $scope.jobNodeState.credentialRequiresPassword) {
|
||||
credentialsWatcher = $scope.$watch('jobNodeState.promptData.prompts.credentials', () => {
|
||||
checkCredentialsForRequiredPasswords();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const finishConfiguringAdd = () => {
|
||||
$scope.selectedTemplate = null;
|
||||
$scope.activeTab = "jobs";
|
||||
const alwaysOption = {
|
||||
label: $scope.strings.get('workflow_maker.ALWAYS'),
|
||||
value: 'always'
|
||||
};
|
||||
const successOption = {
|
||||
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
|
||||
value: 'success'
|
||||
};
|
||||
const failureOption = {
|
||||
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
|
||||
value: 'failure'
|
||||
};
|
||||
$scope.edgeTypeOptions = [alwaysOption];
|
||||
switch($scope.nodeConfig.newNodeIsRoot) {
|
||||
case true:
|
||||
$scope.edgeType = alwaysOption;
|
||||
break;
|
||||
case false:
|
||||
$scope.edgeType = successOption;
|
||||
$scope.edgeTypeOptions.push(successOption, failureOption);
|
||||
break;
|
||||
const clearWatchers = () => {
|
||||
if (promptWatcher) {
|
||||
promptWatcher();
|
||||
}
|
||||
|
||||
if (surveyQuestionWatcher) {
|
||||
surveyQuestionWatcher();
|
||||
}
|
||||
|
||||
if (credentialsWatcher) {
|
||||
credentialsWatcher();
|
||||
}
|
||||
};
|
||||
|
||||
const select2ifyDropdowns = () => {
|
||||
CreateSelect2({
|
||||
element: '#workflow-node-types',
|
||||
multiple: false
|
||||
});
|
||||
CreateSelect2({
|
||||
element: '#workflow_node_edge',
|
||||
multiple: false
|
||||
});
|
||||
};
|
||||
|
||||
$scope.nodeFormDataLoaded = true;
|
||||
const formatPopOverDetails = (model) => {
|
||||
const popOverDetails = {};
|
||||
popOverDetails.playbook = model.playbook || i18n._('NONE SELECTED');
|
||||
Object.keys(model.summary_fields).forEach(field => {
|
||||
if (field === 'project') {
|
||||
popOverDetails.project = model.summary_fields[field].name || i18n._('NONE SELECTED');
|
||||
}
|
||||
if (field === 'inventory') {
|
||||
popOverDetails.inventory = model.summary_fields[field].name || i18n._('NONE SELECTED');
|
||||
}
|
||||
if (field === 'credentials') {
|
||||
if (model.summary_fields[field].length <= 0) {
|
||||
popOverDetails.credentials = i18n._('NONE SELECTED');
|
||||
}
|
||||
else {
|
||||
const credentialNames = model.summary_fields[field].map(({name}) => name);
|
||||
popOverDetails.credentials = credentialNames.join('<br />');
|
||||
}
|
||||
}
|
||||
});
|
||||
model.popOver = `
|
||||
<table>
|
||||
<tr>
|
||||
<td>${i18n._('INVENTORY')} </td>
|
||||
<td>${$filter('sanitize')(popOverDetails.inventory)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${i18n._('PROJECT')} </td>
|
||||
<td>${$filter('sanitize')(popOverDetails.project)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${i18n._('PLAYBOOK')} </td>
|
||||
<td>${$filter('sanitize')(popOverDetails.playbook)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${i18n._('CREDENTIAL')} </td>
|
||||
<td>${$filter('sanitize')(popOverDetails.credentials)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
};
|
||||
|
||||
const updateSelectedRow = () => {
|
||||
let unifiedJobTemplateId;
|
||||
switch($scope.activeTab) {
|
||||
case 'templates':
|
||||
unifiedJobTemplateId = _.get($scope, 'jobNodeState.selectedTemplate.id') || null;
|
||||
$scope.wf_maker_templates.forEach((row, i) => {
|
||||
if (row.type === 'job_template') {
|
||||
formatPopOverDetails(row);
|
||||
}
|
||||
$scope.wf_maker_templates[i].checked = (row.id === unifiedJobTemplateId) ? 1 : 0;
|
||||
});
|
||||
break;
|
||||
case 'project_syncs':
|
||||
unifiedJobTemplateId = _.get($scope, 'projectNodeState.selectedTemplate.id') || null;
|
||||
$scope.wf_maker_projects.forEach((row, i) => {
|
||||
$scope.wf_maker_projects[i].checked = (row.id === unifiedJobTemplateId) ? 1 : 0;
|
||||
});
|
||||
break;
|
||||
case 'inventory_syncs':
|
||||
unifiedJobTemplateId = _.get($scope, 'inventoryNodeState.selectedTemplate.id') || null;
|
||||
$scope.wf_maker_inventory_sources.forEach((row, i) => {
|
||||
$scope.wf_maker_inventory_sources[i].checked = (row.id === unifiedJobTemplateId) ? 1 : 0;
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getEditNodeHelpMessage = (selectedTemplate, workflowJobTemplateObj) => {
|
||||
if (selectedTemplate) {
|
||||
if (selectedTemplate.type === "workflow_job_template") {
|
||||
if (workflowJobTemplateObj.inventory) {
|
||||
if (selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_WILL_OVERRIDE');
|
||||
}
|
||||
if (workflowJobTemplateObj.inventory && selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_WILL_OVERRIDE');
|
||||
}
|
||||
|
||||
if (workflowJobTemplateObj.ask_inventory_on_launch) {
|
||||
if (selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_OVERRIDE');
|
||||
}
|
||||
if (workflowJobTemplateObj.ask_inventory_on_launch && selectedTemplate.ask_inventory_on_launch) {
|
||||
return $scope.strings.get('workflow_maker.INVENTORY_PROMPT_WILL_OVERRIDE');
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,19 +221,19 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
let jobTemplate = templateType === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
|
||||
|
||||
if (_.get($scope, 'nodeConfig.node.promptData') && !_.isEmpty($scope.nodeConfig.node.promptData)) {
|
||||
$scope.promptData = _.cloneDeep($scope.nodeConfig.node.promptData);
|
||||
const launchConf = $scope.promptData.launchConf;
|
||||
$scope.jobNodeState.promptData = _.cloneDeep($scope.nodeConfig.node.promptData);
|
||||
const launchConf = $scope.jobNodeState.promptData.launchConf;
|
||||
|
||||
if (!shouldShowPromptButton(launchConf)) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
$scope.jobNodeState.showPromptButton = false;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = false;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
$scope.jobNodeState.showPromptButton = true;
|
||||
|
||||
if (templateType !== "workflow_job_template" && launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = true;
|
||||
} else {
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = false;
|
||||
}
|
||||
}
|
||||
watchForPromptChanges();
|
||||
@ -285,9 +297,9 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
((!$scope.nodeConfig.node.fullUnifiedJobTemplateObject.inventory && !launchConf.ask_inventory_on_launch) ||
|
||||
!$scope.nodeConfig.node.fullUnifiedJobTemplateObject.project)
|
||||
) {
|
||||
$scope.selectedTemplateInvalid = true;
|
||||
$scope.jobNodeState.selectedTemplateInvalid = true;
|
||||
} else {
|
||||
$scope.selectedTemplateInvalid = false;
|
||||
$scope.jobNodeState.selectedTemplateInvalid = false;
|
||||
}
|
||||
|
||||
let credentialRequiresPassword = false;
|
||||
@ -306,19 +318,19 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
}
|
||||
});
|
||||
|
||||
$scope.credentialRequiresPassword = credentialRequiresPassword;
|
||||
$scope.jobNodeState.credentialRequiresPassword = credentialRequiresPassword;
|
||||
|
||||
if (!shouldShowPromptButton(launchConf)) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
$scope.jobNodeState.showPromptButton = false;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = false;
|
||||
$scope.nodeFormDataLoaded = true;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
$scope.jobNodeState.showPromptButton = true;
|
||||
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = true;
|
||||
} else {
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = false;
|
||||
}
|
||||
|
||||
if (responses[1].data.survey_enabled) {
|
||||
@ -331,7 +343,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
extra_data: jsyaml.safeLoad(prompts.variables.value)
|
||||
});
|
||||
|
||||
$scope.missingSurveyValue = processed.missingSurveyValue;
|
||||
$scope.jobNodeState.missingSurveyValue = processed.missingSurveyValue;
|
||||
|
||||
$scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data);
|
||||
|
||||
@ -341,7 +353,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
value: $scope.extraVars
|
||||
};
|
||||
|
||||
$scope.nodeConfig.node.promptData = $scope.promptData = {
|
||||
$scope.nodeConfig.node.promptData = $scope.jobNodeState.promptData = {
|
||||
launchConf: launchConf,
|
||||
launchOptions: launchOptions,
|
||||
prompts: prompts,
|
||||
@ -350,14 +362,14 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
template: $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id
|
||||
};
|
||||
|
||||
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
|
||||
surveyQuestionWatcher = $scope.$watch('jobNodeState.promptData.surveyQuestions', () => {
|
||||
let missingSurveyValue = false;
|
||||
_.each($scope.promptData.surveyQuestions, (question) => {
|
||||
_.each($scope.jobNodeState.promptData.surveyQuestions, (question) => {
|
||||
if (question.required && (Empty(question.model) || question.model === [])) {
|
||||
missingSurveyValue = true;
|
||||
}
|
||||
});
|
||||
$scope.missingSurveyValue = missingSurveyValue;
|
||||
$scope.jobNodeState.missingSurveyValue = missingSurveyValue;
|
||||
}, true);
|
||||
|
||||
checkCredentialsForRequiredPasswords();
|
||||
@ -367,7 +379,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
$scope.nodeFormDataLoaded = true;
|
||||
});
|
||||
} else {
|
||||
$scope.nodeConfig.node.promptData = $scope.promptData = {
|
||||
$scope.nodeConfig.node.promptData = $scope.jobNodeState.promptData = {
|
||||
launchConf: launchConf,
|
||||
launchOptions: launchOptions,
|
||||
prompts: prompts,
|
||||
@ -388,39 +400,46 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
}
|
||||
|
||||
if (_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject')) {
|
||||
$scope.selectedTemplate = $scope.nodeConfig.node.fullUnifiedJobTemplateObject;
|
||||
const selectedTemplate = $scope.nodeConfig.node.fullUnifiedJobTemplateObject;
|
||||
|
||||
if ($scope.selectedTemplate.unified_job_type) {
|
||||
switch ($scope.selectedTemplate.unified_job_type) {
|
||||
if (selectedTemplate.unified_job_type) {
|
||||
switch (selectedTemplate.unified_job_type) {
|
||||
case "job":
|
||||
$scope.activeTab = "jobs";
|
||||
$scope.activeTab = "templates";
|
||||
$scope.jobNodeState.selectedTemplate = selectedTemplate;
|
||||
break;
|
||||
case "project_update":
|
||||
$scope.activeTab = "project_syncs";
|
||||
$scope.projectNodeState.selectedTemplate = selectedTemplate;
|
||||
break;
|
||||
case "inventory_update":
|
||||
$scope.activeTab = "inventory_syncs";
|
||||
$scope.inventoryNodeState.selectedTemplate = selectedTemplate;
|
||||
break;
|
||||
}
|
||||
} else if ($scope.selectedTemplate.type) {
|
||||
switch ($scope.selectedTemplate.type) {
|
||||
} else if (selectedTemplate.type) {
|
||||
switch (selectedTemplate.type) {
|
||||
case "job_template":
|
||||
$scope.activeTab = "jobs";
|
||||
break;
|
||||
case "workflow_job_template":
|
||||
$scope.activeTab = "jobs";
|
||||
$scope.activeTab = "templates";
|
||||
$scope.jobNodeState.selectedTemplate = selectedTemplate;
|
||||
break;
|
||||
case "project":
|
||||
$scope.activeTab = "project_syncs";
|
||||
$scope.projectNodeState.selectedTemplate = selectedTemplate;
|
||||
break;
|
||||
case "inventory_source":
|
||||
$scope.activeTab = "inventory_syncs";
|
||||
$scope.inventoryNodeState.selectedTemplate = selectedTemplate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateSelectedRow();
|
||||
} else {
|
||||
$scope.activeTab = "jobs";
|
||||
$scope.activeTab = "templates";
|
||||
}
|
||||
|
||||
select2ifyDropdowns();
|
||||
} else {
|
||||
$scope.jobTags = $scope.nodeConfig.node.originalNodeObject.job_tags ? $scope.nodeConfig.node.originalNodeObject.job_tags.split(',').map((tag) => (tag)) : [];
|
||||
$scope.skipTags = $scope.nodeConfig.node.originalNodeObject.skip_tags ? $scope.nodeConfig.node.originalNodeObject.skip_tags.split(',').map((tag) => (tag)) : [];
|
||||
@ -448,120 +467,31 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
|
||||
};
|
||||
|
||||
const templateManuallySelected = (selectedTemplate) => {
|
||||
|
||||
if (promptWatcher) {
|
||||
promptWatcher();
|
||||
}
|
||||
|
||||
if (surveyQuestionWatcher) {
|
||||
surveyQuestionWatcher();
|
||||
}
|
||||
|
||||
if (credentialsWatcher) {
|
||||
credentialsWatcher();
|
||||
}
|
||||
|
||||
$scope.promptData = null;
|
||||
$scope.editNodeHelpMessage = getEditNodeHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj);
|
||||
|
||||
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;
|
||||
|
||||
let credentialRequiresPassword = false;
|
||||
let selectedTemplateInvalid = false;
|
||||
|
||||
if (selectedTemplate.type !== "workflow_job_template") {
|
||||
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
|
||||
selectedTemplateInvalid = true;
|
||||
}
|
||||
|
||||
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
|
||||
credentialRequiresPassword = true;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.credentialRequiresPassword = credentialRequiresPassword;
|
||||
$scope.selectedTemplateInvalid = selectedTemplateInvalid;
|
||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||
|
||||
if (!shouldShowPromptButton(launchConf)) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
|
||||
if (selectedTemplate.type !== "workflow_job_template") {
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (launchConf.survey_enabled) {
|
||||
// go out and get the survey questions
|
||||
jobTemplate.getSurveyQuestions(selectedTemplate.id)
|
||||
.then((surveyQuestionRes) => {
|
||||
|
||||
let processed = PromptService.processSurveyQuestions({
|
||||
surveyQuestions: surveyQuestionRes.data.spec
|
||||
});
|
||||
|
||||
$scope.missingSurveyValue = processed.missingSurveyValue;
|
||||
|
||||
$scope.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data,
|
||||
surveyQuestions: processed.surveyQuestions,
|
||||
template: selectedTemplate.id,
|
||||
templateType: selectedTemplate.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data
|
||||
}),
|
||||
};
|
||||
|
||||
surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => {
|
||||
let missingSurveyValue = false;
|
||||
_.each($scope.promptData.surveyQuestions, (question) => {
|
||||
if (question.required && (Empty(question.model) || question.model === [])) {
|
||||
missingSurveyValue = true;
|
||||
}
|
||||
});
|
||||
$scope.missingSurveyValue = missingSurveyValue;
|
||||
}, true);
|
||||
|
||||
watchForPromptChanges();
|
||||
});
|
||||
} else {
|
||||
$scope.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data,
|
||||
template: selectedTemplate.id,
|
||||
templateType: selectedTemplate.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data
|
||||
}),
|
||||
};
|
||||
|
||||
watchForPromptChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||
$scope.selectedTemplateInvalid = false;
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setupNodeForm = () => {
|
||||
$scope.jobNodeState = {
|
||||
credentialRequiresPassword: false,
|
||||
missingSurveyValue: false,
|
||||
promptData: null,
|
||||
promptModalMissingReqFields: false,
|
||||
searchTags: [],
|
||||
selectedTemplate: null,
|
||||
selectedTemplateInvalid: false,
|
||||
showPromptButton: false
|
||||
};
|
||||
$scope.projectNodeState = {
|
||||
searchTags: [],
|
||||
selectedTemplate: null
|
||||
};
|
||||
$scope.inventoryNodeState = {
|
||||
searchTags: [],
|
||||
selectedTemplate: null,
|
||||
};
|
||||
$scope.approvalNodeState = {
|
||||
name: null,
|
||||
description: null,
|
||||
timeoutMinutes: 0,
|
||||
timeoutSeconds: 0
|
||||
};
|
||||
$scope.nodeFormDataLoaded = false;
|
||||
$scope.wf_maker_template_queryset = {
|
||||
page_size: '10',
|
||||
@ -619,123 +549,250 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService
|
||||
$q.all(listPromises)
|
||||
.then(() => {
|
||||
if ($scope.nodeConfig.mode === "edit") {
|
||||
// Make sure that we have the full unified job template object
|
||||
if (!$scope.nodeConfig.node.fullUnifiedJobTemplateObject) {
|
||||
// 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
|
||||
TemplatesService.getUnifiedJobTemplate($scope.nodeConfig.node.originalNodeObject.summary_fields.unified_job_template.id)
|
||||
.then(({data}) => {
|
||||
$scope.nodeConfig.node.fullUnifiedJobTemplateObject = data.results[0];
|
||||
finishConfiguringEdit();
|
||||
}, (error) => {
|
||||
ProcessErrors($scope, error.data, error.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get unified job template. GET returned ' +
|
||||
'status: ' + error.status
|
||||
});
|
||||
});
|
||||
if ($scope.nodeConfig.node.unifiedJobTemplate && $scope.nodeConfig.node.unifiedJobTemplate.unified_job_type === "workflow_approval") {
|
||||
$scope.activeTab = "approval";
|
||||
select2ifyDropdowns();
|
||||
|
||||
const timeoutMinutes = Math.floor($scope.nodeConfig.node.unifiedJobTemplate.timeout / 60);
|
||||
const timeoutSeconds = $scope.nodeConfig.node.unifiedJobTemplate.timeout - timeoutMinutes * 60;
|
||||
|
||||
$scope.approvalNodeState = {
|
||||
name: $scope.nodeConfig.node.unifiedJobTemplate.name,
|
||||
description: $scope.nodeConfig.node.unifiedJobTemplate.description,
|
||||
timeoutMinutes,
|
||||
timeoutSeconds
|
||||
};
|
||||
|
||||
$scope.nodeFormDataLoaded = true;
|
||||
} else {
|
||||
finishConfiguringEdit();
|
||||
// Make sure that we have the full unified job template object
|
||||
if (!$scope.nodeConfig.node.fullUnifiedJobTemplateObject && _.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.unified_job_template')) {
|
||||
// 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
|
||||
TemplatesService.getUnifiedJobTemplate($scope.nodeConfig.node.originalNodeObject.summary_fields.unified_job_template.id)
|
||||
.then(({data}) => {
|
||||
$scope.nodeConfig.node.fullUnifiedJobTemplateObject = data.results[0];
|
||||
finishConfiguringEdit();
|
||||
}, (error) => {
|
||||
ProcessErrors($scope, error.data, error.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get unified job template. GET returned ' +
|
||||
'status: ' + error.status
|
||||
});
|
||||
});
|
||||
} else {
|
||||
finishConfiguringEdit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finishConfiguringAdd();
|
||||
$scope.activeTab = "templates";
|
||||
const alwaysOption = {
|
||||
label: $scope.strings.get('workflow_maker.ALWAYS'),
|
||||
value: 'always'
|
||||
};
|
||||
const successOption = {
|
||||
label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
|
||||
value: 'success'
|
||||
};
|
||||
const failureOption = {
|
||||
label: $scope.strings.get('workflow_maker.ON_FAILURE'),
|
||||
value: 'failure'
|
||||
};
|
||||
$scope.edgeTypeOptions = [alwaysOption];
|
||||
switch($scope.nodeConfig.newNodeIsRoot) {
|
||||
case true:
|
||||
$scope.edgeType = alwaysOption;
|
||||
break;
|
||||
case false:
|
||||
$scope.edgeType = successOption;
|
||||
$scope.edgeTypeOptions.push(successOption, failureOption);
|
||||
break;
|
||||
}
|
||||
select2ifyDropdowns();
|
||||
|
||||
$scope.nodeFormDataLoaded = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const formatPopOverDetails = (model) => {
|
||||
const popOverDetails = {};
|
||||
popOverDetails.playbook = model.playbook || i18n._('NONE SELECTED');
|
||||
Object.keys(model.summary_fields).forEach(field => {
|
||||
if (field === 'project') {
|
||||
popOverDetails.project = model.summary_fields[field].name || i18n._('NONE SELECTED');
|
||||
}
|
||||
if (field === 'inventory') {
|
||||
popOverDetails.inventory = model.summary_fields[field].name || i18n._('NONE SELECTED');
|
||||
}
|
||||
if (field === 'credentials') {
|
||||
if (model.summary_fields[field].length <= 0) {
|
||||
popOverDetails.credentials = i18n._('NONE SELECTED');
|
||||
}
|
||||
else {
|
||||
const credentialNames = model.summary_fields[field].map(({name}) => name);
|
||||
popOverDetails.credentials = credentialNames.join('<br />');
|
||||
}
|
||||
}
|
||||
});
|
||||
model.popOver = `
|
||||
<dl>
|
||||
<dt>${i18n._('INVENTORY')}</dt>
|
||||
<dd>${$filter('sanitize')(popOverDetails.inventory)}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>${i18n._('PROJECT')}</dt>
|
||||
<dd>${$filter('sanitize')(popOverDetails.project)}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>${i18n._('PLAYBOOK')}</dt>
|
||||
<dd>${$filter('sanitize')(popOverDetails.playbook)}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>${i18n._('CREDENTIAL')}</dt>
|
||||
<dd>${$filter('sanitize')(popOverDetails.credentials)}</dd>
|
||||
</dl>
|
||||
`;
|
||||
$scope.confirmNodeForm = () => {
|
||||
const nodeFormData = {
|
||||
edgeType: $scope.edgeType
|
||||
};
|
||||
|
||||
if ($scope.activeTab === "approval") {
|
||||
const timeout = $scope.approvalNodeState.timeoutMinutes * 60 + $scope.approvalNodeState.timeoutSeconds;
|
||||
|
||||
nodeFormData.selectedTemplate = {
|
||||
name: $scope.approvalNodeState.name,
|
||||
description: $scope.approvalNodeState.description,
|
||||
timeout,
|
||||
unified_job_type: "workflow_approval"
|
||||
};
|
||||
} else if($scope.activeTab === "templates") {
|
||||
nodeFormData.selectedTemplate = $scope.jobNodeState.selectedTemplate;
|
||||
nodeFormData.promptData = $scope.jobNodeState.promptData;
|
||||
} else if($scope.activeTab === "project_syncs") {
|
||||
nodeFormData.selectedTemplate = $scope.projectNodeState.selectedTemplate;
|
||||
} else if($scope.activeTab === "inventory_syncs") {
|
||||
nodeFormData.selectedTemplate = $scope.inventoryNodeState.selectedTemplate;
|
||||
}
|
||||
|
||||
$scope.select({ nodeFormData });
|
||||
};
|
||||
|
||||
$scope.openPromptModal = () => {
|
||||
$scope.promptData.triggerModalOpen = true;
|
||||
$scope.jobNodeState.promptData.triggerModalOpen = true;
|
||||
};
|
||||
|
||||
$scope.toggle_row = (selectedRow) => {
|
||||
$scope.selectIsDisabled = () => {
|
||||
if($scope.activeTab === "templates") {
|
||||
return !($scope.jobNodeState.selectedTemplate) ||
|
||||
$scope.jobNodeState.promptModalMissingReqFields ||
|
||||
$scope.jobNodeState.credentialRequiresPassword ||
|
||||
$scope.jobNodeState.selectedTemplateInvalid;
|
||||
} else if($scope.activeTab === "project_syncs") {
|
||||
return !$scope.projectNodeState.selectedTemplate;
|
||||
} else if($scope.activeTab === "inventory_syncs") {
|
||||
return !$scope.inventoryNodeState.selectedTemplate;
|
||||
} else if ($scope.activeTab === "approval") {
|
||||
return !($scope.approvalNodeState.name && $scope.approvalNodeState.name !== "") || $scope.workflow_approval.pauseTimeoutMinutes.$error.min || $scope.workflow_approval.pauseTimeoutSeconds.$error.min;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.selectTemplate = (selectedTemplate) => {
|
||||
if (!$scope.readOnly) {
|
||||
templateManuallySelected(selectedRow);
|
||||
clearWatchers();
|
||||
|
||||
$scope.approvalNodeState = {
|
||||
name: null,
|
||||
description: null,
|
||||
timeoutMinutes: 0,
|
||||
timeoutSeconds: 0
|
||||
};
|
||||
$scope.editNodeHelpMessage = getEditNodeHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj);
|
||||
|
||||
if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") {
|
||||
let jobTemplate = selectedTemplate.type === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate();
|
||||
$scope.jobNodeState.promptData = null;
|
||||
|
||||
$q.all([jobTemplate.optionsLaunch(selectedTemplate.id), jobTemplate.getLaunch(selectedTemplate.id)])
|
||||
.then((responses) => {
|
||||
let launchConf = responses[1].data;
|
||||
|
||||
let credentialRequiresPassword = false;
|
||||
let selectedTemplateInvalid = false;
|
||||
|
||||
if (selectedTemplate.type !== "workflow_job_template") {
|
||||
if ((!selectedTemplate.inventory && !launchConf.ask_inventory_on_launch) || !selectedTemplate.project) {
|
||||
selectedTemplateInvalid = true;
|
||||
}
|
||||
|
||||
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
|
||||
credentialRequiresPassword = true;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.jobNodeState.credentialRequiresPassword = credentialRequiresPassword;
|
||||
$scope.jobNodeState.selectedTemplateInvalid = selectedTemplateInvalid;
|
||||
$scope.jobNodeState.selectedTemplate = angular.copy(selectedTemplate);
|
||||
updateSelectedRow();
|
||||
|
||||
if (!launchConf.survey_enabled &&
|
||||
!launchConf.ask_inventory_on_launch &&
|
||||
!launchConf.ask_credential_on_launch &&
|
||||
!launchConf.ask_verbosity_on_launch &&
|
||||
!launchConf.ask_job_type_on_launch &&
|
||||
!launchConf.ask_limit_on_launch &&
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.ask_variables_on_launch &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
$scope.jobNodeState.showPromptButton = false;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = false;
|
||||
} else {
|
||||
$scope.jobNodeState.showPromptButton = true;
|
||||
$scope.jobNodeState.promptModalMissingReqFields = false;
|
||||
|
||||
if (selectedTemplate.type !== "workflow_job_template") {
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
|
||||
$scope.jobNodeState.promptModalMissingReqFields = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (launchConf.survey_enabled) {
|
||||
// go out and get the survey questions
|
||||
jobTemplate.getSurveyQuestions(selectedTemplate.id)
|
||||
.then((surveyQuestionRes) => {
|
||||
|
||||
let processed = PromptService.processSurveyQuestions({
|
||||
surveyQuestions: surveyQuestionRes.data.spec
|
||||
});
|
||||
|
||||
$scope.jobNodeState.missingSurveyValue = processed.missingSurveyValue;
|
||||
|
||||
$scope.jobNodeState.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data,
|
||||
surveyQuestions: processed.surveyQuestions,
|
||||
template: selectedTemplate.id,
|
||||
templateType: selectedTemplate.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data
|
||||
}),
|
||||
};
|
||||
|
||||
surveyQuestionWatcher = $scope.$watch('jobNodeState.promptData.surveyQuestions', () => {
|
||||
let missingSurveyValue = false;
|
||||
_.each($scope.jobNodeState.promptData.surveyQuestions, (question) => {
|
||||
if (question.required && (Empty(question.model) || question.model === [])) {
|
||||
missingSurveyValue = true;
|
||||
}
|
||||
});
|
||||
$scope.jobNodeState.missingSurveyValue = missingSurveyValue;
|
||||
}, true);
|
||||
|
||||
watchForPromptChanges();
|
||||
});
|
||||
} else {
|
||||
$scope.jobNodeState.promptData = {
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data,
|
||||
template: selectedTemplate.id,
|
||||
templateType: selectedTemplate.type,
|
||||
prompts: PromptService.processPromptValues({
|
||||
launchConf: responses[1].data,
|
||||
launchOptions: responses[0].data
|
||||
}),
|
||||
};
|
||||
|
||||
watchForPromptChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (selectedTemplate.type === "project") {
|
||||
$scope.projectNodeState.selectedTemplate = angular.copy(selectedTemplate);
|
||||
} else if (selectedTemplate.type === "inventory_source") {
|
||||
$scope.inventoryNodeState.selectedTemplate = angular.copy(selectedTemplate);
|
||||
}
|
||||
updateSelectedRow();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('nodeConfig.nodeId', (newNodeId, oldNodeId) => {
|
||||
if (newNodeId !== oldNodeId) {
|
||||
clearWatchers();
|
||||
setupNodeForm();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watchGroup(['wf_maker_templates', 'wf_maker_projects', 'wf_maker_inventory_sources', 'activeTab', 'selectedTemplate.id'], () => {
|
||||
const unifiedJobTemplateId = _.get($scope, 'selectedTemplate.id') || null;
|
||||
switch($scope.activeTab) {
|
||||
case 'jobs':
|
||||
$scope.wf_maker_templates.forEach((row, i) => {
|
||||
if (row.type === 'job_template') {
|
||||
formatPopOverDetails(row);
|
||||
}
|
||||
if(row.id === unifiedJobTemplateId) {
|
||||
$scope.wf_maker_templates[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_templates[i].checked = 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'project_syncs':
|
||||
$scope.wf_maker_projects.forEach((row, i) => {
|
||||
if(row.id === unifiedJobTemplateId) {
|
||||
$scope.wf_maker_projects[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_projects[i].checked = 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'inventory_syncs':
|
||||
$scope.wf_maker_inventory_sources.forEach((row, i) => {
|
||||
if(row.id === unifiedJobTemplateId) {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 1;
|
||||
}
|
||||
else {
|
||||
$scope.wf_maker_inventory_sources[i].checked = 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
$scope.$watchGroup(['wf_maker_templates', 'wf_maker_projects', 'wf_maker_inventory_sources', 'activeTab'], () => {
|
||||
updateSelectedRow();
|
||||
});
|
||||
|
||||
setupNodeForm();
|
||||
|
||||
@ -1,36 +1,45 @@
|
||||
<div ng-show="nodeFormDataLoaded">
|
||||
<div class="WorkflowMaker-formTitle">{{nodeConfig.mode === 'edit' ? nodeConfig.node.fullUnifiedJobTemplateObject.name || nodeConfig.node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_NODE')}}</div>
|
||||
<div class="Form-tabHolder" ng-show="!readOnly">
|
||||
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'jobs'}" ng-click="activeTab = 'jobs'">{{strings.get('workflow_maker.JOBS')}}</div>
|
||||
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'project_syncs'}" ng-click="activeTab = 'project_syncs'">{{strings.get('workflow_maker.PROJECT_SYNC')}}</div>
|
||||
<div class="Form-tab WorkflowMaker-formTab" ng-class="{'is-selected': activeTab === 'inventory_syncs'}" ng-click="activeTab = 'inventory_syncs'">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</div>
|
||||
<div class="WorkflowMaker-formTypeDropdown" ng-show="!readOnly">
|
||||
<select
|
||||
id="workflow-node-types"
|
||||
ng-model="activeTab"
|
||||
class="form-control Form-dropDown"
|
||||
name="activeTab"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<option value="templates" selected="selected">{{strings.get('workflow_maker.TEMPLATE')}}</option>
|
||||
<option value="project_syncs" selected="selected">{{strings.get('workflow_maker.PROJECT_SYNC')}}</option>
|
||||
<option value="inventory_syncs" selected="selected">{{strings.get('workflow_maker.INVENTORY_SYNC')}}</option>
|
||||
<option value="approval" selected="selected">{{strings.get('workflow_maker.APPROVAL')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="WorkflowMaker-formLists" ng-show="!readOnly">
|
||||
<div id="workflow-jobs-list" ng-show="activeTab === 'jobs'">
|
||||
<div ng-hide="wf_maker_templates.length === 0 && (searchTags | isEmpty)">
|
||||
<smart-search django-model="wf_maker_templates" base-path="unified_job_templates" iterator="wf_maker_template" dataset="wf_maker_template_dataset" list="templateList" collection="wf_maker_templates" default-params="wf_maker_template_default_params" query-set="wf_maker_template_queryset" search-bar-full-width="true" search-tags="searchTags">
|
||||
<div id="workflow-jobs-list" ng-show="activeTab === 'templates'">
|
||||
<div ng-hide="wf_maker_templates.length === 0 && (jobNodeState.searchTags | isEmpty)">
|
||||
<smart-search django-model="wf_maker_templates" base-path="unified_job_templates" iterator="wf_maker_template" dataset="wf_maker_template_dataset" list="templateList" collection="wf_maker_templates" default-params="wf_maker_template_default_params" query-set="wf_maker_template_queryset" search-bar-full-width="true" search-tags="jobNodeState.searchTags">
|
||||
</smart-search>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="wf_maker_templates.length === 0 && !(searchTags | isEmpty)">
|
||||
<div class="row" ng-show="wf_maker_templates.length === 0 && !(jobNodeState.searchTags | isEmpty)">
|
||||
<div class="col-lg-12 List-searchNoResults" translate>No records matched your search.</div>
|
||||
</div>
|
||||
<div class="List-noItems" ng-show="wf_maker_templates.length === 0 && (searchTags | isEmpty)" translate>PLEASE ADD ITEMS TO THIS LIST</div>
|
||||
<div class="List-noItems" ng-show="wf_maker_templates.length === 0 && (jobNodeState.searchTags | isEmpty)" translate>PLEASE ADD ITEMS TO THIS LIST</div>
|
||||
<div class="list-table-container" ng-show="wf_maker_templates.length > 0">
|
||||
<div id="templates_table" class="List-table" is-extended="false">
|
||||
<div class="List-lookupLayout List-tableHeaderRow">
|
||||
<div></div>
|
||||
<div class="d-flex h-100">
|
||||
<div class="col-md-8" base-path="unified_job_templates" collection="wf_maker_templates" dataset="wf_maker_template_dataset" column-sort="" column-field="name" column-iterator="wf_maker_template" column-no-sort="undefined" column-label="Name" column-custom-class="" query-set="wf_maker_template_queryset"></div>
|
||||
<div class="col-md-8" base-path="unified_job_templates" collection="wf_maker_templates" dataset="wf_maker_template_dataset" column-sort="" column-field="name" column-iterator="wf_maker_template" column-no-sort="undefined" column-label="{{:: strings.get('workflow_maker.NAME') }}" column-custom-class="" query-set="wf_maker_template_queryset"></div>
|
||||
<div class="List-tableHeader--info col-md-4" base-path="unified_job_templates" collection="wf_maker_templates" dataset="wf_maker_template_dataset" column-sort="" column-field="info" column-iterator="wf_maker_template" column-no-sort="true" column-label="" column-custom-class="" query-set="wf_maker_template_queryset"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-class="[template.success_class, {'List-tableRow--selected' : $stateParams['template_id'] == wf_maker_template.id}, {'List-tableRow--disabled': !wf_maker_template.summary_fields.user_capabilities.edit}]" id="{{ wf_maker_template.id }}" class="List-lookupLayout List-tableRow template_class" disable-row="{{ !wf_maker_template.summary_fields.user_capabilities.edit }}" ng-repeat="wf_maker_template in wf_maker_templates">
|
||||
<div class="List-centerEnd select-column">
|
||||
<input type="radio" ng-model="wf_maker_template.checked" ng-value="1" ng-false-value="0" name="check_template_{{wf_maker_template.id}}" ng-click="toggle_row(wf_maker_template)" ng-disabled="!wf_maker_template.summary_fields.user_capabilities.edit">
|
||||
<input type="radio" ng-model="wf_maker_template.checked" ng-value="1" ng-false-value="0" name="check_template_{{wf_maker_template.id}}" ng-click="selectTemplate(wf_maker_template)" ng-disabled="!wf_maker_template.summary_fields.user_capabilities.edit">
|
||||
</div>
|
||||
<div class="d-flex h-100">
|
||||
<div class="List-tableCell name-column col-md-8" ng-click="toggle_row(wf_maker_template)">
|
||||
<div class="List-tableCell name-column col-md-8" ng-click="selectTemplate(wf_maker_template)">
|
||||
{{wf_maker_template.name}}
|
||||
<span class="at-RowItem-tag" ng-show="wf_maker_template.type === 'workflow_job_template'">
|
||||
{{:: strings.get('workflow_maker.WORKFLOW') }}
|
||||
@ -44,29 +53,29 @@
|
||||
<paginate base-path="unified_job_templates" collection="wf_maker_templates" dataset="wf_maker_template_dataset" iterator="wf_maker_template" query-set="wf_maker_template_queryset" hide-view-per-page="true" max-visible-pages="5"></paginate>
|
||||
</div>
|
||||
<div id="workflow-project-sync-list" ng-show="activeTab === 'project_syncs'">
|
||||
<div ng-hide="wf_maker_projects.length === 0 && (searchTags | isEmpty)">
|
||||
<smart-search django-model="wf_maker_projects" base-path="projects" iterator="wf_maker_project" dataset="wf_maker_project_dataset" list="projectList" collection="wf_maker_projects" default-params="wf_maker_project_default_params" query-set="wf_maker_project_queryset" search-bar-full-width="true" search-tags="searchTags">
|
||||
<div ng-hide="wf_maker_projects.length === 0 && (projectNodeState.searchTags | isEmpty)">
|
||||
<smart-search django-model="wf_maker_projects" base-path="projects" iterator="wf_maker_project" dataset="wf_maker_project_dataset" list="projectList" collection="wf_maker_projects" default-params="wf_maker_project_default_params" query-set="wf_maker_project_queryset" search-bar-full-width="true" search-tags="projectNodeState.searchTags">
|
||||
</smart-search>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="wf_maker_projects.length === 0 && !(searchTags | isEmpty)">
|
||||
<div class="row" ng-show="wf_maker_projects.length === 0 && !(projectNodeState.searchTags | isEmpty)">
|
||||
<div class="col-lg-12 List-searchNoResults" translate>No records matched your search.</div>
|
||||
</div>
|
||||
<div class="List-noItems" ng-show="wf_maker_projects.length === 0 && (searchTags | isEmpty)">No Projects Have Been Created</div>
|
||||
<div class="List-noItems" ng-show="wf_maker_projects.length === 0 && (projectNodeState.searchTags | isEmpty)" translate>No Projects Have Been Created</div>
|
||||
<div class="list-table-container" ng-show="wf_maker_projects.length > 0">
|
||||
<div id="projects_table" class="List-table" is-extended="false">
|
||||
<div class="List-lookupLayout List-tableHeaderRow">
|
||||
<div></div>
|
||||
<div class="d-flex h-100">
|
||||
<div base-path="projects" collection="wf_maker_projects" dataset="wf_maker_project_dataset" column-sort="" column-field="name" column-iterator="wf_maker_project" column-no-sort="undefined" column-label="Name" column-custom-class="col-md-12" query-set="wf_maker_project_queryset"></div>
|
||||
<div base-path="projects" collection="wf_maker_projects" dataset="wf_maker_project_dataset" column-sort="" column-field="name" column-iterator="wf_maker_project" column-no-sort="undefined" column-label="{{:: strings.get('workflow_maker.NAME') }}" column-custom-class="col-md-12" query-set="wf_maker_project_queryset"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-class="[wf_maker_project.success_class, {'List-tableRow--selected' : $stateParams['project_id'] == wf_maker_project.id}]" id="{{ wf_maker_project.id }}" class="List-lookupLayout List-tableRow project_class" ng-repeat="wf_maker_project in wf_maker_projects">
|
||||
<div class="List-centerEnd select-column">
|
||||
<input type="radio" ng-model="wf_maker_project.checked" ng-value="1" ng-false-value="0" name="check_project_{{wf_maker_project.id}}" ng-click="toggle_row(wf_maker_project)" ng-disabled="undefined">
|
||||
<input type="radio" ng-model="wf_maker_project.checked" ng-value="1" ng-false-value="0" name="check_project_{{wf_maker_project.id}}" ng-click="selectTemplate(wf_maker_project)" ng-disabled="undefined">
|
||||
</div>
|
||||
<div class="d-flex h-100">
|
||||
<div class="List-tableCell name-column col-md-12" ng-click="toggle_row(wf_maker_project)">{{ wf_maker_project.name }}</div>
|
||||
<div class="List-tableCell name-column col-md-12" ng-click="selectTemplate(wf_maker_project)">{{ wf_maker_project.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -74,43 +83,83 @@
|
||||
<paginate base-path="projects" collection="wf_maker_projects" dataset="wf_maker_project_dataset" iterator="wf_maker_project" query-set="wf_maker_project_queryset" hide-view-per-page="true" max-visible-pages="5"></paginate>
|
||||
</div>
|
||||
<div id="workflow-inventory-sync-list" ng-show="activeTab === 'inventory_syncs'">
|
||||
<div ng-hide="wf_maker_inventory_sources.length === 0 && (searchTags | isEmpty)">
|
||||
<smart-search django-model="wf_maker_inventory_sources" base-path="inventory_sources" iterator="wf_maker_inventory_source" dataset="wf_maker_inventory_source_dataset" list="inventorySourceList" collection="wf_maker_inventory_sources" default-params="wf_maker_inventory_source_default_params" query-set="wf_maker_inventory_source_queryset" search-bar-full-width="true" search-tags="searchTags">
|
||||
<div ng-hide="wf_maker_inventory_sources.length === 0 && (inventoryNodeState.searchTags | isEmpty)">
|
||||
<smart-search django-model="wf_maker_inventory_sources" base-path="inventory_sources" iterator="wf_maker_inventory_source" dataset="wf_maker_inventory_source_dataset" list="inventorySourceList" collection="wf_maker_inventory_sources" default-params="wf_maker_inventory_source_default_params" query-set="wf_maker_inventory_source_queryset" search-bar-full-width="true" search-tags="inventoryNodeState.searchTags">
|
||||
</smart-search>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="wf_maker_inventory_sources.length === 0 && !(searchTags | isEmpty)">
|
||||
<div class="row" ng-show="wf_maker_inventory_sources.length === 0 && !(inventoryNodeState.searchTags | isEmpty)">
|
||||
<div class="col-lg-12 List-searchNoResults" translate>No records matched your search.</div>
|
||||
</div>
|
||||
<div class="List-noItems" ng-show="wf_maker_inventory_sources.length === 0 && (searchTags | isEmpty)">PLEASE ADD ITEMS TO THIS LIST</div>
|
||||
<div class="List-noItems" ng-show="wf_maker_inventory_sources.length === 0 && (inventoryNodeState.searchTags | isEmpty)" translate>PLEASE ADD ITEMS TO THIS LIST</div>
|
||||
<div class="list-table-container" ng-show="wf_maker_inventory_sources.length > 0">
|
||||
<div id="workflow_inventory_sources_table" class="List-table" is-extended="false">
|
||||
<div class="List-lookupLayout List-tableHeaderRow">
|
||||
<div></div>
|
||||
<div class="d-flex h-100">
|
||||
<div base-path="inventory_sources" collection="wf_maker_inventory_sources" dataset="wf_maker_inventory_source_dataset" column-sort="" column-field="name" column-iterator="wf_maker_inventory_source" column-no-sort="undefined" column-label="Name" column-custom-class="" query-set="wf_maker_inventory_source_queryset"></div>
|
||||
<div base-path="inventory_sources" collection="wf_maker_inventory_sources" dataset="wf_maker_inventory_source_dataset" column-sort="" column-field="name" column-iterator="wf_maker_inventory_source" column-no-sort="undefined" column-label="{{:: strings.get('workflow_maker.NAME') }}" column-custom-class="" query-set="wf_maker_inventory_source_queryset"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-class="[wf_maker_inventory_source.success_class, {'List-tableRow--selected' : $stateParams['inventory_source_id'] == wf_maker_inventory_source.id}]" id="{{ wf_maker_inventory_source.id }}" class="List-lookupLayout List-tableRow inventory_source_class" ng-repeat="wf_maker_inventory_source in wf_maker_inventory_sources">
|
||||
<div class="List-centerEnd select-column">
|
||||
<input type="radio" ng-model="wf_maker_inventory_source.checked" ng-value="1" ng-false-value="0" name="check_inventory_source_{{wf_maker_inventory_source.id}}" ng-click="toggle_row(wf_maker_inventory_source)" ng-disabled="undefined">
|
||||
<input type="radio" ng-model="wf_maker_inventory_source.checked" ng-value="1" ng-false-value="0" name="check_inventory_source_{{wf_maker_inventory_source.id}}" ng-click="selectTemplate(wf_maker_inventory_source)" ng-disabled="undefined">
|
||||
</div>
|
||||
<div class="d-flex h-100">
|
||||
<div class="List-tableCell name-column col-md-12" ng-click="toggle_row(wf_maker_inventory_source)">{{ wf_maker_inventory_source.name }}</div>
|
||||
<div class="List-tableCell name-column col-md-12" ng-click="selectTemplate(wf_maker_inventory_source)">{{ wf_maker_inventory_source.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<paginate base-path="inventory_sources" collection="wf_maker_inventory_sources" dataset="wf_maker_inventory_source_dataset" iterator="wf_maker_inventory_source" query-set="wf_maker_inventory_source_queryset" hide-view-per-page="true" max-visible-pages="5"></paginate>
|
||||
</div>
|
||||
<form id="workflow_approval" name="workflow_approval" ng-show="activeTab === 'approval'">
|
||||
<div class="form-group Form-formGroup Form-formGroup--singleColumn">
|
||||
<label for="pauseName" class="Form-inputLabelContainer">
|
||||
<span class="Form-requiredAsterisk">*</span>
|
||||
<span class="Form-inputLabel" translate>Name</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text" ng-model="approvalNodeState.name" name="pauseName" id="workflow_job_template_pauseName" class="form-control Form-textInput" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group Form-formGroup Form-formGroup--singleColumn">
|
||||
<label for="pauseDesc" class="Form-inputLabelContainer">
|
||||
<span class="Form-inputLabel" translate>Description</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text" ng-model="approvalNodeState.description" name="pauseDesc" id="workflow_job_template_pauseDesc" class="form-control Form-textInput" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group Form-formGroup Form-formGroup--singleColumn">
|
||||
<label class="Form-inputLabelContainer">
|
||||
<span class="Form-inputLabel" translate>Timeout</span>
|
||||
<a id="workflow-maker-timeout-popover" href="" aw-pop-over="{{:: strings.get('workflow_maker.TIMEOUT_POPOVER') }}" data-placement="top" data-container="body" over-title="{{:: strings.get('workflow_maker.TIMEOUT') }}" class="help-link">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
<div>
|
||||
<span class="WorkflowMaker-timeoutInput">
|
||||
<input type="number" value="0" min=0 aw-min="0" ng-model="approvalNodeState.timeoutMinutes" name="pauseTimeoutMinutes" id="workflow_job_template_pauseTimeoutMinutes" aw-spinner="pauseTimeoutMinutes" integer />
|
||||
</span>
|
||||
<span class="WorkflowMaker-timeoutLabel" translate>min</span>
|
||||
<span class="WorkflowMaker-timeoutInput WorkflowMaker-timeoutSeconds">
|
||||
<input class="WorkflowMaker-timeoutInput" type="number" value="0" min=0 aw-min="0" ng-model="approvalNodeState.timeoutSeconds" name="pauseTimeoutSeconds" id="workflow_job_template_pauseTimeoutSeconds" aw-spinner="pauseTimeoutSeconds" integer />
|
||||
</span>
|
||||
<span class="WorkflowMaker-timeoutLabel" translate>sec</span>
|
||||
<div class="error" id="workflow_job_template_pauseTimeout-minmax-error" ng-show="workflow_approval.pauseTimeoutMinutes.$error.min || workflow_approval.pauseTimeoutSeconds.$error.min" translate>Please enter a number greater than or equal to 0.</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-if="selectedTemplate && selectedTemplateInvalid">
|
||||
<div ng-if="selectedTemplate && jobNodeState.selectedTemplateInvalid">
|
||||
<div class="WorkflowMaker-invalidJobTemplateWarning">
|
||||
<span class="fa fa-warning"></span>
|
||||
<span>{{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="selectedTemplate && credentialRequiresPassword">
|
||||
<div ng-if="selectedTemplate && jobNodeState.credentialRequiresPassword">
|
||||
<div class="WorkflowMaker-invalidJobTemplateWarning">
|
||||
<span class="fa fa-warning"></span>
|
||||
<span>{{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}</span>
|
||||
@ -138,29 +187,27 @@
|
||||
<div
|
||||
class="WorkflowMaker-readOnlyPromptText"
|
||||
ng-show="nodeConfig.node.originalNodeObject.job_type !== null ||
|
||||
(promptData.prompts.credentials.value && promptData.prompts.credentials.value.length > 0) ||
|
||||
nodeConfig.node.originalNodeObject.inventory !== null ||
|
||||
nodeConfig.node.originalNodeObject.limit !== null ||
|
||||
nodeConfig.node.originalNodeObject.verbosity !== null ||
|
||||
nodeConfig.node.originalNodeObject.job_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.skip_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.diff_mode !== null ||
|
||||
nodeConfig.node.originalNodeObject.scm_branch !== null ||
|
||||
showExtraVars">
|
||||
(jobNodeState.promptData.prompts.credentials.value && jobNodeState.promptData.prompts.credentials.value.length > 0) ||
|
||||
nodeConfig.node.originalNodeObject.inventory !== null ||
|
||||
nodeConfig.node.originalNodeObject.limit !== null ||
|
||||
nodeConfig.node.originalNodeObject.verbosity !== null ||
|
||||
nodeConfig.node.originalNodeObject.job_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.skip_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.diff_mode !== null ||
|
||||
showExtraVars">
|
||||
{{:: strings.get('workflow_maker.READ_ONLY_PROMPT_VALUES')}}
|
||||
</div>
|
||||
<div
|
||||
class="WorkflowMaker-readOnlyPromptText"
|
||||
ng-show="!(nodeConfig.node.originalNodeObject.job_type !== null ||
|
||||
(promptData.prompts.credentials.value && promptData.prompts.credentials.value.length > 0) ||
|
||||
nodeConfig.node.originalNodeObject.inventory !== null ||
|
||||
nodeConfig.node.originalNodeObject.limit !== null ||
|
||||
nodeConfig.node.originalNodeObject.verbosity !== null ||
|
||||
nodeConfig.node.originalNodeObject.job_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.skip_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.diff_mode !== null ||
|
||||
nodeConfig.node.originalNodeObject.scm_branch !== null ||
|
||||
showExtraVars)">
|
||||
(jobNodeState.promptData.prompts.credentials.value && jobNodeState.promptData.prompts.credentials.value.length > 0) ||
|
||||
nodeConfig.node.originalNodeObject.inventory !== null ||
|
||||
nodeConfig.node.originalNodeObject.limit !== null ||
|
||||
nodeConfig.node.originalNodeObject.verbosity !== null ||
|
||||
nodeConfig.node.originalNodeObject.job_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.skip_tags !== null ||
|
||||
nodeConfig.node.originalNodeObject.diff_mode !== null ||
|
||||
showExtraVars)">
|
||||
{{:: strings.get('workflow_maker.READ_ONLY_NO_PROMPT_VALUES')}}
|
||||
</div>
|
||||
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.job_type !== null">
|
||||
@ -217,8 +264,8 @@
|
||||
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.diff_mode !== null">
|
||||
<div class="Prompt-previewRowTitle">{{:: strings.get('prompt.SHOW_CHANGES') }}</div>
|
||||
<div class="Prompt-previewRowValue">
|
||||
<span ng-if="promptData.prompts.diffMode.value">{{:: strings.get('ON') }}</span>
|
||||
<span ng-if="!promptData.prompts.diffMode.value">{{:: strings.get('OFF') }}</span>
|
||||
<span ng-if="jobNodeState.promptData.prompts.diffMode.value">{{:: strings.get('ON') }}</span>
|
||||
<span ng-if="!jobNodeState.promptData.prompts.diffMode.value">{{:: strings.get('OFF') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Prompt-previewRow--flex" ng-if="nodeConfig.node.originalNodeObject.scm_branch !== null">
|
||||
@ -232,13 +279,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="editNodeHelpMessage && activeTab === 'jobs'" class="WorkflowMaker-formHelp" ng-bind="editNodeHelpMessage"></div>
|
||||
<div ng-show="editNodeHelpMessage && activeTab === 'templates'" class="WorkflowMaker-formHelp" ng-bind="editNodeHelpMessage"></div>
|
||||
<br />
|
||||
<div class="buttons Form-buttons" id="workflow_maker_controls">
|
||||
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton && activeTab == 'jobs' " ng-click="openPromptModal()"> {{:: strings.get('prompt.PROMPT') }}</button>
|
||||
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="jobNodeState.showPromptButton && activeTab == 'templates' " ng-click="openPromptModal()"> {{:: strings.get('prompt.PROMPT') }}</button>
|
||||
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_node_btn" ng-show="!readOnly" ng-click="cancel()"> {{:: strings.get('CANCEL') }}</button>
|
||||
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_close_node_btn" ng-show="readOnly" ng-click="cancel()"> {{:: strings.get('CLOSE') }}</button>
|
||||
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_node_btn" ng-show="!readOnly" ng-click="select({selectedTemplate, promptData, edgeType})" ng-disabled="!selectedTemplate || promptModalMissingReqFields || credentialRequiresPassword || selectedTemplateInvalid"> {{:: strings.get('workflow_maker.SELECT') }}</button>
|
||||
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_node_btn" ng-show="!readOnly" ng-click="confirmNodeForm()" ng-disabled="selectIsDisabled()"> {{:: strings.get('workflow_maker.SELECT') }}</button>
|
||||
</div>
|
||||
<prompt prompt-data="promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="readOnly"></prompt>
|
||||
</div>
|
||||
<prompt prompt-data="jobNodeState.promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="readOnly"></prompt>
|
||||
</div>
|
||||
@ -0,0 +1,67 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['TemplateList', 'ProjectList', 'InventorySourcesList', 'i18n',
|
||||
function(TemplateList, ProjectList, InventorySourcesList, i18n){
|
||||
return {
|
||||
inventorySourceListDefinition: function() {
|
||||
const inventorySourceList = _.cloneDeep(InventorySourcesList);
|
||||
inventorySourceList.name = 'wf_maker_inventory_sources';
|
||||
inventorySourceList.iterator = 'wf_maker_inventory_source';
|
||||
inventorySourceList.maxVisiblePages = 5;
|
||||
inventorySourceList.searchBarFullWidth = true;
|
||||
inventorySourceList.disableRow = "{{ readOnly }}";
|
||||
inventorySourceList.disableRowValue = 'readOnly';
|
||||
|
||||
return inventorySourceList;
|
||||
},
|
||||
projectListDefinition: function(){
|
||||
const projectList = _.cloneDeep(ProjectList);
|
||||
delete projectList.fields.status;
|
||||
delete projectList.fields.scm_type;
|
||||
delete projectList.fields.last_updated;
|
||||
projectList.name = 'wf_maker_projects';
|
||||
projectList.iterator = 'wf_maker_project';
|
||||
projectList.fields.name.columnClass = "col-md-11";
|
||||
projectList.maxVisiblePages = 5;
|
||||
projectList.searchBarFullWidth = true;
|
||||
projectList.disableRow = "{{ readOnly }}";
|
||||
projectList.disableRowValue = 'readOnly';
|
||||
|
||||
return projectList;
|
||||
},
|
||||
templateListDefinition: function(){
|
||||
const templateList = _.cloneDeep(TemplateList);
|
||||
delete templateList.actions;
|
||||
delete templateList.fields.type;
|
||||
delete templateList.fields.description;
|
||||
delete templateList.fields.smart_status;
|
||||
delete templateList.fields.labels;
|
||||
delete templateList.fieldActions;
|
||||
templateList.name = 'wf_maker_templates';
|
||||
templateList.iterator = 'wf_maker_template';
|
||||
templateList.fields.name.columnClass = "col-md-8";
|
||||
templateList.fields.name.tag = i18n._('WORKFLOW');
|
||||
templateList.fields.name.showTag = "{{wf_maker_template.type === 'workflow_job_template'}}";
|
||||
templateList.disableRow = "{{ readOnly }}";
|
||||
templateList.disableRowValue = 'readOnly';
|
||||
templateList.basePath = 'unified_job_templates';
|
||||
templateList.fields.info = {
|
||||
ngInclude: "'/static/partials/job-template-details.html'",
|
||||
type: 'template',
|
||||
columnClass: 'col-md-3',
|
||||
infoHeaderClass: 'col-md-3',
|
||||
label: '',
|
||||
nosort: true
|
||||
};
|
||||
templateList.maxVisiblePages = 5;
|
||||
templateList.searchBarFullWidth = true;
|
||||
|
||||
return templateList;
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -7,6 +7,15 @@
|
||||
.ui-dialog-buttonpane, .ui-dialog-titlebar {
|
||||
display:none;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
|
||||
.WorkflowMaker-header {
|
||||
@ -296,8 +305,8 @@
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.WorkflowMaker-formTab {
|
||||
margin-right: 10px;
|
||||
.WorkflowMaker-formTypeDropdown {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.WorkflowMaker-preventBodyScrolling {
|
||||
@ -314,6 +323,20 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.WorkflowMaker-timeoutInput {
|
||||
.ui-spinner {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.WorkflowMaker-timeoutSeconds {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.WorkflowMaker-timeoutLabel {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.Key-list {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
@ -373,11 +396,11 @@
|
||||
|
||||
.Key-icon--circle {
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: @default-bg;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
line-height: 24px;
|
||||
margin: 4px 5px 5px 0px;
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', 'TemplatesService',
|
||||
'ProcessErrors', '$q',
|
||||
'ProcessErrors', '$q', 'Rest',
|
||||
'PromptService', 'TemplatesStrings', 'WorkflowChartService',
|
||||
'Wait', '$state',
|
||||
function ($scope, TemplatesService,
|
||||
ProcessErrors, $q,
|
||||
ProcessErrors, $q, Rest,
|
||||
PromptService, TemplatesStrings, WorkflowChartService,
|
||||
Wait, $state
|
||||
) {
|
||||
@ -127,7 +127,7 @@ export default ['$scope', 'TemplatesService',
|
||||
|
||||
if (_.has(node, 'fullUnifiedJobTemplateObject') &&
|
||||
(node.fullUnifiedJobTemplateObject.type === "workflow_job_template" ||
|
||||
node.fullUnifiedJobTemplateObject.type === "job_template") &&
|
||||
node.fullUnifiedJobTemplateObject.type === "job_template") &&
|
||||
node.promptData
|
||||
) {
|
||||
sendableNodeData = PromptService.bundlePromptDataForSaving({
|
||||
@ -140,59 +140,116 @@ export default ['$scope', 'TemplatesService',
|
||||
};
|
||||
|
||||
if ($scope.graphState.arrayOfNodesForChart.length > 1) {
|
||||
let approvalTemplatePromises = [];
|
||||
let addPromises = [];
|
||||
let editPromises = [];
|
||||
let credentialRequests = [];
|
||||
|
||||
Object.keys(nodeRef).map((workflowMakerNodeId) => {
|
||||
if (nodeRef[workflowMakerNodeId].isNew) {
|
||||
addPromises.push(TemplatesService.addWorkflowNode({
|
||||
url: $scope.workflowJobTemplateObj.related.workflow_nodes,
|
||||
data: buildSendableNodeData(nodeRef[workflowMakerNodeId])
|
||||
}).then(({data}) => {
|
||||
nodeRef[workflowMakerNodeId].originalNodeObject = data;
|
||||
nodeIdToChartNodeIdMapping[data.id] = parseInt(workflowMakerNodeId);
|
||||
if (_.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.ask_credential_on_launch')) {
|
||||
// This finds the credentials that were selected in the prompt but don't occur
|
||||
// in the template defaults
|
||||
let credentialIdsToPost = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.value.filter((credFromPrompt) => {
|
||||
let defaultCreds = _.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.defaults.credentials', []);
|
||||
return !defaultCreds.some((defaultCred) => {
|
||||
return credFromPrompt.id === defaultCred.id;
|
||||
const node = nodeRef[workflowMakerNodeId];
|
||||
if (node.isNew) {
|
||||
if (node.unifiedJobTemplate && node.unifiedJobTemplate.unified_job_type === "workflow_approval") {
|
||||
addPromises.push(TemplatesService.addWorkflowNode({
|
||||
url: $scope.workflowJobTemplateObj.related.workflow_nodes,
|
||||
data: {}
|
||||
}).then(({data: newNodeData}) => {
|
||||
Rest.setUrl(newNodeData.related.create_approval_template);
|
||||
approvalTemplatePromises.push(Rest.post({
|
||||
name: node.unifiedJobTemplate.name,
|
||||
description: node.unifiedJobTemplate.description,
|
||||
timeout: node.unifiedJobTemplate.timeout
|
||||
}).then(() => {
|
||||
node.originalNodeObject = newNodeData;
|
||||
nodeIdToChartNodeIdMapping[newNodeData.id] = parseInt(workflowMakerNodeId);
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
}));
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
addPromises.push(TemplatesService.addWorkflowNode({
|
||||
url: $scope.workflowJobTemplateObj.related.workflow_nodes,
|
||||
data: buildSendableNodeData(node)
|
||||
}).then(({data: newNodeData}) => {
|
||||
node.originalNodeObject = newNodeData;
|
||||
nodeIdToChartNodeIdMapping[newNodeData.id] = parseInt(workflowMakerNodeId);
|
||||
if (_.get(node, 'promptData.launchConf.ask_credential_on_launch')) {
|
||||
// This finds the credentials that were selected in the prompt but don't occur
|
||||
// in the template defaults
|
||||
let credentialIdsToPost = node.promptData.prompts.credentials.value.filter((credFromPrompt) => {
|
||||
let defaultCreds = _.get(node, 'promptData.launchConf.defaults.credentials', []);
|
||||
return !defaultCreds.some((defaultCred) => {
|
||||
return credFromPrompt.id === defaultCred.id;
|
||||
});
|
||||
});
|
||||
|
||||
credentialIdsToPost.forEach((credentialToPost) => {
|
||||
credentialRequests.push({
|
||||
id: data.id,
|
||||
data: {
|
||||
id: credentialToPost.id
|
||||
}
|
||||
credentialIdsToPost.forEach((credentialToPost) => {
|
||||
credentialRequests.push({
|
||||
id: newNodeData.id,
|
||||
data: {
|
||||
id: credentialToPost.id
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
}));
|
||||
}
|
||||
} else if (node.isEdited) {
|
||||
if (node.unifiedJobTemplate && node.unifiedJobTemplate.unified_job_type === "workflow_approval") {
|
||||
if (node.originalNodeObject.summary_fields.unified_job_template.unified_job_type === "workflow_approval") {
|
||||
Rest.setUrl(node.originalNodeObject.related.unified_job_template);
|
||||
approvalTemplatePromises.push(Rest.patch({
|
||||
name: node.unifiedJobTemplate.name,
|
||||
description: node.unifiedJobTemplate.description,
|
||||
timeout: node.unifiedJobTemplate.timeout
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
Rest.setUrl(node.originalNodeObject.related.create_approval_template);
|
||||
approvalTemplatePromises.push(Rest.post({
|
||||
name: node.unifiedJobTemplate.name,
|
||||
description: node.unifiedJobTemplate.description,
|
||||
timeout: node.unifiedJobTemplate.timeout
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
}));
|
||||
}
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
}));
|
||||
} else if (nodeRef[workflowMakerNodeId].isEdited) {
|
||||
editPromises.push(TemplatesService.editWorkflowNode({
|
||||
id: nodeRef[workflowMakerNodeId].originalNodeObject.id,
|
||||
data: buildSendableNodeData(nodeRef[workflowMakerNodeId])
|
||||
}));
|
||||
} else {
|
||||
editPromises.push(TemplatesService.editWorkflowNode({
|
||||
id: node.originalNodeObject.id,
|
||||
data: buildSendableNodeData(node)
|
||||
}));
|
||||
}
|
||||
|
||||
if (_.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.ask_credential_on_launch')) {
|
||||
let credentialsNotInPriorCredentials = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.value.filter((credFromPrompt) => {
|
||||
let defaultCreds = _.get(nodeRef[workflowMakerNodeId], 'promptData.launchConf.defaults.credentials', []);
|
||||
if (_.get(node, 'promptData.launchConf.ask_credential_on_launch')) {
|
||||
let credentialsNotInPriorCredentials = node.promptData.prompts.credentials.value.filter((credFromPrompt) => {
|
||||
let defaultCreds = _.get(node, 'promptData.launchConf.defaults.credentials', []);
|
||||
return !defaultCreds.some((defaultCred) => {
|
||||
return credFromPrompt.id === defaultCred.id;
|
||||
});
|
||||
});
|
||||
|
||||
let credentialsToAdd = credentialsNotInPriorCredentials.filter((credNotInPrior) => {
|
||||
let previousOverrides = _.get(nodeRef[workflowMakerNodeId], 'promptData.prompts.credentials.previousOverrides', []);
|
||||
let previousOverrides = _.get(node, 'promptData.prompts.credentials.previousOverrides', []);
|
||||
return !previousOverrides.some((priorCred) => {
|
||||
return credNotInPrior.id === priorCred.id;
|
||||
});
|
||||
@ -200,8 +257,8 @@ export default ['$scope', 'TemplatesService',
|
||||
|
||||
let credentialsToRemove = [];
|
||||
|
||||
if (_.has(nodeRef[workflowMakerNodeId], 'promptData.prompts.credentials.previousOverrides')) {
|
||||
credentialsToRemove = nodeRef[workflowMakerNodeId].promptData.prompts.credentials.previousOverrides.filter((priorCred) => {
|
||||
if (_.has(node, 'promptData.prompts.credentials.previousOverrides')) {
|
||||
credentialsToRemove = node.promptData.prompts.credentials.previousOverrides.filter((priorCred) => {
|
||||
return !credentialsNotInPriorCredentials.some((credNotInPrior) => {
|
||||
return priorCred.id === credNotInPrior.id;
|
||||
});
|
||||
@ -210,7 +267,7 @@ export default ['$scope', 'TemplatesService',
|
||||
|
||||
credentialsToAdd.forEach((credentialToAdd) => {
|
||||
credentialRequests.push({
|
||||
id: nodeRef[workflowMakerNodeId].originalNodeObject.id,
|
||||
id: node.originalNodeObject.id,
|
||||
data: {
|
||||
id: credentialToAdd.id
|
||||
}
|
||||
@ -219,7 +276,7 @@ export default ['$scope', 'TemplatesService',
|
||||
|
||||
credentialsToRemove.forEach((credentialToRemove) => {
|
||||
credentialRequests.push({
|
||||
id: nodeRef[workflowMakerNodeId].originalNodeObject.id,
|
||||
id: node.originalNodeObject.id,
|
||||
data: {
|
||||
id: credentialToRemove.id,
|
||||
disassociate: true
|
||||
@ -237,162 +294,165 @@ export default ['$scope', 'TemplatesService',
|
||||
|
||||
$q.all(addPromises.concat(editPromises, deletePromises))
|
||||
.then(() => {
|
||||
let disassociatePromises = [];
|
||||
let associatePromises = [];
|
||||
let linkMap = {};
|
||||
|
||||
// Build a link map for easy access
|
||||
$scope.graphState.arrayOfLinksForChart.forEach(link => {
|
||||
// link.source.id of 1 is our artificial start node
|
||||
if (link.source.id !== 1) {
|
||||
const sourceNodeId = nodeRef[link.source.id].originalNodeObject.id;
|
||||
const targetNodeId = nodeRef[link.target.id].originalNodeObject.id;
|
||||
if (!linkMap[sourceNodeId]) {
|
||||
linkMap[sourceNodeId] = {};
|
||||
}
|
||||
|
||||
linkMap[sourceNodeId][targetNodeId] = link.edgeType;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(nodeRef).map((workflowNodeId) => {
|
||||
let nodeId = nodeRef[workflowNodeId].originalNodeObject.id;
|
||||
if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) {
|
||||
nodeRef[workflowNodeId].originalNodeObject.success_nodes.forEach((successNodeId) => {
|
||||
if (
|
||||
!deletedNodeIds.includes(successNodeId) &&
|
||||
(!linkMap[nodeId] ||
|
||||
!linkMap[nodeId][successNodeId] ||
|
||||
linkMap[nodeId][successNodeId] !== "success")
|
||||
) {
|
||||
disassociatePromises.push(
|
||||
TemplatesService.disassociateWorkflowNode({
|
||||
parentId: nodeId,
|
||||
nodeId: successNodeId,
|
||||
edge: "success"
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (nodeRef[workflowNodeId].originalNodeObject.failure_nodes) {
|
||||
nodeRef[workflowNodeId].originalNodeObject.failure_nodes.forEach((failureNodeId) => {
|
||||
if (
|
||||
!deletedNodeIds.includes(failureNodeId) &&
|
||||
(!linkMap[nodeId] ||
|
||||
!linkMap[nodeId][failureNodeId] ||
|
||||
linkMap[nodeId][failureNodeId] !== "failure")
|
||||
) {
|
||||
disassociatePromises.push(
|
||||
TemplatesService.disassociateWorkflowNode({
|
||||
parentId: nodeId,
|
||||
nodeId: failureNodeId,
|
||||
edge: "failure"
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (nodeRef[workflowNodeId].originalNodeObject.always_nodes) {
|
||||
nodeRef[workflowNodeId].originalNodeObject.always_nodes.forEach((alwaysNodeId) => {
|
||||
if (
|
||||
!deletedNodeIds.includes(alwaysNodeId) &&
|
||||
(!linkMap[nodeId] ||
|
||||
!linkMap[nodeId][alwaysNodeId] ||
|
||||
linkMap[nodeId][alwaysNodeId] !== "always")
|
||||
) {
|
||||
disassociatePromises.push(
|
||||
TemplatesService.disassociateWorkflowNode({
|
||||
parentId: nodeId,
|
||||
nodeId: alwaysNodeId,
|
||||
edge: "always"
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(linkMap).map((sourceNodeId) => {
|
||||
Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => {
|
||||
const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId];
|
||||
const targetChartNodeId = nodeIdToChartNodeIdMapping[targetNodeId];
|
||||
switch(linkMap[sourceNodeId][targetNodeId]) {
|
||||
case "success":
|
||||
if (
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes ||
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
|
||||
) {
|
||||
associatePromises.push(
|
||||
TemplatesService.associateWorkflowNode({
|
||||
parentId: parseInt(sourceNodeId),
|
||||
nodeId: parseInt(targetNodeId),
|
||||
edge: "success"
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "failure":
|
||||
if (
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes ||
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
|
||||
) {
|
||||
associatePromises.push(
|
||||
TemplatesService.associateWorkflowNode({
|
||||
parentId: parseInt(sourceNodeId),
|
||||
nodeId: parseInt(targetNodeId),
|
||||
edge: "failure"
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "always":
|
||||
if (
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes ||
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
|
||||
) {
|
||||
associatePromises.push(
|
||||
TemplatesService.associateWorkflowNode({
|
||||
parentId: parseInt(sourceNodeId),
|
||||
nodeId: parseInt(targetNodeId),
|
||||
edge: "always"
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$q.all(disassociatePromises)
|
||||
$q.all(approvalTemplatePromises)
|
||||
.then(() => {
|
||||
let credentialPromises = credentialRequests.map((request) => {
|
||||
return TemplatesService.postWorkflowNodeCredential({
|
||||
id: request.id,
|
||||
data: request.data
|
||||
});
|
||||
let disassociatePromises = [];
|
||||
let linkMap = {};
|
||||
|
||||
// Build a link map for easy access
|
||||
$scope.graphState.arrayOfLinksForChart.forEach(link => {
|
||||
// link.source.id of 1 is our artificial start node
|
||||
if (link.source.id !== 1) {
|
||||
const sourceNodeId = nodeRef[link.source.id].originalNodeObject.id;
|
||||
const targetNodeId = nodeRef[link.target.id].originalNodeObject.id;
|
||||
if (!linkMap[sourceNodeId]) {
|
||||
linkMap[sourceNodeId] = {};
|
||||
}
|
||||
|
||||
linkMap[sourceNodeId][targetNodeId] = link.edgeType;
|
||||
}
|
||||
});
|
||||
|
||||
return $q.all(associatePromises.concat(credentialPromises))
|
||||
Object.keys(nodeRef).map((workflowNodeId) => {
|
||||
let nodeId = nodeRef[workflowNodeId].originalNodeObject.id;
|
||||
if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) {
|
||||
nodeRef[workflowNodeId].originalNodeObject.success_nodes.forEach((successNodeId) => {
|
||||
if (
|
||||
!deletedNodeIds.includes(successNodeId) &&
|
||||
(!linkMap[nodeId] ||
|
||||
!linkMap[nodeId][successNodeId] ||
|
||||
linkMap[nodeId][successNodeId] !== "success")
|
||||
) {
|
||||
disassociatePromises.push(
|
||||
TemplatesService.disassociateWorkflowNode({
|
||||
parentId: nodeId,
|
||||
nodeId: successNodeId,
|
||||
edge: "success"
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (nodeRef[workflowNodeId].originalNodeObject.failure_nodes) {
|
||||
nodeRef[workflowNodeId].originalNodeObject.failure_nodes.forEach((failureNodeId) => {
|
||||
if (
|
||||
!deletedNodeIds.includes(failureNodeId) &&
|
||||
(!linkMap[nodeId] ||
|
||||
!linkMap[nodeId][failureNodeId] ||
|
||||
linkMap[nodeId][failureNodeId] !== "failure")
|
||||
) {
|
||||
disassociatePromises.push(
|
||||
TemplatesService.disassociateWorkflowNode({
|
||||
parentId: nodeId,
|
||||
nodeId: failureNodeId,
|
||||
edge: "failure"
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (nodeRef[workflowNodeId].originalNodeObject.always_nodes) {
|
||||
nodeRef[workflowNodeId].originalNodeObject.always_nodes.forEach((alwaysNodeId) => {
|
||||
if (
|
||||
!deletedNodeIds.includes(alwaysNodeId) &&
|
||||
(!linkMap[nodeId] ||
|
||||
!linkMap[nodeId][alwaysNodeId] ||
|
||||
linkMap[nodeId][alwaysNodeId] !== "always")
|
||||
) {
|
||||
disassociatePromises.push(
|
||||
TemplatesService.disassociateWorkflowNode({
|
||||
parentId: nodeId,
|
||||
nodeId: alwaysNodeId,
|
||||
edge: "always"
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$q.all(disassociatePromises)
|
||||
.then(() => {
|
||||
Wait('stop');
|
||||
$scope.workflowChangesUnsaved = false;
|
||||
$scope.workflowChangesStarted = false;
|
||||
$scope.closeDialog();
|
||||
}).catch(({ data, status }) => {
|
||||
let associatePromises = [];
|
||||
Object.keys(linkMap).map((sourceNodeId) => {
|
||||
Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => {
|
||||
const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId];
|
||||
const targetChartNodeId = nodeIdToChartNodeIdMapping[targetNodeId];
|
||||
switch(linkMap[sourceNodeId][targetNodeId]) {
|
||||
case "success":
|
||||
if (
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes ||
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
|
||||
) {
|
||||
associatePromises.push(
|
||||
TemplatesService.associateWorkflowNode({
|
||||
parentId: parseInt(sourceNodeId),
|
||||
nodeId: parseInt(targetNodeId),
|
||||
edge: "success"
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "failure":
|
||||
if (
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes ||
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
|
||||
) {
|
||||
associatePromises.push(
|
||||
TemplatesService.associateWorkflowNode({
|
||||
parentId: parseInt(sourceNodeId),
|
||||
nodeId: parseInt(targetNodeId),
|
||||
edge: "failure"
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "always":
|
||||
if (
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes ||
|
||||
!nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id)
|
||||
) {
|
||||
associatePromises.push(
|
||||
TemplatesService.associateWorkflowNode({
|
||||
parentId: parseInt(sourceNodeId),
|
||||
nodeId: parseInt(targetNodeId),
|
||||
edge: "always"
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let credentialPromises = credentialRequests.map((request) => {
|
||||
return TemplatesService.postWorkflowNodeCredential({
|
||||
id: request.id,
|
||||
data: request.data
|
||||
});
|
||||
});
|
||||
|
||||
return $q.all(associatePromises.concat(credentialPromises))
|
||||
.then(() => {
|
||||
Wait('stop');
|
||||
$scope.workflowChangesUnsaved = false;
|
||||
$scope.workflowChangesStarted = false;
|
||||
$scope.closeDialog();
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
});
|
||||
}).catch(({
|
||||
data,
|
||||
status
|
||||
}) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
});
|
||||
}).catch(({
|
||||
data,
|
||||
status
|
||||
}) => {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
});
|
||||
}).catch(({ data, status }) => {
|
||||
Wait('stop');
|
||||
@ -400,7 +460,6 @@ export default ['$scope', 'TemplatesService',
|
||||
hdr: $scope.strings.get('error.HEADER')
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
let deletePromises = deletedNodeIds.map((nodeId) => {
|
||||
@ -511,17 +570,33 @@ export default ['$scope', 'TemplatesService',
|
||||
$scope.formState.showNodeForm = true;
|
||||
};
|
||||
|
||||
$scope.confirmNodeForm = (selectedTemplate, promptData, edgeType) => {
|
||||
$scope.confirmNodeForm = (nodeFormData) => {
|
||||
const { edgeType, selectedTemplate, promptData } = nodeFormData;
|
||||
const isPauseNode = selectedTemplate.type === "workflow_approval" ||
|
||||
selectedTemplate.unified_job_type === "workflow_approval";
|
||||
// edgeType, selectedTemplate, promptData
|
||||
// can determine pause node by looking at the type (?) or maybe unified_job_type
|
||||
$scope.workflowChangesUnsaved = true;
|
||||
const nodeId = $scope.nodeConfig.nodeId;
|
||||
if ($scope.nodeConfig.mode === "add") {
|
||||
if (selectedTemplate && edgeType && edgeType.value) {
|
||||
nodeRef[$scope.nodeConfig.nodeId] = {
|
||||
fullUnifiedJobTemplateObject: selectedTemplate,
|
||||
promptData,
|
||||
isNew: true
|
||||
};
|
||||
|
||||
if (edgeType && edgeType.value && selectedTemplate) {
|
||||
if (isPauseNode) {
|
||||
nodeRef[$scope.nodeConfig.nodeId] = {
|
||||
unifiedJobTemplate: {
|
||||
name: selectedTemplate.name,
|
||||
description: selectedTemplate.description,
|
||||
timeout: selectedTemplate.timeout,
|
||||
unified_job_type: "workflow_approval"
|
||||
},
|
||||
isNew: true
|
||||
};
|
||||
} else {
|
||||
nodeRef[$scope.nodeConfig.nodeId] = {
|
||||
fullUnifiedJobTemplateObject: selectedTemplate,
|
||||
promptData,
|
||||
isNew: true
|
||||
};
|
||||
}
|
||||
$scope.graphState.nodeBeingAdded = null;
|
||||
|
||||
$scope.graphState.arrayOfLinksForChart.map( (link) => {
|
||||
@ -533,9 +608,23 @@ export default ['$scope', 'TemplatesService',
|
||||
}
|
||||
} else if ($scope.nodeConfig.mode === "edit") {
|
||||
if (selectedTemplate) {
|
||||
nodeRef[$scope.nodeConfig.nodeId].fullUnifiedJobTemplateObject = selectedTemplate;
|
||||
nodeRef[$scope.nodeConfig.nodeId].promptData = _.cloneDeep(promptData);
|
||||
nodeRef[$scope.nodeConfig.nodeId].isEdited = true;
|
||||
if (isPauseNode) {
|
||||
// If it's a _new_ pause node then we'll want to create the new ujt
|
||||
// If it's an existing pause node then we'll want to update the ujt
|
||||
nodeRef[$scope.nodeConfig.nodeId].unifiedJobTemplate = {
|
||||
name: selectedTemplate.name,
|
||||
description: selectedTemplate.description,
|
||||
timeout: selectedTemplate.timeout,
|
||||
unified_job_type: "workflow_approval"
|
||||
};
|
||||
nodeRef[$scope.nodeConfig.nodeId].isEdited = true;
|
||||
} else {
|
||||
nodeRef[$scope.nodeConfig.nodeId].fullUnifiedJobTemplateObject = selectedTemplate;
|
||||
nodeRef[$scope.nodeConfig.nodeId].unifiedJobTemplate = selectedTemplate;
|
||||
nodeRef[$scope.nodeConfig.nodeId].promptData = _.cloneDeep(promptData);
|
||||
nodeRef[$scope.nodeConfig.nodeId].isEdited = true;
|
||||
}
|
||||
|
||||
$scope.graphState.nodeBeingEdited = null;
|
||||
|
||||
$scope.graphState.arrayOfLinksForChart.map( (link) => {
|
||||
@ -551,7 +640,17 @@ export default ['$scope', 'TemplatesService',
|
||||
|
||||
$scope.graphState.arrayOfNodesForChart.map( (node) => {
|
||||
if (node.id === nodeId) {
|
||||
node.unifiedJobTemplate = selectedTemplate;
|
||||
if (isPauseNode) {
|
||||
node.unifiedJobTemplate = {
|
||||
unified_job_type: 'workflow_approval',
|
||||
name: selectedTemplate.name,
|
||||
description: selectedTemplate.description,
|
||||
timeout: selectedTemplate.timeout,
|
||||
};
|
||||
} else {
|
||||
node.unifiedJobTemplate = selectedTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -68,39 +68,7 @@
|
||||
<div class="WorkflowLegend-maker">
|
||||
<div class="WorkflowLegend-maker--left">
|
||||
<i ng-class="{{ keyClassList }}" class="fa fa-compass Key-menuIcon" ng-click="toggleKey()"></i>
|
||||
<ul ng-show="showKey" class="Key-list noselect">
|
||||
<li class="Key-listItem">
|
||||
<p class="Key-heading">{{strings.get('workflow_maker.KEY')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--success"></div>
|
||||
<p class="Key-listItemContent">{{strings.get('workflow_maker.ON_SUCCESS')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--fail"></div>
|
||||
<p class="Key-listItemContent">{{strings.get('workflow_maker.ON_FAILURE')}}</p>
|
||||
</li>
|
||||
<li class="Key-listItem">
|
||||
<div class="Key-icon Key-icon--always"></div>
|
||||
<p class="Key-listItemContent">{{strings.get('workflow_maker.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.get('workflow_maker.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.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>
|
||||
</li>
|
||||
</ul>
|
||||
<workflow-key ng-show="showKey"></workflow-key>
|
||||
</div>
|
||||
<div class="WorkflowLegend-maker--right">
|
||||
<span class="WorkflowMaker-totalJobs">{{strings.get('workflow_maker.TOTAL_NODES')}}</span>
|
||||
@ -128,7 +96,7 @@
|
||||
</div>
|
||||
<div class="WorkflowMaker-contentRight">
|
||||
<span ng-if="formState.showNodeForm">
|
||||
<workflow-node-form node-config="nodeConfig" workflow-job-template-obj="workflowJobTemplateObj" select="confirmNodeForm(selectedTemplate, promptData, edgeType)" cancel="cancelNodeForm()" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"/>
|
||||
<workflow-node-form node-config="nodeConfig" workflow-job-template-obj="workflowJobTemplateObj" select="confirmNodeForm(nodeFormData)" cancel="cancelNodeForm()" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit"/>
|
||||
</span>
|
||||
<span ng-if="formState.showLinkForm">
|
||||
<workflow-link-form link-config="linkConfig" read-only="!workflowJobTemplateObj.summary_fields.user_capabilities.edit" select="confirmLinkForm(edgeType)" cancel="cancelLinkForm()" unlink="unlink()"/>
|
||||
|
||||
@ -327,35 +327,7 @@
|
||||
<div class="WorkflowLegend-details">
|
||||
<div class="WorkflowLegend-details--left">
|
||||
<i ng-class="{{ keyClassList }}" class="fa fa-compass 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>
|
||||
<workflow-key ng-show="showKey"></workflow-key>
|
||||
</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>
|
||||
|
||||
@ -15,13 +15,6 @@ export default {
|
||||
parent: 'jobs',
|
||||
label: '{{ workflow.id }} - {{ workflow.name }}'
|
||||
},
|
||||
data: {
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
},
|
||||
templateUrl: templateUrl('workflow-results/workflow-results'),
|
||||
controller: workflowResultsController,
|
||||
resolve: {
|
||||
|
||||
@ -32,6 +32,7 @@ from .workflow_job_templates import * # NOQA
|
||||
from .workflow_job_template_nodes import * # NOQA
|
||||
from .workflow_jobs import * # NOQA
|
||||
from .workflow_job_nodes import * # NOQA
|
||||
from .workflow_approvals import * # NOQA
|
||||
from .settings import * # NOQA
|
||||
from .instances import * # NOQA
|
||||
from .instance_groups import * # NOQA
|
||||
|
||||
30
awxkit/awxkit/api/pages/workflow_approvals.py
Normal file
30
awxkit/awxkit/api/pages/workflow_approvals.py
Normal file
@ -0,0 +1,30 @@
|
||||
from awxkit.api.pages import UnifiedJob
|
||||
from awxkit.api.resources import resources
|
||||
from . import page
|
||||
from awxkit import exceptions
|
||||
|
||||
|
||||
class WorkflowApproval(UnifiedJob):
|
||||
|
||||
def approve(self):
|
||||
try:
|
||||
self.related.approve.post()
|
||||
except exceptions.NoContent:
|
||||
pass
|
||||
|
||||
def deny(self):
|
||||
try:
|
||||
self.related.deny.post()
|
||||
except exceptions.NoContent:
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.workflow_approval, WorkflowApproval)
|
||||
|
||||
|
||||
class WorkflowApprovals(page.PageList, WorkflowApproval):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.workflow_approvals, WorkflowApprovals)
|
||||
@ -3,7 +3,7 @@ import awxkit.exceptions as exc
|
||||
from awxkit.api.pages import base, WorkflowJobTemplate, UnifiedJobTemplate, JobTemplate
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.utils import update_payload, PseudoNamespace, suppress
|
||||
from awxkit.utils import update_payload, PseudoNamespace, suppress, random_title
|
||||
from . import page
|
||||
|
||||
|
||||
@ -12,11 +12,24 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
|
||||
dependencies = [WorkflowJobTemplate, UnifiedJobTemplate]
|
||||
|
||||
def payload(self, workflow_job_template, unified_job_template, **kwargs):
|
||||
payload = PseudoNamespace(workflow_job_template=workflow_job_template.id,
|
||||
unified_job_template=unified_job_template.id)
|
||||
if not unified_job_template:
|
||||
# May pass "None" to explicitly create an approval node
|
||||
payload = PseudoNamespace(
|
||||
workflow_job_template=workflow_job_template.id)
|
||||
else:
|
||||
payload = PseudoNamespace(
|
||||
workflow_job_template=workflow_job_template.id,
|
||||
unified_job_template=unified_job_template.id)
|
||||
|
||||
optional_fields = ('diff_mode', 'extra_data', 'limit', 'job_tags', 'job_type', 'skip_tags', 'verbosity',
|
||||
'extra_data')
|
||||
optional_fields = (
|
||||
'diff_mode',
|
||||
'extra_data',
|
||||
'limit',
|
||||
'job_tags',
|
||||
'job_type',
|
||||
'skip_tags',
|
||||
'verbosity',
|
||||
'extra_data')
|
||||
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
|
||||
@ -25,21 +38,45 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
|
||||
|
||||
return payload
|
||||
|
||||
def create_payload(self, workflow_job_template=WorkflowJobTemplate, unified_job_template=JobTemplate, **kwargs):
|
||||
self.create_and_update_dependencies(workflow_job_template, unified_job_template)
|
||||
payload = self.payload(workflow_job_template=self.ds.workflow_job_template,
|
||||
unified_job_template=self.ds.unified_job_template, **kwargs)
|
||||
def create_payload(
|
||||
self,
|
||||
workflow_job_template=WorkflowJobTemplate,
|
||||
unified_job_template=JobTemplate,
|
||||
**kwargs):
|
||||
if not unified_job_template:
|
||||
self.create_and_update_dependencies(workflow_job_template)
|
||||
payload = self.payload(
|
||||
workflow_job_template=self.ds.workflow_job_template,
|
||||
unified_job_template=None,
|
||||
**kwargs)
|
||||
else:
|
||||
self.create_and_update_dependencies(
|
||||
workflow_job_template, unified_job_template)
|
||||
payload = self.payload(
|
||||
workflow_job_template=self.ds.workflow_job_template,
|
||||
unified_job_template=self.ds.unified_job_template,
|
||||
**kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, workflow_job_template=WorkflowJobTemplate, unified_job_template=JobTemplate, **kwargs):
|
||||
payload = self.create_payload(workflow_job_template=workflow_job_template,
|
||||
unified_job_template=unified_job_template, **kwargs)
|
||||
return self.update_identity(WorkflowJobTemplateNodes(self.connection).post(payload))
|
||||
def create(
|
||||
self,
|
||||
workflow_job_template=WorkflowJobTemplate,
|
||||
unified_job_template=JobTemplate,
|
||||
**kwargs):
|
||||
payload = self.create_payload(
|
||||
workflow_job_template=workflow_job_template,
|
||||
unified_job_template=unified_job_template,
|
||||
**kwargs)
|
||||
return self.update_identity(
|
||||
WorkflowJobTemplateNodes(
|
||||
self.connection).post(payload))
|
||||
|
||||
def _add_node(self, endpoint, unified_job_template):
|
||||
node = endpoint.post(dict(unified_job_template=unified_job_template.id))
|
||||
node.create_and_update_dependencies(self.ds.workflow_job_template, unified_job_template)
|
||||
node = endpoint.post(
|
||||
dict(unified_job_template=unified_job_template.id))
|
||||
node.create_and_update_dependencies(
|
||||
self.ds.workflow_job_template, unified_job_template)
|
||||
return node
|
||||
|
||||
def add_always_node(self, unified_job_template):
|
||||
@ -67,9 +104,20 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
|
||||
self.related.credentials.post(
|
||||
dict(id=cred.id, disassociate=True))
|
||||
|
||||
def make_approval_node(
|
||||
self,
|
||||
**kwargs
|
||||
):
|
||||
if 'name' not in kwargs:
|
||||
kwargs['name'] = 'approval node {}'.format(random_title())
|
||||
self.related.create_approval_template.post(kwargs)
|
||||
return self.get()
|
||||
|
||||
|
||||
page.register_page([resources.workflow_job_template_node,
|
||||
(resources.workflow_job_template_nodes, 'post')], WorkflowJobTemplateNode)
|
||||
(resources.workflow_job_template_nodes,
|
||||
'post')],
|
||||
WorkflowJobTemplateNode)
|
||||
|
||||
|
||||
class WorkflowJobTemplateNodes(page.PageList, WorkflowJobTemplateNode):
|
||||
@ -81,4 +129,5 @@ page.register_page([resources.workflow_job_template_nodes,
|
||||
resources.workflow_job_template_workflow_nodes,
|
||||
resources.workflow_job_template_node_always_nodes,
|
||||
resources.workflow_job_template_node_failure_nodes,
|
||||
resources.workflow_job_template_node_success_nodes], WorkflowJobTemplateNodes)
|
||||
resources.workflow_job_template_node_success_nodes],
|
||||
WorkflowJobTemplateNodes)
|
||||
|
||||
@ -81,6 +81,8 @@ class Resources(object):
|
||||
_inventory_update_events = r'inventory_updates/\d+/events/'
|
||||
_inventory_updates = 'inventory_updates/'
|
||||
_inventory_variable_data = r'inventories/\d+/variable_data/'
|
||||
_workflow_approval = r'workflow_approvals/\d+/'
|
||||
_workflow_approvals = 'workflow_approvals/'
|
||||
_job = r'jobs/\d+/'
|
||||
_job_cancel = r'jobs/\d+/cancel/'
|
||||
_job_create_schedule = r'jobs/\d+/create_schedule/'
|
||||
|
||||
@ -54,6 +54,32 @@ In the event that spawning the workflow would result in recursion, the child wor
|
||||
will be marked as failed with a message explaining that recursion was detected.
|
||||
This is to prevent saturation of the task system with an infinite chain of workflows.
|
||||
|
||||
#### Workflow Approval Nodes
|
||||
|
||||
The workflow approval node feature enables users to add steps in a workflow in between nodes within workflows so that a user (as long as they have approval permissions, explained in further detail below) can give the "yes" or "no" to continue on to the next step in the workflow.
|
||||
|
||||
**RBAC Setup for Workflow Approval Nodes**
|
||||
|
||||
A user can _create_ a workflow approval if they are:
|
||||
- a Superuser
|
||||
- an Org Admin of the organization connected to the workflow
|
||||
- a Workflow Admin in the organization connected to the workflow
|
||||
- assigned as admins to a particular workflow
|
||||
|
||||
A user can _approve_ a workflow when they are:
|
||||
- a Superuser
|
||||
- a Workflow Admin
|
||||
- an Organization Admin
|
||||
- any user who has explicitly been assigned the "approver" role
|
||||
|
||||
A user can _view_ approvals if they:
|
||||
- have Read access to the associated Workflow Job Template
|
||||
|
||||
**Other Workflow Approval Node Features**
|
||||
|
||||
A timeout (in minutes and seconds) can be set for each approval node. These fields default to `0` for no expiration.
|
||||
|
||||
|
||||
### DAG Formation and Restrictions
|
||||
The DAG structure of a workflow is enforced by associating workflow job template nodes via endpoints `/workflow_job_template_nodes/\d+/*_nodes/`, where `*` has options `success`, `failure` and `always`. There is one restriction that is enforced when setting up new connections and that is the cycle restriction, since it's a DAG.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user