mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Merge branch 'devel' into workflow-channels
This commit is contained in:
commit
a80e54a62e
@ -1 +1,2 @@
|
||||
.git
|
||||
awx/ui/node_modules
|
||||
|
||||
4
Makefile
4
Makefile
@ -231,10 +231,14 @@ clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle
|
||||
rm -rf awx/public
|
||||
rm -rf awx/lib/site-packages
|
||||
rm -rf dist/*
|
||||
rm -rf awx/job_status
|
||||
rm -rf reports
|
||||
rm -f awx/awx_test.sqlite3
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
||||
find . -type f -regex ".*\.py[co]$$" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
|
||||
# convenience target to assert environment variables are defined
|
||||
guard-%:
|
||||
|
||||
@ -2682,9 +2682,10 @@ class NotificationTemplateSerializer(BaseSerializer):
|
||||
def to_representation(self, obj):
|
||||
ret = super(NotificationTemplateSerializer, self).to_representation(obj)
|
||||
for field in obj.notification_class.init_parameters:
|
||||
if field in ret['notification_configuration'] and \
|
||||
force_text(ret['notification_configuration'][field]).startswith('$encrypted$'):
|
||||
ret['notification_configuration'][field] = '$encrypted$'
|
||||
config = obj.notification_configuration
|
||||
if field in config and force_text(config[field]).startswith('$encrypted$'):
|
||||
config[field] = '$encrypted$'
|
||||
ret['notification_configuration'] = config
|
||||
return ret
|
||||
|
||||
def get_related(self, obj):
|
||||
|
||||
@ -2332,23 +2332,20 @@ class JobTemplateSurveySpec(GenericAPIView):
|
||||
|
||||
if not request.user.can_access(self.model, 'change', obj, None):
|
||||
raise PermissionDenied()
|
||||
try:
|
||||
obj.survey_spec = json.dumps(request.data)
|
||||
except ValueError:
|
||||
return Response(dict(error=_("Invalid JSON when parsing survey spec.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
if "name" not in obj.survey_spec:
|
||||
new_spec = request.data
|
||||
if "name" not in new_spec:
|
||||
return Response(dict(error=_("'name' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
if "description" not in obj.survey_spec:
|
||||
if "description" not in new_spec:
|
||||
return Response(dict(error=_("'description' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
if "spec" not in obj.survey_spec:
|
||||
if "spec" not in new_spec:
|
||||
return Response(dict(error=_("'spec' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
if not isinstance(obj.survey_spec["spec"], list):
|
||||
if not isinstance(new_spec["spec"], list):
|
||||
return Response(dict(error=_("'spec' must be a list of items.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
if len(obj.survey_spec["spec"]) < 1:
|
||||
if len(new_spec["spec"]) < 1:
|
||||
return Response(dict(error=_("'spec' doesn't contain any items.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
idx = 0
|
||||
variable_set = set()
|
||||
for survey_item in obj.survey_spec["spec"]:
|
||||
for survey_item in new_spec["spec"]:
|
||||
if not isinstance(survey_item, dict):
|
||||
return Response(dict(error=_("Survey question %s is not a json object.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
|
||||
if "type" not in survey_item:
|
||||
@ -2365,7 +2362,8 @@ class JobTemplateSurveySpec(GenericAPIView):
|
||||
if "required" not in survey_item:
|
||||
return Response(dict(error=_("'required' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
|
||||
idx += 1
|
||||
obj.save()
|
||||
obj.survey_spec = new_spec
|
||||
obj.save(update_fields=['survey_spec'])
|
||||
return Response()
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
|
||||
@ -1956,7 +1956,7 @@ class LabelAccess(BaseAccess):
|
||||
def get_queryset(self):
|
||||
if self.user.is_superuser or self.user.is_system_auditor:
|
||||
return self.model.objects.all()
|
||||
return self.model.objects.filter(
|
||||
return self.model.objects.all().filter(
|
||||
organization__in=Organization.accessible_objects(self.user, 'read_role')
|
||||
)
|
||||
|
||||
|
||||
0
awx/main/tests/functional/__init__.py
Normal file
0
awx/main/tests/functional/__init__.py
Normal file
@ -3,6 +3,7 @@
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import os
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
@ -10,9 +11,17 @@ from django.core.urlresolvers import reverse
|
||||
# AWX
|
||||
from awx.conf.models import Setting
|
||||
|
||||
'''
|
||||
Ensures that tests don't pick up dev container license file
|
||||
'''
|
||||
@pytest.fixture
|
||||
def mock_no_license_file(mocker):
|
||||
os.environ['AWX_LICENSE_FILE'] = '/does_not_exist'
|
||||
return None
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_license_cannot_be_removed_via_system_settings(get, put, patch, delete, admin, enterprise_license):
|
||||
def test_license_cannot_be_removed_via_system_settings(mock_no_license_file, get, put, patch, delete, admin, enterprise_license):
|
||||
|
||||
url = reverse('api:setting_singleton_detail', args=('system',))
|
||||
response = get(url, user=admin, expect=200)
|
||||
assert not response.data['LICENSE']
|
||||
|
||||
@ -101,8 +101,11 @@ class TestInventoryUpdateLatestDict():
|
||||
inv_src2 = InventorySource.objects.create(group=g2, update_on_launch=False, inventory=inventory)
|
||||
inv_src3 = InventorySource.objects.create(group=g3, update_on_launch=True, inventory=inventory)
|
||||
|
||||
import time
|
||||
iu1 = InventoryUpdate.objects.create(inventory_source=inv_src1, status='successful')
|
||||
time.sleep(0.1)
|
||||
iu2 = InventoryUpdate.objects.create(inventory_source=inv_src2, status='waiting')
|
||||
time.sleep(0.1)
|
||||
iu3 = InventoryUpdate.objects.create(inventory_source=inv_src3, status='waiting')
|
||||
return [iu1, iu2, iu3]
|
||||
|
||||
@ -114,7 +117,7 @@ class TestInventoryUpdateLatestDict():
|
||||
inventory_updates_expected = [inventory_updates[0], inventory_updates[2]]
|
||||
|
||||
assert 2 == len(tasks)
|
||||
for i, inventory_update in enumerate(inventory_updates_expected):
|
||||
assert inventory_update.id == tasks[i]['id']
|
||||
|
||||
task_ids = [task['id'] for task in tasks]
|
||||
for inventory_update in inventory_updates_expected:
|
||||
inventory_update.id in task_ids
|
||||
|
||||
|
||||
@ -6,8 +6,9 @@ from awx.main.access import (
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_label_get_queryset_user(label, user):
|
||||
access = LabelAccess(user('user', False))
|
||||
label.organization.member_role.members.add(user('user', False))
|
||||
u = user('user', False)
|
||||
access = LabelAccess(u)
|
||||
label.organization.member_role.members.add(u)
|
||||
assert access.get_queryset().count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
0
awx/main/tests/unit/__init__.py
Normal file
0
awx/main/tests/unit/__init__.py
Normal file
48
awx/main/tests/unit/test_python_requirements.py
Normal file
48
awx/main/tests/unit/test_python_requirements.py
Normal file
@ -0,0 +1,48 @@
|
||||
from pip.operations import freeze
|
||||
from django.conf import settings
|
||||
|
||||
def test_req():
|
||||
def check_is_in(src, dests):
|
||||
if src not in dests:
|
||||
src2 = [src[0].replace('_', '-'), src[1]]
|
||||
if src2 not in dests:
|
||||
print("%s not in" % src2)
|
||||
return False
|
||||
else:
|
||||
print("%s not in" % src)
|
||||
return False
|
||||
return True
|
||||
|
||||
base_dir = settings.BASE_DIR
|
||||
|
||||
reqs_actual = []
|
||||
xs = freeze.freeze(local_only=True, requirement=base_dir + "/../requirements/requirements.txt")
|
||||
for x in xs:
|
||||
if '## The following requirements were added by pip freeze' in x:
|
||||
break
|
||||
reqs_actual.append(x.split('=='))
|
||||
|
||||
reqs_expected = []
|
||||
with open(base_dir + "/../requirements/requirements.txt") as f:
|
||||
for line in f:
|
||||
line.rstrip()
|
||||
# TODO: process git requiremenst and use egg
|
||||
if line.strip().startswith('#') or line.strip().startswith('git'):
|
||||
continue
|
||||
if line.startswith('-e'):
|
||||
continue
|
||||
line.rstrip()
|
||||
reqs_expected.append(line.rstrip().split('=='))
|
||||
|
||||
for r in reqs_actual:
|
||||
print(r)
|
||||
|
||||
not_found = []
|
||||
for r in reqs_expected:
|
||||
res = check_is_in(r, reqs_actual)
|
||||
if res is False:
|
||||
not_found.append(r)
|
||||
|
||||
raise RuntimeError("%s not found in \n\n%s" % (not_found, reqs_expected))
|
||||
|
||||
|
||||
@ -905,6 +905,12 @@ input[type="checkbox"].checkbox-no-label {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
.radio-inline + .radio-inline {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
.radio-inline + .radio-inline,
|
||||
.checkbox-inline + .checkbox-inline {
|
||||
|
||||
@ -184,6 +184,7 @@
|
||||
.Form-formGroup--fullWidth {
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
.Form-formGroup--checkbox{
|
||||
@ -553,19 +554,24 @@ input[type='radio']:checked:before {
|
||||
color: @btn-txt;
|
||||
}
|
||||
|
||||
.Form-surveyButton {
|
||||
.Form-primaryButton {
|
||||
background-color: @default-link;
|
||||
color: @default-bg;
|
||||
text-transform: uppercase;
|
||||
padding-left:15px;
|
||||
padding-right: 15px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.Form-surveyButton:hover{
|
||||
.Form-primaryButton:hover {
|
||||
background-color: @default-link-hov;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.Form-primaryButton.Form-tab--disabled:hover {
|
||||
background-color: @default-link;
|
||||
}
|
||||
|
||||
.Form-formGroup--singleColumn {
|
||||
width: 100% !important;
|
||||
padding-right: 0px;
|
||||
|
||||
@ -357,6 +357,32 @@ table, tbody {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.List-dropdownButton {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.List-dropdownSuccess {
|
||||
background-color: @submit-button-bg;
|
||||
color: @submit-button-text;
|
||||
border-color: @submit-button-bg-hov;
|
||||
}
|
||||
|
||||
.List-dropdownSuccess:hover,
|
||||
.List-dropdownSuccess:focus {
|
||||
color: @submit-button-text;
|
||||
background-color: @submit-button-bg-hov;
|
||||
}
|
||||
|
||||
.List-dropdownCarat {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
vertical-align: middle;
|
||||
border-top: 4px dashed;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.List-searchWidget + .List-searchWidget {
|
||||
margin-top: 20px;
|
||||
|
||||
@ -38,7 +38,6 @@ if ($basePath) {
|
||||
|
||||
// Modules
|
||||
import './helpers';
|
||||
import * as forms from './forms';
|
||||
import './lists';
|
||||
import './widgets';
|
||||
import './filters';
|
||||
@ -53,6 +52,7 @@ import inventoryScripts from './inventory-scripts/main';
|
||||
import organizations from './organizations/main';
|
||||
import managementJobs from './management-jobs/main';
|
||||
import jobDetail from './job-detail/main';
|
||||
import workflowResults from './workflow-results/main';
|
||||
import jobSubmission from './job-submission/main';
|
||||
import notifications from './notifications/main';
|
||||
import about from './about/main';
|
||||
@ -122,6 +122,7 @@ var tower = angular.module('Tower', [
|
||||
activityStream.name,
|
||||
footer.name,
|
||||
jobDetail.name,
|
||||
workflowResults.name,
|
||||
jobSubmission.name,
|
||||
notifications.name,
|
||||
standardOut.name,
|
||||
@ -168,7 +169,6 @@ var tower = angular.module('Tower', [
|
||||
'ProjectsHelper',
|
||||
'CompletedJobsDefinition',
|
||||
'AllJobsDefinition',
|
||||
'JobFormDefinition',
|
||||
'JobSummaryDefinition',
|
||||
'ParseHelper',
|
||||
'ChildrenHelper',
|
||||
@ -202,6 +202,9 @@ var tower = angular.module('Tower', [
|
||||
'ActivityStreamHelper',
|
||||
'gettext',
|
||||
'I18N',
|
||||
'WorkflowFormDefinition',
|
||||
'InventorySourcesListDefinition',
|
||||
'WorkflowMakerFormDefinition'
|
||||
])
|
||||
|
||||
.constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
|
||||
|
||||
@ -22,7 +22,6 @@ export default [
|
||||
'CreateSelect2',
|
||||
'GenerateForm',
|
||||
'ParseTypeChange',
|
||||
'Wait',
|
||||
function(
|
||||
$scope,
|
||||
$state,
|
||||
@ -40,8 +39,7 @@ export default [
|
||||
ConfigurationUtils,
|
||||
CreateSelect2,
|
||||
GenerateForm,
|
||||
ParseTypeChange,
|
||||
Wait
|
||||
ParseTypeChange
|
||||
) {
|
||||
var authVm = this;
|
||||
|
||||
|
||||
@ -122,6 +122,30 @@ export default [
|
||||
|
||||
$scope.configDataResolve = configDataResolve;
|
||||
|
||||
var triggerModal = function(msg, title, buttons) {
|
||||
if ($scope.removeModalReady) {
|
||||
$scope.removeModalReady();
|
||||
}
|
||||
$scope.removeModalReady = $scope.$on('ModalReady', function() {
|
||||
// $('#lookup-save-button').attr('disabled', 'disabled');
|
||||
$('#FormModal-dialog').dialog('open');
|
||||
});
|
||||
|
||||
$('#FormModal-dialog').html(msg);
|
||||
|
||||
CreateDialog({
|
||||
scope: $scope,
|
||||
buttons: buttons,
|
||||
width: 600,
|
||||
height: 200,
|
||||
minWidth: 500,
|
||||
title: title,
|
||||
id: 'FormModal-dialog',
|
||||
resizable: false,
|
||||
callback: 'ModalReady'
|
||||
});
|
||||
};
|
||||
|
||||
function activeTabCheck(setForm) {
|
||||
if(!$scope[formTracker.currentFormName()].$dirty) {
|
||||
active(setForm);
|
||||
@ -212,7 +236,7 @@ export default [
|
||||
payload[key] = $scope.configDataResolve[key].default;
|
||||
|
||||
ConfigurationService.patchConfiguration(payload)
|
||||
.then(function(data) {
|
||||
.then(function() {
|
||||
$scope[key] = $scope.configDataResolve[key].default;
|
||||
})
|
||||
.catch(function(error) {
|
||||
@ -228,30 +252,6 @@ export default [
|
||||
});
|
||||
};
|
||||
|
||||
var triggerModal = function(msg, title, buttons) {
|
||||
if ($scope.removeModalReady) {
|
||||
$scope.removeModalReady();
|
||||
}
|
||||
$scope.removeModalReady = $scope.$on('ModalReady', function() {
|
||||
// $('#lookup-save-button').attr('disabled', 'disabled');
|
||||
$('#FormModal-dialog').dialog('open');
|
||||
});
|
||||
|
||||
$('#FormModal-dialog').html(msg);
|
||||
|
||||
CreateDialog({
|
||||
scope: $scope,
|
||||
buttons: buttons,
|
||||
width: 600,
|
||||
height: 200,
|
||||
minWidth: 500,
|
||||
title: title,
|
||||
id: 'FormModal-dialog',
|
||||
resizable: false,
|
||||
callback: 'ModalReady'
|
||||
});
|
||||
};
|
||||
|
||||
function clearApiErrors() {
|
||||
var currentForm = formDefs[formTracker.getCurrent()];
|
||||
for (var fld in currentForm.fields) {
|
||||
@ -332,7 +332,7 @@ export default [
|
||||
var payload = {};
|
||||
payload[key] = $scope[key];
|
||||
ConfigurationService.patchConfiguration(payload)
|
||||
.then(function(results) {
|
||||
.then(function() {
|
||||
//TODO consider updating form values with returned data here
|
||||
})
|
||||
.catch(function(error, status) {
|
||||
@ -352,7 +352,7 @@ export default [
|
||||
var resetAll = function() {
|
||||
Wait('start');
|
||||
ConfigurationService.resetAll()
|
||||
.then(function(results) {
|
||||
.then(function() {
|
||||
populateFromApi();
|
||||
$scope[formTracker.currentFormName].$setPristine();
|
||||
})
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['GetBasePath', 'ProcessErrors', '$q', '$http', 'Rest', '$rootScope', '$timeout', 'Wait',
|
||||
function(GetBasePath, ProcessErrors, $q, $http, Rest, $rootScope, $timeout, Wait) {
|
||||
export default ['GetBasePath', 'ProcessErrors', '$q', '$http', 'Rest',
|
||||
function(GetBasePath, ProcessErrors, $q, $http, Rest) {
|
||||
var url = GetBasePath('settings');
|
||||
|
||||
return {
|
||||
|
||||
@ -8,7 +8,7 @@ export default [
|
||||
function() {
|
||||
|
||||
return {
|
||||
listToArray: function(input, key) {
|
||||
listToArray: function(input) {
|
||||
if (input.indexOf('\n') !== -1) {
|
||||
//Parse multiline input
|
||||
return input.replace(/^\s+|\s+$/g, "").split('\n');
|
||||
@ -17,7 +17,7 @@ export default [
|
||||
}
|
||||
},
|
||||
|
||||
arrayToList: function(input, key) {
|
||||
arrayToList: function(input) {
|
||||
var multiLineInput = false;
|
||||
_.each(input, function(statement) {
|
||||
if (statement.indexOf(',') !== -1) {
|
||||
@ -40,7 +40,7 @@ export default [
|
||||
return true;
|
||||
},
|
||||
|
||||
formatPlaceholder: function(input, key) {
|
||||
formatPlaceholder: function(input) {
|
||||
if(input !== null && typeof input === 'object') {
|
||||
if(Array.isArray(input)) {
|
||||
var multiLineInput = false;
|
||||
|
||||
@ -41,7 +41,7 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $
|
||||
};
|
||||
|
||||
$scope.relaunchJob = function(event, id) {
|
||||
var list, job, typeId;
|
||||
var job, typeId;
|
||||
try {
|
||||
$(event.target).tooltip('hide');
|
||||
} catch (e) {
|
||||
@ -84,6 +84,9 @@ export function JobsListController($state, $rootScope, $log, $scope, $compile, $
|
||||
case 'inventory_update':
|
||||
goToJobDetails('inventorySyncStdout');
|
||||
break;
|
||||
case 'workflow_job':
|
||||
goToJobDetails('workflowResults');
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -114,7 +114,7 @@ UsersList.$inject = ['$scope', '$rootScope', '$stateParams',
|
||||
|
||||
export function UsersAdd($scope, $rootScope, $stateParams, UserForm,
|
||||
GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, ClearScope,
|
||||
GetBasePath, ResetForm, Wait, CreateSelect2, $state, i18n) {
|
||||
GetBasePath, ResetForm, Wait, CreateSelect2, $state, $location) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -201,7 +201,7 @@ export function UsersAdd($scope, $rootScope, $stateParams, UserForm,
|
||||
|
||||
UsersAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'UserForm', 'GenerateForm',
|
||||
'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath',
|
||||
'ResetForm', 'Wait', 'CreateSelect2', '$state', 'i18n'
|
||||
'ResetForm', 'Wait', 'CreateSelect2', '$state', '$location'
|
||||
];
|
||||
|
||||
export function UsersEdit($scope, $rootScope, $location,
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
export default
|
||||
[ 'InitiatePlaybookRun',
|
||||
'templateUrl',
|
||||
'$location',
|
||||
function JobTemplatesList(InitiatePlaybookRun, templateUrl, $location) {
|
||||
'$state',
|
||||
function JobTemplatesList(InitiatePlaybookRun, templateUrl, $state) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: link,
|
||||
@ -47,7 +47,7 @@ export default
|
||||
};
|
||||
|
||||
scope.editJobTemplate = function (jobTemplateId) {
|
||||
$location.path( '/job_templates/' + jobTemplateId);
|
||||
$state.go('templates.editJobTemplate', {id: jobTemplateId});
|
||||
};
|
||||
}
|
||||
}];
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<h3 class="DashboardList-headerText">
|
||||
<translate>RECENTLY USED JOB TEMPLATES</translate>
|
||||
</h3>
|
||||
<a href="/#/job_templates" class="DashboardList-viewAll">
|
||||
<a href="/#/templates" class="DashboardList-viewAll">
|
||||
<translate>VIEW ALL</translate>
|
||||
</a>
|
||||
</div>
|
||||
@ -25,7 +25,7 @@
|
||||
ng-class-even="'List-tableRow--evenRow'"
|
||||
ng-repeat = "job_template in job_templates">
|
||||
<td class="DashboardList-nameCell">
|
||||
<a href="#/job_templates/{{ job_template.id }}" class="DashboardList-nameContainer">
|
||||
<a href="#/templates/{{ job_template.id }}" class="DashboardList-nameContainer">
|
||||
{{ job_template.name }}
|
||||
</a>
|
||||
</td>
|
||||
@ -53,7 +53,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="DashboardList-container">
|
||||
<p class="DashboardList-noJobs" translate>No job templates were recently used.<br />
|
||||
You can create a job template <a href="#/job_templates/add">here</a>.</p>
|
||||
<p class="DashboardList-noJobs">No job templates were recently used.<br />
|
||||
You can create a job template <a href="#/templates/add">here</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,6 @@ import JobEventData from "./forms/JobEventData";
|
||||
import JobSummary from "./forms/JobSummary";
|
||||
import JobTemplates from "./forms/JobTemplates";
|
||||
import JobVarsPrompt from "./forms/JobVarsPrompt";
|
||||
import Jobs from "./forms/Jobs";
|
||||
import LogViewerOptions from "./forms/LogViewerOptions";
|
||||
import LogViewerStatus from "./forms/LogViewerStatus";
|
||||
import Organizations from "./forms/Organizations";
|
||||
@ -24,6 +23,8 @@ import ProjectStatus from "./forms/ProjectStatus";
|
||||
import Projects from "./forms/Projects";
|
||||
import Teams from "./forms/Teams";
|
||||
import Users from "./forms/Users";
|
||||
import WorkflowMaker from "./forms/WorkflowMaker";
|
||||
import Workflows from "./forms/Workflows";
|
||||
|
||||
|
||||
export
|
||||
@ -39,12 +40,13 @@ export
|
||||
JobSummary,
|
||||
JobTemplates,
|
||||
JobVarsPrompt,
|
||||
Jobs,
|
||||
LogViewerOptions,
|
||||
LogViewerStatus,
|
||||
Organizations,
|
||||
ProjectStatus,
|
||||
Projects,
|
||||
Teams,
|
||||
Users
|
||||
Users,
|
||||
WorkflowMaker,
|
||||
Workflows
|
||||
};
|
||||
|
||||
@ -132,7 +132,7 @@ angular.module('InventoryFormDefinition', ['ScanJobsListDefinition'])
|
||||
}
|
||||
},
|
||||
|
||||
relatedSets: function(urls) {
|
||||
relatedSets: function() {
|
||||
return {
|
||||
permissions: {
|
||||
awToolTip: i18n._('Please save before assigning permissions'),
|
||||
|
||||
@ -20,10 +20,12 @@ export default
|
||||
addTitle: i18n._('New Job Template'),
|
||||
editTitle: '{{ name }}',
|
||||
name: 'job_template',
|
||||
breadcrumbName: 'JOB TEMPLATE',
|
||||
basePath: 'job_templates',
|
||||
// the top-most node of generated state tree
|
||||
stateTree: 'jobTemplates',
|
||||
stateTree: 'templates',
|
||||
tabs: true,
|
||||
activeEditState: 'templates.editJobTemplate',
|
||||
// (optional) array of supporting templates to ng-include inside generated html
|
||||
include: ['/static/partials/survey-maker-modal.html'],
|
||||
|
||||
@ -31,7 +33,7 @@ export default
|
||||
name: {
|
||||
label: i18n._('Name'),
|
||||
type: 'text',
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)',
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
|
||||
required: true,
|
||||
column: 1
|
||||
},
|
||||
@ -39,7 +41,7 @@ export default
|
||||
label: i18n._('Description'),
|
||||
type: 'text',
|
||||
column: 1,
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
job_type: {
|
||||
label: i18n._('Job Type'),
|
||||
@ -61,7 +63,7 @@ export default
|
||||
ngShow: "!job_type.value || job_type.value !== 'scan'",
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
inventory: {
|
||||
label: i18n._('Inventory'),
|
||||
@ -85,7 +87,7 @@ export default
|
||||
ngShow: "!job_type.value || job_type.value !== 'scan'",
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
project: {
|
||||
label: i18n._('Project'),
|
||||
@ -108,13 +110,13 @@ export default
|
||||
dataTitle: i18n._('Project'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
playbook: {
|
||||
label: i18n._('Playbook'),
|
||||
type:'select',
|
||||
ngOptions: 'book for book in playbook_options track by book',
|
||||
ngDisabled: "(job_type.value === 'scan' && project_name === 'Default') || !(job_template_obj.summary_fields.user_capabilities.edit || canAdd)",
|
||||
ngDisabled: "(job_type.value === 'scan' && project_name === 'Default') || !(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)",
|
||||
id: 'playbook-select',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "playbookrequired",
|
||||
@ -152,7 +154,7 @@ export default
|
||||
variable: 'ask_credential_on_launch',
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
cloud_credential: {
|
||||
label: i18n._('Cloud Credential'),
|
||||
@ -170,7 +172,7 @@ export default
|
||||
dataTitle: i18n._('Cloud Credential'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
network_credential: {
|
||||
label: i18n._('Network Credential'),
|
||||
@ -187,7 +189,7 @@ export default
|
||||
dataTitle: i18n._('Network Credential'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
forks: {
|
||||
label: i18n._('Forks'),
|
||||
@ -205,7 +207,7 @@ export default
|
||||
dataTitle: i18n._('Forks'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' // TODO: get working
|
||||
},
|
||||
limit: {
|
||||
label: i18n._('Limit'),
|
||||
@ -221,7 +223,7 @@ export default
|
||||
variable: 'ask_limit_on_launch',
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
verbosity: {
|
||||
label: i18n._('Verbosity'),
|
||||
@ -234,7 +236,7 @@ export default
|
||||
dataTitle: i18n._('Verbosity'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
job_tags: {
|
||||
label: i18n._('Job Tags'),
|
||||
@ -252,7 +254,7 @@ export default
|
||||
variable: 'ask_tags_on_launch',
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
skip_tags: {
|
||||
label: i18n._('Skip Tags'),
|
||||
@ -270,7 +272,7 @@ export default
|
||||
variable: 'ask_skip_tags_on_launch',
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
checkbox_group: {
|
||||
label: i18n._('Options'),
|
||||
@ -285,7 +287,7 @@ export default
|
||||
dataTitle: i18n._('Become Privilege Escalation'),
|
||||
dataContainer: "body",
|
||||
labelClass: 'stack-inline',
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
}, {
|
||||
name: 'allow_callbacks',
|
||||
label: i18n._('Allow Provisioning Callbacks'),
|
||||
@ -298,7 +300,7 @@ export default
|
||||
dataTitle: i18n._('Allow Provisioning Callbacks'),
|
||||
dataContainer: "body",
|
||||
labelClass: 'stack-inline',
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
}]
|
||||
},
|
||||
callback_url: {
|
||||
@ -312,7 +314,7 @@ export default
|
||||
dataPlacement: 'top',
|
||||
dataTitle: i18n._('Provisioning Callback URL'),
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
host_config_key: {
|
||||
label: i18n._('Host Config Key'),
|
||||
@ -326,7 +328,7 @@ export default
|
||||
dataPlacement: 'right',
|
||||
dataTitle: i18n._("Host Config Key"),
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
labels: {
|
||||
label: i18n._('Labels'),
|
||||
@ -338,7 +340,7 @@ export default
|
||||
dataPlacement: 'right',
|
||||
awPopOver: i18n._("<p>Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.</p>"),
|
||||
dataContainer: 'body',
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
variables: {
|
||||
label: i18n._('Extra Variables'),
|
||||
@ -360,14 +362,14 @@ export default
|
||||
variable: 'ask_variables_on_launch',
|
||||
text: i18n._('Prompt on launch')
|
||||
},
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working
|
||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' // TODO: get working
|
||||
}
|
||||
},
|
||||
|
||||
buttons: { //for now always generates <button> tags
|
||||
add_survey: {
|
||||
ngClick: 'addSurvey()',
|
||||
ngShow: 'job_type.value !== "scan" && !survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAdd)',
|
||||
ngShow: 'job_type.value !== "scan" && !survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
|
||||
awFeature: 'surveys',
|
||||
awToolTip: 'Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.',
|
||||
dataPlacement: 'top'
|
||||
@ -375,25 +377,25 @@ export default
|
||||
edit_survey: {
|
||||
ngClick: 'editSurvey()',
|
||||
awFeature: 'surveys',
|
||||
ngShow: 'job_type.value !== "scan" && survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngShow: 'job_type.value !== "scan" && survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
view_survey: {
|
||||
ngClick: 'editSurvey()',
|
||||
awFeature: 'surveys',
|
||||
ngShow: 'job_type.value !== "scan" && survey_exists && !(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngShow: 'job_type.value !== "scan" && survey_exists && !(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'formCancel()',
|
||||
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
close: {
|
||||
ngClick: 'formCancel()',
|
||||
ngShow: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngShow: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
},
|
||||
save: {
|
||||
ngClick: 'formSave()', //$scope.function to call on click, optional
|
||||
ngDisabled: "job_templates_form.$invalid",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
|
||||
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
}
|
||||
},
|
||||
|
||||
@ -420,7 +422,7 @@ export default
|
||||
awToolTip: 'Add a permission',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ADD',
|
||||
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name forms.function:Jobs
|
||||
* @description This form is for adding/editing a Job
|
||||
*/
|
||||
|
||||
export default
|
||||
angular.module('JobFormDefinition', [])
|
||||
.value('JobForm', {
|
||||
|
||||
addTitle: 'Create Job',
|
||||
editTitle: '{{ id }} - {{ name }}',
|
||||
name: 'jobs',
|
||||
stateTree: 'jobs',
|
||||
well: true,
|
||||
base: 'jobs',
|
||||
tabs: true,
|
||||
|
||||
navigationLinks: {
|
||||
details: {
|
||||
href: "/#/jobs/{{ job_id }}",
|
||||
label: 'Status',
|
||||
icon: 'icon-zoom-in',
|
||||
active: true,
|
||||
ngShow: "job_id !== null"
|
||||
},
|
||||
events: {
|
||||
href: "/#/jobs/{{ job_id }}/job_events",
|
||||
label: 'Events',
|
||||
icon: 'icon-list-ul'
|
||||
},
|
||||
hosts: {
|
||||
href: "/#/jobs/{{ job_id }}/job_host_summaries",
|
||||
label: 'Host Summary',
|
||||
icon: 'icon-laptop'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
type: 'custom',
|
||||
control: "<i class=\"fa icon-job-{{ status }}\"></i> {{ job_explanation }}",
|
||||
readonly: true
|
||||
},
|
||||
result_stdout: {
|
||||
label: 'Standard Out',
|
||||
type: 'textarea',
|
||||
readonly: true,
|
||||
xtraWide: true,
|
||||
rows: "{{ stdout_rows }}",
|
||||
"class": 'nowrap mono-space allowresize',
|
||||
ngShow: "result_stdout != ''"
|
||||
},
|
||||
result_traceback: {
|
||||
label: 'Traceback',
|
||||
type: 'textarea',
|
||||
xtraWide: true,
|
||||
readonly: true,
|
||||
rows: "{{ traceback_rows }}",
|
||||
"class": 'nowrap mono-space allowresize',
|
||||
ngShow: "result_traceback != ''"
|
||||
},
|
||||
type: {
|
||||
label: 'Job Type',
|
||||
type: 'text',
|
||||
readonly: true
|
||||
},
|
||||
launch_type: {
|
||||
label: 'Launch Type',
|
||||
type: 'text',
|
||||
readonly: true
|
||||
},
|
||||
created: {
|
||||
label: 'Created On',
|
||||
type: 'text',
|
||||
readonly: true
|
||||
},
|
||||
modified: {
|
||||
label: 'Last Updated',
|
||||
type: 'text',
|
||||
readonly: true
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
refresh: {
|
||||
dataPlacement: 'top',
|
||||
icon: "icon-refresh",
|
||||
iconSize: 'large',
|
||||
mode: 'all',
|
||||
//ngShow: "job_status == 'pending' || job_status == 'waiting' || job_status == 'running'",
|
||||
'class': 'btn-xs btn-primary',
|
||||
awToolTip: "Refresh the page",
|
||||
ngClick: "refresh()"
|
||||
}
|
||||
},
|
||||
|
||||
related: {
|
||||
job_template: {
|
||||
type: 'collection',
|
||||
title: 'Job Tempate',
|
||||
iterator: 'job',
|
||||
index: false,
|
||||
open: false,
|
||||
|
||||
fields: { }
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
188
awx/ui/client/src/forms/WorkflowMaker.js
Normal file
188
awx/ui/client/src/forms/WorkflowMaker.js
Normal file
@ -0,0 +1,188 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name forms.function:JobTemplate
|
||||
* @description This form is for adding/editing a Job Template
|
||||
*/
|
||||
|
||||
// export default
|
||||
// angular.module('WorkflowMakerFormDefinition', [])
|
||||
//
|
||||
// .value ('WorkflowMakerFormObject', {
|
||||
|
||||
export default
|
||||
angular.module('WorkflowMakerFormDefinition', [])
|
||||
|
||||
.factory('WorkflowMakerFormObject', ['i18n', function(i18n) {
|
||||
return {
|
||||
|
||||
addTitle: '',
|
||||
editTitle: '',
|
||||
name: 'workflow_maker',
|
||||
basePath: 'job_templates',
|
||||
tabs: false,
|
||||
cancelButton: false,
|
||||
showHeader: false,
|
||||
|
||||
fields: {
|
||||
edgeType: {
|
||||
label: i18n._('Type'),
|
||||
type: 'radio_group',
|
||||
ngShow: 'selectedTemplate && showTypeOptions',
|
||||
ngDisabled: '!canAddWorkflowJobTemplate',
|
||||
options: [
|
||||
{
|
||||
label: i18n._('On Success'),
|
||||
value: 'success',
|
||||
ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "successFailure"'
|
||||
},
|
||||
{
|
||||
label: i18n._('On Failure'),
|
||||
value: 'failure',
|
||||
ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "successFailure"'
|
||||
},
|
||||
{
|
||||
label: i18n._('Always'),
|
||||
value: 'always',
|
||||
ngShow: '!edgeTypeRestriction || edgeTypeRestriction === "always"'
|
||||
}
|
||||
],
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'showTypeOptions'
|
||||
}
|
||||
},
|
||||
credential: {
|
||||
label: i18n._('Credential'),
|
||||
type: 'lookup',
|
||||
sourceModel: 'credential',
|
||||
sourceField: 'name',
|
||||
ngClick: 'lookUpCredential()',
|
||||
requiredErrorMsg: i18n._("Please select a Credential."),
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
awPopOver: i18n._("<p>Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing " +
|
||||
" the username and SSH key or password that Ansible will need to log into the remote hosts.</p>"),
|
||||
dataTitle: i18n._('Credential'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngShow: "selectedTemplate.ask_credential_on_launch",
|
||||
ngDisabled: '!canAddWorkflowJobTemplate',
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'selectedTemplate && selectedTemplate.ask_credential_on_launch'
|
||||
}
|
||||
},
|
||||
inventory: {
|
||||
label: i18n._('Inventory'),
|
||||
type: 'lookup',
|
||||
sourceModel: 'inventory',
|
||||
sourceField: 'name',
|
||||
list: 'OrganizationList',
|
||||
basePath: 'organization',
|
||||
ngClick: 'lookUpInventory()',
|
||||
requiredErrorMsg: i18n._("Please select an Inventory."),
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
awPopOver: i18n._("<p>Select the inventory containing the hosts you want this job to manage.</p>"),
|
||||
dataTitle: i18n._('Inventory'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngShow: "selectedTemplate.ask_inventory_on_launch",
|
||||
ngDisabled: '!canAddWorkflowJobTemplate',
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'selectedTemplate && selectedTemplate.ask_inventory_on_launch'
|
||||
}
|
||||
},
|
||||
job_type: {
|
||||
label: i18n._('Job Type'),
|
||||
type: 'select',
|
||||
ngOptions: 'type.label for type in job_type_options track by type.value',
|
||||
"default": 0,
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
awPopOver: i18n._("<p>When this template is submitted as a job, setting the type to <em>run</em> will execute the playbook, running tasks " +
|
||||
" on the selected hosts.</p> <p>Setting the type to <em>check</em> will not execute the playbook. Instead, <code>ansible</code> will check playbook " +
|
||||
" syntax, test environment setup and report problems.</p> <p>Setting the type to <em>scan</em> will execute the playbook and store any " +
|
||||
" scanned facts for use with Tower's System Tracking feature.</p>"),
|
||||
dataTitle: i18n._('Job Type'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngShow: "selectedTemplate.ask_job_type_on_launch",
|
||||
ngDisabled: '!canAddWorkflowJobTemplate',
|
||||
awRequiredWhen: {
|
||||
reqExpression: 'selectedTemplate && selectedTemplate.ask_job_type_on_launch'
|
||||
}
|
||||
},
|
||||
limit: {
|
||||
label: i18n._('Limit'),
|
||||
type: 'text',
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
awPopOver: i18n._("<p>Provide a host pattern to further constrain the list of hosts that will be managed or affected by the playbook. " +
|
||||
"Multiple patterns can be separated by ; : or ,</p><p>For more information and examples see " +
|
||||
"<a href=\"http://docs.ansible.com/intro_patterns.html\" target=\"_blank\">the Patterns topic at docs.ansible.com</a>.</p>"),
|
||||
dataTitle: i18n._('Limit'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngShow: "selectedTemplate.ask_limit_on_launch",
|
||||
ngDisabled: '!canAddWorkflowJobTemplate'
|
||||
},
|
||||
job_tags: {
|
||||
label: i18n._('Job Tags'),
|
||||
type: 'textarea',
|
||||
rows: 5,
|
||||
'elementClass': 'Form-textInput',
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
awPopOver: i18n._("<p>Provide a comma separated list of tags.</p>\n" +
|
||||
"<p>Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.</p>" +
|
||||
"<p>Consult the Ansible documentation for further details on the usage of tags.</p>"),
|
||||
dataTitle: i18n._("Job Tags"),
|
||||
dataPlacement: "right",
|
||||
dataContainer: "body",
|
||||
ngShow: "selectedTemplate.ask_tags_on_launch",
|
||||
ngDisabled: '!canAddWorkflowJobTemplate'
|
||||
},
|
||||
skip_tags: {
|
||||
label: i18n._('Skip Tags'),
|
||||
type: 'textarea',
|
||||
rows: 5,
|
||||
'elementClass': 'Form-textInput',
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
awPopOver: i18n._("<p>Provide a comma separated list of tags.</p>\n" +
|
||||
"<p>Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task.</p>" +
|
||||
"<p>Consult the Ansible documentation for further details on the usage of tags.</p>"),
|
||||
dataTitle: i18n._("Skip Tags"),
|
||||
dataPlacement: "right",
|
||||
dataContainer: "body",
|
||||
ngShow: "selectedTemplate.ask_skip_tags_on_launch",
|
||||
ngDisabled: '!canAddWorkflowJobTemplate'
|
||||
}
|
||||
},
|
||||
buttons: {
|
||||
cancel: {
|
||||
ngClick: 'cancelNodeForm()',
|
||||
ngShow: 'canAddWorkflowJobTemplate'
|
||||
},
|
||||
close: {
|
||||
ngClick: 'cancelNodeForm()',
|
||||
ngShow: '!canAddWorkflowJobTemplate'
|
||||
},
|
||||
save: {
|
||||
ngClick: 'saveNodeForm()',
|
||||
ngDisabled: "workflow_maker_form.$invalid || !selectedTemplate",
|
||||
ngShow: 'canAddWorkflowJobTemplate'
|
||||
}
|
||||
}
|
||||
};}])
|
||||
.factory('WorkflowMakerForm', ['WorkflowMakerFormObject', 'NotificationsList', function(WorkflowMakerFormObject, NotificationsList) {
|
||||
return function() {
|
||||
var itm;
|
||||
for (itm in WorkflowMakerFormObject.related) {
|
||||
if (WorkflowMakerFormObject.related[itm].include === "NotificationsList") {
|
||||
WorkflowMakerFormObject.related[itm] = NotificationsList;
|
||||
WorkflowMakerFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list
|
||||
}
|
||||
}
|
||||
return WorkflowMakerFormObject;
|
||||
};
|
||||
}]);
|
||||
208
awx/ui/client/src/forms/Workflows.js
Normal file
208
awx/ui/client/src/forms/Workflows.js
Normal file
@ -0,0 +1,208 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name forms.function:Workflow
|
||||
* @description This form is for adding/editing a Workflow
|
||||
*/
|
||||
|
||||
export default
|
||||
angular.module('WorkflowFormDefinition', [])
|
||||
|
||||
.factory('WorkflowFormObject', ['i18n', function(i18n) {
|
||||
return {
|
||||
|
||||
addTitle: i18n._('New Workflow'),
|
||||
editTitle: '{{ name }}',
|
||||
name: 'workflow_job_template',
|
||||
base: 'workflow',
|
||||
basePath: 'workflow_job_templates',
|
||||
// the top-most node of generated state tree
|
||||
stateTree: 'templates',
|
||||
activeEditState: 'templates.editWorkflowJobTemplate',
|
||||
tabs: true,
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
label: i18n._('Name'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)',
|
||||
column: 1
|
||||
},
|
||||
description: {
|
||||
label: i18n._('Description'),
|
||||
type: 'text',
|
||||
column: 1,
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
},
|
||||
organization: {
|
||||
label: i18n._('Organization'),
|
||||
type: 'lookup',
|
||||
sourceModel: 'organization',
|
||||
basePath: 'organizations',
|
||||
list: 'OrganizationList',
|
||||
sourceField: 'name',
|
||||
dataTitle: i18n._('Organization'),
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
column: 1,
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
},
|
||||
labels: {
|
||||
label: i18n._('Labels'),
|
||||
type: 'select',
|
||||
class: 'Form-formGroup--fullWidth',
|
||||
ngOptions: 'label.label for label in labelOptions track by label.value',
|
||||
multiSelect: true,
|
||||
dataTitle: i18n._('Labels'),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: i18n._("<p>Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs in the Tower display.</p>"),
|
||||
dataContainer: 'body',
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
},
|
||||
variables: {
|
||||
label: i18n._('Extra Variables'),
|
||||
type: 'textarea',
|
||||
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
||||
rows: 6,
|
||||
"default": "---",
|
||||
column: 2,
|
||||
awPopOver: i18n._("<p>Pass extra command line variables to the playbook. This is the <code>-e</code> or <code>--extra-vars</code> command line parameter " +
|
||||
"for <code>ansible-playbook</code>. Provide key/value pairs using either YAML or JSON.</p>" +
|
||||
"JSON:<br />\n" +
|
||||
"<blockquote>{<br /> \"somevar\": \"somevalue\",<br /> \"password\": \"magic\"<br /> }</blockquote>\n" +
|
||||
"YAML:<br />\n" +
|
||||
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n"),
|
||||
dataTitle: i18n._('Extra Variables'),
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)' // TODO: get working
|
||||
}
|
||||
},
|
||||
|
||||
buttons: { //for now always generates <button> tags
|
||||
cancel: {
|
||||
ngClick: 'formCancel()',
|
||||
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
},
|
||||
close: {
|
||||
ngClick: 'formCancel()',
|
||||
ngShow: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
},
|
||||
save: {
|
||||
ngClick: 'formSave()', //$scope.function to call on click, optional
|
||||
ngDisabled: "workflow_form.$invalid || can_edit!==true", //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
|
||||
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
}
|
||||
},
|
||||
|
||||
related: {
|
||||
permissions: {
|
||||
awToolTip: i18n._('Please save before assigning permissions'),
|
||||
dataPlacement: 'top',
|
||||
basePath: 'api/v1/workflow_job_templates/{{$stateParams.workflow_job_template_id}}/access_list/',
|
||||
search: {
|
||||
order_by: 'username'
|
||||
},
|
||||
type: 'collection',
|
||||
title: i18n._('Permissions'),
|
||||
iterator: 'permission',
|
||||
index: false,
|
||||
open: false,
|
||||
actions: {
|
||||
add: {
|
||||
ngClick: "$state.go('.add')",
|
||||
label: 'Add',
|
||||
awToolTip: 'Add a permission',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ADD',
|
||||
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
username: {
|
||||
key: true,
|
||||
label: 'User',
|
||||
linkBase: 'users',
|
||||
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
|
||||
},
|
||||
role: {
|
||||
label: 'Role',
|
||||
type: 'role',
|
||||
noSort: true,
|
||||
class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4',
|
||||
},
|
||||
team_roles: {
|
||||
label: 'Team Roles',
|
||||
type: 'team_roles',
|
||||
noSort: true,
|
||||
class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4',
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
include: "NotificationsList"
|
||||
}
|
||||
},
|
||||
|
||||
relatedButtons: {
|
||||
add_survey: {
|
||||
ngClick: 'addSurvey()',
|
||||
ngShow: '!survey_exists',
|
||||
awFeature: 'surveys',
|
||||
awToolTip: i18n._('Please save before adding a survey'),
|
||||
dataPlacement: 'top',
|
||||
label: i18n._('Add Survey'),
|
||||
class: 'Form-primaryButton'
|
||||
},
|
||||
edit_survey: {
|
||||
ngClick: 'editSurvey()',
|
||||
awFeature: 'surveys',
|
||||
ngShow: 'survey_exists',
|
||||
label: i18n._('Edit Survey'),
|
||||
class: 'Form-primaryButton'
|
||||
},
|
||||
workflow_editor: {
|
||||
ngClick: 'openWorkflowMaker()',
|
||||
awToolTip: i18n._('Please save before defining the workflow graph'),
|
||||
dataPlacement: 'top',
|
||||
label: i18n._('Workflow Editor'),
|
||||
class: 'Form-primaryButton'
|
||||
}
|
||||
},
|
||||
|
||||
relatedSets: function(urls) {
|
||||
return {
|
||||
permissions: {
|
||||
iterator: 'permission',
|
||||
url: urls.access_list
|
||||
},
|
||||
notifications: {
|
||||
iterator: 'notification',
|
||||
url: '/api/v1/notification_templates/'
|
||||
}
|
||||
};
|
||||
}
|
||||
};}])
|
||||
|
||||
.factory('WorkflowForm', ['WorkflowFormObject', 'NotificationsList',
|
||||
function(WorkflowFormObject, NotificationsList) {
|
||||
return function() {
|
||||
var itm;
|
||||
|
||||
for (itm in WorkflowFormObject.related) {
|
||||
if (WorkflowFormObject.related[itm].include === "NotificationsList") {
|
||||
WorkflowFormObject.related[itm] = NotificationsList;
|
||||
WorkflowFormObject.related[itm].generateList = true; // tell form generator to call list generator and inject a list
|
||||
}
|
||||
}
|
||||
|
||||
return WorkflowFormObject;
|
||||
};
|
||||
}]);
|
||||
@ -1039,7 +1039,6 @@ export default
|
||||
//plays = JSON.parse(JSON.stringify(scope.jobData.plays)),
|
||||
plays = scope.jobData.plays,
|
||||
filteredListX = [],
|
||||
filteredListA = [],
|
||||
filteredListB = [],
|
||||
key,
|
||||
keys;
|
||||
@ -1113,7 +1112,6 @@ export default
|
||||
var scope = params.scope,
|
||||
result = [],
|
||||
filteredListX = [],
|
||||
filteredListA = [],
|
||||
filteredListB = [],
|
||||
idx, key, keys, newKeys, tasks, t;
|
||||
|
||||
@ -1196,11 +1194,9 @@ export default
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
result = [],
|
||||
filteredListA = [],
|
||||
filteredListB = [],
|
||||
idx = 0,
|
||||
hostResults,
|
||||
key,
|
||||
keys;
|
||||
|
||||
if (scope.activePlay && scope.activeTask && scope.jobData.plays[scope.activePlay] &&
|
||||
@ -1232,7 +1228,7 @@ export default
|
||||
// else {
|
||||
// filteredListB = filteredListA;
|
||||
// }
|
||||
|
||||
|
||||
keys = Object.keys(filteredListB);
|
||||
keys.sort(function compare(a, b) {
|
||||
if (filteredListB[a].name === filteredListB[b].name) {
|
||||
|
||||
@ -381,7 +381,6 @@ export default
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
id = params.id,
|
||||
callback = params.callback,
|
||||
url = GetBasePath('schedules') + id +'/';
|
||||
|
||||
// Perform the update
|
||||
|
||||
@ -22,8 +22,7 @@ export default
|
||||
return function (params) {
|
||||
|
||||
var scope = params.scope,
|
||||
set = params.set,
|
||||
iterator = params.iterator;
|
||||
set = params.set;
|
||||
|
||||
// Listeners to perform lookups after main inventory list loads
|
||||
|
||||
@ -74,66 +73,4 @@ export default
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('TeamLookUpOrganizationInit', ['Alert', 'Rest', 'OrganizationList', 'generateList',
|
||||
function (Alert, Rest, OrganizationList, GenerateList) {
|
||||
return function (params) {
|
||||
|
||||
var scope = params.scope;
|
||||
|
||||
// Show pop-up to select organization
|
||||
scope.lookUpOrganization = function () {
|
||||
var list = OrganizationList,
|
||||
listGenerator = GenerateList,
|
||||
listScope = listGenerator.inject(list, { mode: 'lookup', hdr: 'Select Organization' }),
|
||||
defaultUrl = '/api/v1/organizations/';
|
||||
|
||||
listScope.selectAction = function () {
|
||||
var i, found = false;
|
||||
for (i = 0; i < listScope[list.name].length; i++) {
|
||||
if (listScope[list.iterator + "_" + listScope[list.name][i].id + "_class"] === "success") {
|
||||
found = true;
|
||||
scope.organization = listScope[list.name][i].id;
|
||||
scope.organization_name = listScope[list.name][i].name;
|
||||
scope.team_form.$setDirty();
|
||||
listGenerator.hide();
|
||||
}
|
||||
}
|
||||
if (found === false) {
|
||||
Alert('No Selection', 'Click on a row to select an Organization before clicking the Select button.');
|
||||
}
|
||||
};
|
||||
|
||||
listScope.toggle_organization = function (id) {
|
||||
// when user clicks a row, remove 'success' class from all rows except clicked-on row
|
||||
if (listScope[list.name]) {
|
||||
for (var i = 0; i < listScope[list.name].length; i++) {
|
||||
listScope[list.iterator + "_" + listScope[list.name][i].id + "_class"] = "";
|
||||
}
|
||||
}
|
||||
if (id !== null && id !== undefined) {
|
||||
listScope[list.iterator + "_" + id + "_class"] = "success";
|
||||
}
|
||||
};
|
||||
|
||||
// @issue: OLD SEARCH
|
||||
// SearchInit({
|
||||
// scope: listScope,
|
||||
// set: list.name,
|
||||
// list: list,
|
||||
// url: defaultUrl
|
||||
// });
|
||||
// PaginateInit({
|
||||
// scope: listScope,
|
||||
// list: list,
|
||||
// url: defaultUrl,
|
||||
// mode: 'lookup'
|
||||
// });
|
||||
// scope.search(list.iterator);
|
||||
|
||||
listScope.toggle_organization(scope.organization);
|
||||
};
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
/* jshint ignore:start */
|
||||
|
||||
function isString(arg) {
|
||||
return typeof arg === 'string';
|
||||
}
|
||||
@ -83,7 +85,7 @@ export default
|
||||
gettextCatalog.setCurrentLanguage(langInfo);
|
||||
// TODO: the line below is commented out temporarily until
|
||||
// the .po files are received from the i18n team, in order to avoid
|
||||
// 404 file not found console errors in dev
|
||||
// 404 file not found console errors in dev
|
||||
// gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
|
||||
};
|
||||
}])
|
||||
|
||||
@ -14,7 +14,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
||||
$log, $stateParams, InventoryForm, Rest, Alert, ProcessErrors,
|
||||
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
|
||||
ParseVariableString, Prompt, InitiatePlaybookRun,
|
||||
deleteJobTemplate, $state, $filter) {
|
||||
JobTemplateService, $state, $filter) {
|
||||
|
||||
// Inject dynamic view
|
||||
var defaultUrl = GetBasePath('inventory'),
|
||||
@ -141,25 +141,23 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
||||
$location.path($location.path() + '/job_templates/' + this.scan_job_template.id);
|
||||
};
|
||||
|
||||
$scope.deleteScanJob = function() {
|
||||
var id = this.scan_job_template.id,
|
||||
action = function() {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
deleteJobTemplate(id)
|
||||
.success(function() {
|
||||
$('#prompt-modal').modal('hide');
|
||||
// @issue: OLD SEARCH
|
||||
// $scope.search(form.related.scan_job_templates.iterator);
|
||||
})
|
||||
.error(function(data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'DELETE returned status: ' + status
|
||||
});
|
||||
});
|
||||
};
|
||||
$scope.deleteScanJob = function () {
|
||||
var id = this.scan_job_template.id ,
|
||||
action = function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
JobTemplateService.deleteJobTemplate(id)
|
||||
.success(function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
// @issue: OLD SEARCH
|
||||
// $scope.search(form.related.scan_job_templates.iterator);
|
||||
})
|
||||
.error(function (data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({
|
||||
hdr: 'Delete',
|
||||
@ -176,5 +174,5 @@ export default ['$scope', '$rootScope', '$compile', '$location',
|
||||
'$log', '$stateParams', 'InventoryForm', 'Rest', 'Alert',
|
||||
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait',
|
||||
'ToJSON', 'ParseVariableString', 'Prompt', 'InitiatePlaybookRun',
|
||||
'deleteJobTemplate', '$state', '$filter', InventoriesEdit,
|
||||
'JobTemplateService', '$state', '$filter', InventoriesEdit,
|
||||
];
|
||||
|
||||
@ -8,8 +8,8 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList
|
||||
'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService', 'ToJSON',
|
||||
function($state, $stateParams, $scope, GroupForm, CredentialList, ParseTypeChange, GenerateForm, inventoryData,
|
||||
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, ToJSON) {
|
||||
var generator = GenerateForm,
|
||||
form = GroupForm();
|
||||
|
||||
let form = GroupForm();
|
||||
init();
|
||||
|
||||
function init() {
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
export default
|
||||
['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroups', 'generateList', 'InventoryUpdate',
|
||||
'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath',
|
||||
'InventoryManageService', 'groupsUrl', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'groupsDataset',
|
||||
'InventoryManageService', 'groupsUrl', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'groupsDataset', 'Find',
|
||||
function($scope, $rootScope, $state, $stateParams, InventoryGroups, generateList, InventoryUpdate,
|
||||
GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath,
|
||||
InventoryManageService, groupsUrl, GetSyncStatusMsg, GetHostsStatusMsg, groupsDataset){
|
||||
InventoryManageService, groupsUrl, GetSyncStatusMsg, GetHostsStatusMsg, groupsDataset, Find){
|
||||
|
||||
let list = InventoryGroups;
|
||||
|
||||
|
||||
@ -525,7 +525,7 @@ export default
|
||||
scope.job_template_name = data.name;
|
||||
scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||
scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||
scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
||||
scope.job_template_url = '/#/templates/' + data.unified_job_template;
|
||||
scope.inventory_url = (scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||
scope.project_url = (scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||
scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
||||
|
||||
@ -10,10 +10,10 @@ export default
|
||||
var scope = params.scope.$new(),
|
||||
id = params.id,
|
||||
relaunch = params.relaunch || false,
|
||||
system_job = params.system_job || false;
|
||||
job_type = params.job_type;
|
||||
scope.job_template_id = id;
|
||||
|
||||
var el = $compile( "<submit-job data-submit-job-id=" + id + " data-submit-job-system=" + system_job + " data-submit-job-relaunch=" + relaunch + "></submit-job>" )( scope );
|
||||
var el = $compile( "<submit-job data-submit-job-id=" + id + " submit-job-type=" + job_type + " data-submit-job-relaunch=" + relaunch + "></submit-job>" )( scope );
|
||||
$('#content-container').remove('submit-job').append( el );
|
||||
};
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ export default
|
||||
Rest.post(job_launch_data)
|
||||
.success(function(data) {
|
||||
Wait('stop');
|
||||
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command;
|
||||
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command || data.workflow_job;
|
||||
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) || (base === 'home')){
|
||||
// use $state.go with reload: true option to re-instantiate sockets in
|
||||
|
||||
@ -123,6 +123,9 @@ export default
|
||||
if(_.has(data, 'job')) {
|
||||
goToJobDetails('jobDetail');
|
||||
}
|
||||
else if(_.has(data, 'workflow_job')) {
|
||||
goToJobDetails('workflowResults');
|
||||
}
|
||||
else if(_.has(data, 'ad_hoc_command')) {
|
||||
goToJobDetails('adHocJobStdout');
|
||||
}
|
||||
|
||||
@ -136,7 +136,12 @@ export default
|
||||
// jobs, and jobDetails $states.
|
||||
|
||||
if (!$scope.submitJobRelaunch) {
|
||||
launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/';
|
||||
if($scope.submitJobType && $scope.submitJobType === 'job_template') {
|
||||
launch_url = GetBasePath('job_templates') + $scope.submitJobId + '/launch/';
|
||||
}
|
||||
else if($scope.submitJobType && $scope.submitJobType === 'workflow_job_template') {
|
||||
launch_url = GetBasePath('workflow_job_templates') + $scope.submitJobId + '/launch/';
|
||||
}
|
||||
}
|
||||
else {
|
||||
launch_url = GetBasePath('jobs') + $scope.submitJobId + '/relaunch/';
|
||||
@ -191,7 +196,7 @@ export default
|
||||
updateRequiredPasswords();
|
||||
}
|
||||
|
||||
if( ($scope.submitJobRelaunch && !$scope.password_needed) || (!$scope.submitJobRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) {
|
||||
if( ($scope.submitJobType === 'workflow_job_template' && !$scope.survey_enabled) || ($scope.submitJobRelaunch && !$scope.password_needed) || (!$scope.submitJobRelaunch && $scope.can_start_without_user_input && !$scope.ask_inventory_on_launch && !$scope.ask_credential_on_launch && !$scope.has_other_prompts && !$scope.survey_enabled)) {
|
||||
// The job can be launched if
|
||||
// a) It's a relaunch and no passwords are needed
|
||||
// or
|
||||
|
||||
@ -11,7 +11,7 @@ export default [ 'templateUrl', 'CreateDialog', 'Wait', 'CreateSelect2', 'ParseT
|
||||
return {
|
||||
scope: {
|
||||
submitJobId: '=',
|
||||
submitJobSystem: '=',
|
||||
submitJobType: '@',
|
||||
submitJobRelaunch: '='
|
||||
},
|
||||
templateUrl: templateUrl('job-submission/job-submission'),
|
||||
|
||||
@ -280,7 +280,7 @@
|
||||
|
||||
|
||||
function saveCompleted(id) {
|
||||
$state.go('jobTemplates.edit', {job_template_id: id}, {reload: true});
|
||||
$state.go('templates.editJobTemplate', {job_template_id: id}, {reload: true});
|
||||
}
|
||||
|
||||
if ($scope.removeTemplateSaveSuccess) {
|
||||
@ -426,7 +426,7 @@
|
||||
try {
|
||||
for (fld in form.fields) {
|
||||
if (form.fields[fld].type === 'select' &&
|
||||
fld !== 'playbook') {
|
||||
fld !== 'playbook' && $scope[fld]) {
|
||||
data[fld] = $scope[fld].value;
|
||||
}
|
||||
else if(form.fields[fld].type === 'checkbox_group') {
|
||||
@ -501,14 +501,13 @@
|
||||
|
||||
} catch (err) {
|
||||
Wait('stop');
|
||||
console.log(err)
|
||||
Alert("Error", "Error parsing extra variables. " +
|
||||
"Parser returned: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.formCancel = function () {
|
||||
$state.go('jobTemplates');
|
||||
$state.transitionTo('templates');
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -4,8 +4,8 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import controller from './job-templates-add.controller';
|
||||
import controller from './job-template-add.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplatesAdd', [])
|
||||
.controller('JobTemplatesAdd', controller);
|
||||
angular.module('jobTemplateAdd', [])
|
||||
.controller('JobTemplateAdd', controller);
|
||||
@ -4,8 +4,8 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import controller from './job-templates-edit.controller';
|
||||
import controller from './workflow-add.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplatesEdit', [])
|
||||
.controller('JobTemplatesEdit', controller);
|
||||
angular.module('workflowAdd', [])
|
||||
.controller('WorkflowAdd', controller);
|
||||
@ -0,0 +1,191 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ '$scope', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors', 'ClearScope',
|
||||
'Wait', '$state', 'CreateSelect2', 'JobTemplateService', 'ToJSON',
|
||||
'ParseTypeChange', 'OrganizationList', '$q', 'Rest', 'GetBasePath',
|
||||
function(
|
||||
$scope, WorkflowForm, GenerateForm, Alert, ProcessErrors, ClearScope,
|
||||
Wait, $state, CreateSelect2, JobTemplateService, ToJSON,
|
||||
ParseTypeChange, OrganizationList, $q, Rest, GetBasePath
|
||||
) {
|
||||
|
||||
Rest.setUrl(GetBasePath('workflow_job_templates'));
|
||||
Rest.options()
|
||||
.success(function(data) {
|
||||
if (!data.actions.POST) {
|
||||
$state.go("^");
|
||||
Alert('Permission Error', 'You do not have permission to add a workflow job template.', 'alert-info');
|
||||
}
|
||||
});
|
||||
|
||||
ClearScope();
|
||||
// Inject dynamic view
|
||||
let form = WorkflowForm(),
|
||||
generator = GenerateForm;
|
||||
|
||||
function init() {
|
||||
$scope.parseType = 'yaml';
|
||||
$scope.can_edit = true;
|
||||
// apply form definition's default field values
|
||||
GenerateForm.applyDefaults(form, $scope);
|
||||
|
||||
// Make the variables textarea look pretty
|
||||
ParseTypeChange({
|
||||
scope: $scope,
|
||||
field_id: 'workflow_job_template_variables',
|
||||
onChange: function() {
|
||||
// Make sure the form controller knows there was a change
|
||||
$scope[form.name + '_form'].$setDirty();
|
||||
}
|
||||
});
|
||||
|
||||
// Go out and grab the possible labels
|
||||
JobTemplateService.getLabelOptions()
|
||||
.then(function(data){
|
||||
$scope.labelOptions = data;
|
||||
// select2-ify the labels input
|
||||
CreateSelect2({
|
||||
element:'#workflow_job_template_labels',
|
||||
multiple: true,
|
||||
addNew: true
|
||||
});
|
||||
}, function(error){
|
||||
ProcessErrors($scope, error.data, error.status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get labels. GET returned ' +
|
||||
'status: ' + error.status
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$scope.formSave = function () {
|
||||
let fld, data = {};
|
||||
|
||||
generator.clearApiErrors($scope);
|
||||
|
||||
Wait('start');
|
||||
|
||||
try {
|
||||
for (fld in form.fields) {
|
||||
data[fld] = $scope[fld];
|
||||
}
|
||||
|
||||
data.extra_vars = ToJSON($scope.parseType,
|
||||
$scope.variables, true);
|
||||
|
||||
// The idea here is that we want to find the new option elements that also have a label that exists in the dom
|
||||
$("#workflow_job_template_labels > option")
|
||||
.filter("[data-select2-tag=true]")
|
||||
.each(function(optionIndex, option) {
|
||||
$("#workflow_job_template_labels")
|
||||
.siblings(".select2").first().find(".select2-selection__choice")
|
||||
.each(function(labelIndex, label) {
|
||||
if($(option).text() === $(label).attr('title')) {
|
||||
// Mark that the option has a label present so that we can filter by that down below
|
||||
$(option).attr('data-label-is-present', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.newLabels = $("#workflow_job_template_labels > option")
|
||||
.filter("[data-select2-tag=true]")
|
||||
.filter("[data-label-is-present=true]")
|
||||
.map((i, val) => ({name: $(val).text()}));
|
||||
|
||||
JobTemplateService.createWorkflowJobTemplate(data)
|
||||
.then(function(data) {
|
||||
|
||||
let orgDefer = $q.defer();
|
||||
let associationDefer = $q.defer();
|
||||
|
||||
Rest.setUrl(data.data.related.labels);
|
||||
|
||||
let currentLabels = Rest.get()
|
||||
.then(function(data) {
|
||||
return data.data.results
|
||||
.map(val => val.id);
|
||||
});
|
||||
|
||||
currentLabels.then(function (current) {
|
||||
let labelsToAdd = ($scope.labels || [])
|
||||
.map(val => val.value);
|
||||
let labelsToDisassociate = current
|
||||
.filter(val => labelsToAdd
|
||||
.indexOf(val) === -1)
|
||||
.map(val => ({id: val, disassociate: true}));
|
||||
let labelsToAssociate = labelsToAdd
|
||||
.filter(val => current
|
||||
.indexOf(val) === -1)
|
||||
.map(val => ({id: val, associate: true}));
|
||||
let pass = labelsToDisassociate
|
||||
.concat(labelsToAssociate);
|
||||
associationDefer.resolve(pass);
|
||||
});
|
||||
|
||||
Rest.setUrl(GetBasePath("organizations"));
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
orgDefer.resolve(data.results[0].id);
|
||||
});
|
||||
|
||||
orgDefer.promise.then(function(orgId) {
|
||||
let toPost = [];
|
||||
$scope.newLabels = $scope.newLabels
|
||||
.map(function(i, val) {
|
||||
val.organization = orgId;
|
||||
return val;
|
||||
});
|
||||
|
||||
$scope.newLabels.each(function(i, val) {
|
||||
toPost.push(val);
|
||||
});
|
||||
|
||||
associationDefer.promise.then(function(arr) {
|
||||
toPost = toPost
|
||||
.concat(arr);
|
||||
|
||||
Rest.setUrl(data.data.related.labels);
|
||||
|
||||
let defers = [];
|
||||
for (let i = 0; i < toPost.length; i++) {
|
||||
defers.push(Rest.post(toPost[i]));
|
||||
}
|
||||
$q.all(defers)
|
||||
.then(function() {
|
||||
// If we follow the same pattern as job templates then the survey logic will go here
|
||||
|
||||
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: data.data.id}, {reload: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}, function (error) {
|
||||
ProcessErrors($scope, error.data, error.status, form,
|
||||
{
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to add new workflow. ' +
|
||||
'POST returned status: ' +
|
||||
error.status
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Wait('stop');
|
||||
Alert("Error", "Error parsing extra variables. " +
|
||||
"Parser returned: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.formCancel = function () {
|
||||
$state.transitionTo('templates');
|
||||
};
|
||||
|
||||
init();
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,4 @@
|
||||
<div class="tab-pane" id="workflow_create">
|
||||
<div ui-view></div>
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
</div>
|
||||
@ -19,7 +19,11 @@
|
||||
jobTemplateCopyService.set(res)
|
||||
.success(function(res){
|
||||
Wait('stop');
|
||||
$state.go('jobTemplates.edit', {id: res.id}, {reload: true});
|
||||
if(res.type && res.type === 'job_template') {
|
||||
$state.go('templates.editJobTemplate', {id: res.id}, {reload: true});
|
||||
}
|
||||
// Workflow edit to be implemented post 3.1 but we'll need to handle the
|
||||
// state transition for that here
|
||||
});
|
||||
})
|
||||
.error(function(res, status){
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
|
||||
export default {
|
||||
name: 'jobTemplates.copy',
|
||||
name: 'templates.copy',
|
||||
route: '/:id/copy',
|
||||
controller: 'jobTemplateCopyController'
|
||||
};
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['Rest', 'GetBasePath', function(Rest, GetBasePath){
|
||||
return {
|
||||
deleteJobTemplate: function(id){
|
||||
var url = GetBasePath('job_templates');
|
||||
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.destroy();
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -31,7 +31,7 @@ export default
|
||||
|
||||
$scope.$watch('job_template_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||
if (val === false) {
|
||||
$scope.canAdd = false;
|
||||
$scope.canAddJobTemplate = false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -187,7 +187,6 @@ export default
|
||||
Rest.setUrl(GetBasePath('projects') + $scope.project + '/');
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
console.log(data)
|
||||
var msg;
|
||||
switch (data.status) {
|
||||
case 'failed':
|
||||
@ -252,7 +251,7 @@ export default
|
||||
if ($scope.cloudCredentialReadyRemove) {
|
||||
$scope.cloudCredentialReadyRemove();
|
||||
}
|
||||
$scope.cloudCredentialReadyRemove = $scope.$on('cloudCredentialReady', function (e, name) {
|
||||
$scope.cloudCredentialReadyRemove = $scope.$on('cloudCredentialReady', function () {
|
||||
$scope.$emit('jobTemplateLoadFinished');
|
||||
});
|
||||
|
||||
@ -261,7 +260,7 @@ export default
|
||||
if ($scope.jobTemplateLoadedRemove) {
|
||||
$scope.jobTemplateLoadedRemove();
|
||||
}
|
||||
$scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, related_cloud_credential, masterObject, relatedSets) {
|
||||
$scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, related_cloud_credential, masterObject) {
|
||||
var dft;
|
||||
|
||||
master = masterObject;
|
||||
11
awx/ui/client/src/job-templates/edit-job-template/main.js
Normal file
11
awx/ui/client/src/job-templates/edit-job-template/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import controller from './job-template-edit.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplateEdit', [])
|
||||
.controller('JobTemplateEdit', controller);
|
||||
11
awx/ui/client/src/job-templates/edit-workflow/main.js
Normal file
11
awx/ui/client/src/job-templates/edit-workflow/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import controller from './workflow-edit.controller';
|
||||
|
||||
export default
|
||||
angular.module('workflowEdit', [])
|
||||
.controller('WorkflowEdit', controller);
|
||||
@ -0,0 +1,737 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ '$scope', '$stateParams', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
||||
'ClearScope', 'GetBasePath', '$q', 'ParseTypeChange', 'Wait', 'Empty',
|
||||
'ToJSON', 'initSurvey', '$state', 'CreateSelect2', 'ParseVariableString',
|
||||
'JobTemplateService', 'OrganizationList', 'Rest',
|
||||
function(
|
||||
$scope, $stateParams, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
||||
ClearScope, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
||||
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
||||
JobTemplateService, OrganizationList, Rest
|
||||
) {window.state = $state;
|
||||
|
||||
ClearScope();
|
||||
|
||||
$scope.$watch('workflow_job_template_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||
if (val === false) {
|
||||
$scope.canAddWorkflowJobTemplate = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Inject dynamic view
|
||||
let form = WorkflowForm(),
|
||||
generator = GenerateForm,
|
||||
id = $stateParams.workflow_job_template_id;
|
||||
|
||||
$scope.mode = 'edit';
|
||||
$scope.parseType = 'yaml';
|
||||
$scope.includeWorkflowMaker = false;
|
||||
|
||||
// What is this used for? Permissions?
|
||||
$scope.can_edit = true;
|
||||
|
||||
$scope.editRequests = [];
|
||||
$scope.associateRequests = [];
|
||||
$scope.disassociateRequests = [];
|
||||
|
||||
$scope.workflowTree = {
|
||||
data: {
|
||||
id: 1,
|
||||
canDelete: false,
|
||||
canEdit: false,
|
||||
canAddTo: true,
|
||||
isStartNode: true,
|
||||
unifiedJobTemplate: {
|
||||
name: "Workflow Launch"
|
||||
},
|
||||
children: [],
|
||||
deletedNodes: [],
|
||||
totalNodes: 0
|
||||
},
|
||||
nextIndex: 2
|
||||
};
|
||||
|
||||
function buildBranch(params) {
|
||||
// params.nodeId
|
||||
// params.parentId
|
||||
// params.edgeType
|
||||
// params.nodesObj
|
||||
// params.isRoot
|
||||
|
||||
let treeNode = {
|
||||
children: [],
|
||||
c: "#D7D7D7",
|
||||
id: $scope.workflowTree.nextIndex,
|
||||
nodeId: params.nodeId,
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
canAddTo: true,
|
||||
placeholder: false,
|
||||
edgeType: params.edgeType,
|
||||
unifiedJobTemplate: _.clone(params.nodesObj[params.nodeId].summary_fields.unified_job_template),
|
||||
isNew: false,
|
||||
edited: false,
|
||||
originalEdge: params.edgeType,
|
||||
originalNodeObj: _.clone(params.nodesObj[params.nodeId]),
|
||||
promptValues: {},
|
||||
isRoot: params.isRoot ? params.isRoot : false
|
||||
};
|
||||
|
||||
$scope.workflowTree.data.totalNodes++;
|
||||
|
||||
$scope.workflowTree.nextIndex++;
|
||||
|
||||
if(params.parentId) {
|
||||
treeNode.originalParentId = params.parentId;
|
||||
}
|
||||
|
||||
// Loop across the success nodes and add them recursively
|
||||
_.forEach(params.nodesObj[params.nodeId].success_nodes, function(successNodeId) {
|
||||
treeNode.children.push(buildBranch({
|
||||
nodeId: successNodeId,
|
||||
parentId: params.nodeId,
|
||||
edgeType: "success",
|
||||
nodesObj: params.nodesObj
|
||||
}));
|
||||
});
|
||||
|
||||
// failure nodes
|
||||
_.forEach(params.nodesObj[params.nodeId].failure_nodes, function(failureNodesId) {
|
||||
treeNode.children.push(buildBranch({
|
||||
nodeId: failureNodesId,
|
||||
parentId: params.nodeId,
|
||||
edgeType: "failure",
|
||||
nodesObj: params.nodesObj
|
||||
}));
|
||||
});
|
||||
|
||||
// always nodes
|
||||
_.forEach(params.nodesObj[params.nodeId].always_nodes, function(alwaysNodesId) {
|
||||
treeNode.children.push(buildBranch({
|
||||
nodeId: alwaysNodesId,
|
||||
parentId: params.nodeId,
|
||||
edgeType: "always",
|
||||
nodesObj: params.nodesObj
|
||||
}));
|
||||
});
|
||||
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
// Select2-ify the lables input
|
||||
CreateSelect2({
|
||||
element:'#workflow_job_template_labels',
|
||||
multiple: true,
|
||||
addNew: true
|
||||
});
|
||||
|
||||
Rest.setUrl('api/v1/labels');
|
||||
Wait("start");
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
$scope.labelOptions = data.results
|
||||
.map((i) => ({label: i.name, value: i.id}));
|
||||
|
||||
var seeMoreResolve = $q.defer();
|
||||
|
||||
var getNext = function(data, arr, resolve) {
|
||||
Rest.setUrl(data.next);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (data.next) {
|
||||
getNext(data, arr.concat(data.results), resolve);
|
||||
} else {
|
||||
resolve.resolve(arr.concat(data.results));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Rest.setUrl(GetBasePath('workflow_job_templates') + id +
|
||||
"/labels");
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
if (data.next) {
|
||||
getNext(data, data.results, seeMoreResolve);
|
||||
} else {
|
||||
seeMoreResolve.resolve(data.results);
|
||||
}
|
||||
|
||||
seeMoreResolve.promise.then(function (labels) {
|
||||
$scope.$emit("choicesReady");
|
||||
var opts = labels
|
||||
.map(i => ({id: i.id + "",
|
||||
test: i.name}));
|
||||
CreateSelect2({
|
||||
element:'#workflow_job_template_labels',
|
||||
multiple: true,
|
||||
addNew: true,
|
||||
opts: opts
|
||||
});
|
||||
Wait("stop");
|
||||
});
|
||||
}).error(function(){
|
||||
// job template id is null in this case
|
||||
$scope.$emit("choicesReady");
|
||||
});
|
||||
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get labels. GET returned ' +
|
||||
'status: ' + status
|
||||
});
|
||||
});
|
||||
|
||||
// Get the workflow nodes
|
||||
JobTemplateService.getWorkflowJobTemplateNodes(id)
|
||||
.then(function(data){
|
||||
|
||||
let nodesArray = data.data.results;
|
||||
let nodesObj = {};
|
||||
let nonRootNodeIds = [];
|
||||
let allNodeIds = [];
|
||||
|
||||
// Determine which nodes are root nodes
|
||||
_.forEach(nodesArray, function(node) {
|
||||
nodesObj[node.id] = _.clone(node);
|
||||
|
||||
allNodeIds.push(node.id);
|
||||
|
||||
_.forEach(node.success_nodes, function(nodeId){
|
||||
nonRootNodeIds.push(nodeId);
|
||||
});
|
||||
_.forEach(node.failure_nodes, function(nodeId){
|
||||
nonRootNodeIds.push(nodeId);
|
||||
});
|
||||
_.forEach(node.always_nodes, function(nodeId){
|
||||
nonRootNodeIds.push(nodeId);
|
||||
});
|
||||
});
|
||||
|
||||
let rootNodes = _.difference(allNodeIds, nonRootNodeIds);
|
||||
|
||||
// Loop across the root nodes and re-build the tree
|
||||
_.forEach(rootNodes, function(rootNodeId) {
|
||||
let branch = buildBranch({
|
||||
nodeId: rootNodeId,
|
||||
edgeType: "always",
|
||||
nodesObj: nodesObj,
|
||||
isRoot: true
|
||||
});
|
||||
|
||||
$scope.workflowTree.data.children.push(branch);
|
||||
});
|
||||
|
||||
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
|
||||
// this workflowTree object and removing the children object for some reason (?)
|
||||
// This happens on occasion and I think is a race condition (?)
|
||||
if(!$scope.workflowTree.data.children) {
|
||||
$scope.workflowTree.data.children = [];
|
||||
}
|
||||
|
||||
// In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable.
|
||||
// It won't get included until this the tree has been built - I'm open to better ways of doing this.
|
||||
$scope.includeWorkflowMaker = true;
|
||||
|
||||
}, function(error){
|
||||
ProcessErrors($scope, error.data, error.status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get workflow job template nodes. GET returned ' +
|
||||
'status: ' + error.status
|
||||
});
|
||||
});
|
||||
|
||||
// Go out and GET the workflow job temlate data needed to populate the form
|
||||
JobTemplateService.getWorkflowJobTemplate(id)
|
||||
.then(function(data){
|
||||
let workflowJobTemplateData = data.data;
|
||||
$scope.workflow_job_template_obj = workflowJobTemplateData;
|
||||
$scope.name = workflowJobTemplateData.name;
|
||||
let fld, i;
|
||||
for (fld in form.fields) {
|
||||
if (fld !== 'variables' && fld !== 'survey' && workflowJobTemplateData[fld] !== null && workflowJobTemplateData[fld] !== undefined) {
|
||||
if (form.fields[fld].type === 'select') {
|
||||
if ($scope[fld + '_options'] && $scope[fld + '_options'].length > 0) {
|
||||
for (i = 0; i < $scope[fld + '_options'].length; i++) {
|
||||
if (workflowJobTemplateData[fld] === $scope[fld + '_options'][i].value) {
|
||||
$scope[fld] = $scope[fld + '_options'][i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$scope[fld] = workflowJobTemplateData[fld];
|
||||
}
|
||||
} else {
|
||||
$scope[fld] = workflowJobTemplateData[fld];
|
||||
if(!Empty(workflowJobTemplateData.summary_fields.survey)) {
|
||||
$scope.survey_exists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fld === 'variables') {
|
||||
// Parse extra_vars, converting to YAML.
|
||||
$scope.variables = ParseVariableString(workflowJobTemplateData.extra_vars);
|
||||
|
||||
ParseTypeChange({ scope: $scope, field_id: 'workflow_job_template_variables' });
|
||||
}
|
||||
if (form.fields[fld].type === 'lookup' && workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
Wait('stop');
|
||||
$scope.url = workflowJobTemplateData.url;
|
||||
$scope.survey_enabled = workflowJobTemplateData.survey_enabled;
|
||||
|
||||
}, function(error){
|
||||
ProcessErrors($scope, error.data, error.status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get workflow job template. GET returned ' +
|
||||
'status: ' + error.status
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function recursiveNodeUpdates(params, completionCallback) {
|
||||
// params.parentId
|
||||
// params.node
|
||||
|
||||
let generatePostUrl = function(){
|
||||
|
||||
let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.workflow_job_template_obj.related.workflow_nodes;
|
||||
|
||||
if(params.parentId) {
|
||||
if(params.node.edgeType === 'success') {
|
||||
base += "/success_nodes";
|
||||
}
|
||||
else if(params.node.edgeType === 'failure') {
|
||||
base += "/failure_nodes";
|
||||
}
|
||||
else if(params.node.edgeType === 'always') {
|
||||
base += "/always_nodes";
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
|
||||
};
|
||||
|
||||
let buildSendableNodeData = function() {
|
||||
// Create the node
|
||||
let sendableNodeData = {
|
||||
unified_job_template: params.node.unifiedJobTemplate.id
|
||||
};
|
||||
|
||||
// Check to see if the user has provided any prompt values that are different
|
||||
// from the defaults in the job template
|
||||
|
||||
if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) {
|
||||
if(params.node.unifiedJobTemplate.ask_credential_on_launch) {
|
||||
sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null;
|
||||
}
|
||||
if(params.node.unifiedJobTemplate.ask_inventory_on_launch) {
|
||||
sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null;
|
||||
}
|
||||
if(params.node.unifiedJobTemplate.ask_limit_on_launch) {
|
||||
sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null;
|
||||
}
|
||||
if(params.node.unifiedJobTemplate.ask_job_type_on_launch) {
|
||||
sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null;
|
||||
}
|
||||
if(params.node.unifiedJobTemplate.ask_tags_on_launch) {
|
||||
sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null;
|
||||
}
|
||||
if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) {
|
||||
sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null;
|
||||
}
|
||||
}
|
||||
|
||||
return sendableNodeData;
|
||||
};
|
||||
|
||||
let continueRecursing = function(parentId) {
|
||||
$scope.totalIteratedNodes++;
|
||||
|
||||
if($scope.totalIteratedNodes === $scope.workflowTree.data.totalNodes) {
|
||||
// We're done recursing, lets move on
|
||||
completionCallback();
|
||||
}
|
||||
else {
|
||||
if(params.node.children && params.node.children.length > 0) {
|
||||
_.forEach(params.node.children, function(child) {
|
||||
if(child.edgeType === "success") {
|
||||
recursiveNodeUpdates({
|
||||
parentId: parentId,
|
||||
node: child
|
||||
}, completionCallback);
|
||||
}
|
||||
else if(child.edgeType === "failure") {
|
||||
recursiveNodeUpdates({
|
||||
parentId: parentId,
|
||||
node: child
|
||||
}, completionCallback);
|
||||
}
|
||||
else if(child.edgeType === "always") {
|
||||
recursiveNodeUpdates({
|
||||
parentId: parentId,
|
||||
node: child
|
||||
}, completionCallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(params.node.isNew) {
|
||||
|
||||
JobTemplateService.addWorkflowNode({
|
||||
url: generatePostUrl(),
|
||||
data: buildSendableNodeData()
|
||||
})
|
||||
.then(function(data) {
|
||||
continueRecursing(data.data.id);
|
||||
}, function(error) {
|
||||
ProcessErrors($scope, error.data, error.status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to add workflow node. ' +
|
||||
'POST returned status: ' +
|
||||
error.status
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
|
||||
|
||||
if(params.node.edited) {
|
||||
|
||||
$scope.editRequests.push({
|
||||
id: params.node.nodeId,
|
||||
data: buildSendableNodeData()
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep
|
||||
|
||||
$scope.disassociateRequests.push({
|
||||
parentId: params.node.originalParentId,
|
||||
nodeId: params.node.nodeId,
|
||||
edge: params.node.originalEdge
|
||||
});
|
||||
|
||||
// Can only associate if we have a parent.
|
||||
// If we don't have a parent then this is a root node
|
||||
// and the act of disassociating will make it a root node
|
||||
if(params.parentId) {
|
||||
$scope.associateRequests.push({
|
||||
parentId: params.parentId,
|
||||
nodeId: params.node.nodeId,
|
||||
edge: params.node.edgeType
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
else if(!params.node.originalParentId && params.parentId) {
|
||||
// This used to be a root node but is now not a root node
|
||||
$scope.associateRequests.push({
|
||||
parentId: params.parentId,
|
||||
nodeId: params.node.nodeId,
|
||||
edge: params.node.edgeType
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
continueRecursing(params.node.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.openWorkflowMaker = function() {
|
||||
$state.go('.workflowMaker');
|
||||
};
|
||||
|
||||
$scope.formSave = function () {
|
||||
let fld, data = {};
|
||||
$scope.invalid_survey = false;
|
||||
|
||||
// Can't have a survey enabled without a survey
|
||||
if($scope.survey_enabled === true && $scope.survey_exists!==true){
|
||||
$scope.survey_enabled = false;
|
||||
}
|
||||
|
||||
generator.clearApiErrors($scope);
|
||||
|
||||
Wait('start');
|
||||
|
||||
try {
|
||||
for (fld in form.fields) {
|
||||
data[fld] = $scope[fld];
|
||||
}
|
||||
|
||||
data.extra_vars = ToJSON($scope.parseType,
|
||||
$scope.variables, true);
|
||||
|
||||
// The idea here is that we want to find the new option elements that also have a label that exists in the dom
|
||||
$("#workflow_job_template_labels > option").filter("[data-select2-tag=true]").each(function(optionIndex, option) {
|
||||
$("#workflow_job_template_labels").siblings(".select2").first().find(".select2-selection__choice").each(function(labelIndex, label) {
|
||||
if($(option).text() === $(label).attr('title')) {
|
||||
// Mark that the option has a label present so that we can filter by that down below
|
||||
$(option).attr('data-label-is-present', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.newLabels = $("#workflow_job_template_labels > option")
|
||||
.filter("[data-select2-tag=true]")
|
||||
.filter("[data-label-is-present=true]")
|
||||
.map((i, val) => ({name: $(val).text()}));
|
||||
|
||||
$scope.totalIteratedNodes = 0;
|
||||
|
||||
// TODO: this is the only way that I could figure out to get
|
||||
// these promise arrays to play nicely. I tried to just append
|
||||
// a single promise to deletePromises but it just wasn't working
|
||||
let editWorkflowJobTemplate = [id].map(function(id) {
|
||||
return JobTemplateService.updateWorkflowJobTemplate({
|
||||
id: id,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
|
||||
if($scope.workflowTree && $scope.workflowTree.data && $scope.workflowTree.data.children && $scope.workflowTree.data.children.length > 0) {
|
||||
let completionCallback = function() {
|
||||
|
||||
let disassociatePromises = $scope.disassociateRequests.map(function(request) {
|
||||
return JobTemplateService.disassociateWorkflowNode({
|
||||
parentId: request.parentId,
|
||||
nodeId: request.nodeId,
|
||||
edge: request.edge
|
||||
});
|
||||
});
|
||||
|
||||
let editNodePromises = $scope.editRequests.map(function(request) {
|
||||
return JobTemplateService.editWorkflowNode({
|
||||
id: request.id,
|
||||
data: request.data
|
||||
});
|
||||
});
|
||||
|
||||
$q.all(disassociatePromises.concat(editNodePromises).concat(editWorkflowJobTemplate))
|
||||
.then(function() {
|
||||
|
||||
let associatePromises = $scope.associateRequests.map(function(request) {
|
||||
return JobTemplateService.associateWorkflowNode({
|
||||
parentId: request.parentId,
|
||||
nodeId: request.nodeId,
|
||||
edge: request.edge
|
||||
});
|
||||
});
|
||||
|
||||
let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) {
|
||||
return JobTemplateService.deleteWorkflowJobTemplateNode(nodeId);
|
||||
});
|
||||
|
||||
$q.all(associatePromises.concat(deletePromises))
|
||||
.then(function() {
|
||||
|
||||
var orgDefer = $q.defer();
|
||||
var associationDefer = $q.defer();
|
||||
var associatedLabelsDefer = $q.defer();
|
||||
|
||||
var getNext = function(data, arr, resolve) {
|
||||
Rest.setUrl(data.next);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (data.next) {
|
||||
getNext(data, arr.concat(data.results), resolve);
|
||||
} else {
|
||||
resolve.resolve(arr.concat(data.results));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
if (data.next) {
|
||||
getNext(data, data.results, associatedLabelsDefer);
|
||||
} else {
|
||||
associatedLabelsDefer.resolve(data.results);
|
||||
}
|
||||
});
|
||||
|
||||
associatedLabelsDefer.promise.then(function (current) {
|
||||
current = current.map(data => data.id);
|
||||
var labelsToAdd = $scope.labels
|
||||
.map(val => val.value);
|
||||
var labelsToDisassociate = current
|
||||
.filter(val => labelsToAdd
|
||||
.indexOf(val) === -1)
|
||||
.map(val => ({id: val, disassociate: true}));
|
||||
var labelsToAssociate = labelsToAdd
|
||||
.filter(val => current
|
||||
.indexOf(val) === -1)
|
||||
.map(val => ({id: val, associate: true}));
|
||||
var pass = labelsToDisassociate
|
||||
.concat(labelsToAssociate);
|
||||
associationDefer.resolve(pass);
|
||||
});
|
||||
|
||||
Rest.setUrl(GetBasePath("organizations"));
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
orgDefer.resolve(data.results[0].id);
|
||||
});
|
||||
|
||||
orgDefer.promise.then(function(orgId) {
|
||||
var toPost = [];
|
||||
$scope.newLabels = $scope.newLabels
|
||||
.map(function(i, val) {
|
||||
val.organization = orgId;
|
||||
return val;
|
||||
});
|
||||
|
||||
$scope.newLabels.each(function(i, val) {
|
||||
toPost.push(val);
|
||||
});
|
||||
|
||||
associationDefer.promise.then(function(arr) {
|
||||
toPost = toPost
|
||||
.concat(arr);
|
||||
|
||||
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||
|
||||
var defers = [];
|
||||
for (var i = 0; i < toPost.length; i++) {
|
||||
defers.push(Rest.post(toPost[i]));
|
||||
}
|
||||
$q.all(defers)
|
||||
.then(function() {
|
||||
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_.forEach($scope.workflowTree.data.children, function(child) {
|
||||
recursiveNodeUpdates({
|
||||
node: child
|
||||
}, completionCallback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) {
|
||||
return JobTemplateService.deleteWorkflowJobTemplateNode(nodeId);
|
||||
});
|
||||
|
||||
$q.all(deletePromises.concat(editWorkflowJobTemplate))
|
||||
.then(function() {
|
||||
var orgDefer = $q.defer();
|
||||
var associationDefer = $q.defer();
|
||||
var associatedLabelsDefer = $q.defer();
|
||||
|
||||
var getNext = function(data, arr, resolve) {
|
||||
Rest.setUrl(data.next);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (data.next) {
|
||||
getNext(data, arr.concat(data.results), resolve);
|
||||
} else {
|
||||
resolve.resolve(arr.concat(data.results));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
if (data.next) {
|
||||
getNext(data, data.results, associatedLabelsDefer);
|
||||
} else {
|
||||
associatedLabelsDefer.resolve(data.results);
|
||||
}
|
||||
});
|
||||
|
||||
associatedLabelsDefer.promise.then(function (current) {
|
||||
current = current.map(data => data.id);
|
||||
var labelsToAdd = $scope.labels
|
||||
.map(val => val.value);
|
||||
var labelsToDisassociate = current
|
||||
.filter(val => labelsToAdd
|
||||
.indexOf(val) === -1)
|
||||
.map(val => ({id: val, disassociate: true}));
|
||||
var labelsToAssociate = labelsToAdd
|
||||
.filter(val => current
|
||||
.indexOf(val) === -1)
|
||||
.map(val => ({id: val, associate: true}));
|
||||
var pass = labelsToDisassociate
|
||||
.concat(labelsToAssociate);
|
||||
associationDefer.resolve(pass);
|
||||
});
|
||||
|
||||
Rest.setUrl(GetBasePath("organizations"));
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
orgDefer.resolve(data.results[0].id);
|
||||
});
|
||||
|
||||
orgDefer.promise.then(function(orgId) {
|
||||
var toPost = [];
|
||||
$scope.newLabels = $scope.newLabels
|
||||
.map(function(i, val) {
|
||||
val.organization = orgId;
|
||||
return val;
|
||||
});
|
||||
|
||||
$scope.newLabels.each(function(i, val) {
|
||||
toPost.push(val);
|
||||
});
|
||||
|
||||
associationDefer.promise.then(function(arr) {
|
||||
toPost = toPost
|
||||
.concat(arr);
|
||||
|
||||
Rest.setUrl($scope.workflow_job_template_obj.related.labels);
|
||||
|
||||
var defers = [];
|
||||
for (var i = 0; i < toPost.length; i++) {
|
||||
defers.push(Rest.post(toPost[i]));
|
||||
}
|
||||
$q.all(defers)
|
||||
.then(function() {
|
||||
$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
//$state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true});
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Wait('stop');
|
||||
Alert("Error", "Error saving workflow job template. " +
|
||||
"Parser returned: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.formCancel = function () {
|
||||
$state.transitionTo('templates');
|
||||
};
|
||||
|
||||
init();
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tab-pane" id="workflow_edit">
|
||||
<div ui-view></div>
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
<workflow-maker ng-if="includeWorkflowMaker" tree-data="workflowTree"></workflow-maker>
|
||||
</div>
|
||||
190
awx/ui/client/src/job-templates/job-template.service.js
Normal file
190
awx/ui/client/src/job-templates/job-template.service.js
Normal file
@ -0,0 +1,190 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['Rest', 'GetBasePath', '$q', function(Rest, GetBasePath, $q){
|
||||
return {
|
||||
deleteJobTemplate: function(id){
|
||||
var url = GetBasePath('job_templates');
|
||||
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.destroy();
|
||||
},
|
||||
deleteWorkflowJobTemplate: function(id) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.destroy();
|
||||
},
|
||||
createJobTemplate: function(data){
|
||||
var url = GetBasePath('job_templates');
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.post(data);
|
||||
},
|
||||
createWorkflowJobTemplate: function(data) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.post(data);
|
||||
},
|
||||
getLabelOptions: function(){
|
||||
var url = GetBasePath('labels');
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
// Turn the labels into something consumable
|
||||
var labels = data.results.map((i) => ({label: i.name, value: i.id}));
|
||||
deferred.resolve(labels);
|
||||
}).error(function(msg, code) {
|
||||
deferred.reject(msg, code);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
|
||||
},
|
||||
getJobTemplate: function(id) {
|
||||
var url = GetBasePath('job_templates');
|
||||
|
||||
url = url + id;
|
||||
|
||||
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
|
||||
|
||||
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');
|
||||
|
||||
url = url + id + '/launch';
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getWorkflowJobTemplateNodes: function(id) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + id + '/workflow_nodes';
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
updateWorkflowJobTemplate: function(params) {
|
||||
// params.id
|
||||
// params.data
|
||||
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
url = url + params.id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.put(params.data);
|
||||
},
|
||||
getWorkflowJobTemplate: function(id) {
|
||||
var url = GetBasePath('workflow_job_templates');
|
||||
|
||||
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.destroy();
|
||||
},
|
||||
disassociateWorkflowNode: function(params) {
|
||||
//params.parentId
|
||||
//params.nodeId
|
||||
//params.edge
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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');
|
||||
|
||||
url = url + "?id=" + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getCredential: function(id) {
|
||||
var url = GetBasePath('credentials');
|
||||
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
},
|
||||
getInventory: function(id) {
|
||||
var url = GetBasePath('inventory');
|
||||
|
||||
url = url + id;
|
||||
|
||||
Rest.setUrl(url);
|
||||
return Rest.get();
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -6,16 +6,17 @@
|
||||
|
||||
export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Alert',
|
||||
'JobTemplateList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath',
|
||||
'InitiatePlaybookRun', 'Wait', '$state', '$filter', 'Dataset', 'rbacUiControlService',
|
||||
'InitiatePlaybookRun', 'Wait', '$state', '$filter', 'Dataset', 'rbacUiControlService', 'JobTemplateService',
|
||||
'QuerySet',
|
||||
function(
|
||||
$scope, $rootScope, $location, $stateParams, Rest, Alert,
|
||||
JobTemplateList, Prompt, ClearScope, ProcessErrors, GetBasePath,
|
||||
InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService
|
||||
InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService, JobTemplateService,
|
||||
qs
|
||||
) {
|
||||
ClearScope();
|
||||
|
||||
var list = JobTemplateList,
|
||||
defaultUrl = GetBasePath('job_templates');
|
||||
var list = JobTemplateList;
|
||||
|
||||
init();
|
||||
|
||||
@ -23,8 +24,13 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Al
|
||||
$scope.canAdd = false;
|
||||
|
||||
rbacUiControlService.canAdd("job_templates")
|
||||
.then(function(canAdd) {
|
||||
$scope.canAdd = canAdd;
|
||||
.then(function(canAddJobTemplate) {
|
||||
$scope.canAddJobTemplate = canAddJobTemplate;
|
||||
});
|
||||
|
||||
rbacUiControlService.canAdd("workflow_job_templates")
|
||||
.then(function(canAddWorkflowJobTemplate) {
|
||||
$scope.canAddWorkflowJobTemplate = canAddWorkflowJobTemplate;
|
||||
});
|
||||
// search init
|
||||
$scope.list = list;
|
||||
@ -35,50 +41,128 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest', 'Al
|
||||
}
|
||||
|
||||
$scope.$on(`ws-jobs`, function () {
|
||||
// @issue - this is ham-fisted, expose a simple QuerySet.reload() fn that'll re-fetch dataset
|
||||
$state.reload();
|
||||
// @issue - this is no longer quite as ham-fisted but I'd like for someone else to take a peek
|
||||
// calling $state.reload(); here was problematic when launching a job because job launch also
|
||||
// attempts to transition the state and they were squashing each other.
|
||||
|
||||
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||
qs.search(path, $stateParams[`${list.iterator}_search`])
|
||||
.then(function(searchResponse) {
|
||||
$scope[`${list.iterator}_dataset`] = searchResponse.data;
|
||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||
});
|
||||
});
|
||||
$scope.addJobTemplate = function() {
|
||||
$state.go('jobTemplates.add');
|
||||
};
|
||||
|
||||
$scope.editJobTemplate = function(id) {
|
||||
$state.go('jobTemplates.edit', { job_template_id: id });
|
||||
$scope.editJobTemplate = function(template) {
|
||||
if(template) {
|
||||
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||
$state.transitionTo('templates.editJobTemplate', {job_template_id: template.id});
|
||||
}
|
||||
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||
$state.transitionTo('templates.editWorkflowJobTemplate', {workflow_job_template_id: template.id});
|
||||
}
|
||||
else {
|
||||
// Something went wrong - Let the user know that we're unable to launch because we don't know
|
||||
// what type of job template this is
|
||||
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to edit.');
|
||||
}
|
||||
}
|
||||
else {
|
||||
Alert('Error: Unable to edit template', 'Template parameter is missing');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteJobTemplate = function(id, name) {
|
||||
var action = function() {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
var url = defaultUrl + id + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.destroy()
|
||||
.success(function() {
|
||||
$state.go('^', null, { reload: true });
|
||||
})
|
||||
.error(function(data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status
|
||||
});
|
||||
$scope.deleteJobTemplate = function(template) {
|
||||
if(template) {
|
||||
Prompt({
|
||||
hdr: 'Delete',
|
||||
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the ' + (template.type === "Workflow Job Template" ? 'workflow ' : '') + 'job template below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(template.name) + '</div>',
|
||||
action: function() {
|
||||
|
||||
function handleSuccessfulDelete() {
|
||||
// TODO: look at this
|
||||
if (parseInt($state.params.id) === template.id) {
|
||||
$state.go("^", null, {reload: true});
|
||||
} else {
|
||||
$state.go(".", null, {reload: true});
|
||||
}
|
||||
Wait('stop');
|
||||
}
|
||||
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||
JobTemplateService.deleteWorkflowJobTemplate(template.id)
|
||||
.then(function () {
|
||||
handleSuccessfulDelete();
|
||||
}, function (data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to delete workflow job template failed. DELETE returned status: ' + status });
|
||||
});
|
||||
}
|
||||
else if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||
JobTemplateService.deleteJobTemplate(template.id)
|
||||
.then(function () {
|
||||
handleSuccessfulDelete();
|
||||
}, function (data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to delete job template failed. DELETE returned status: ' + status });
|
||||
});
|
||||
}
|
||||
else {
|
||||
Wait('stop');
|
||||
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while deleting.');
|
||||
}
|
||||
},
|
||||
actionText: 'DELETE'
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({
|
||||
hdr: 'Delete',
|
||||
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the job template below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(name) + '</div>',
|
||||
action: action,
|
||||
actionText: 'DELETE'
|
||||
});
|
||||
}
|
||||
else {
|
||||
Alert('Error: Unable to delete template', 'Template parameter is missing');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.submitJob = function(id) {
|
||||
InitiatePlaybookRun({ scope: $scope, id: id });
|
||||
$scope.submitJob = function(template) {
|
||||
if(template) {
|
||||
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'job_template' });
|
||||
}
|
||||
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' });
|
||||
}
|
||||
else {
|
||||
// Something went wrong - Let the user know that we're unable to launch because we don't know
|
||||
// what type of job template this is
|
||||
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.');
|
||||
}
|
||||
}
|
||||
else {
|
||||
Alert('Error: Unable to launch template', 'Template parameter is missing');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.scheduleJob = function(id) {
|
||||
$state.go('jobTemplateSchedules', { id: id });
|
||||
$scope.scheduleJob = function(template) {
|
||||
if(template) {
|
||||
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
|
||||
$state.go('jobTemplateSchedules', {id: template.id});
|
||||
}
|
||||
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
|
||||
$state.go('workflowJobTemplateSchedules', {id: template.id});
|
||||
}
|
||||
else {
|
||||
// Something went wrong - Let the user know that we're unable to redirect to schedule because we don't know
|
||||
// what type of job template this is
|
||||
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to schedule.');
|
||||
}
|
||||
}
|
||||
else {
|
||||
Alert('Error: Unable to schedule job', 'Template parameter is missing');
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default {
|
||||
name: 'templates',
|
||||
route: '/templates',
|
||||
ncyBreadcrumb: {
|
||||
label: "TEMPLATES"
|
||||
},
|
||||
data: {
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
},
|
||||
params: {
|
||||
template_search: {
|
||||
value: {
|
||||
type: 'workflow_job_template,job_template'
|
||||
}
|
||||
}
|
||||
},
|
||||
searchPrefix: 'template',
|
||||
views: {
|
||||
'@': {
|
||||
controller: 'JobTemplatesListController',
|
||||
templateProvider: function(JobTemplateList, generateList) {
|
||||
let html = generateList.build({
|
||||
list: JobTemplateList,
|
||||
mode: 'edit'
|
||||
});
|
||||
html = generateList.wrapPanel(html);
|
||||
return generateList.insertFormView() + html;
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
Dataset: ['JobTemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
function(list, qs, $stateParams, GetBasePath) {
|
||||
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@ -8,4 +8,4 @@ import controller from './job-templates-list.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplatesList', [])
|
||||
.controller('JobTemplatesList', controller);
|
||||
.controller('JobTemplatesListController', controller);
|
||||
|
||||
@ -4,47 +4,490 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import deleteJobTemplate from './delete-job-template.service';
|
||||
import jobTemplateService from './job-template.service';
|
||||
|
||||
import surveyMaker from './survey-maker/main';
|
||||
import jobTemplatesList from './list/main';
|
||||
import jobTemplatesAdd from './add/main';
|
||||
import jobTemplatesEdit from './edit/main';
|
||||
import jobTemplatesAdd from './add-job-template/main';
|
||||
import jobTemplatesEdit from './edit-job-template/main';
|
||||
import jobTemplatesCopy from './copy/main';
|
||||
import workflowAdd from './add-workflow/main';
|
||||
import workflowEdit from './edit-workflow/main';
|
||||
import labels from './labels/main';
|
||||
import workflowChart from './workflow-chart/main';
|
||||
import workflowMaker from './workflow-maker/main';
|
||||
import jobTemplatesListRoute from './list/job-templates-list.route';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplates', [surveyMaker.name, jobTemplatesList.name, jobTemplatesAdd.name,
|
||||
jobTemplatesEdit.name, jobTemplatesCopy.name, labels.name
|
||||
jobTemplatesEdit.name, jobTemplatesCopy.name, labels.name, workflowAdd.name, workflowEdit.name,
|
||||
workflowChart.name, workflowMaker.name
|
||||
])
|
||||
.service('deleteJobTemplate', deleteJobTemplate)
|
||||
.config(['$stateProvider', 'stateDefinitionsProvider',
|
||||
function($stateProvider, stateDefinitionsProvider) {
|
||||
let stateDefinitions = stateDefinitionsProvider.$get();
|
||||
.service('JobTemplateService', jobTemplateService)
|
||||
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
|
||||
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
|
||||
let stateTree, addJobTemplate, editJobTemplate, addWorkflow, editWorkflow,
|
||||
workflowMaker, inventoryLookup, credentialLookup,
|
||||
stateDefinitions = stateDefinitionsProvider.$get(),
|
||||
stateExtender = $stateExtenderProvider.$get();
|
||||
|
||||
$stateProvider.state({
|
||||
name: 'jobTemplates',
|
||||
url: '/job_templates',
|
||||
lazyLoad: () => stateDefinitions.generateTree({
|
||||
parent: 'jobTemplates',
|
||||
modes: ['add', 'edit'],
|
||||
list: 'JobTemplateList',
|
||||
function generateStateTree() {
|
||||
|
||||
addJobTemplate = stateDefinitions.generateTree({
|
||||
name: 'templates.addJobTemplate',
|
||||
url: '/add_job_template',
|
||||
modes: ['add'],
|
||||
form: 'JobTemplateForm',
|
||||
controllers: {
|
||||
list: 'JobTemplatesList',
|
||||
add: 'JobTemplatesAdd',
|
||||
edit: 'JobTemplatesEdit'
|
||||
},
|
||||
add: 'JobTemplateAdd'
|
||||
}
|
||||
});
|
||||
|
||||
editJobTemplate = stateDefinitions.generateTree({
|
||||
name: 'templates.editJobTemplate',
|
||||
url: '/job_template/:job_template_id',
|
||||
modes: ['edit'],
|
||||
form: 'JobTemplateForm',
|
||||
controllers: {
|
||||
edit: 'JobTemplateEdit'
|
||||
}
|
||||
});
|
||||
|
||||
addWorkflow = stateDefinitions.generateTree({
|
||||
name: 'templates.addWorkflowJobTemplate',
|
||||
url: '/add_workflow_job_template',
|
||||
modes: ['add'],
|
||||
form: 'WorkflowForm',
|
||||
controllers: {
|
||||
add: 'WorkflowAdd'
|
||||
}
|
||||
});
|
||||
|
||||
editWorkflow = stateDefinitions.generateTree({
|
||||
name: 'templates.editWorkflowJobTemplate',
|
||||
url: '/workflow_job_template/:workflow_job_template_id',
|
||||
modes: ['edit'],
|
||||
form: 'WorkflowForm',
|
||||
controllers: {
|
||||
edit: 'WorkflowEdit'
|
||||
}
|
||||
});
|
||||
|
||||
workflowMaker = {
|
||||
name: 'templates.editWorkflowJobTemplate.workflowMaker',
|
||||
url: '/workflow-maker',
|
||||
// ncyBreadcrumb: {
|
||||
// label: 'WORKFLOW MAKER'
|
||||
// },
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'job_template',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
formChildState: true
|
||||
},
|
||||
params: {
|
||||
job_template_search: {
|
||||
value: {
|
||||
page_size: '5',
|
||||
type: 'job_template'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
},
|
||||
project_search: {
|
||||
value: {
|
||||
page_size: '5'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
},
|
||||
inventory_source_search: {
|
||||
value: {
|
||||
page_size: '5'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'modal': {
|
||||
template: ` <workflow-maker ng-if="includeWorkflowMaker" tree-data="workflowTree" can-add-workflow-job-template="canAddWorkflowJobTemplate"></workflow-maker>`
|
||||
},
|
||||
'jobTemplateList@templates.editWorkflowJobTemplate.workflowMaker': {
|
||||
templateProvider: function(WorkflowMakerJobTemplateList, generateList) {
|
||||
//debugger;
|
||||
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.toggle_job_template = function(id) {
|
||||
|
||||
$scope.job_templates.forEach(function(row, i) {
|
||||
if (row.id === id) {
|
||||
$scope.job_templates[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
$scope.templateSelected(row);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.$on('templateSelected', function(e, options) {
|
||||
if(options.activeTab !== 'jobs') {
|
||||
// Clear out any selected job
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
},
|
||||
'inventorySyncList@templates.editWorkflowJobTemplate.workflowMaker': {
|
||||
templateProvider: function(InventorySourcesList, generateList) {
|
||||
let list = _.cloneDeep(InventorySourcesList);
|
||||
// mutate list definition here!
|
||||
let html = generateList.build({
|
||||
list: list,
|
||||
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', 'InventorySourcesList', '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.toggle_inventory_source = function(id) {
|
||||
|
||||
$scope.workflow_inventory_sources.forEach(function(row, i) {
|
||||
if (row.id === id) {
|
||||
$scope.workflow_inventory_sources[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
$scope.templateSelected(row);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.$on('templateSelected', function(e, options) {
|
||||
if(options.activeTab !== 'project_sync') {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
},
|
||||
'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.toggle_project = function(id) {
|
||||
|
||||
$scope.projects.forEach(function(row, i) {
|
||||
if (row.id === id) {
|
||||
$scope.projects[i].checked = 1;
|
||||
$scope.selection[list.iterator] = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
$scope.templateSelected(row);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.$on('templateSelected', function(e, options) {
|
||||
if(options.activeTab !== 'inventory_sync') {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
},
|
||||
'workflowForm@templates.editWorkflowJobTemplate.workflowMaker': {
|
||||
templateProvider: function(WorkflowMakerForm, GenerateForm) {
|
||||
let form = WorkflowMakerForm();
|
||||
let html = GenerateForm.buildHTML(form, {
|
||||
mode: 'add',
|
||||
related: false,
|
||||
noPanel: true
|
||||
});
|
||||
return html;
|
||||
},
|
||||
controller: ['$scope', '$timeout', 'CreateSelect2',
|
||||
function($scope, $timeout, CreateSelect2) {
|
||||
function resetPromptFields() {
|
||||
$scope.credential = null;
|
||||
$scope.credential_name = null;
|
||||
$scope.inventory = null;
|
||||
$scope.inventory_name = null;
|
||||
$scope.job_type = null;
|
||||
$scope.limit = null;
|
||||
$scope.job_tags = null;
|
||||
$scope.skip_tags = null;
|
||||
}
|
||||
|
||||
$scope.saveNodeForm = function(){
|
||||
// Gather up all of our form data - then let the main scope know what
|
||||
// the new data is
|
||||
|
||||
$scope.confirmNodeForm({
|
||||
skip_tags: $scope.skip_tags,
|
||||
job_tags: $scope.job_tags,
|
||||
limit: $scope.limit,
|
||||
credential: $scope.credential,
|
||||
credential_name: $scope.credential_name,
|
||||
inventory: $scope.inventory,
|
||||
inventory_name: $scope.inventory_name,
|
||||
edgeType: $scope.edgeType,
|
||||
job_type: $scope.job_type
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('templateSelected', function(e, options) {
|
||||
resetPromptFields();
|
||||
// Loop across the preset values and attach them to scope
|
||||
_.forOwn(options.presetValues, function(value, key) {
|
||||
$scope[key] = value;
|
||||
});
|
||||
|
||||
// The default needs to be in place before we can select2-ify the dropdown
|
||||
$timeout(function() {
|
||||
CreateSelect2({
|
||||
element: '#workflow_maker_job_type',
|
||||
multiple: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$on('setEdgeType', function(e, edgeType) {
|
||||
$scope.edgeType = edgeType;
|
||||
});
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
JobTemplateDataset: ['WorkflowMakerJobTemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.basePath);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
],
|
||||
ProjectDataset: ['ProjectList', '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: ['JobTemplateList',
|
||||
(JobTemplateList) => {
|
||||
let list = _.cloneDeep(JobTemplateList);
|
||||
delete list.fields.type;
|
||||
delete list.fields.description;
|
||||
delete list.fields.smart_status;
|
||||
delete list.fields.labels;
|
||||
delete list.fieldActions;
|
||||
list.fields.name.columnClass = "col-md-11";
|
||||
list.iterator = 'job_template';
|
||||
list.name = 'job_templates';
|
||||
|
||||
return list;
|
||||
}
|
||||
],
|
||||
WorkflowProjectList: ['ProjectList',
|
||||
(ProjectList) => {
|
||||
let list = _.cloneDeep(ProjectList);
|
||||
delete list.fields.status;
|
||||
delete list.fields.scm_type;
|
||||
delete list.fields.last_updated;
|
||||
list.fields.name.columnClass = "col-md-11";
|
||||
|
||||
return list;
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
inventoryLookup = {
|
||||
searchPrefix: 'inventory',
|
||||
name: 'templates.editWorkflowJobTemplate.workflowMaker.inventory',
|
||||
url: '/inventory',
|
||||
data: {
|
||||
formChildState: true
|
||||
},
|
||||
params: {
|
||||
inventory_search: {
|
||||
value: {
|
||||
page_size: '5'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'related': {
|
||||
templateProvider: function(ListDefinition, generateList) {
|
||||
let list_html = generateList.build({
|
||||
mode: 'lookup',
|
||||
list: ListDefinition,
|
||||
input_type: 'radio'
|
||||
});
|
||||
return `<lookup-modal>${list_html}</lookup-modal>`;
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
resolve: {
|
||||
ListDefinition: ['InventoryList', function(list) {
|
||||
// mutate the provided list definition here
|
||||
return list;
|
||||
}],
|
||||
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.name) || GetBasePath(list.basePath);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
]
|
||||
},
|
||||
onExit: function($state) {
|
||||
if ($state.transition) {
|
||||
$('#form-modal').modal('hide');
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
credentialLookup = {
|
||||
searchPrefix: 'credential',
|
||||
name: 'templates.editWorkflowJobTemplate.workflowMaker.credential',
|
||||
url: '/credential',
|
||||
data: {
|
||||
formChildState: true
|
||||
},
|
||||
params: {
|
||||
credential_search: {
|
||||
value: {
|
||||
page_size: '5'
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'related': {
|
||||
templateProvider: function(ListDefinition, generateList) {
|
||||
let list_html = generateList.build({
|
||||
mode: 'lookup',
|
||||
list: ListDefinition,
|
||||
input_type: 'radio'
|
||||
});
|
||||
return `<lookup-modal>${list_html}</lookup-modal>`;
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
ListDefinition: ['CredentialList', function(list) {
|
||||
// mutate the provided list definition here
|
||||
return list;
|
||||
}],
|
||||
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.name) || GetBasePath(list.basePath);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
]
|
||||
},
|
||||
onExit: function($state) {
|
||||
if ($state.transition) {
|
||||
$('#form-modal').modal('hide');
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return Promise.all([
|
||||
addJobTemplate,
|
||||
editJobTemplate,
|
||||
addWorkflow,
|
||||
editWorkflow
|
||||
]).then((generated) => {
|
||||
return {
|
||||
states: _.reduce(generated, (result, definition) => {
|
||||
return result.concat(definition.states);
|
||||
}, [
|
||||
stateExtender.buildDefinition(jobTemplatesListRoute),
|
||||
stateExtender.buildDefinition(workflowMaker),
|
||||
stateExtender.buildDefinition(inventoryLookup),
|
||||
stateExtender.buildDefinition(credentialLookup)
|
||||
])
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
stateTree = {
|
||||
name: 'templates',
|
||||
url: '/templates',
|
||||
lazyLoad: () => generateStateTree()
|
||||
};
|
||||
|
||||
$stateProvider.state(stateTree);
|
||||
|
||||
}
|
||||
]);
|
||||
|
||||
11
awx/ui/client/src/job-templates/workflow-chart/main.js
Normal file
11
awx/ui/client/src/job-templates/workflow-chart/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import workflowChart from './workflow-chart.directive';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplatesWorkflowChart', [])
|
||||
.directive('workflowChart', workflowChart);
|
||||
@ -0,0 +1,69 @@
|
||||
@import "./client/src/shared/branding/colors.default.less";
|
||||
|
||||
.nodeConnector circle, .nodeConnector .linkCross, .node .addCircle, .node .removeCircle, .node .WorkflowChart-hoverPath {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.node .addCircle, .nodeConnector .addCircle {
|
||||
fill: @default-succ;
|
||||
}
|
||||
|
||||
.addCircle.addHovering {
|
||||
fill: @default-succ-hov;
|
||||
}
|
||||
|
||||
.node .removeCircle {
|
||||
fill: @default-err;
|
||||
}
|
||||
|
||||
.removeCircle.removeHovering {
|
||||
fill: @default-err-hov;
|
||||
}
|
||||
|
||||
.node .WorkflowChart-defaultText {
|
||||
font-size: 12px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
fill: @default-interface-txt;
|
||||
}
|
||||
|
||||
.node .rect {
|
||||
fill: @default-secondary-bg;
|
||||
}
|
||||
|
||||
.rect.placeholder {
|
||||
stroke-dasharray: 3;
|
||||
}
|
||||
|
||||
.node .transparentRect {
|
||||
fill: @default-bg;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.WorkflowChart-alwaysShowAdd circle,
|
||||
.WorkflowChart-alwaysShowAdd path,
|
||||
.WorkflowChart-alwaysShowAdd .linkCross,
|
||||
.hovering .addCircle,
|
||||
.hovering .removeCircle,
|
||||
.hovering .WorkflowChart-hoverPath,
|
||||
.hovering .linkCross {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.link.placeholder {
|
||||
stroke-dasharray: 3;
|
||||
}
|
||||
|
||||
.WorkflowChart-svg {
|
||||
background-color: @default-no-items-bord;
|
||||
}
|
||||
.WorkflowChart-nodeTypeCircle {
|
||||
fill: @default-icon;
|
||||
}
|
||||
.WorkflowChart-nodeTypeLetter {
|
||||
fill: @default-bg;
|
||||
}
|
||||
@ -0,0 +1,467 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default [
|
||||
function() {
|
||||
|
||||
return {
|
||||
scope: {
|
||||
treeData: '=',
|
||||
canAddWorkflowJobTemplate: '=',
|
||||
addNode: '&',
|
||||
editNode: '&',
|
||||
deleteNode: '&'
|
||||
},
|
||||
restrict: 'E',
|
||||
link: function(scope, element) {
|
||||
|
||||
scope.$watch('canAddWorkflowJobTemplate', function() {
|
||||
// Redraw the graph if permissions change
|
||||
update();
|
||||
});
|
||||
|
||||
let margin = {top: 20, right: 20, bottom: 20, left: 20},
|
||||
width = 950,
|
||||
height = 590 - margin.top - margin.bottom,
|
||||
i = 0,
|
||||
rectW = 120,
|
||||
rectH = 60,
|
||||
rootW = 60,
|
||||
rootH = 40;
|
||||
|
||||
let tree = d3.layout.tree()
|
||||
.size([height, width]);
|
||||
|
||||
let line = d3.svg.line()
|
||||
.x(function(d){return d.x;})
|
||||
.y(function(d){return d.y;});
|
||||
|
||||
function lineData(d){
|
||||
|
||||
let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + rectW;
|
||||
let sourceY = d.source.isStartNode ? d.source.x + 10 + rootH / 2 : d.source.x + rectH / 2;
|
||||
let targetX = d.target.y;
|
||||
let targetY = d.target.x + rectH / 2;
|
||||
|
||||
let points = [
|
||||
{
|
||||
x: sourceX,
|
||||
y: sourceY
|
||||
},
|
||||
{
|
||||
x: targetX,
|
||||
y: targetY
|
||||
}
|
||||
];
|
||||
|
||||
return line(points);
|
||||
}
|
||||
|
||||
// TODO: this function is hacky and we need to come up with a better solution
|
||||
// see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752
|
||||
function wrap(text) {
|
||||
if(text && text.length > 15) {
|
||||
return text.substring(0,15) + '...';
|
||||
}
|
||||
else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
let svg = d3.select(element[0]).append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("class", "WorkflowChart-svg")
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
let node = svg.selectAll(".node"),
|
||||
link = svg.selectAll(".link");
|
||||
|
||||
function update() {
|
||||
// Declare the nodes
|
||||
let nodes = tree.nodes(scope.treeData);
|
||||
node = node.data(nodes, function(d) { d.y = d.depth * 180; return d.id || (d.id = ++i); });
|
||||
link = link.data(tree.links(nodes), function(d) { return d.source.id + "-" + d.target.id; });
|
||||
|
||||
let nodeEnter = node.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("id", function(d){return "node-" + d.id;})
|
||||
.attr("parent", function(d){return d.parent ? d.parent.id : null;})
|
||||
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
|
||||
.attr("fill", "red");
|
||||
|
||||
nodeEnter.each(function(d) {
|
||||
let thisNode = d3.select(this);
|
||||
if(d.isStartNode) {
|
||||
thisNode.append("rect")
|
||||
.attr("width", 60)
|
||||
.attr("height", 40)
|
||||
.attr("y", 10)
|
||||
.attr("rx", 5)
|
||||
.attr("ry", 5)
|
||||
.attr("fill", "#5cb85c")
|
||||
.attr("class", "WorkflowChart-rootNode")
|
||||
.call(add_node);
|
||||
thisNode.append("path")
|
||||
.style("fill", "white")
|
||||
.attr("transform", function() { return "translate(" + 30 + "," + 30 + ")"; })
|
||||
.attr("d", d3.svg.symbol()
|
||||
.size(120)
|
||||
.type("cross")
|
||||
)
|
||||
.call(add_node);
|
||||
thisNode.append("text")
|
||||
.attr("x", 14)
|
||||
.attr("y", 0)
|
||||
.attr("dy", ".35em")
|
||||
.attr("class", "WorkflowChart-defaultText")
|
||||
.text(function () { return "START"; });
|
||||
}
|
||||
else {
|
||||
thisNode.append("rect")
|
||||
.attr("width", rectW)
|
||||
.attr("height", rectH)
|
||||
.attr("rx", 5)
|
||||
.attr("ry", 5)
|
||||
.attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; })
|
||||
.attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; })
|
||||
.attr("class", function(d) {
|
||||
return d.placeholder ? "rect placeholder" : "rect";
|
||||
});
|
||||
thisNode.append("text")
|
||||
.attr("x", rectW / 2)
|
||||
.attr("y", rectH / 2)
|
||||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("class", "WorkflowChart-defaultText WorkflowChart-nameText")
|
||||
.text(function (d) {
|
||||
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
||||
}).each(wrap);
|
||||
|
||||
thisNode.append("circle")
|
||||
.attr("cy", rectH)
|
||||
.attr("r", 10)
|
||||
.attr("class", "WorkflowChart-nodeTypeCircle")
|
||||
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; });
|
||||
|
||||
thisNode.append("text")
|
||||
.attr("y", rectH)
|
||||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("class", "WorkflowChart-nodeTypeLetter")
|
||||
.text(function (d) {
|
||||
return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : "");
|
||||
})
|
||||
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; });
|
||||
|
||||
thisNode.append("rect")
|
||||
.attr("width", rectW)
|
||||
.attr("height", rectH)
|
||||
.attr("class", "transparentRect")
|
||||
.call(edit_node)
|
||||
.on("mouseover", function(d) {
|
||||
if(!d.isStartNode) {
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", true);
|
||||
}
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
if(!d.isStartNode) {
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", false);
|
||||
}
|
||||
});
|
||||
thisNode.append("circle")
|
||||
.attr("id", function(d){return "node-" + d.id + "-add";})
|
||||
.attr("cx", rectW)
|
||||
.attr("r", 10)
|
||||
.attr("class", "addCircle nodeCircle")
|
||||
.style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; })
|
||||
.call(add_node)
|
||||
.on("mouseover", function(d) {
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", true);
|
||||
d3.select("#node-" + d.id + "-add")
|
||||
.classed("addHovering", true);
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", false);
|
||||
d3.select("#node-" + d.id + "-add")
|
||||
.classed("addHovering", false);
|
||||
});
|
||||
thisNode.append("path")
|
||||
.attr("class", "nodeAddCross WorkflowChart-hoverPath")
|
||||
.style("fill", "white")
|
||||
.attr("transform", function() { return "translate(" + rectW + "," + 0 + ")"; })
|
||||
.attr("d", d3.svg.symbol()
|
||||
.size(60)
|
||||
.type("cross")
|
||||
)
|
||||
.style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; })
|
||||
.call(add_node)
|
||||
.on("mouseover", function(d) {
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", true);
|
||||
d3.select("#node-" + d.id + "-add")
|
||||
.classed("addHovering", true);
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", false);
|
||||
d3.select("#node-" + d.id + "-add")
|
||||
.classed("addHovering", false);
|
||||
});
|
||||
thisNode.append("circle")
|
||||
.attr("id", function(d){return "node-" + d.id + "-remove";})
|
||||
.attr("cx", rectW)
|
||||
.attr("cy", rectH)
|
||||
.attr("r", 10)
|
||||
.attr("class", "removeCircle")
|
||||
.style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; })
|
||||
.call(remove_node)
|
||||
.on("mouseover", function(d) {
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", true);
|
||||
d3.select("#node-" + d.id + "-remove")
|
||||
.classed("removeHovering", true);
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", false);
|
||||
d3.select("#node-" + d.id + "-remove")
|
||||
.classed("removeHovering", false);
|
||||
});
|
||||
thisNode.append("path")
|
||||
.attr("class", "nodeRemoveCross WorkflowChart-hoverPath")
|
||||
.style("fill", "white")
|
||||
.attr("transform", function() { return "translate(" + rectW + "," + rectH + ") rotate(-45)"; })
|
||||
.attr("d", d3.svg.symbol()
|
||||
.size(60)
|
||||
.type("cross")
|
||||
)
|
||||
.style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; })
|
||||
.call(remove_node)
|
||||
.on("mouseover", function(d) {
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", true);
|
||||
d3.select("#node-" + d.id + "-remove")
|
||||
.classed("removeHovering", true);
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
d3.select("#node-" + d.id)
|
||||
.classed("hovering", false);
|
||||
d3.select("#node-" + d.id + "-remove")
|
||||
.classed("removeHovering", false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
node.exit().remove();
|
||||
|
||||
let linkEnter = link.enter().append("g")
|
||||
.attr("class", "nodeConnector")
|
||||
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
|
||||
|
||||
// Add entering links in the parent’s old position.
|
||||
linkEnter.insert("path", ".node")
|
||||
.attr("class", function(d) {
|
||||
return (d.source.placeholder || d.target.placeholder) ? "link placeholder" : "link";
|
||||
})
|
||||
.attr("d", lineData)
|
||||
.attr('stroke', function(d) {
|
||||
if(d.target.edgeType) {
|
||||
if(d.target.edgeType === "failure") {
|
||||
return "#d9534f";
|
||||
}
|
||||
else if(d.target.edgeType === "success") {
|
||||
return "#5cb85c";
|
||||
}
|
||||
else if(d.target.edgeType === "always"){
|
||||
return "#337ab7";
|
||||
}
|
||||
}
|
||||
else {
|
||||
return "#D7D7D7";
|
||||
}
|
||||
});
|
||||
|
||||
linkEnter.append("circle")
|
||||
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";})
|
||||
.attr("cx", function(d) {
|
||||
return (d.target.y + d.source.y + rectW) / 2;
|
||||
})
|
||||
.attr("cy", function(d) {
|
||||
return (d.target.x + d.source.x + rectH) / 2;
|
||||
})
|
||||
.attr("r", 10)
|
||||
.attr("class", "addCircle linkCircle")
|
||||
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; })
|
||||
.call(add_node_between)
|
||||
.on("mouseover", function(d) {
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||
.classed("hovering", true);
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||
.classed("addHovering", true);
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||
.classed("hovering", false);
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||
.classed("addHovering", false);
|
||||
});
|
||||
|
||||
linkEnter.append("path")
|
||||
.attr("class", "linkCross")
|
||||
.style("fill", "white")
|
||||
.attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; })
|
||||
.attr("d", d3.svg.symbol()
|
||||
.size(60)
|
||||
.type("cross")
|
||||
)
|
||||
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; })
|
||||
.call(add_node_between)
|
||||
.on("mouseover", function(d) {
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||
.classed("hovering", true);
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||
.classed("addHovering", true);
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id)
|
||||
.classed("hovering", false);
|
||||
d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
|
||||
.classed("addHovering", false);
|
||||
});
|
||||
|
||||
link.exit().remove();
|
||||
|
||||
// Transition nodes and links to their new positions.
|
||||
let t = svg.transition();
|
||||
|
||||
t.selectAll(".nodeCircle")
|
||||
.style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; });
|
||||
|
||||
t.selectAll(".nodeAddCross")
|
||||
.style("display", function(d) { return d.placeholder || scope.canAddWorkflowJobTemplate === false ? "none" : null; });
|
||||
|
||||
t.selectAll(".removeCircle")
|
||||
.style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; });
|
||||
|
||||
t.selectAll(".nodeRemoveCross")
|
||||
.style("display", function(d) { return (d.canDelete === false || d.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; });
|
||||
|
||||
t.selectAll(".link")
|
||||
.attr("class", function(d) {
|
||||
return (d.source.placeholder || d.target.placeholder) ? "link placeholder" : "link";
|
||||
})
|
||||
.attr("d", lineData)
|
||||
.attr('stroke', function(d) {
|
||||
if(d.target.edgeType) {
|
||||
if(d.target.edgeType === "failure") {
|
||||
return "#d9534f";
|
||||
}
|
||||
else if(d.target.edgeType === "success") {
|
||||
return "#5cb85c";
|
||||
}
|
||||
else if(d.target.edgeType === "always"){
|
||||
return "#337ab7";
|
||||
}
|
||||
}
|
||||
else {
|
||||
return "#D7D7D7";
|
||||
}
|
||||
});
|
||||
|
||||
t.selectAll(".linkCircle")
|
||||
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; })
|
||||
.attr("cx", function(d) {
|
||||
return (d.target.y + d.source.y + rectW) / 2;
|
||||
})
|
||||
.attr("cy", function(d) {
|
||||
return (d.target.x + d.source.x + rectH) / 2;
|
||||
});
|
||||
|
||||
t.selectAll(".linkCross")
|
||||
.style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; })
|
||||
.attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; });
|
||||
|
||||
t.selectAll(".rect")
|
||||
.attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; })
|
||||
.attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; })
|
||||
.attr("class", function(d) {
|
||||
return d.placeholder ? "rect placeholder" : "rect";
|
||||
});
|
||||
|
||||
t.selectAll(".WorkflowChart-nameText")
|
||||
.text(function (d) {
|
||||
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : "";
|
||||
});
|
||||
|
||||
t.selectAll(".node")
|
||||
.attr("transform", function(d) {d.px = d.x; d.py = d.y; return "translate(" + d.y + "," + d.x + ")"; });
|
||||
|
||||
t.selectAll(".WorkflowChart-nodeTypeCircle")
|
||||
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update" ) ? null : "none"; });
|
||||
|
||||
t.selectAll(".WorkflowChart-nodeTypeLetter")
|
||||
.text(function (d) {
|
||||
return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : "");
|
||||
})
|
||||
.style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; });
|
||||
|
||||
}
|
||||
|
||||
function add_node() {
|
||||
this.on("click", function(d) {
|
||||
if(scope.canAddWorkflowJobTemplate !== false) {
|
||||
scope.addNode({
|
||||
parent: d,
|
||||
betweenTwoNodes: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function add_node_between() {
|
||||
this.on("click", function(d) {
|
||||
if(scope.canAddWorkflowJobTemplate !== false) {
|
||||
scope.addNode({
|
||||
parent: d,
|
||||
betweenTwoNodes: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function remove_node() {
|
||||
this.on("click", function(d) {
|
||||
if(scope.canAddWorkflowJobTemplate !== false) {
|
||||
scope.deleteNode({
|
||||
nodeToDelete: d
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function edit_node() {
|
||||
this.on("click", function(d) {
|
||||
if(d.canEdit){
|
||||
scope.editNode({
|
||||
nodeToEdit: d
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.$on('refreshWorkflowChart', function(){
|
||||
update();
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}];
|
||||
11
awx/ui/client/src/job-templates/workflow-maker/main.js
Normal file
11
awx/ui/client/src/job-templates/workflow-maker/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
import helper from './workflow-help.service';
|
||||
import workflowMaker from './workflow-maker.directive';
|
||||
import WorkflowMakerController from './workflow-maker.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobTemplates.workflowMaker', [])
|
||||
.service('WorkflowHelpService', helper)
|
||||
// In order to test this controller I had to expose it at the module level
|
||||
// like so. Is this correct? Is there a better pattern for doing this?
|
||||
.controller('WorkflowMakerController', WorkflowMakerController)
|
||||
.directive('workflowMaker', workflowMaker);
|
||||
@ -0,0 +1,127 @@
|
||||
export default ['CreateDialog', 'Wait', '$q', '$state', function(CreateDialog, Wait, $q, $state){
|
||||
return {
|
||||
closeDialog: function() {
|
||||
$('#workflow-modal-dialog').dialog('destroy');
|
||||
|
||||
$state.go('^');
|
||||
},
|
||||
searchTree: function(params) {
|
||||
// params.element
|
||||
// params.matchingId
|
||||
|
||||
if(params.element.id === params.matchingId){
|
||||
return params.element;
|
||||
}else if (params.element.children && params.element.children.length > 0){
|
||||
let result = null;
|
||||
const thisService = this;
|
||||
_.forEach(params.element.children, function(child) {
|
||||
result = thisService.searchTree({
|
||||
element: child,
|
||||
matchingId: params.matchingId
|
||||
});
|
||||
if(result) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
removeNodeFromTree: function(params) {
|
||||
// params.tree
|
||||
// params.nodeToBeDeleted
|
||||
|
||||
let parentNode = this.searchTree({
|
||||
element: params.tree,
|
||||
matchingId: params.nodeToBeDeleted.parent.id
|
||||
});
|
||||
let nodeToBeDeleted = this.searchTree({
|
||||
element: parentNode,
|
||||
matchingId: params.nodeToBeDeleted.id
|
||||
});
|
||||
|
||||
if(nodeToBeDeleted.children) {
|
||||
_.forEach(nodeToBeDeleted.children, function(child) {
|
||||
if(nodeToBeDeleted.isRoot) {
|
||||
child.isRoot = true;
|
||||
child.edgeType = "always";
|
||||
}
|
||||
|
||||
parentNode.children.push(child);
|
||||
});
|
||||
}
|
||||
|
||||
_.forEach(parentNode.children, function(child, index) {
|
||||
if(child.id === params.nodeToBeDeleted.id) {
|
||||
parentNode.children.splice(index, 1);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
addPlaceholderNode: function(params) {
|
||||
// params.parent
|
||||
// params.betweenTwoNodes
|
||||
// params.tree
|
||||
// params.id
|
||||
|
||||
let placeholder = {
|
||||
children: [],
|
||||
c: "#D7D7D7",
|
||||
id: params.id,
|
||||
canDelete: true,
|
||||
canEdit: false,
|
||||
canAddTo: true,
|
||||
placeholder: true,
|
||||
isNew: true,
|
||||
edited: false
|
||||
};
|
||||
|
||||
let parentNode = (params.betweenTwoNodes) ? this.searchTree({element: params.tree, matchingId: params.parent.source.id}) : this.searchTree({element: params.tree, matchingId: params.parent.id});
|
||||
let placeholderRef;
|
||||
|
||||
if(params.betweenTwoNodes) {
|
||||
_.forEach(parentNode.children, function(child, index) {
|
||||
if(child.id === params.parent.target.id) {
|
||||
placeholder.children.push(angular.copy(child));
|
||||
parentNode.children[index] = placeholder;
|
||||
placeholderRef = parentNode.children[index];
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
if(parentNode.children) {
|
||||
parentNode.children.push(placeholder);
|
||||
placeholderRef = parentNode.children[parentNode.children.length - 1];
|
||||
} else {
|
||||
parentNode.children = [placeholder];
|
||||
placeholderRef = parentNode.children[0];
|
||||
}
|
||||
}
|
||||
|
||||
return placeholderRef;
|
||||
},
|
||||
getSiblingConnectionTypes: function(params) {
|
||||
// params.parentId
|
||||
// params.tree
|
||||
|
||||
let siblingConnectionTypes = {};
|
||||
|
||||
let parentNode = this.searchTree({
|
||||
element: params.tree,
|
||||
matchingId: params.parentId
|
||||
});
|
||||
|
||||
if(parentNode.children && parentNode.children.length > 0) {
|
||||
// Loop across them and add the types as keys to siblingConnectionTypes
|
||||
_.forEach(parentNode.children, function(child) {
|
||||
if(!child.placeholder && child.edgeType) {
|
||||
siblingConnectionTypes[child.edgeType] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(siblingConnectionTypes);
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -0,0 +1,194 @@
|
||||
@import "./client/src/shared/branding/colors.default.less";
|
||||
|
||||
.WorkflowMaker-header {
|
||||
display: flex;
|
||||
height: 34px;
|
||||
}
|
||||
.WorkflowMaker-title {
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
height: 34px;
|
||||
}
|
||||
.WorkflowMaker-titleText {
|
||||
color: @list-title-txt;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.WorkflowMaker-exitHolder {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
}
|
||||
.WorkflowMaker-exit{
|
||||
cursor:pointer;
|
||||
padding:0px;
|
||||
border: none;
|
||||
height:20px;
|
||||
font-size: 20px;
|
||||
background-color:@default-bg;
|
||||
color:@d7grey;
|
||||
transition: color 0.2s;
|
||||
line-height:1;
|
||||
}
|
||||
.WorkflowMaker-exit:hover{
|
||||
color:@default-icon;
|
||||
}
|
||||
.WorkflowMaker-contentHolder {
|
||||
display: flex;
|
||||
border: 1px solid #EBEBEB;
|
||||
height: ~"calc(100% - 85px)";
|
||||
}
|
||||
.WorkflowMaker-contentLeft {
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.WorkflowMaker-contentRight {
|
||||
flex: 0 0 400px;
|
||||
border-left: 1px solid #EBEBEB;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.WorkflowMaker-buttonHolder {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.WorkflowMaker-saveButton{
|
||||
background-color: @submit-button-bg;
|
||||
color: @submit-button-text;
|
||||
text-transform: uppercase;
|
||||
transition: background-color 0.2s;
|
||||
padding-left:15px;
|
||||
padding-right: 15px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.WorkflowMaker-saveButton:disabled{
|
||||
background-color: @submit-button-bg-dis;
|
||||
}
|
||||
|
||||
.WorkflowMaker-saveButton:hover{
|
||||
background-color: @submit-button-bg-hov;
|
||||
color: @submit-button-text;
|
||||
}
|
||||
|
||||
.WorkflowMaker-cancelButton{
|
||||
background-color: @default-bg;
|
||||
color: @btn-txt;
|
||||
text-transform: uppercase;
|
||||
border-radius: 5px;
|
||||
border: 1px solid @btn-bord;
|
||||
transition: background-color 0.2s;
|
||||
padding-left:15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.WorkflowMaker-cancelButton:hover{
|
||||
background-color: @btn-bg-hov;
|
||||
color: @btn-txt;
|
||||
}
|
||||
|
||||
.WorkflowMaker-deleteOverlay {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.WorkflowMaker-deleteModal {
|
||||
height: 200px;
|
||||
width: 600px;
|
||||
background-color: @default-bg;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.WorkflowMaker-formTitle {
|
||||
color: @list-title-txt;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.WorkflowMaker-formHelp {
|
||||
color: #707070;
|
||||
}
|
||||
.WorkflowMaker-formLists {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.WorkflowMaker-formTitle {
|
||||
display: flex;
|
||||
color: #707070;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.WorkflowMaker-formLabel {
|
||||
font-weight: normal;
|
||||
}
|
||||
.WorkflowMaker-formElement {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.WorkflowMaker-legend {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
color: #707070;
|
||||
}
|
||||
.WorkflowMaker-chart {
|
||||
display: flex;
|
||||
}
|
||||
.WorkflowMaker-legendLeft {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.WorkflowMaker-legendRight {
|
||||
flex: 0 0 170px;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.WorkflowMaker-legendItem {
|
||||
display: flex;
|
||||
}
|
||||
.WorkflowMaker-legendItem:not(:last-child) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.WorkflowMaker-onSuccessLegend {
|
||||
height: 4px;
|
||||
width: 20px;
|
||||
background-color: @submit-button-bg;
|
||||
margin: 18px 5px 18px 0px;
|
||||
}
|
||||
.WorkflowMaker-onFailLegend {
|
||||
height: 4px;
|
||||
width: 20px;
|
||||
background-color: #d9534f;
|
||||
margin: 18px 5px 18px 0px;
|
||||
}
|
||||
.WorkflowMaker-alwaysLegend {
|
||||
height: 4px;
|
||||
width: 20px;
|
||||
background-color: #337ab7;
|
||||
margin: 18px 5px 18px 0px;
|
||||
}
|
||||
.WorkflowMaker-letterCircle{
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #848992;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
margin: 10px 5px 10px 0px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.WorkflowMaker-totalJobs {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@ -0,0 +1,520 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', 'WorkflowHelpService', 'generateList', 'JobTemplateList', 'ProjectList',
|
||||
'GetBasePath', 'Wait', 'JobTemplateService', '$state',
|
||||
'ProcessErrors', 'InventorySourcesList', 'CreateSelect2', 'WorkflowMakerForm',
|
||||
'GenerateForm', 'InventoryList', 'CredentialList', '$q',
|
||||
function($scope, WorkflowHelpService, GenerateList, JobTemplateList, ProjectList,
|
||||
GetBasePath, Wait, JobTemplateService, $state,
|
||||
ProcessErrors, InventorySourcesList, CreateSelect2, WorkflowMakerForm,
|
||||
GenerateForm, InventoryList, CredentialList, $q) {
|
||||
|
||||
let form = WorkflowMakerForm();
|
||||
|
||||
$scope.workflowMakerFormConfig = {
|
||||
nodeMode: "idle",
|
||||
activeTab: "jobs",
|
||||
formIsValid: false
|
||||
};
|
||||
|
||||
$scope.job_type_options = [{
|
||||
label: "Run",
|
||||
value: "run"
|
||||
}, {
|
||||
label: "Check",
|
||||
value: "check"
|
||||
}];
|
||||
|
||||
function init() {
|
||||
$scope.treeDataMaster = angular.copy($scope.treeData.data);
|
||||
$scope.$broadcast("refreshWorkflowChart");
|
||||
}
|
||||
|
||||
function resetNodeForm() {
|
||||
$scope.workflowMakerFormConfig.nodeMode = "idle";
|
||||
$scope.showTypeOptions = false;
|
||||
delete $scope.selectedTemplate;
|
||||
delete $scope.workflow_job_templates;
|
||||
delete $scope.workflow_projects;
|
||||
delete $scope.workflow_inventory_sources;
|
||||
delete $scope.placeholderNode;
|
||||
delete $scope.betweenTwoNodes;
|
||||
$scope.nodeBeingEdited = null;
|
||||
$scope.edgeTypeRestriction = null;
|
||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||
}
|
||||
|
||||
$scope.lookUpInventory = function(){
|
||||
$state.go('.inventory');
|
||||
};
|
||||
|
||||
$scope.lookUpCredential = function(){
|
||||
$state.go('.credential');
|
||||
};
|
||||
|
||||
$scope.closeWorkflowMaker = function() {
|
||||
// Revert the data to the master which was created when the dialog was opened
|
||||
$scope.treeData.data = angular.copy($scope.treeDataMaster);
|
||||
WorkflowHelpService.closeDialog();
|
||||
};
|
||||
|
||||
$scope.saveWorkflowMaker = function() {
|
||||
WorkflowHelpService.closeDialog();
|
||||
};
|
||||
|
||||
/* ADD NODE FUNCTIONS */
|
||||
|
||||
$scope.startAddNode = function(parent, betweenTwoNodes) {
|
||||
|
||||
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
|
||||
$scope.cancelNodeForm();
|
||||
}
|
||||
|
||||
$scope.workflowMakerFormConfig.nodeMode = "add";
|
||||
$scope.addParent = parent;
|
||||
$scope.betweenTwoNodes = betweenTwoNodes;
|
||||
|
||||
$scope.placeholderNode = WorkflowHelpService.addPlaceholderNode({
|
||||
parent: parent,
|
||||
betweenTwoNodes: betweenTwoNodes,
|
||||
tree: $scope.treeData.data,
|
||||
id: $scope.treeData.nextIndex
|
||||
});
|
||||
|
||||
$scope.treeData.nextIndex++;
|
||||
|
||||
let siblingConnectionTypes = WorkflowHelpService.getSiblingConnectionTypes({
|
||||
tree: $scope.treeData.data,
|
||||
parentId: betweenTwoNodes ? parent.source.id : parent.id
|
||||
});
|
||||
|
||||
// Set the default to success
|
||||
let edgeType = "success";
|
||||
|
||||
if (parent && ((betweenTwoNodes && parent.source.isStartNode) || (!betweenTwoNodes && parent.isStartNode))) {
|
||||
// We don't want to give the user the option to select
|
||||
// a type as this node will always be executed
|
||||
edgeType = "always";
|
||||
$scope.showTypeOptions = false;
|
||||
} else {
|
||||
if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) {
|
||||
// This is a problem...
|
||||
} else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
|
||||
$scope.edgeTypeRestriction = "successFailure";
|
||||
edgeType = "success";
|
||||
} else if (_.includes(siblingConnectionTypes, "always")) {
|
||||
$scope.edgeTypeRestriction = "always";
|
||||
edgeType = "always";
|
||||
}
|
||||
|
||||
$scope.showTypeOptions = true;
|
||||
}
|
||||
|
||||
$scope.$broadcast("setEdgeType", edgeType);
|
||||
$scope.$broadcast("refreshWorkflowChart");
|
||||
|
||||
};
|
||||
|
||||
$scope.confirmNodeForm = function(formValues) {
|
||||
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
|
||||
if ($scope.selectedTemplate && formValues.edgeType) {
|
||||
|
||||
$scope.placeholderNode.unifiedJobTemplate = $scope.selectedTemplate;
|
||||
$scope.placeholderNode.edgeType = formValues.edgeType;
|
||||
if ($scope.placeholderNode.unifiedJobTemplate.type === 'job_template') {
|
||||
$scope.placeholderNode.promptValues = {
|
||||
credential: {
|
||||
id: formValues.credential,
|
||||
name: formValues.credential_name
|
||||
},
|
||||
inventory: {
|
||||
id: formValues.inventory,
|
||||
name: formValues.inventory_name
|
||||
},
|
||||
limit: formValues.limit,
|
||||
job_type: formValues.job_type && formValues.job_type.value ? formValues.job_type.value : null,
|
||||
job_tags: formValues.job_tags,
|
||||
skip_tags: formValues.skip_tags
|
||||
};
|
||||
}
|
||||
$scope.placeholderNode.canEdit = true;
|
||||
|
||||
delete $scope.placeholderNode.placeholder;
|
||||
|
||||
resetNodeForm();
|
||||
|
||||
// Increment the total node counter
|
||||
$scope.treeData.data.totalNodes++;
|
||||
|
||||
}
|
||||
} else if ($scope.workflowMakerFormConfig.nodeMode === "edit") {
|
||||
if ($scope.selectedTemplate && formValues.edgeType) {
|
||||
$scope.nodeBeingEdited.unifiedJobTemplate = $scope.selectedTemplate;
|
||||
$scope.nodeBeingEdited.edgeType = formValues.edgeType;
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === 'job_template') {
|
||||
$scope.nodeBeingEdited.promptValues = {
|
||||
credential: {
|
||||
id: formValues.credential,
|
||||
name: formValues.credential_name
|
||||
},
|
||||
inventory: {
|
||||
id: formValues.inventory,
|
||||
name: formValues.inventory_name
|
||||
},
|
||||
limit: formValues.limit,
|
||||
job_type: formValues.job_type && formValues.job_type.value ? formValues.job_type.value : null,
|
||||
job_tags: formValues.job_tags,
|
||||
skip_tags: formValues.skip_tags
|
||||
};
|
||||
}
|
||||
|
||||
$scope.nodeBeingEdited.isActiveEdit = false;
|
||||
|
||||
$scope.nodeBeingEdited.edited = true;
|
||||
|
||||
resetNodeForm();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$broadcast("refreshWorkflowChart");
|
||||
};
|
||||
|
||||
$scope.cancelNodeForm = function() {
|
||||
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
|
||||
// Remove the placeholder node from the tree
|
||||
WorkflowHelpService.removeNodeFromTree({
|
||||
tree: $scope.treeData.data,
|
||||
nodeToBeDeleted: $scope.placeholderNode
|
||||
});
|
||||
} else if ($scope.workflowMakerFormConfig.nodeMode === "edit") {
|
||||
$scope.nodeBeingEdited.isActiveEdit = false;
|
||||
}
|
||||
|
||||
// Reset the form
|
||||
resetNodeForm();
|
||||
|
||||
$scope.$broadcast("refreshWorkflowChart");
|
||||
};
|
||||
|
||||
/* EDIT NODE FUNCTIONS */
|
||||
|
||||
$scope.startEditNode = function(nodeToEdit) {
|
||||
|
||||
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
|
||||
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
|
||||
$scope.cancelNodeForm();
|
||||
}
|
||||
|
||||
$scope.workflowMakerFormConfig.nodeMode = "edit";
|
||||
|
||||
let parent = WorkflowHelpService.searchTree({
|
||||
element: $scope.treeData.data,
|
||||
matchingId: nodeToEdit.parent.id
|
||||
});
|
||||
|
||||
$scope.nodeBeingEdited = WorkflowHelpService.searchTree({
|
||||
element: parent,
|
||||
matchingId: nodeToEdit.id
|
||||
});
|
||||
|
||||
$scope.nodeBeingEdited.isActiveEdit = true;
|
||||
|
||||
let finishConfiguringEdit = function() {
|
||||
|
||||
let formValues = {};
|
||||
|
||||
// build any prompt values
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_credential_on_launch) {
|
||||
if ($scope.nodeBeingEdited.promptValues && $scope.nodeBeingEdited.promptValues.credential) {
|
||||
formValues.credential_name = $scope.nodeBeingEdited.promptValues.credential.name;
|
||||
formValues.credential = $scope.nodeBeingEdited.promptValues.credential.id;
|
||||
} else if ($scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential) {
|
||||
formValues.credential_name = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.name ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.name : null;
|
||||
formValues.credential = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.id ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.credential.id : null;
|
||||
} else {
|
||||
formValues.credential_name = null;
|
||||
formValues.credential = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_inventory_on_launch) {
|
||||
if ($scope.nodeBeingEdited.promptValues && $scope.nodeBeingEdited.promptValues.inventory) {
|
||||
formValues.inventory_name = $scope.nodeBeingEdited.promptValues.inventory.name;
|
||||
formValues.inventory = $scope.nodeBeingEdited.promptValues.inventory.id;
|
||||
} else if ($scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory) {
|
||||
formValues.inventory_name = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.name ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.name : null;
|
||||
formValues.inventory = $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.id ? $scope.nodeBeingEdited.unifiedJobTemplate.summary_fields.inventory.id : null;
|
||||
} else {
|
||||
formValues.inventory_name = null;
|
||||
formValues.inventory = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_job_type_on_launch) {
|
||||
if ($scope.nodeBeingEdited.promptValues && $scope.nodeBeingEdited.promptValues.job_type) {
|
||||
formValues.job_type = {
|
||||
value: $scope.nodeBeingEdited.promptValues.job_type
|
||||
};
|
||||
} else if ($scope.nodeBeingEdited.originalNodeObj.job_type) {
|
||||
formValues.job_type = {
|
||||
value: $scope.nodeBeingEdited.originalNodeObj.job_type
|
||||
};
|
||||
} else if ($scope.nodeBeingEdited.unifiedJobTemplate.job_type) {
|
||||
formValues.job_type = {
|
||||
value: $scope.nodeBeingEdited.unifiedJobTemplate.job_type
|
||||
};
|
||||
} else {
|
||||
formValues.job_type = {
|
||||
value: null
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_limit_on_launch) {
|
||||
if ($scope.nodeBeingEdited.promptValues && typeof $scope.nodeBeingEdited.promptValues.limit === 'string') {
|
||||
formValues.limit = $scope.nodeBeingEdited.promptValues.limit;
|
||||
} else if (typeof $scope.nodeBeingEdited.originalNodeObj.limit === 'string') {
|
||||
formValues.limit = $scope.nodeBeingEdited.originalNodeObj.limit;
|
||||
} else if (typeof $scope.nodeBeingEdited.unifiedJobTemplate.limit === 'string') {
|
||||
formValues.limit = $scope.nodeBeingEdited.unifiedJobTemplate.limit;
|
||||
} else {
|
||||
formValues.limit = null;
|
||||
}
|
||||
}
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_skip_tags_on_launch) {
|
||||
if ($scope.nodeBeingEdited.promptValues && typeof $scope.nodeBeingEdited.promptValues.skip_tags === 'string') {
|
||||
formValues.skip_tags = $scope.nodeBeingEdited.promptValues.skip_tags;
|
||||
} else if (typeof $scope.nodeBeingEdited.originalNodeObj.skip_tags === 'string') {
|
||||
formValues.skip_tags = $scope.nodeBeingEdited.originalNodeObj.skip_tags;
|
||||
} else if (typeof $scope.nodeBeingEdited.unifiedJobTemplate.skip_tags === 'string') {
|
||||
formValues.skip_tags = $scope.nodeBeingEdited.unifiedJobTemplate.skip_tags;
|
||||
} else {
|
||||
formValues.skip_tags = null;
|
||||
}
|
||||
}
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_tags_on_launch) {
|
||||
if ($scope.nodeBeingEdited.promptValues && typeof $scope.nodeBeingEdited.promptValues.job_tags === 'string') {
|
||||
formValues.job_tags = $scope.nodeBeingEdited.promptValues.job_tags;
|
||||
} else if (typeof $scope.nodeBeingEdited.originalNodeObj.job_tags === 'string') {
|
||||
formValues.job_tags = $scope.nodeBeingEdited.originalNodeObj.job_tags;
|
||||
} else if (typeof $scope.nodeBeingEdited.unifiedJobTemplate.job_tags === 'string') {
|
||||
formValues.job_tags = $scope.nodeBeingEdited.unifiedJobTemplate.job_tags;
|
||||
} else {
|
||||
formValues.job_tags = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.type === "job_template") {
|
||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||
}
|
||||
|
||||
$scope.selectedTemplate = $scope.nodeBeingEdited.unifiedJobTemplate;
|
||||
|
||||
switch ($scope.nodeBeingEdited.unifiedJobTemplate.type) {
|
||||
case "job_template":
|
||||
$scope.workflowMakerFormConfig.activeTab = "jobs";
|
||||
break;
|
||||
case "project":
|
||||
$scope.workflowMakerFormConfig.activeTab = "project_sync";
|
||||
break;
|
||||
case "inventory_source":
|
||||
$scope.workflowMakerFormConfig.activeTab = "inventory_sync";
|
||||
break;
|
||||
}
|
||||
|
||||
//formValues.edgeType = $scope.nodeBeingEdited.edgeType;
|
||||
$scope.showTypeOptions = (parent && parent.isStartNode) ? false : true;
|
||||
|
||||
$scope.$broadcast('setEdgeType', $scope.nodeBeingEdited.edgeType);
|
||||
|
||||
$scope.$broadcast('templateSelected', {
|
||||
presetValues: formValues,
|
||||
activeTab: $scope.workflowMakerFormConfig.activeTab
|
||||
});
|
||||
|
||||
$scope.$broadcast("refreshWorkflowChart");
|
||||
};
|
||||
|
||||
// Determine whether or not we need to go out and GET this nodes unified job template
|
||||
// in order to determine whether or not prompt fields are needed
|
||||
|
||||
if (!$scope.nodeBeingEdited.isNew && !$scope.nodeBeingEdited.edited && $scope.nodeBeingEdited.unifiedJobTemplate.unified_job_type && $scope.nodeBeingEdited.unifiedJobTemplate.unified_job_type === 'job') {
|
||||
// This is a node that we got back from the api with an incomplete
|
||||
// unified job template so we're going to pull down the whole object
|
||||
|
||||
JobTemplateService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id)
|
||||
.then(function(data) {
|
||||
|
||||
$scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]);
|
||||
|
||||
let defers = [];
|
||||
let retrievingCredential = false;
|
||||
let retrievingInventory = false;
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_credential_on_launch && $scope.nodeBeingEdited.originalNodeObj.credential) {
|
||||
defers.push(JobTemplateService.getCredential($scope.nodeBeingEdited.originalNodeObj.credential));
|
||||
retrievingCredential = true;
|
||||
}
|
||||
|
||||
if ($scope.nodeBeingEdited.unifiedJobTemplate.ask_inventory_on_launch && $scope.nodeBeingEdited.originalNodeObj.inventory) {
|
||||
defers.push(JobTemplateService.getInventory($scope.nodeBeingEdited.originalNodeObj.inventory));
|
||||
retrievingInventory = true;
|
||||
}
|
||||
|
||||
$q.all(defers)
|
||||
.then(function(responses) {
|
||||
if (retrievingCredential) {
|
||||
$scope.nodeBeingEdited.promptValues.credential = {
|
||||
name: responses[0].data.name,
|
||||
id: responses[0].data.id
|
||||
};
|
||||
|
||||
if (retrievingInventory) {
|
||||
$scope.nodeBeingEdited.promptValues.inventory = {
|
||||
name: responses[1].data.name,
|
||||
id: responses[1].data.id
|
||||
};
|
||||
}
|
||||
} else if (retrievingInventory) {
|
||||
$scope.nodeBeingEdited.promptValues.inventory = {
|
||||
name: responses[0].data.name,
|
||||
id: responses[0].data.id
|
||||
};
|
||||
}
|
||||
finishConfiguringEdit();
|
||||
});
|
||||
|
||||
|
||||
}, function(error) {
|
||||
ProcessErrors($scope, error.data, error.status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get unified job template. GET returned ' +
|
||||
'status: ' + error.status
|
||||
});
|
||||
});
|
||||
} else {
|
||||
finishConfiguringEdit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/* DELETE NODE FUNCTIONS */
|
||||
|
||||
function resetDeleteNode() {
|
||||
$scope.nodeToBeDeleted = null;
|
||||
$scope.deleteOverlayVisible = false;
|
||||
}
|
||||
|
||||
$scope.startDeleteNode = function(nodeToDelete) {
|
||||
$scope.nodeToBeDeleted = nodeToDelete;
|
||||
$scope.deleteOverlayVisible = true;
|
||||
};
|
||||
|
||||
$scope.cancelDeleteNode = function() {
|
||||
resetDeleteNode();
|
||||
};
|
||||
|
||||
$scope.confirmDeleteNode = function() {
|
||||
if ($scope.nodeToBeDeleted) {
|
||||
|
||||
// TODO: turn this into a promise so that we can handle errors
|
||||
|
||||
WorkflowHelpService.removeNodeFromTree({
|
||||
tree: $scope.treeData.data,
|
||||
nodeToBeDeleted: $scope.nodeToBeDeleted
|
||||
});
|
||||
|
||||
if ($scope.nodeToBeDeleted.isNew !== true) {
|
||||
$scope.treeData.data.deletedNodes.push($scope.nodeToBeDeleted.nodeId);
|
||||
}
|
||||
|
||||
if ($scope.nodeToBeDeleted.isActiveEdit) {
|
||||
resetNodeForm();
|
||||
}
|
||||
|
||||
resetDeleteNode();
|
||||
|
||||
$scope.$broadcast("refreshWorkflowChart");
|
||||
|
||||
$scope.treeData.data.totalNodes--;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
$scope.toggleFormTab = function(tab) {
|
||||
if ($scope.workflowMakerFormConfig.activeTab !== tab) {
|
||||
$scope.workflowMakerFormConfig.activeTab = tab;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.templateSelected = function(selectedTemplate) {
|
||||
|
||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||
|
||||
let formValues = {};
|
||||
|
||||
if ($scope.selectedTemplate.ask_credential_on_launch) {
|
||||
if ($scope.selectedTemplate.summary_fields.credential) {
|
||||
formValues.credential_name = $scope.selectedTemplate.summary_fields.credential.name ? $scope.selectedTemplate.summary_fields.credential.name : null;
|
||||
formValues.credential = $scope.selectedTemplate.summary_fields.credential.id ? $scope.selectedTemplate.summary_fields.credential.id : null;
|
||||
} else {
|
||||
formValues.credential_name = null;
|
||||
formValues.credential = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.selectedTemplate.ask_inventory_on_launch) {
|
||||
if ($scope.selectedTemplate.summary_fields.inventory) {
|
||||
formValues.inventory_name = $scope.selectedTemplate.summary_fields.inventory.name ? $scope.selectedTemplate.summary_fields.inventory.name : null;
|
||||
formValues.inventory = $scope.selectedTemplate.summary_fields.inventory.id ? $scope.selectedTemplate.summary_fields.inventory.id : null;
|
||||
} else {
|
||||
formValues.inventory_name = null;
|
||||
formValues.inventory = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.selectedTemplate.ask_job_type_on_launch) {
|
||||
formValues.job_type = {
|
||||
value: $scope.selectedTemplate.job_type ? $scope.selectedTemplate.job_type : null
|
||||
};
|
||||
|
||||
// The default needs to be in place before we can select2-ify the dropdown
|
||||
CreateSelect2({
|
||||
element: '#workflow_maker_job_type',
|
||||
multiple: false
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.selectedTemplate.ask_limit_on_launch) {
|
||||
formValues.limit = $scope.selectedTemplate.limit ? $scope.selectedTemplate.limit : null;
|
||||
}
|
||||
|
||||
if ($scope.selectedTemplate.ask_skip_tags_on_launch) {
|
||||
formValues.skip_tags = $scope.selectedTemplate.skip_tags ? $scope.selectedTemplate.skip_tags : null;
|
||||
}
|
||||
|
||||
if ($scope.selectedTemplate.ask_tags_on_launch) {
|
||||
formValues.job_tags = $scope.selectedTemplate.job_tags ? $scope.selectedTemplate.job_tags : null;
|
||||
}
|
||||
|
||||
// Communicate down the scope chain to our children that a template has been selected. This
|
||||
// will handle populating the form properly as well as clearing out any previously selected
|
||||
// templates in different lists
|
||||
$scope.$broadcast('templateSelected', {
|
||||
presetValues: formValues,
|
||||
activeTab: $scope.workflowMakerFormConfig.activeTab
|
||||
});
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,55 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import workflowMakerController from './workflow-maker.controller';
|
||||
|
||||
export default ['templateUrl', 'CreateDialog', 'Wait',
|
||||
function(templateUrl, CreateDialog, Wait) {
|
||||
return {
|
||||
scope: {
|
||||
treeData: '=',
|
||||
canAddWorkflowJobTemplate: '='
|
||||
},
|
||||
restrict: 'E',
|
||||
templateUrl: templateUrl('job-templates/workflow-maker/workflow-maker'),
|
||||
controller: workflowMakerController,
|
||||
link: function(scope) {
|
||||
CreateDialog({
|
||||
id: 'workflow-modal-dialog',
|
||||
scope: scope,
|
||||
width: 1400,
|
||||
height: 720,
|
||||
draggable: false,
|
||||
dialogClass: 'SurveyMaker-dialog',
|
||||
position: ['center', 20],
|
||||
onClose: function() {
|
||||
$('#workflow-modal-dialog').empty();
|
||||
},
|
||||
onOpen: function() {
|
||||
Wait('stop');
|
||||
|
||||
// Let the modal height be variable based on the content
|
||||
// and set a uniform padding
|
||||
$('#workflow-modal-dialog').css({ 'padding': '20px' });
|
||||
|
||||
},
|
||||
_allowInteraction: function(e) {
|
||||
return !!$(e.target).is('.select2-input') || this._super(e);
|
||||
},
|
||||
callback: 'WorkflowDialogReady'
|
||||
});
|
||||
if (scope.removeWorkflowDialogReady) {
|
||||
scope.removeWorkflowDialogReady();
|
||||
}
|
||||
scope.removeWorkflowDialogReady = scope.$on('WorkflowDialogReady', function() {
|
||||
$('#workflow-modal-dialog').dialog('open');
|
||||
|
||||
scope.$broadcast("refreshWorkflowChart");
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,87 @@
|
||||
<div id="workflow-modal-dialog" style="display: none;">
|
||||
<div class="WorkflowMaker-deleteOverlay" ng-show="deleteOverlayVisible">
|
||||
<div class="Modal-content modal-content">
|
||||
<div class="Modal-header">
|
||||
<div class="Modal-title">REMOVE</div>
|
||||
<div class="Modal-exitHolder">
|
||||
<button class="close Modal-exit" ng-click="cancelDeleteNode()">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Modal-body ng-binding">
|
||||
<div class="Prompt-bodyQuery">Are you sure you want to remove the template below?</div>
|
||||
<div class="Prompt-bodyTarget">{{nodeToBeDeleted.unifiedJobTemplate.name}}</div>
|
||||
</div>
|
||||
<div class="Modal-footer">
|
||||
<button ng-click="cancelDeleteNode()" class="btn Modal-defaultButton Modal-footerButton">CANCEL</a>
|
||||
<button ng-click="confirmDeleteNode()" class="btn Modal-footerButton ng-binding Modal-errorButton">DELETE</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-header">
|
||||
<div class="WorkflowMaker-title">
|
||||
<div class="WorkflowMaker-titleText">EDIT WORKFLOW</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-exitHolder">
|
||||
<button class="WorkflowMaker-exit" ng-click="closeWorkflowMaker()">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-contentHolder">
|
||||
<div class="WorkflowMaker-contentLeft">
|
||||
<div class="WorkflowMaker-legend">
|
||||
<div class="WorkflowMaker-legendLeft">
|
||||
<div class="WorkflowMaker-legendItem">KEY:</div>
|
||||
<div class="WorkflowMaker-legendItem">
|
||||
<div class="WorkflowMaker-onSuccessLegend"></div>
|
||||
<div>On Success</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-legendItem">
|
||||
<div class="WorkflowMaker-onFailLegend"></div>
|
||||
<div>On Fail</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-legendItem">
|
||||
<div class="WorkflowMaker-alwaysLegend"></div>
|
||||
<div>Always</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-legendItem">
|
||||
<div class="WorkflowMaker-letterCircle">P</div>
|
||||
<div>Project Sync</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-legendItem">
|
||||
<div class="WorkflowMaker-letterCircle">I</div>
|
||||
<div>Inventory Sync</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-legendRight">
|
||||
<span class="WorkflowMaker-totalJobs">TOTAL JOBS</span>
|
||||
<span class="badge List-titleBadge" ng-bind="treeData.data.totalNodes"></span>
|
||||
</div>
|
||||
</div>
|
||||
<workflow-chart tree-data="treeData.data" add-node="startAddNode(parent, betweenTwoNodes)" edit-node="startEditNode(nodeToEdit)" delete-node="startDeleteNode(nodeToDelete)" can-add-workflow-job-template="canAddWorkflowJobTemplate" class="WorkflowMaker-chart"></workflow-chart>
|
||||
</div>
|
||||
<div class="WorkflowMaker-contentRight">
|
||||
<div class="WorkflowMaker-formTitle">{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited && nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "ADD A TEMPLATE"}}</div>
|
||||
<div class="WorkflowMaker-formHelp" ng-show="workflowMakerFormConfig.nodeMode === 'idle'">Please hover over a template and click the Add button.</div>
|
||||
<div class="WorkflowMaker-form" ng-show="workflowMakerFormConfig.nodeMode === 'add' || workflowMakerFormConfig.nodeMode === 'edit'">
|
||||
<div class="Form-tabHolder">
|
||||
<div class="Form-tab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'jobs'}" ng-click="toggleFormTab('jobs')">JOBS</div>
|
||||
<div class="Form-tab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'project_sync'}" ng-click="toggleFormTab('project_sync')">PROJECT SYNC</div>
|
||||
<div class="Form-tab" ng-class="{'is-selected': workflowMakerFormConfig.activeTab === 'inventory_sync'}" ng-click="toggleFormTab('inventory_sync')">INVENTORY SYNC</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-formLists">
|
||||
<div id="workflow-jobs-list" ui-view="jobTemplateList" ng-show="workflowMakerFormConfig.activeTab === 'jobs'"></div>
|
||||
<div id="workflow-project-sync-list" ng-show="workflowMakerFormConfig.activeTab === 'project_sync'" ui-view="projectSyncList"></div>
|
||||
<div id="workflow-inventory-sync-list" ng-show="workflowMakerFormConfig.activeTab === 'inventory_sync'" ui-view="inventorySyncList"></div>
|
||||
</div>
|
||||
<div id="workflow-maker-form" ui-view="workflowForm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="WorkflowMaker-buttonHolder">
|
||||
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> Close</button>
|
||||
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate"> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -13,6 +13,48 @@ export default
|
||||
function( Wait, $state, $scope, $rootScope, $location, GetBasePath, Rest,
|
||||
ProcessErrors, CheckLicense, moment, $window, ConfigService,
|
||||
FeaturesService, pendoService, i18n){
|
||||
|
||||
var calcDaysRemaining = function(seconds){
|
||||
// calculate the number of days remaining on the license
|
||||
var duration = moment.duration(seconds, 'seconds').asDays();
|
||||
duration = Math.floor(duration);
|
||||
if(duration < 0 ){
|
||||
duration = 0;
|
||||
}
|
||||
duration = (duration!==1) ? `${duration} Days` : `${duration} Day`;
|
||||
return duration;
|
||||
};
|
||||
|
||||
|
||||
var calcExpiresOn = function(days){
|
||||
// calculate the expiration date of the license
|
||||
days = parseInt(days);
|
||||
return moment().add(days, 'days').calendar();
|
||||
};
|
||||
|
||||
var reset = function(){
|
||||
document.getElementById('License-form').reset();
|
||||
};
|
||||
|
||||
var init = function(){
|
||||
// license/license.partial.html compares fileName
|
||||
$scope.fileName = N_("No file selected.");
|
||||
$scope.title = $rootScope.licenseMissing ? ("Tower " + i18n._("License")) : i18n._("License Management");
|
||||
Wait('start');
|
||||
ConfigService.getConfig().then(function(config){
|
||||
$scope.license = config;
|
||||
$scope.license.version = config.version.split('-')[0];
|
||||
$scope.time = {};
|
||||
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
|
||||
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);
|
||||
$scope.valid = CheckLicense.valid($scope.license.license_info);
|
||||
$scope.compliant = $scope.license.license_info.compliant;
|
||||
Wait('stop');
|
||||
});
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
$scope.getKey = function(event){
|
||||
// Mimic HTML5 spec, show filename
|
||||
$scope.fileName = event.target.files[0].name;
|
||||
@ -73,43 +115,5 @@ export default
|
||||
});
|
||||
});
|
||||
};
|
||||
var calcDaysRemaining = function(seconds){
|
||||
// calculate the number of days remaining on the license
|
||||
var duration = moment.duration(seconds, 'seconds').asDays();
|
||||
duration = Math.floor(duration);
|
||||
if(duration < 0 ){
|
||||
duration = 0;
|
||||
}
|
||||
duration = (duration!==1) ? `${duration} Days` : `${duration} Day`;
|
||||
return duration;
|
||||
};
|
||||
|
||||
|
||||
var calcExpiresOn = function(days){
|
||||
// calculate the expiration date of the license
|
||||
days = parseInt(days);
|
||||
return moment().add(days, 'days').calendar();
|
||||
};
|
||||
|
||||
var init = function(){
|
||||
// license/license.partial.html compares fileName
|
||||
$scope.fileName = N_("No file selected.");
|
||||
$scope.title = $rootScope.licenseMissing ? ("Tower " + i18n._("License")) : i18n._("License Management");
|
||||
Wait('start');
|
||||
ConfigService.getConfig().then(function(config){
|
||||
$scope.license = config;
|
||||
$scope.license.version = config.version.split('-')[0];
|
||||
$scope.time = {};
|
||||
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
|
||||
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);
|
||||
$scope.valid = CheckLicense.valid($scope.license.license_info);
|
||||
$scope.compliant = $scope.license.license_info.compliant;
|
||||
Wait('stop');
|
||||
});
|
||||
};
|
||||
var reset = function(){
|
||||
document.getElementById('License-form').reset();
|
||||
};
|
||||
init();
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
@ -13,6 +13,7 @@ import Hosts from "./lists/Hosts";
|
||||
import Inventories from "./lists/Inventories";
|
||||
import InventoryGroups from "./lists/InventoryGroups";
|
||||
import InventoryHosts from "./lists/InventoryHosts";
|
||||
import InventorySources from "./lists/InventorySources";
|
||||
import JobEvents from "./lists/JobEvents";
|
||||
import JobHosts from "./lists/JobHosts";
|
||||
import JobTemplates from "./lists/JobTemplates";
|
||||
@ -38,6 +39,7 @@ export
|
||||
Inventories,
|
||||
InventoryGroups,
|
||||
InventoryHosts,
|
||||
InventorySources,
|
||||
JobEvents,
|
||||
JobHosts,
|
||||
JobTemplates,
|
||||
|
||||
30
awx/ui/client/src/lists/InventorySources.js
Normal file
30
awx/ui/client/src/lists/InventorySources.js
Normal file
@ -0,0 +1,30 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('InventorySourcesListDefinition', [])
|
||||
.value('InventorySourcesList', {
|
||||
|
||||
name: 'workflow_inventory_sources',
|
||||
iterator: 'inventory_source',
|
||||
basePath: 'inventory_sources',
|
||||
listTitle: 'Inventory Sources',
|
||||
index: false,
|
||||
hover: true,
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Name',
|
||||
columnClass: 'col-md-11'
|
||||
}
|
||||
},
|
||||
|
||||
actions: {},
|
||||
|
||||
fieldActions: {}
|
||||
});
|
||||
@ -10,11 +10,12 @@ export default
|
||||
.factory('JobTemplateList', ['i18n', function(i18n) {
|
||||
return {
|
||||
|
||||
name: 'job_templates',
|
||||
iterator: 'job_template',
|
||||
selectTitle: i18n._('Add Job Template'),
|
||||
editTitle: i18n._('Job Templates'),
|
||||
listTitle: i18n._('Job Templates'),
|
||||
name: 'templates',
|
||||
iterator: 'template',
|
||||
basePath: 'unified_job_templates',
|
||||
selectTitle: i18n._('Template'),
|
||||
editTitle: i18n._('Templates'),
|
||||
listTitle: i18n._('Templates'),
|
||||
selectInstructions: "Click on a row to select it, and click Finished when done. Use the <i class=\"icon-plus\"></i> " +
|
||||
"button to create a new job template.",
|
||||
index: false,
|
||||
@ -24,7 +25,14 @@ export default
|
||||
name: {
|
||||
key: true,
|
||||
label: i18n._('Name'),
|
||||
columnClass: 'col-lg-2 col-md-2 col-sm-4 col-xs-9'
|
||||
columnClass: 'col-lg-2 col-md-2 col-sm-4 col-xs-9',
|
||||
ngClick: "editJobTemplate(template)"
|
||||
},
|
||||
type: {
|
||||
label: i18n._('Type'),
|
||||
searchType: 'select',
|
||||
searchOptions: [], // will be set by Options call to job templates resource
|
||||
columnClass: 'col-lg-2 col-md-2 col-sm-4 hidden-xs'
|
||||
},
|
||||
description: {
|
||||
label: i18n._('Description'),
|
||||
@ -41,73 +49,85 @@ export default
|
||||
label: i18n._('Labels'),
|
||||
type: 'labels',
|
||||
nosort: true,
|
||||
columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs'
|
||||
columnClass: 'List-tableCell col-lg-2 col-md-4 hidden-sm hidden-xs'
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
add: {
|
||||
mode: 'all', // One of: edit, select, all
|
||||
ngClick: 'addJobTemplate()',
|
||||
basePaths: ['job_templates'],
|
||||
type: 'buttonDropdown',
|
||||
basePaths: ['templates'],
|
||||
awToolTip: i18n._('Create a new template'),
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: i18n._('+ ADD'),
|
||||
ngShow: 'canAdd'
|
||||
actionClass: 'btn List-dropdownSuccess',
|
||||
buttonContent: i18n._('ADD'),
|
||||
options: [
|
||||
{
|
||||
optionContent: 'Job Template',
|
||||
optionSref: 'templates.addJobTemplate',
|
||||
ngShow: 'canAddJobTemplate'
|
||||
},
|
||||
{
|
||||
optionContent: 'Workflow Job Template',
|
||||
optionSref: 'templates.addWorkflowJobTemplate',
|
||||
ngShow: 'canAddWorkflowJobTemplate'
|
||||
}
|
||||
],
|
||||
ngShow: 'canAddJobTemplate || canAddWorkflowJobTemplate'
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
|
||||
columnClass: 'col-lg-2 col-md-3 col-sm-3 col-xs-3',
|
||||
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-3',
|
||||
|
||||
submit: {
|
||||
label: i18n._('Launch'),
|
||||
mode: 'all',
|
||||
ngClick: 'submitJob(job_template.id)',
|
||||
ngClick: 'submitJob(template)',
|
||||
awToolTip: i18n._('Start a job using this template'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'job_template.summary_fields.user_capabilities.start'
|
||||
ngShow: 'template.summary_fields.user_capabilities.start'
|
||||
},
|
||||
schedule: {
|
||||
label: i18n._('Schedule'),
|
||||
mode: 'all',
|
||||
ngClick: 'scheduleJob(job_template.id)',
|
||||
ngClick: 'scheduleJob(template)',
|
||||
awToolTip: i18n._('Schedule future job template runs'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'job_template.summary_fields.user_capabilities.schedule'
|
||||
ngShow: 'template.summary_fields.user_capabilities.schedule'
|
||||
},
|
||||
copy: {
|
||||
label: i18n._('Copy'),
|
||||
'ui-sref': 'jobTemplates.copy({id: job_template.id})',
|
||||
'ui-sref': 'templates.copy({id: template.id})',
|
||||
"class": 'btn-danger btn-xs',
|
||||
awToolTip: i18n._('Copy template'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'job_template.summary_fields.user_capabilities.copy'
|
||||
ngShow: 'template.summary_fields.user_capabilities.copy'
|
||||
},
|
||||
edit: {
|
||||
label: i18n._('Edit'),
|
||||
ngClick: "editJobTemplate(job_template.id)",
|
||||
ngClick: "editJobTemplate(template)",
|
||||
awToolTip: i18n._('Edit template'),
|
||||
"class": 'btn-default btn-xs',
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'job_template.summary_fields.user_capabilities.edit'
|
||||
ngShow: 'template.summary_fields.user_capabilities.edit'
|
||||
},
|
||||
view: {
|
||||
label: i18n._('View'),
|
||||
ngClick: "editJobTemplate(job_template.id)",
|
||||
ngClick: "editJobTemplate(template.id)",
|
||||
awToolTip: i18n._('View template'),
|
||||
"class": 'btn-default btn-xs',
|
||||
dataPlacement: 'top',
|
||||
ngShow: '!job_template.summary_fields.user_capabilities.edit'
|
||||
ngShow: '!template.summary_fields.user_capabilities.edit'
|
||||
},
|
||||
"delete": {
|
||||
label: i18n._('Delete'),
|
||||
ngClick: "deleteJobTemplate(job_template.id, job_template.name)",
|
||||
ngClick: "deleteJobTemplate(template)",
|
||||
"class": 'btn-danger btn-xs',
|
||||
awToolTip: i18n._('Delete template'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'job_template.summary_fields.user_capabilities.delete'
|
||||
ngShow: 'template.summary_fields.user_capabilities.delete'
|
||||
}
|
||||
}
|
||||
};}]);
|
||||
|
||||
@ -23,7 +23,7 @@ export default
|
||||
key: true,
|
||||
label: i18n._('Name'),
|
||||
columnClass: 'col-lg-5 col-md-5 col-sm-9 col-xs-8',
|
||||
linkTo: '/#/job_templates/{{job_template.id}}',
|
||||
linkTo: '/#/templates/{{job_template.id}}'
|
||||
},
|
||||
description: {
|
||||
label: i18n._('Description'),
|
||||
|
||||
@ -11,6 +11,7 @@ export default
|
||||
|
||||
name: 'projects',
|
||||
iterator: 'project',
|
||||
basePath: 'projects',
|
||||
selectTitle: i18n._('Add Project'),
|
||||
editTitle: i18n._('Projects'),
|
||||
listTitle: i18n._('Projects'),
|
||||
|
||||
@ -94,10 +94,10 @@ export default
|
||||
},
|
||||
|
||||
issuePendoIdentity: function () {
|
||||
var config,
|
||||
options,
|
||||
var options,
|
||||
c = ConfigService.get(),
|
||||
config = c.license_info;
|
||||
config = c.license_info;
|
||||
|
||||
config.analytics_status = c.analytics_status;
|
||||
config.version = c.version;
|
||||
config.ansible_version = c.ansible_version;
|
||||
@ -114,7 +114,7 @@ export default
|
||||
});
|
||||
}
|
||||
else {
|
||||
$log.debug('Pendo is turned off.')
|
||||
$log.debug('Pendo is turned off.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
/* jshint ignore:start */
|
||||
|
||||
/*
|
||||
* pendo.io Angular Module
|
||||
*
|
||||
@ -25,7 +27,7 @@
|
||||
setTimeout(waitFn, delay);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
angular.module('pendolytics', [])
|
||||
.provider('$pendolytics', function() {
|
||||
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default
|
||||
[ '$rootScope', '$pendolytics', 'Rest', 'GetBasePath', 'ProcessErrors', '$q',
|
||||
'Store', '$log',
|
||||
function ($rootScope, $pendolytics, Rest, GetBasePath, ProcessErrors, $q,
|
||||
Store, $log) {
|
||||
return {
|
||||
setPendoOptions: function (config) {
|
||||
var tower_version = config.version.split('-')[0],
|
||||
options = {
|
||||
visitor: {
|
||||
id: null,
|
||||
role: null,
|
||||
email: null
|
||||
},
|
||||
account: {
|
||||
id: null,
|
||||
planLevel: config.license_type,
|
||||
planPrice: config.instance_count,
|
||||
creationDate: config.license_date,
|
||||
trial: config.trial,
|
||||
tower_version: tower_version,
|
||||
ansible_version: config.ansible_version
|
||||
}
|
||||
};
|
||||
if(config.analytics_status === 'detailed'){
|
||||
this.setDetailed(options, config);
|
||||
}
|
||||
else if(config.analytics_status === 'anonymous'){
|
||||
this.setAnonymous(options);
|
||||
}
|
||||
return options;
|
||||
|
||||
},
|
||||
|
||||
setDetailed: function(options, config) {
|
||||
// Detailed mode
|
||||
// VisitorId: username+hash of license_key
|
||||
// AccountId: hash of license_key from license
|
||||
// email: contact_email from license OR email from Tower account
|
||||
|
||||
options.visitor.id = $rootScope.current_user.username + '@' + config.deployment_id;
|
||||
options.account.id = config.deployment_id;
|
||||
options.visitor.email = $rootScope.current_user.email;
|
||||
},
|
||||
|
||||
setAnonymous: function (options) {
|
||||
//Anonymous mode
|
||||
// VisitorId: <some hardcoded id that is the same across all anonymous>
|
||||
// AccountId: <some hardcoded id that is the same across all anonymous>
|
||||
// email: <blank>
|
||||
|
||||
options.visitor.id = 0;
|
||||
options.account.id = "tower.ansible.com";
|
||||
options.visitor.email = "";
|
||||
},
|
||||
|
||||
setRole: function(options) {
|
||||
var deferred = $q.defer();
|
||||
if($rootScope.current_user.is_superuser === true){
|
||||
options.visitor.role = 'admin';
|
||||
deferred.resolve(options);
|
||||
}
|
||||
else{
|
||||
var url = GetBasePath('users') + $rootScope.current_user.id + '/admin_of_organizations/';
|
||||
Rest.setUrl(url);
|
||||
var promise = Rest.get();
|
||||
promise.then(function (response) {
|
||||
if(response.data.count > 0 ) {
|
||||
options.visitor.role = "orgadmin";
|
||||
deferred.resolve(options);
|
||||
}
|
||||
else {
|
||||
options.visitor.role = "user";
|
||||
deferred.resolve(options);
|
||||
}
|
||||
});
|
||||
promise.catch(function (response) {
|
||||
ProcessErrors($rootScope, response.data, response.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get inventory name. GET returned status: ' +
|
||||
response.status });
|
||||
deferred.reject('Could not resolve pendo role.');
|
||||
});
|
||||
}
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
getConfig: function () {
|
||||
var config = Store('license'),
|
||||
deferred = $q.defer();
|
||||
if(_.isEmpty(config)){
|
||||
var url = GetBasePath('config');
|
||||
Rest.setUrl(url);
|
||||
var promise = Rest.get();
|
||||
promise.then(function (response) {
|
||||
config = response.data.license_info;
|
||||
config.analytics_status = response.data.analytics_status;
|
||||
config.version = response.data.version;
|
||||
config.ansible_version = response.data.ansible_version;
|
||||
if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){
|
||||
$pendolytics.bootstrap();
|
||||
deferred.resolve(config);
|
||||
}
|
||||
else {
|
||||
deferred.reject('Pendo is turned off.');
|
||||
}
|
||||
});
|
||||
promise.catch(function (response) {
|
||||
ProcessErrors($rootScope, response.data, response.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get inventory name. GET returned status: ' +
|
||||
response.status });
|
||||
deferred.reject('Could not resolve pendo config.');
|
||||
});
|
||||
}
|
||||
else if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){
|
||||
$pendolytics.bootstrap();
|
||||
deferred.resolve(config);
|
||||
}
|
||||
else {
|
||||
deferred.reject('Pendo is turned off.');
|
||||
}
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
issuePendoIdentity: function () {
|
||||
var that = this;
|
||||
this.getConfig().then(function(config){
|
||||
var options = that.setPendoOptions(config);
|
||||
that.setRole(options).then(function(options){
|
||||
$log.debug('Pendo status is '+ config.analytics_status + '. Object below:');
|
||||
$log.debug(options);
|
||||
$pendolytics.identify(options);
|
||||
}, function(reason){
|
||||
// reject function for setRole
|
||||
$log.debug(reason);
|
||||
});
|
||||
}, function(reason){
|
||||
// reject function for getConfig
|
||||
$log.debug(reason);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -29,10 +29,10 @@
|
||||
</a>
|
||||
<a class="MainMenu-item"
|
||||
id="main_menu_job_templates_mobile_link"
|
||||
href="/#/job_templates"
|
||||
ng-class="{'is-currentRoute' : isCurrentState('jobTemplates')}">
|
||||
href="/#/templates"
|
||||
ng-class="{'is-currentRoute' : isCurrentState('templates')}">
|
||||
<span class="MainMenu-itemText">
|
||||
<translate>JOB TEMPLATES</translate>
|
||||
<translate>TEMPLATES</translate>
|
||||
</span>
|
||||
</a>
|
||||
<a class="MainMenu-item"
|
||||
@ -106,11 +106,11 @@
|
||||
</a>
|
||||
<a class="MainMenu-item MainMenu-item--notMobile MainMenu-item--left"
|
||||
id="main_menu_job_templates_link"
|
||||
href="/#/job_templates"
|
||||
href="/#/templates"
|
||||
ng-hide="licenseMissing"
|
||||
ng-class="{'is-currentRoute' : isCurrentState('jobTemplates'), 'is-loggedOut' : !current_user || !current_user.username}">
|
||||
ng-class="{'is-currentRoute' : isCurrentState('templates'), 'is-loggedOut' : !current_user || !current_user.username}">
|
||||
<span class="MainMenu-itemText">
|
||||
<translate>JOB TEMPLATES</translate>
|
||||
<translate>TEMPLATES</translate>
|
||||
</span>
|
||||
</a>
|
||||
<a class="MainMenu-item MainMenu-item--notMobile MainMenu-item--left MainMenu-item--lastLeft"
|
||||
|
||||
@ -11,15 +11,11 @@
|
||||
* Controller for handling permissions adding
|
||||
*/
|
||||
|
||||
export default ['$scope', '$rootScope', 'ProcessErrors', 'generateList', 'GetBasePath',
|
||||
export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath',
|
||||
'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait',
|
||||
function($scope, $rootScope, ProcessErrors, generateList, GetBasePath,
|
||||
function($scope, $rootScope, ProcessErrors, GetBasePath,
|
||||
SelectionInit, templateUrl, $state, Rest, $q, Wait) {
|
||||
$scope.$on("linkLists", function() {
|
||||
var generator = generateList,
|
||||
//list = AddUserList,
|
||||
id = "addUsersList",
|
||||
mode = "add";
|
||||
|
||||
if ($state.current.name.split(".")[1] === "users") {
|
||||
$scope.addType = "Users";
|
||||
@ -36,7 +32,7 @@ function($scope, $rootScope, ProcessErrors, generateList, GetBasePath,
|
||||
$scope.add_users = $scope.$parent.add_user_dataset.results;
|
||||
|
||||
$scope.selectedItems = [];
|
||||
$scope.$on('selectedOrDeselected', (item)=>{
|
||||
$scope.$on('selectedOrDeselected', ()=>{
|
||||
throw {name: 'NotYetImplemented'};
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import routes from './organizations-linkout.route';
|
||||
import AddUsers from './addUsers/main';
|
||||
|
||||
export default angular.module('organizationsLinkout', [AddUsers.name]);
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import { templateUrl } from '../../shared/template-url/template-url.factory';
|
||||
import OrganizationsAdmins from './controllers/organizations-admins.controller';
|
||||
import OrganizationsInventories from './controllers/organizations-inventories.controller';
|
||||
import OrganizationsJobTemplates from './controllers/organizations-job-templates.controller';
|
||||
|
||||
@ -1 +1 @@
|
||||
<aw-smart-status jobs="job_template.summary_fields.recent_jobs"></aw-smart-status>
|
||||
<aw-smart-status jobs="template.summary_fields.recent_jobs"></aw-smart-status>
|
||||
|
||||
@ -23,7 +23,7 @@ export default
|
||||
// job templates
|
||||
$stateExtender.addState({
|
||||
name: 'jobTemplateSchedules',
|
||||
route: '/job_templates/:job_template_id/schedules',
|
||||
route: '/templates/job_template/:id/schedules',
|
||||
templateUrl: templateUrl("scheduler/scheduler"),
|
||||
controller: 'schedulerListController',
|
||||
data: {
|
||||
@ -32,7 +32,7 @@ export default
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'jobTemplates.edit',
|
||||
parent: 'templates.editJobTemplate',
|
||||
label: 'SCHEDULES'
|
||||
}
|
||||
});
|
||||
@ -57,9 +57,45 @@ export default
|
||||
}
|
||||
});
|
||||
|
||||
// workflows
|
||||
$stateExtender.addState({
|
||||
name: 'workflowJobTemplateSchedules',
|
||||
route: '/templates/workflow_job_template/:id/schedules',
|
||||
templateUrl: templateUrl("scheduler/scheduler"),
|
||||
controller: 'schedulerController',
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'job_template',
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'templates.editWorkflowJobTemplate',
|
||||
label: 'SCHEDULES'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'workflowJobTemplateSchedules.add',
|
||||
route: '/add',
|
||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||
controller: 'schedulerAddController',
|
||||
ncyBreadcrumb: {
|
||||
parent: 'workflowJobTemplateSchedules',
|
||||
label: 'CREATE SCHEDULE'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'workflowJobTemplateSchedules.edit',
|
||||
route: '/:schedule_id',
|
||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||
controller: 'schedulerEditController',
|
||||
ncyBreadcrumb: {
|
||||
parent: 'workflowJobTemplateSchedules',
|
||||
label: '{{schedule_obj.name}}'
|
||||
}
|
||||
});
|
||||
// projects
|
||||
$stateExtender.addState({
|
||||
searchPrefix: 'schedule',
|
||||
searchPrefix: 'schedule',
|
||||
name: 'projectSchedules',
|
||||
route: '/projects/:id/schedules',
|
||||
data: {
|
||||
|
||||
@ -90,9 +90,8 @@ export default [
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
base = $location.path().replace(/^\//, '').split('/')[0];
|
||||
console.log(base)
|
||||
|
||||
if (base === 'management_jobs') {
|
||||
$scope.base = base = 'system_job_templates';
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
forms = _.chain([params.form]).flatten().compact().value(),
|
||||
buttons,
|
||||
id = params.id,
|
||||
position = (params.position === undefined) ? { my: "center", at: "center", of: window } : params.position,
|
||||
x, y, wh, ww;
|
||||
|
||||
function updateButtonStatus(isValid) {
|
||||
@ -91,7 +92,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
|
||||
// Set modal dimensions based on viewport width
|
||||
ww = $(document).width();
|
||||
wh = $('body').height();
|
||||
wh = $(document).height();
|
||||
x = (width > ww) ? ww - 10 : width;
|
||||
y = (height > wh) ? wh - 10 : height;
|
||||
|
||||
@ -108,6 +109,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
resizable: resizable,
|
||||
draggable: draggable,
|
||||
dialogClass: dialogClass,
|
||||
position: position,
|
||||
create: function () {
|
||||
// Fix the close button
|
||||
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).html('<i class="fa fa-times-circle"></i>');
|
||||
|
||||
@ -164,12 +164,21 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
// Not a very good way to do this
|
||||
// Form sub-states expect to target ui-views related@stateName & modal@stateName
|
||||
// Also wraps mess of generated HTML in a .Panel
|
||||
wrapPanel(html){
|
||||
return `<div class="Panel">
|
||||
${html}
|
||||
<div ui-view="related"></div>
|
||||
<div ui-view="modal"></div>
|
||||
</div>`;
|
||||
wrapPanel(html, ignorePanel){
|
||||
if(ignorePanel) {
|
||||
return `<div>
|
||||
${html}
|
||||
<div ui-view="related"></div>
|
||||
<div ui-view="modal"></div>
|
||||
</div>`;
|
||||
}
|
||||
else {
|
||||
return `<div class="Panel">
|
||||
${html}
|
||||
<div ui-view="related"></div>
|
||||
<div ui-view="modal"></div>
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
inject: function (form, options) {
|
||||
@ -377,7 +386,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
// html = GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope });
|
||||
|
||||
this.mode = options.mode;
|
||||
this.modal = (options.modal) ? true : false;
|
||||
//this.modal = (options.modal) ? true : false;
|
||||
this.setForm(form);
|
||||
return this.build(options);
|
||||
},
|
||||
@ -728,7 +737,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
|
||||
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
|
||||
|
||||
if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock') {
|
||||
if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock' && field.type !== 'workflow-chart') {
|
||||
|
||||
|
||||
html += "<div class='form-group Form-formGroup ";
|
||||
@ -1270,7 +1279,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += label();
|
||||
|
||||
html += "<div ";
|
||||
html += (horizontal) ? "class=\"" + getFieldWidth() + "\"" : "";
|
||||
html += (field.ngShow) ? "ng-show=\"" + field.ngShow + "\" " : "";
|
||||
html += (horizontal) ? "class=\"radio-group " + getFieldWidth() + "\"" : "class=\"radio-group\"";
|
||||
html += ">\n";
|
||||
|
||||
for (i = 0; i < field.options.length; i++) {
|
||||
@ -1284,11 +1294,17 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += (field.ngChange) ? this.attr(field, 'ngChange') : "";
|
||||
html += (field.readonly) ? "disabled " : "";
|
||||
html += (field.required) ? "required " : "";
|
||||
html += (field.ngshow) ? "ng-show=\"" + field.ngShow + "\" " : "";
|
||||
if(field.awRequiredWhen) {
|
||||
html += field.awRequiredWhen.init ? "data-awrequired-init=\"" + field.awRequiredWhen.init + "\" " : "";
|
||||
html += field.awRequiredWhen.reqExpression ? "aw-required-when=\"" + field.awRequiredWhen.reqExpression + "\" " : "";
|
||||
html += field.awRequiredWhen.alwaysShowAsterisk ? "data-awrequired-always-show-asterisk=true " : "";
|
||||
}
|
||||
html += (field.ngDisabled) ? this.attr(field, 'ngDisabled') : "";
|
||||
html += " > " + field.options[i].label + "\n";
|
||||
html += "</label>\n";
|
||||
}
|
||||
if (field.required) {
|
||||
if (field.required || field.awRequiredWhen) {
|
||||
html += "<div class=\"error\" id=\"" + this.form.name + "-" + fld + "-required-error\" ng-show=\"" +
|
||||
this.form.name + '_form.' + fld + ".$dirty && " +
|
||||
this.form.name + '_form.' + fld + ".$error.required\">Please select a value.</div>\n";
|
||||
@ -1433,8 +1449,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
// Generate HTML. Do NOT call this function directly. Called by inject(). Returns an HTML
|
||||
// string to be injected into the current view.
|
||||
//
|
||||
var btn, button, fld, field, html = '', i, section, group,
|
||||
tab, sectionShow, offset, width,ngDisabled, itm;
|
||||
var btn, button, fld, field, html = '', section, group,
|
||||
sectionShow, offset, width,ngDisabled, itm;
|
||||
|
||||
// title and exit button
|
||||
if(!(this.form.showHeader !== undefined && this.form.showHeader === false)) {
|
||||
@ -1473,14 +1489,14 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += "</div>"; //end of Form-header
|
||||
}
|
||||
|
||||
if (!_.isEmpty(this.form.related)) {
|
||||
if (!_.isEmpty(this.form.related) || !_.isEmpty(this.form.relatedButtons)) {
|
||||
var collection, details = i18n._('Details');
|
||||
html += `<div class="Form-tabHolder">`;
|
||||
html += "<div class=\"Form-tabHolder\">";
|
||||
|
||||
if(this.mode === "edit"){
|
||||
html += `<div id="${this.form.name}_tab" class="Form-tab" ` +
|
||||
`ng-click="$state.go('${this.form.stateTree}.edit')" ` +
|
||||
`ng-class="{'is-selected': $state.is('${this.form.activeEditState}') || $state.is('${this.form.stateTree}.edit') || $state.$curruent.data.lookup }">` +
|
||||
`ng-class="{'is-selected': $state.is('${this.form.activeEditState}') || $state.is('${this.form.stateTree}.edit') || $state.$current.data.formChildState }">` +
|
||||
`${details}</div>`;
|
||||
|
||||
for (itm in this.form.related) {
|
||||
@ -1499,6 +1515,45 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
}
|
||||
html += `}">${(collection.title || collection.editTitle)}</div>`;
|
||||
}
|
||||
|
||||
for (itm in this.form.relatedButtons) {
|
||||
button = this.form.relatedButtons[itm];
|
||||
|
||||
// Build button HTML
|
||||
html += "<button type=\"button\" ";
|
||||
html += "class=\"btn btn-sm";
|
||||
html += (button['class']) ? " " + button['class'] : "";
|
||||
html += "\" ";
|
||||
html += "id=\"" + this.form.name + "_" + btn + "_btn\" ";
|
||||
|
||||
if(button.ngShow){
|
||||
html += this.attr(button, 'ngShow');
|
||||
}
|
||||
if (button.ngClick) {
|
||||
html += this.attr(button, 'ngClick');
|
||||
}
|
||||
if (button.awFeature) {
|
||||
html += this.attr(button, 'awFeature');
|
||||
}
|
||||
if (button.ngDisabled) {
|
||||
ngDisabled = (button.ngDisabled===true) ? this.form.name+"_form.$invalid" : button.ngDisabled;
|
||||
if (btn !== 'reset') {
|
||||
//html += "ng-disabled=\"" + this.form.name + "_form.$pristine || " + this.form.name + "_form.$invalid";
|
||||
html += "ng-disabled=\"" + ngDisabled;
|
||||
//html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : "";
|
||||
html += "\" ";
|
||||
} else {
|
||||
//html += "ng-disabled=\"" + this.form.name + "_form.$pristine";
|
||||
//html += (this.form.allowReadonly) ? " || " + this.form.name + "ReadOnly == true" : "";
|
||||
//html += "\" ";
|
||||
}
|
||||
}
|
||||
if(button.awToolTip) {
|
||||
html += " aw-tool-tip='" + button.awToolTip + "' data-placement='" + button.dataPlacement + "' data-tip-watch='" + button.dataTipWatch + "'";
|
||||
}
|
||||
html += ">";
|
||||
html += " " + button.label + "</button>\n";
|
||||
}
|
||||
}
|
||||
else if(this.mode === "add"){
|
||||
html += "<div id=\"" + this.form.name + "_tab\""+
|
||||
@ -1512,12 +1567,35 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
"class=\"Form-tab Form-tab--disabled\">" + (collection.title || collection.editTitle) +
|
||||
"</div>\n";
|
||||
}
|
||||
|
||||
for (itm in this.form.relatedButtons) {
|
||||
button = this.form.relatedButtons[itm];
|
||||
|
||||
// Build button HTML
|
||||
html += "<button type=\"button\" ";
|
||||
html += "class=\"btn btn-sm Form-tab--disabled";
|
||||
html += (button['class']) ? " " + button['class'] : "";
|
||||
html += "\" ";
|
||||
html += "id=\"" + this.form.name + "_" + btn + "_btn\" ";
|
||||
|
||||
if(button.ngShow){
|
||||
html += this.attr(button, 'ngShow');
|
||||
}
|
||||
if (button.awFeature) {
|
||||
html += this.attr(button, 'awFeature');
|
||||
}
|
||||
if(button.awToolTip) {
|
||||
html += " aw-tool-tip='" + button.awToolTip + "' data-placement='" + button.dataPlacement + "' data-tip-watch='" + button.dataTipWatch + "'";
|
||||
}
|
||||
html += ">";
|
||||
html += " " + button.label + "</button>\n";
|
||||
}
|
||||
}
|
||||
html += "</div>";//tabHolder
|
||||
}
|
||||
|
||||
if(!_.isEmpty(this.form.related) && this.mode === "edit"){
|
||||
html += `<div class="Form-tabSection" ng-class="{'is-selected' : $state.is('${this.form.activeEditState}') || $state.is('${this.form.stateTree}.edit') || $state.$current.data.lookup }">`;
|
||||
if(!_.isEmpty(this.form.related) && this.mode === "edit"){// TODO: either include $state.is('templates.editWorkflowJobTemplate') or figure out something else to do here
|
||||
html += `<div class="Form-tabSection" ng-class="{'is-selected' : $state.is('${this.form.activeEditState}') || $state.is('${this.form.stateTree}.edit') || $state.$current.data.formChildState }">`;
|
||||
}
|
||||
|
||||
html += "<form class=\"Form";
|
||||
@ -1645,6 +1723,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
button.label = i18n._('View Survey');
|
||||
button['class'] = 'Form-surveyButton';
|
||||
}
|
||||
if (btn === 'workflow_editor') {
|
||||
button.label = i18n._('Workflow Editor');
|
||||
button['class'] = 'Form-primaryButton';
|
||||
}
|
||||
|
||||
// Build button HTML
|
||||
html += "<button type=\"button\" ";
|
||||
@ -1697,7 +1779,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
});
|
||||
}
|
||||
// console.log(html)
|
||||
return this.wrapPanel(html);
|
||||
return this.wrapPanel(html, options.noPanel);
|
||||
},
|
||||
|
||||
buildCollection: function (params) {
|
||||
|
||||
@ -2,35 +2,63 @@
|
||||
hiddenOnCurrentPage(options.basePaths) ||
|
||||
hiddenInCurrentMode(options.mode)">
|
||||
<div class="List-action--showTooltipOnDisabled" ng-if="options.showTipWhenDisabled" aw-tool-tip="{{options.awToolTip}}" data-tip-watch="{{options.dataTipWatch}}" data-placement="{{options.dataPlacement}}" data-container="{{options.dataContainer}}" data-title="{{options.dataTitle}}" data-tooltip-inner-class="{{options.tooltipInnerClass}}">
|
||||
<button
|
||||
toolbar-button
|
||||
mode="options.mode"
|
||||
class="{{options.actionClass}}"
|
||||
ng-disabled="{{options.ngDisabled}}"
|
||||
ng-show="{{options.ngShow}}"
|
||||
ng-click="$eval(options.ngClick)"
|
||||
toolbar="true"
|
||||
aw-feature="{{options.awFeature}}">
|
||||
<span ng-bind-html="options.buttonContent"></span>
|
||||
</button>
|
||||
<div class="btn-group" ng-if="options.type === 'buttonDropdown'" ng-disabled="{{options.ngDisabled}}">
|
||||
<button type="button" class="{{options.actionClass}} List-dropdownButton" ng-bind-html="options.buttonContent"></button>
|
||||
<button type="button" class="{{options.actionClass}} List-dropdownButton List-dropdownCaratButton dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="List-dropdownCarat"></span>
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li ng-repeat="option in options.options">
|
||||
<a ui-sref="{{option.optionSref}}" ng-bind-html="option.optionContent" ng-show="{{option.ngShow}}"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span ng-if="!options.type || options.type === 'button'">
|
||||
<button
|
||||
toolbar-button
|
||||
mode="options.mode"
|
||||
class="{{options.actionClass}}"
|
||||
ng-disabled="{{options.ngDisabled}}"
|
||||
ng-show="{{options.ngShow}}"
|
||||
ng-click="$eval(options.ngClick)"
|
||||
toolbar="true"
|
||||
aw-feature="{{options.awFeature}}">
|
||||
<span ng-bind-html="options.buttonContent"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<span ng-if="!options.showTipWhenDisabled">
|
||||
<button
|
||||
toolbar-button
|
||||
mode="options.mode"
|
||||
aw-tool-tip="{{options.awToolTip}}"
|
||||
data-tip-watch="{{options.dataTipWatch}}"
|
||||
data-placement="{{options.dataPlacement}}"
|
||||
data-container="{{options.dataContainer}}"
|
||||
class="{{options.actionClass}}"
|
||||
data-title="{{options.dataTitle}}"
|
||||
ng-disabled="{{options.ngDisabled}}"
|
||||
ng-click="$eval(options.ngClick)"
|
||||
ng-show="{{options.ngShow}}"
|
||||
toolbar="true"
|
||||
aw-feature="{{options.awFeature}}">
|
||||
<span ng-bind-html="options.buttonContent"></span>
|
||||
</button>
|
||||
<div class="btn-group" ng-if="options.type === 'buttonDropdown'" ng-disabled="{{options.ngDisabled}}" aw-tool-tip="{{options.awToolTip}}" data-tip-watch="{{options.dataTipWatch}}" data-placement="{{options.dataPlacement}}" data-container="{{options.dataContainer}}">
|
||||
<button type="button" class="{{options.actionClass}} List-dropdownButton dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span>{{options.buttonContent}} </span>
|
||||
<span class="List-dropdownCarat"></span>
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li ng-repeat="option in options.options">
|
||||
<a ui-sref="{{option.optionSref}}" ng-bind-html="option.optionContent" ng-show="{{option.ngShow}}"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span ng-if="!options.type || options.type === 'button'">
|
||||
<button
|
||||
toolbar-button
|
||||
mode="options.mode"
|
||||
aw-tool-tip="{{options.awToolTip}}"
|
||||
data-tip-watch="{{options.dataTipWatch}}"
|
||||
data-placement="{{options.dataPlacement}}"
|
||||
data-container="{{options.dataContainer}}"
|
||||
class="{{options.actionClass}}"
|
||||
data-title="{{options.dataTitle}}"
|
||||
ng-disabled="{{options.ngDisabled}}"
|
||||
ng-click="$eval(options.ngClick)"
|
||||
ng-show="{{options.ngShow}}"
|
||||
toolbar="true"
|
||||
aw-feature="{{options.awFeature}}">
|
||||
<span ng-bind-html="options.buttonContent"></span>
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export default ['templateUrl', '$compile', function(templateUrl, $compile) {
|
||||
export default ['templateUrl', function(templateUrl) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
@ -29,6 +29,9 @@ export default ['templateUrl', '$compile', function(templateUrl, $compile) {
|
||||
$scope.$parent[list.iterator] = $scope.selection[list.iterator].id;
|
||||
$state.go('^');
|
||||
};
|
||||
$scope.cancelForm = function() {
|
||||
$state.go('^');
|
||||
};
|
||||
}]
|
||||
};
|
||||
}];
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<!-- optional: transclude header fields -->
|
||||
<div class="Form-header--fields"></div>
|
||||
<div class="Form-exitHolder">
|
||||
<button type="button" class="Form-exit" data-dismiss="modal" aria-hidden="true" ng-click="$state.go('^')">
|
||||
<button type="button" class="Form-exit" ng-click="cancelForm()">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -16,7 +16,7 @@
|
||||
<!-- see: lookup-modal.directive.js -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button data-target="#form-modal" ng-click="$state.go('^')" data-dismiss="modal" class="Lookup-cancel btn btn-default">Cancel</button>
|
||||
<button ng-click="cancelForm()" class="Lookup-cancel btn btn-default">Cancel</button>
|
||||
<button ng-click="saveForm()" class="Lookup-save btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,6 @@ import lookupModal from './lookup/main';
|
||||
import smartSearch from './smart-search/main';
|
||||
import paginate from './paginate/main';
|
||||
import columnSort from './column-sort/main';
|
||||
import title from './title.directive';
|
||||
import lodashAsPromised from './lodash-as-promised';
|
||||
import stringFilters from './string-filters/main';
|
||||
import truncatedText from './truncated-text.directive';
|
||||
|
||||
@ -50,9 +50,9 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q
|
||||
}
|
||||
|
||||
function calcDataRange() {
|
||||
if ($scope.current() == 1 && $scope.dataset.count < parseInt(pageSize)) {
|
||||
if ($scope.current() === 1 && $scope.dataset.count < parseInt(pageSize)) {
|
||||
return `1 - ${$scope.dataset.count}`;
|
||||
} else if ($scope.current() == 1) {
|
||||
} else if ($scope.current() === 1) {
|
||||
return `1 - ${pageSize}`;
|
||||
} else {
|
||||
let floor = (($scope.current() - 1) * parseInt(pageSize)) + 1;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'GetBasePath',
|
||||
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, GetBasePath) {
|
||||
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory',
|
||||
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory) {
|
||||
return {
|
||||
// kick off building a model for a specific endpoint
|
||||
// this is usually a list's basePath
|
||||
|
||||
@ -50,13 +50,13 @@ export default
|
||||
$log.debug('Websocket Error Logged: ' + error); //log errors
|
||||
};
|
||||
|
||||
self.socket.onconnecting = function (event) {
|
||||
self.socket.onconnecting = function () {
|
||||
self.checkStatus();
|
||||
$log.debug('Websocket reconnecting');
|
||||
needsResubscribing = true;
|
||||
};
|
||||
|
||||
self.socket.onclose = function (event) {
|
||||
self.socket.onclose = function () {
|
||||
self.checkStatus();
|
||||
$log.debug(`Websocket disconnected`);
|
||||
};
|
||||
|
||||
@ -9,8 +9,6 @@
|
||||
* generateLookupNodes - Attaches to a form node. Builds an abstract '*.lookup' node with field-specific 'lookup.*' children e.g. {name: 'projects.add.lookup.organizations', ...}
|
||||
*/
|
||||
|
||||
import { templateUrl } from './template-url/template-url.factory';
|
||||
|
||||
export default ['$injector', '$stateExtender', '$log', function($injector, $stateExtender, $log) {
|
||||
return {
|
||||
/**
|
||||
@ -140,12 +138,14 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
|
||||
let formNode, states = [];
|
||||
switch (mode) {
|
||||
case 'add':
|
||||
// breadcrumbName necessary for resources that are more than one word like
|
||||
// job templates. form.name can't have spaces in it or it busts form gen
|
||||
formNode = $stateExtender.buildDefinition({
|
||||
name: params.name || `${params.parent}.add`,
|
||||
url: params.url || '/add',
|
||||
ncyBreadcrumb: {
|
||||
[params.parent ? 'parent' : null]: `${params.parent}`,
|
||||
label: `CREATE ${form.name}`
|
||||
label: `CREATE ${form.breadcrumbName || form.name}`
|
||||
},
|
||||
views: {
|
||||
'form': {
|
||||
@ -370,7 +370,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
|
||||
// a lookup field's basePath takes precedence over generic list definition's basePath, if supplied
|
||||
data: {
|
||||
basePath: field.basePath || null,
|
||||
lookup: true
|
||||
formChildState: true
|
||||
},
|
||||
params: {
|
||||
[field.sourceModel + '_search']: {
|
||||
|
||||
@ -52,7 +52,7 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
||||
$scope.created_by = data.summary_fields.created_by;
|
||||
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||
$scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
||||
$scope.job_template_url = '/#/templates/' + data.unified_job_template;
|
||||
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
||||
|
||||
16
awx/ui/client/src/workflow-results/main.js
Normal file
16
awx/ui/client/src/workflow-results/main.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import workflowStatusBar from './workflow-status-bar/main';
|
||||
import route from './workflow-results.route.js';
|
||||
import workflowResultsService from './workflow-results.service';
|
||||
|
||||
export default
|
||||
angular.module('workflowResults', [workflowStatusBar.name])
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
$stateExtender.addState(route);
|
||||
}])
|
||||
.service('workflowResultsService', workflowResultsService);
|
||||
112
awx/ui/client/src/workflow-results/workflow-results.block.less
Normal file
112
awx/ui/client/src/workflow-results/workflow-results.block.less
Normal file
@ -0,0 +1,112 @@
|
||||
@import '../shared/branding/colors.less';
|
||||
@import '../shared/branding/colors.default.less';
|
||||
@import '../shared/layouts/one-plus-two.less';
|
||||
|
||||
@breakpoint-md: 1200px;
|
||||
@breakpoint-sm: 623px;
|
||||
|
||||
.WorkflowResults {
|
||||
.OnePlusTwo-container(100%, @breakpoint-md);
|
||||
|
||||
&.fullscreen {
|
||||
.WorkflowResults-rightSide {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.WorkflowResults-leftSide {
|
||||
.OnePlusTwo-left--panel(100%, @breakpoint-md);
|
||||
height: ~"calc(100vh - 177px)";
|
||||
}
|
||||
|
||||
.WorkflowResults-rightSide {
|
||||
.OnePlusTwo-right--panel(100%, @breakpoint-md);
|
||||
height: ~"calc(100vh - 177px)";
|
||||
|
||||
@media (max-width: @breakpoint-md - 1px) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.WorkflowResults-stdoutActionButton--active {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
flex:none;
|
||||
width:0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.WorkflowResults-panelHeader {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.WorkflowResults-panelHeaderText {
|
||||
color: @default-interface-txt;
|
||||
flex: 1 0 auto;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.WorkflowResults-resultRow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.WorkflowResults-resultRow--variables {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.WorkflowResults-resultRowLabel {
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
font-weight: normal!important;
|
||||
width: 30%;
|
||||
margin-right: 20px;
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
flex: 2.5 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.WorkflowResults-resultRowLabel--fullWidth {
|
||||
width: 100%;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.WorkflowResults-resultRowText {
|
||||
width: ~"calc(70% - 20px)";
|
||||
flex: 1 0 auto;
|
||||
text-transform: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.WorkflowResults-resultRowText--fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.WorkflowResults-statusResultIcon {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.WorkflowResults-badgeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.WorkflowResults-badgeTitle{
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
margin-left: 20px;
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
export default ['workflowData',
|
||||
'workflowResultsService',
|
||||
'workflowDataOptions',
|
||||
'jobLabels',
|
||||
'workflowNodes',
|
||||
'$scope',
|
||||
'ParseTypeChange',
|
||||
'ParseVariableString',
|
||||
function(workflowData,
|
||||
workflowResultsService,
|
||||
workflowDataOptions,
|
||||
jobLabels,
|
||||
workflowNodes,
|
||||
$scope,
|
||||
ParseTypeChange,
|
||||
ParseVariableString
|
||||
) {
|
||||
var getTowerLinks = function() {
|
||||
var getTowerLink = function(key) {
|
||||
if ($scope.workflow.related[key]) {
|
||||
return '/#/' + $scope.workflow.related[key]
|
||||
.split('api/v1/')[1];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.workflow_template_link = '/#/templates/workflow_job_template/'+$scope.workflow.workflow_job_template;
|
||||
$scope.created_by_link = getTowerLink('created_by');
|
||||
$scope.cloud_credential_link = getTowerLink('cloud_credential');
|
||||
$scope.network_credential_link = getTowerLink('network_credential');
|
||||
};
|
||||
|
||||
var getTowerLabels = function() {
|
||||
var getTowerLabel = function(key) {
|
||||
if ($scope.workflowOptions && $scope.workflowOptions[key]) {
|
||||
return $scope.workflowOptions[key].choices
|
||||
.filter(val => val[0] === $scope.workflow[key])
|
||||
.map(val => val[1])[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.status_label = getTowerLabel('status');
|
||||
$scope.type_label = getTowerLabel('job_type');
|
||||
$scope.verbosity_label = getTowerLabel('verbosity');
|
||||
};
|
||||
|
||||
// var getTotalHostCount = function(count) {
|
||||
// return Object
|
||||
// .keys(count).reduce((acc, i) => acc += count[i], 0);
|
||||
// };
|
||||
|
||||
// put initially resolved request data on scope
|
||||
$scope.workflow = workflowData;
|
||||
$scope.workflow_nodes = workflowNodes;
|
||||
$scope.workflowOptions = workflowDataOptions.actions.GET;
|
||||
$scope.labels = jobLabels;
|
||||
|
||||
// turn related api browser routes into tower routes
|
||||
getTowerLinks();
|
||||
|
||||
// use options labels to manipulate display of details
|
||||
getTowerLabels();
|
||||
|
||||
// set up a read only code mirror for extra vars
|
||||
$scope.variables = ParseVariableString($scope.workflow.extra_vars);
|
||||
$scope.parseType = 'yaml';
|
||||
ParseTypeChange({ scope: $scope,
|
||||
field_id: 'pre-formatted-variables',
|
||||
readOnly: true });
|
||||
|
||||
// Click binding for the expand/collapse button on the standard out log
|
||||
$scope.stdoutFullScreen = false;
|
||||
$scope.toggleStdoutFullscreen = function() {
|
||||
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
|
||||
};
|
||||
|
||||
$scope.deleteJob = function() {
|
||||
workflowResultsService.deleteJob($scope.workflow);
|
||||
};
|
||||
|
||||
$scope.cancelJob = function() {
|
||||
workflowResultsService.cancelJob($scope.workflow);
|
||||
};
|
||||
|
||||
$scope.relaunchJob = function() {
|
||||
workflowResultsService.relaunchJob($scope);
|
||||
};
|
||||
|
||||
$scope.stdoutArr = [];
|
||||
|
||||
// EVENT STUFF BELOW
|
||||
|
||||
// just putting the event queue on scope so it can be inspected in the
|
||||
// console
|
||||
// $scope.event_queue = eventQueue.queue;
|
||||
// $scope.defersArr = eventQueue.populateDefers;
|
||||
|
||||
// This is where the async updates to the UI actually happen.
|
||||
// Flow is event queue munging in the service -> $scope setting in here
|
||||
// var processEvent = function(event) {
|
||||
// // put the event in the queue
|
||||
// eventQueue.populate(event).then(mungedEvent => {
|
||||
// // make changes to ui based on the event returned from the queue
|
||||
// if (mungedEvent.changes) {
|
||||
// mungedEvent.changes.forEach(change => {
|
||||
// // we've got a change we need to make to the UI!
|
||||
// // update the necessary scope and make the change
|
||||
// if (change === 'startTime' && !$scope.workflow.start) {
|
||||
// $scope.workflow.start = mungedEvent.startTime;
|
||||
// }
|
||||
//
|
||||
// if (change === 'count' && !$scope.countFinished) {
|
||||
// // for all events that affect the host count,
|
||||
// // update the status bar as well as the host
|
||||
// // count badge
|
||||
// $scope.count = mungedEvent.count;
|
||||
// $scope.hostCount = getTotalHostCount(mungedEvent
|
||||
// .count);
|
||||
// }
|
||||
//
|
||||
// if (change === 'playCount') {
|
||||
// $scope.playCount = mungedEvent.playCount;
|
||||
// }
|
||||
//
|
||||
// if (change === 'taskCount') {
|
||||
// $scope.taskCount = mungedEvent.taskCount;
|
||||
// }
|
||||
//
|
||||
// if (change === 'finishedTime' && !$scope.workflow.finished) {
|
||||
// $scope.workflow.finished = mungedEvent.finishedTime;
|
||||
// }
|
||||
//
|
||||
// if (change === 'countFinished') {
|
||||
// // the playbook_on_stats event actually lets
|
||||
// // us know that we don't need to iteratively
|
||||
// // look at event to update the host counts
|
||||
// // any more.
|
||||
// $scope.countFinished = true;
|
||||
// }
|
||||
//
|
||||
// if(change === 'stdout'){
|
||||
// angular
|
||||
// .element(".JobResultsStdOut-stdoutContainer")
|
||||
// .append($compile(mungedEvent
|
||||
// .stdout)($scope));
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // the changes have been processed in the ui, mark it in the queue
|
||||
// eventQueue.markProcessed(event);
|
||||
// });
|
||||
// };
|
||||
|
||||
// PULL! grab completed event data and process each event
|
||||
// TODO: implement retry logic in case one of these requests fails
|
||||
// var getEvents = function(url) {
|
||||
// workflowResultsService.getEvents(url)
|
||||
// .then(events => {
|
||||
// events.results.forEach(event => {
|
||||
// // get the name in the same format as the data
|
||||
// // coming over the websocket
|
||||
// event.event_name = event.event;
|
||||
// processEvent(event);
|
||||
// });
|
||||
// if (events.next) {
|
||||
// getEvents(events.next);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
// getEvents($scope.job.related.job_events);
|
||||
|
||||
// // Processing of job_events messages from the websocket
|
||||
// $scope.$on(`ws-job_events-${$scope.workflow.id}`, function(e, data) {
|
||||
// processEvent(data);
|
||||
// });
|
||||
|
||||
// Processing of job-status messages from the websocket
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) === parseInt($scope.workflow.id,10)) {
|
||||
$scope.workflow.status = data.status;
|
||||
}
|
||||
});
|
||||
}];
|
||||
236
awx/ui/client/src/workflow-results/workflow-results.partial.html
Normal file
236
awx/ui/client/src/workflow-results/workflow-results.partial.html
Normal file
@ -0,0 +1,236 @@
|
||||
<div class="tab-pane" id="workflow-results">
|
||||
<div ng-cloak
|
||||
id="htmlTemplate"
|
||||
class="WorkflowResults"
|
||||
ng-class="{'fullscreen': stdoutFullScreen}">
|
||||
<div ui-view></div>
|
||||
|
||||
<!-- LEFT PANE -->
|
||||
<div class="WorkflowResults-leftSide"
|
||||
ng-class="{'WorkflowResults-stdoutActionButton--active': stdoutFullScreen}">
|
||||
<div class="Panel"
|
||||
ng-show="!stdoutFullScreen">
|
||||
|
||||
<!-- LEFT PANE HEADER -->
|
||||
<div class="WorkflowResults-panelHeader">
|
||||
<div
|
||||
class="WorkflowResults-panelHeaderText">
|
||||
RESULTS
|
||||
</div>
|
||||
|
||||
<!-- LEFT PANE HEADER ACTIONS -->
|
||||
<div>
|
||||
|
||||
<!-- RELAUNCH ACTION -->
|
||||
<button class="List-actionButton"
|
||||
data-placement="top"
|
||||
mode="all"
|
||||
ng-click="relaunchJob()"
|
||||
aw-tool-tip="Relaunch using the same parameters"
|
||||
data-original-title=""
|
||||
title="">
|
||||
<i class="icon-launch"></i>
|
||||
</button>
|
||||
|
||||
<!-- CANCEL ACTION -->
|
||||
<button class="List-actionButton
|
||||
List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="deleteJob()"
|
||||
ng-show="workflow_status.status == 'running' ||
|
||||
job_status.status=='pending' "
|
||||
aw-tool-tip="Cancel"
|
||||
data-original-title="" title="">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
|
||||
<!-- DELETE ACTION -->
|
||||
<button class="List-actionButton
|
||||
List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="deleteJob()"
|
||||
ng-hide="job_status.status == 'running' ||
|
||||
job_status.status == 'pending' "
|
||||
aw-tool-tip="Delete"
|
||||
data-original-title=""
|
||||
title="">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEFT PANE DETAILS GROUP -->
|
||||
<div>
|
||||
|
||||
<!-- START TIME DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.started">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
Started
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
{{ workflow.started | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FINISHED TIME DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.started">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
Finished
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
{{ (workflow.finished |
|
||||
longDate) || "Not Finished" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TEMPLATE DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.name">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
Template
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
<a href="{{ workflow_template_link }}"
|
||||
aw-tool-tip="Edit the job template"
|
||||
data-placement="top">
|
||||
{{ workflow.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JOB TYPE DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.type">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
Job Type
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
Workflow Job
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CREATED BY DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="workflow.summary_fields.created_by.username">
|
||||
<label class="WorkflowResults-resultRowLabel">
|
||||
Launched By
|
||||
</label>
|
||||
<div class="WorkflowResults-resultRowText">
|
||||
<a href="{{ created_by_link }}"
|
||||
aw-tool-tip="Edit the User"
|
||||
data-placement="top">
|
||||
{{ workflow.summary_fields.created_by.username }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- EXTRA VARIABLES DETAIL -->
|
||||
<div class="WorkflowResults-resultRow
|
||||
WorkflowResults-resultRow--variables"
|
||||
ng-show="variables">
|
||||
<label class="WorkflowResults-resultRowLabel
|
||||
WorkflowResults-resultRowLabel--fullWidth">
|
||||
Extra Variables
|
||||
</label>
|
||||
<textarea
|
||||
rows="6"
|
||||
ng-model="variables"
|
||||
name="variables"
|
||||
class="WorkflowResults-extraVars"
|
||||
id="pre-formatted-variables">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<!-- LABELS DETAIL -->
|
||||
<div class="WorkflowResults-resultRow"
|
||||
ng-show="labels && labels.length > 0">
|
||||
<label class="WorkflowResults-resultRowLabel
|
||||
WorkflowResults-resultRowLabel--fullWidth">
|
||||
Labels
|
||||
</label>
|
||||
<div class="LabelList
|
||||
WorkflowResults-resultRowText
|
||||
WorkflowResults-resultRowText--fullWidth">
|
||||
<div ng-repeat="label in labels"
|
||||
class="LabelList-tagContainer">
|
||||
<div class="LabelList-tag">
|
||||
<div class="LabelList-name">
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT PANE -->
|
||||
<div class="WorkflowResults-rightSide">
|
||||
<div class="Panel">
|
||||
|
||||
<!-- RIGHT PANE HEADER -->
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText">
|
||||
<i class="WorkflowResults-statusResultIcon
|
||||
fa icon-job-{{ job.status }}">
|
||||
</i>
|
||||
{{ workflow.name }}
|
||||
</div>
|
||||
|
||||
<!-- HEADER COUNTS -->
|
||||
<div class="WorkflowResults-badgeRow">
|
||||
<!-- PLAYS COUNT -->
|
||||
<div class="WorkflowResults-badgeTitle">
|
||||
Total Jobs
|
||||
</div>
|
||||
<span class="badge List-titleBadge">
|
||||
{{ workflow_nodes.length || 0}}
|
||||
</span>
|
||||
|
||||
<!-- ELAPSED TIME -->
|
||||
<div class="WorkflowResults-badgeTitle">
|
||||
Elapsed
|
||||
</div>
|
||||
<span class="badge List-titleBadge">
|
||||
{{ job.elapsed * 1000 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- HEADER ACTIONS -->
|
||||
<div class="StandardOut-panelHeaderActions">
|
||||
|
||||
<!-- FULL-SCREEN TOGGLE ACTION -->
|
||||
<button class="StandardOut-actionButton"
|
||||
aw-tool-tip="Toggle Output"
|
||||
data-placement="top"
|
||||
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
|
||||
ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
|
||||
<!-- DOWNLOAD ACTION -->
|
||||
<a ng-show="workflow_status.status === 'failed' ||
|
||||
job_status.status === 'successful' ||
|
||||
job_status.status === 'canceled'"
|
||||
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
|
||||
<button class="StandardOut-actionButton"
|
||||
aw-tool-tip="Download Output"
|
||||
data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<workflow-status-bar></workflow-status-bar>
|
||||
<!-- <job-results-standard-out></job-results-standard-out> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user