diff --git a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js index 8f3d924ec2..5b6825676f 100644 --- a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js +++ b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js @@ -6,9 +6,9 @@ /* jshint unused: vars */ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateList', 'ProjectList', - 'InventoryList', 'CredentialList', '$compile', 'generateList', 'GetBasePath', 'SelectionInit', + 'InventoryList', 'CredentialList', '$compile', 'generateList', 'GetBasePath', function(addPermissionsTeamsList, addPermissionsUsersList, TemplateList, ProjectList, - InventoryList, CredentialList, $compile, generateList, GetBasePath, SelectionInit) { + InventoryList, CredentialList, $compile, generateList, GetBasePath) { return { restrict: 'E', scope: { diff --git a/awx/ui/client/src/activity-stream/get-target-title.factory.js b/awx/ui/client/src/activity-stream/get-target-title.factory.js new file mode 100644 index 0000000000..85c6c7a80b --- /dev/null +++ b/awx/ui/client/src/activity-stream/get-target-title.factory.js @@ -0,0 +1,51 @@ +export default + function GetTargetTitle(i18n) { + return function (target) { + + var rtnTitle = i18n._('ALL ACTIVITY'); + + switch(target) { + case 'project': + rtnTitle = i18n._('PROJECTS'); + break; + case 'inventory': + rtnTitle = i18n._('INVENTORIES'); + break; + case 'credential': + rtnTitle = i18n._('CREDENTIALS'); + break; + case 'user': + rtnTitle = i18n._('USERS'); + break; + case 'team': + rtnTitle = i18n._('TEAMS'); + break; + case 'notification_template': + rtnTitle = i18n._('NOTIFICATION TEMPLATES'); + break; + case 'organization': + rtnTitle = i18n._('ORGANIZATIONS'); + break; + case 'job': + rtnTitle = i18n._('JOBS'); + break; + case 'custom_inventory_script': + rtnTitle = i18n._('INVENTORY SCRIPTS'); + break; + case 'schedule': + rtnTitle = i18n._('SCHEDULES'); + break; + case 'host': + rtnTitle = i18n._('HOSTS'); + break; + case 'template': + rtnTitle = i18n._('TEMPLATES'); + break; + } + + return rtnTitle; + + }; + } + +GetTargetTitle.$inject = ['i18n']; diff --git a/awx/ui/client/src/activity-stream/main.js b/awx/ui/client/src/activity-stream/main.js index 44c82217ab..3cb93e3003 100644 --- a/awx/ui/client/src/activity-stream/main.js +++ b/awx/ui/client/src/activity-stream/main.js @@ -12,6 +12,8 @@ import BuildAnchor from './factories/build-anchor.factory'; import BuildDescription from './factories/build-description.factory'; import ShowDetail from './factories/show-detail.factory'; import Stream from './factories/stream.factory'; +import GetTargetTitle from './get-target-title.factory'; +import ModelToBasePathKey from './model-to-base-path-key.factory'; export default angular.module('activityStream', [streamDetailModal.name]) .controller('activityStreamController', activityStreamController) @@ -20,6 +22,8 @@ export default angular.module('activityStream', [streamDetailModal.name]) .factory('BuildDescription', BuildDescription) .factory('ShowDetail', ShowDetail) .factory('Stream', Stream) + .factory('GetTargetTitle', GetTargetTitle) + .factory('ModelToBasePathKey', ModelToBasePathKey) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(activityStreamRoute); }]); diff --git a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js new file mode 100644 index 0000000000..bc6cfe74b8 --- /dev/null +++ b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js @@ -0,0 +1,59 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name helpers.function:ApiModel + * @description Helper functions to convert singular/plural versions of our models to the opposite +*/ + +export default + function ModelToBasePathKey() { + return function(model) { + // This function takes in the singular model string and returns the key needed + // to get the base path from $rootScope/local storage. + + var basePathKey; + + switch(model) { + case 'project': + basePathKey = 'projects'; + break; + case 'inventory': + basePathKey = 'inventory'; + break; + case 'job_template': + basePathKey = 'job_templates'; + break; + case 'credential': + basePathKey = 'credentials'; + break; + case 'user': + basePathKey = 'users'; + break; + case 'team': + basePathKey = 'teams'; + break; + case 'notification_template': + basePathKey = 'notification_templates'; + break; + case 'organization': + basePathKey = 'organizations'; + break; + case 'management_job': + basePathKey = 'management_jobs'; + break; + case 'custom_inventory_script': + basePathKey = 'inventory_scripts'; + break; + case 'workflow_job_template': + basePathKey = 'workflow_job_templates'; + break; + } + + return basePathKey; + }; + } diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index f17df84b89..dd0ce79cbe 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -37,7 +37,7 @@ if ($basePath) { } // Modules -import './helpers'; +import './forms'; import './lists'; import './filters'; import portalMode from './portal-mode/main'; @@ -71,7 +71,6 @@ import projects from './projects/main'; import RestServices from './rest/main'; import access from './access/main'; -import './shared/Modal'; import './shared/prompt-dialog'; import './shared/directives'; import './shared/filters'; @@ -91,6 +90,7 @@ var tower = angular.module('Tower', [ require('angular-sanitize'), require('angular-scheduler').name, require('angular-tz-extensions'), + require('angular-md5'), require('lr-infinite-scroll'), require('ng-toast'), 'gettext', @@ -138,66 +138,46 @@ var tower = angular.module('Tower', [ 'OrganizationListDefinition', 'templates', 'UserListDefinition', - 'UserHelper', 'PromptDialog', 'AWDirectives', 'InventoriesListDefinition', 'InventoryFormDefinition', - 'InventoryHelper', 'InventoryGroupsDefinition', 'InventoryHostsDefinition', - 'HostsHelper', 'AWFilters', 'HostFormDefinition', 'HostListDefinition', 'GroupFormDefinition', 'GroupListDefinition', - 'GroupsHelper', 'TeamsListDefinition', 'TeamFormDefinition', - 'TeamHelper', 'CredentialsListDefinition', 'CredentialFormDefinition', 'TemplatesListDefinition', 'PortalJobTemplatesListDefinition', 'JobTemplateFormDefinition', - 'JobTemplatesHelper', - 'JobSubmissionHelper', 'ProjectsListDefinition', 'ProjectFormDefinition', 'ProjectStatusDefinition', - 'ProjectsHelper', 'CompletedJobsDefinition', 'AllJobsDefinition', 'JobSummaryDefinition', - 'ParseHelper', - 'ChildrenHelper', - 'ProjectPathHelper', - 'md5Helper', - 'SelectionHelper', 'HostGroupsFormDefinition', - 'JobsHelper', - 'CredentialsHelper', 'StreamListDefinition', 'ActivityDetailDefinition', - 'VariablesHelper', 'SchedulesListDefinition', 'ScheduledJobsDefinition', //'Timezones', - 'SchedulesHelper', 'JobsListDefinition', 'LogViewerStatusDefinition', 'StandardOutHelper', 'LogViewerOptionsDefinition', 'lrInfiniteScroll', - 'LoadConfigHelper', 'PortalJobsListDefinition', 'features', 'longDateFilter', 'pendolytics', scheduler.name, - 'ApiModelHelper', - 'ActivityStreamHelper', 'WorkflowFormDefinition', 'InventorySourcesListDefinition', 'WorkflowMakerFormDefinition' diff --git a/awx/ui/client/src/credentials/add/credentials-add.controller.js b/awx/ui/client/src/credentials/add/credentials-add.controller.js index fa6c436e66..88f2a41e57 100644 --- a/awx/ui/client/src/credentials/add/credentials-add.controller.js +++ b/awx/ui/client/src/credentials/add/credentials-add.controller.js @@ -7,11 +7,11 @@ export default ['$scope', '$rootScope', '$compile', '$location', '$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'BecomeMethodChange', - 'OwnerChange', 'FormSave', '$state', 'CreateSelect2', 'i18n', + 'OwnerChange', 'CredentialFormSave', '$state', 'CreateSelect2', 'i18n', function($scope, $rootScope, $compile, $location, $log, $stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, ClearScope, GetBasePath, GetChoices, Empty, KindChange, BecomeMethodChange, - OwnerChange, FormSave, $state, CreateSelect2, i18n) { + OwnerChange, CredentialFormSave, $state, CreateSelect2, i18n) { ClearScope(); @@ -126,7 +126,7 @@ export default ['$scope', '$rootScope', '$compile', '$location', // Save $scope.formSave = function() { if ($scope[form.name + '_form'].$valid) { - FormSave({ scope: $scope, mode: 'add' }); + CredentialFormSave({ scope: $scope, mode: 'add' }); } }; diff --git a/awx/ui/client/src/credentials/edit/credentials-edit.controller.js b/awx/ui/client/src/credentials/edit/credentials-edit.controller.js index 2dd3ae1de2..9073b80bdb 100644 --- a/awx/ui/client/src/credentials/edit/credentials-edit.controller.js +++ b/awx/ui/client/src/credentials/edit/credentials-edit.controller.js @@ -8,10 +8,10 @@ export default ['$scope', '$rootScope', '$compile', '$location', '$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', 'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange', - 'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', + 'CredentialFormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', function($scope, $rootScope, $compile, $location, $log, $stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt, - GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, FormSave, Wait, + GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, CredentialFormSave, Wait, $state, CreateSelect2, Authorization, i18n) { ClearScope(); @@ -236,7 +236,7 @@ export default ['$scope', '$rootScope', '$compile', '$location', // Save changes to the parent $scope.formSave = function() { if ($scope[form.name + '_form'].$valid) { - FormSave({ scope: $scope, mode: 'edit' }); + CredentialFormSave({ scope: $scope, mode: 'edit' }); } }; diff --git a/awx/ui/client/src/credentials/factories/become-method-change.factory.js b/awx/ui/client/src/credentials/factories/become-method-change.factory.js new file mode 100644 index 0000000000..0727553528 --- /dev/null +++ b/awx/ui/client/src/credentials/factories/become-method-change.factory.js @@ -0,0 +1,105 @@ +export default + function BecomeMethodChange(Empty, i18n) { + return function(params) { + var scope = params.scope; + + if (!Empty(scope.kind)) { + // Apply kind specific settings + switch (scope.kind.value) { + case 'aws': + scope.aws_required = true; + break; + case 'rax': + scope.rackspace_required = true; + scope.username_required = true; + break; + case 'ssh': + scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' + scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); + scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); + break; + case 'scm': + scope.sshKeyDataLabel = i18n._('SCM Private Key'); + scope.passwordLabel = i18n._('Password'); + break; + case 'gce': + scope.usernameLabel = i18n._('Service Account Email Address'); + scope.sshKeyDataLabel = i18n._('RSA Private Key'); + scope.email_required = true; + scope.key_required = true; + scope.project_required = true; + scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); + scope.projectLabel = i18n._("Project"); + scope.project_required = false; + scope.projectPopOver = "

" + i18n._("The Project ID is the " + + "GCE assigned identification. It is constructed as " + + "two words followed by a three digit number. Such " + + "as: ") + "

adjective-noun-000

"; + break; + case 'azure': + scope.sshKeyDataLabel = i18n._('Management Certificate'); + scope.subscription_required = true; + scope.key_required = true; + scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); + break; + case 'azure_rm': + scope.usernameLabel = i18n._("Username"); + scope.subscription_required = true; + scope.passwordLabel = i18n._('Password'); + scope.azure_rm_required = true; + break; + case 'vmware': + scope.username_required = true; + scope.host_required = true; + scope.password_required = true; + scope.hostLabel = "vCenter Host"; + scope.passwordLabel = i18n._('Password'); + scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); + break; + case 'openstack': + scope.hostLabel = i18n._("Host (Authentication URL)"); + scope.projectLabel = i18n._("Project (Tenant Name)"); + scope.domainLabel = i18n._("Domain Name"); + scope.password_required = true; + scope.project_required = true; + scope.host_required = true; + scope.username_required = true; + scope.projectPopOver = "

" + i18n._("This is the tenant name. " + + " This value is usually the same " + + " as the username.") + "

"; + scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + + "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); + break; + case 'satellite6': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("Satellite 6 URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + + "Red Hat Satellite 6 server. %s" + + "For example, %s"), "
", "
", "https://satellite.example.org"); + break; + case 'cloudforms': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("CloudForms URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + + "corresponds to your CloudForm instance. %s" + + "For example, %s"), "
", "
", "https://cloudforms.example.org"); + break; + case 'net': + scope.username_required = true; + scope.password_required = false; + scope.passwordLabel = i18n._('Password'); + scope.sshKeyDataLabel = i18n._('SSH Key'); + break; + } + } + }; + } + +BecomeMethodChange.$inject = + [ 'Empty', 'i18n' ]; diff --git a/awx/ui/client/src/credentials/factories/credential-form-save.factory.js b/awx/ui/client/src/credentials/factories/credential-form-save.factory.js new file mode 100644 index 0000000000..d8efc83ffd --- /dev/null +++ b/awx/ui/client/src/credentials/factories/credential-form-save.factory.js @@ -0,0 +1,105 @@ +export default + function CredentialFormSave($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) { + return function(params) { + var scope = params.scope, + mode = params.mode, + form = CredentialForm, + data = {}, fld, url; + + for (fld in form.fields) { + if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' && + fld !== 'ssh_password') { + if (fld === "organization" && !scope[fld]) { + data.user = $rootScope.current_user.id; + } else if (scope[fld] === null) { + data[fld] = ""; + } else { + data[fld] = scope[fld]; + } + } + } + + data.kind = scope.kind.value; + if (scope.become_method === null || typeof scope.become_method === 'undefined') { + data.become_method = ""; + data.become_username = ""; + data.become_password = ""; + } else { + data.become_method = (scope.become_method.value) ? scope.become_method.value : ""; + } + switch (data.kind) { + case 'ssh': + data.password = scope.ssh_password; + break; + case 'aws': + data.username = scope.access_key; + data.password = scope.secret_key; + break; + case 'rax': + data.password = scope.api_key; + break; + case 'gce': + data.username = scope.email_address; + data.project = scope.project; + break; + case 'azure': + data.username = scope.subscription; + } + + Wait('start'); + if (mode === 'add') { + url = GetBasePath("credentials"); + Rest.setUrl(url); + Rest.post(data) + .success(function (data) { + scope.addedItem = data.id; + + Wait('stop'); + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'credentials') { + $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); + } + else { + ReturnToCaller(1); + } + }) + .error(function (data, status) { + Wait('stop'); + // TODO: hopefully this conditional error handling will to away in a future version of tower. The reason why we cannot + // simply pass this error to ProcessErrors is because it will actually match the form element 'ssh_key_unlock' and show + // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the + // error is never shown. In the future, the API will hopefully either behave or respond differently. + if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) { + scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported."); + } + else { + ProcessErrors(scope, data, status, form, { + hdr: i18n._('Error!'), + msg: i18n._('Failed to create new Credential. POST status: ') + status + }); + } + }); + } else { + url = GetBasePath('credentials') + scope.id + '/'; + Rest.setUrl(url); + Rest.put(data) + .success(function () { + Wait('stop'); + $state.go($state.current, {}, {reload: true}); + }) + .error(function (data, status) { + Wait('stop'); + ProcessErrors(scope, data, status, form, { + hdr: i18n._('Error!'), + msg: i18n._('Failed to update Credential. PUT status: ') + status + }); + }); + } + }; + } + +CredentialFormSave.$inject = + [ '$rootScope', '$location', 'Alert', 'Rest', + 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', + 'ReturnToCaller', 'Wait', '$state', 'i18n' + ]; diff --git a/awx/ui/client/src/credentials/factories/kind-change.factory.js b/awx/ui/client/src/credentials/factories/kind-change.factory.js new file mode 100644 index 0000000000..e35bedc526 --- /dev/null +++ b/awx/ui/client/src/credentials/factories/kind-change.factory.js @@ -0,0 +1,192 @@ +export default + function KindChange(Empty, i18n) { + return function(params) { + var scope = params.scope, + reset = params.reset, + collapse, id; + + $('.popover').each(function() { + // remove lingering popover

. Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each( function() { + // close any lingering tool tipss + $(this).hide(); + }); + // Put things in a default state + scope.usernameLabel = i18n._('Username'); + scope.aws_required = false; + scope.email_required = false; + scope.rackspace_required = false; + scope.sshKeyDataLabel = i18n._('Private Key'); + scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) + scope.key_required = false; // JT -- doing the same for key and project + scope.project_required = false; + scope.subscription_required = false; + scope.key_description = i18n.sprintf(i18n._("Paste the contents of the SSH private key file.%s or click to close%s"), "
Esc", "
"); + scope.host_required = false; + scope.password_required = false; + scope.hostLabel = ''; + scope.passwordLabel = i18n._('Password'); + + $('.popover').each(function() { + // remove lingering popover
. Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each( function() { + // close any lingering tool tipss + $(this).hide(); + }); + // Put things in a default state + scope.usernameLabel = i18n._('Username'); + scope.aws_required = false; + scope.email_required = false; + scope.rackspace_required = false; + scope.sshKeyDataLabel = i18n._('Private Key'); + scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) + scope.key_required = false; // JT -- doing the same for key and project + scope.project_required = false; + scope.domain_required = false; + scope.subscription_required = false; + scope.key_description = i18n._("Paste the contents of the SSH private key file."); + scope.host_required = false; + scope.password_required = false; + scope.hostLabel = ''; + scope.projectLabel = ''; + scope.domainLabel = ''; + scope.project_required = false; + scope.passwordLabel = i18n._('Password (API Key)'); + scope.projectPopOver = "

" + i18n._("The project value") + "

"; + scope.hostPopOver = "

" + i18n._("The host value") + "

"; + scope.ssh_key_data_api_error = ''; + + if (!Empty(scope.kind)) { + // Apply kind specific settings + switch (scope.kind.value) { + case 'aws': + scope.aws_required = true; + break; + case 'rax': + scope.rackspace_required = true; + scope.username_required = true; + break; + case 'ssh': + scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' + scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); + scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); + break; + case 'scm': + scope.sshKeyDataLabel = i18n._('SCM Private Key'); + scope.passwordLabel = i18n._('Password'); + break; + case 'gce': + scope.usernameLabel = i18n._('Service Account Email Address'); + scope.sshKeyDataLabel = i18n._('RSA Private Key'); + scope.email_required = true; + scope.key_required = true; + scope.project_required = true; + scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); + scope.projectLabel = i18n._("Project"); + scope.project_required = false; + scope.projectPopOver = "

" + i18n._("The Project ID is the " + + "GCE assigned identification. It is constructed as " + + "two words followed by a three digit number. Such " + + "as: ") + "

adjective-noun-000

"; + break; + case 'azure': + scope.sshKeyDataLabel = i18n._('Management Certificate'); + scope.subscription_required = true; + scope.key_required = true; + scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); + break; + case 'azure_rm': + scope.usernameLabel = i18n._("Username"); + scope.subscription_required = true; + scope.passwordLabel = i18n._('Password'); + scope.azure_rm_required = true; + break; + case 'vmware': + scope.username_required = true; + scope.host_required = true; + scope.password_required = true; + scope.hostLabel = "vCenter Host"; + scope.passwordLabel = i18n._('Password'); + scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); + break; + case 'openstack': + scope.hostLabel = i18n._("Host (Authentication URL)"); + scope.projectLabel = i18n._("Project (Tenant Name)"); + scope.domainLabel = i18n._("Domain Name"); + scope.password_required = true; + scope.project_required = true; + scope.host_required = true; + scope.username_required = true; + scope.projectPopOver = "

" + i18n._("This is the tenant name. " + + " This value is usually the same " + + " as the username.") + "

"; + scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + + "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); + break; + case 'satellite6': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("Satellite 6 URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + + "Red Hat Satellite 6 server. %s" + + "For example, %s"), "
", "
", "https://satellite.example.org"); + break; + case 'cloudforms': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("CloudForms URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + + "corresponds to your CloudForm instance. %s" + + "For example, %s"), "
", "
", "https://cloudforms.example.org"); + break; + case 'net': + scope.username_required = true; + scope.password_required = false; + scope.passwordLabel = i18n._('Password'); + scope.sshKeyDataLabel = i18n._('SSH Key'); + break; + } + } + + // Reset all the field values related to Kind. + if (reset) { + scope.access_key = null; + scope.secret_key = null; + scope.api_key = null; + scope.username = null; + scope.password = null; + scope.password_confirm = null; + scope.ssh_key_data = null; + scope.ssh_key_unlock = null; + scope.ssh_key_unlock_confirm = null; + scope.become_username = null; + scope.become_password = null; + scope.authorize = false; + scope.authorize_password = null; + } + + // Collapse or open help widget based on whether scm value is selected + collapse = $('#credential_kind').parent().find('.panel-collapse').first(); + id = collapse.attr('id'); + if (!Empty(scope.kind) && scope.kind.value !== '') { + if ($('#' + id + '-icon').hasClass('icon-minus')) { + scope.accordionToggle('#' + id); + } + } else { + if ($('#' + id + '-icon').hasClass('icon-plus')) { + scope.accordionToggle('#' + id); + } + } + }; + } + +KindChange.$inject = + [ 'Empty', 'i18n' ]; diff --git a/awx/ui/client/src/credentials/factories/owner-change.factory.js b/awx/ui/client/src/credentials/factories/owner-change.factory.js new file mode 100644 index 0000000000..60b77110bb --- /dev/null +++ b/awx/ui/client/src/credentials/factories/owner-change.factory.js @@ -0,0 +1,18 @@ +export default + function OwnerChange() { + return function(params) { + var scope = params.scope, + owner = scope.owner; + if (owner === 'team') { + scope.team_required = true; + scope.user_required = false; + scope.user = null; + scope.user_username = null; + } else { + scope.team_required = false; + scope.user_required = true; + scope.team = null; + scope.team_name = null; + } + }; + } diff --git a/awx/ui/client/src/credentials/main.js b/awx/ui/client/src/credentials/main.js index 16cc13aad0..e4bb2f6f2d 100644 --- a/awx/ui/client/src/credentials/main.js +++ b/awx/ui/client/src/credentials/main.js @@ -8,11 +8,19 @@ import ownerList from './ownerList.directive'; import CredentialsList from './list/credentials-list.controller'; import CredentialsAdd from './add/credentials-add.controller'; import CredentialsEdit from './edit/credentials-edit.controller'; +import BecomeMethodChange from './factories/become-method-change.factory'; +import CredentialFormSave from './factories/credential-form-save.factory'; +import KindChange from './factories/kind-change.factory'; +import OwnerChange from './factories/owner-change.factory'; import { N_ } from '../i18n'; export default angular.module('credentials', []) .directive('ownerList', ownerList) + .factory('BecomeMethodChange', BecomeMethodChange) + .factory('CredentialFormSave', CredentialFormSave) + .factory('KindChange', KindChange) + .factory('OwnerChange', OwnerChange) .controller('CredentialsList', CredentialsList) .controller('CredentialsAdd', CredentialsAdd) .controller('CredentialsEdit', CredentialsEdit) diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js deleted file mode 100644 index 765b81be5f..0000000000 --- a/awx/ui/client/src/helpers.js +++ /dev/null @@ -1,58 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import './forms'; -import './lists'; - -import Children from "./helpers/Children"; -import Credentials from "./helpers/Credentials"; -import Events from "./helpers/Events"; -import Groups from "./helpers/Groups"; -import Hosts from "./helpers/Hosts"; -import JobSubmission from "./helpers/JobSubmission"; -import JobTemplates from "./helpers/JobTemplates"; -import Jobs from "./helpers/Jobs"; -import LoadConfig from "./helpers/LoadConfig"; -import Parse from "./helpers/Parse"; -import ProjectPath from "./helpers/ProjectPath"; -import Projects from "./helpers/Projects"; -import Schedules from "./helpers/Schedules"; -import Selection from "./helpers/Selection"; -import Users from "./helpers/Users"; -import Variables from "./helpers/Variables"; -import ApiDefaults from "./helpers/api-defaults"; -import inventory from "./helpers/inventory"; -import MD5 from "./helpers/md5"; -import Teams from "./helpers/teams"; -import AdhocHelper from "./helpers/Adhoc"; -import ApiModelHelper from "./helpers/ApiModel"; -import ActivityStreamHelper from "./helpers/ActivityStream"; - -export - { Children, - Credentials, - Events, - Groups, - Hosts, - JobSubmission, - JobTemplates, - Jobs, - LoadConfig, - Parse, - ProjectPath, - Projects, - Schedules, - Selection, - Users, - Variables, - ApiDefaults, - inventory, - MD5, - Teams, - AdhocHelper, - ApiModelHelper, - ActivityStreamHelper - }; diff --git a/awx/ui/client/src/helpers/ActivityStream.js b/awx/ui/client/src/helpers/ActivityStream.js deleted file mode 100644 index 02bd972eee..0000000000 --- a/awx/ui/client/src/helpers/ActivityStream.js +++ /dev/null @@ -1,64 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:ActivityStream - * @description Helper functions for the activity stream -*/ - -export default - angular.module('ActivityStreamHelper', ['Utilities']) - .factory('GetTargetTitle', ['i18n', - function (i18n) { - return function (target) { - - var rtnTitle = i18n._('ALL ACTIVITY'); - - switch(target) { - case 'project': - rtnTitle = i18n._('PROJECTS'); - break; - case 'inventory': - rtnTitle = i18n._('INVENTORIES'); - break; - case 'credential': - rtnTitle = i18n._('CREDENTIALS'); - break; - case 'user': - rtnTitle = i18n._('USERS'); - break; - case 'team': - rtnTitle = i18n._('TEAMS'); - break; - case 'notification_template': - rtnTitle = i18n._('NOTIFICATION TEMPLATES'); - break; - case 'organization': - rtnTitle = i18n._('ORGANIZATIONS'); - break; - case 'job': - rtnTitle = i18n._('JOBS'); - break; - case 'custom_inventory_script': - rtnTitle = i18n._('INVENTORY SCRIPTS'); - break; - case 'schedule': - rtnTitle = i18n._('SCHEDULES'); - break; - case 'host': - rtnTitle = i18n._('HOSTS'); - break; - case 'template': - rtnTitle = i18n._('TEMPLATES'); - break; - } - - return rtnTitle; - - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Adhoc.js b/awx/ui/client/src/helpers/Adhoc.js deleted file mode 100644 index d29e591ce5..0000000000 --- a/awx/ui/client/src/helpers/Adhoc.js +++ /dev/null @@ -1,172 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:Adhoc - * @description These routines are shared by adhoc command related controllers. - * The content here is very similar to the JobSubmission helper, and in fact, - * certain services are pulled from that helper. This leads to an important - * point: if you need to create functionality that is shared between the command - * and playbook run process, put that code in the JobSubmission helper and make - * it into a reusable step (by specifying a callback parameter in the factory). - * For a good example of this, please see how the AdhocLaunch factory in this - * file utilizes the CheckPasswords factory from the JobSubmission helper. - * - * #AdhocRelaunch Step 1: preparing the GET to ad_hoc_commands/n/relaunch - * The adhoc relaunch process is called from the JobSubmission helper. It is a - * separate process from the initial adhoc run becuase of the way the API - * endpoints work. For AdhocRelaunch, we have access to the original run and - * we can pull the related relaunch URL by knowing the original Adhoc runs ID. - * - * #AdhocRelaunch Step 2: If we got passwords back, add them - * The relaunch URL gives us back the passwords we need to prompt for (if any). - * We'll go to step 3 if there are passwords, and step 4 if not. - * - * #AdhocRelaunch Step 3: PromptForPasswords and the CreateLaunchDialog - * - * #AdhocRelaunch Step 5: StartAdhocRun - * - * #AdhocRelaunch Step 6: LaunchJob and navigate to the standard out page. - - * **If you are - * TODO: once the API endpoint is figured out for running an adhoc command - * from the form is figured out, the rest work should probably be excised from - * the controller and moved into here. See the todo statements in the - * controller for more information about this. - */ - -export default - angular.module('AdhocHelper', ['RestServices', 'Utilities', - 'CredentialFormDefinition', 'CredentialsListDefinition', - 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', - 'FormGenerator', 'JobVarsPromptFormDefinition']) - - /** - * @ngdoc method - * @name helpers.function:JobSubmission#AdhocRun - * @methodOf helpers.function:JobSubmission - * @description The adhoc Run function is run when the user clicks the relaunch button - * - */ - // Submit request to run an adhoc comamand - .factory('AdhocRun', ['$location','$stateParams', 'LaunchJob', - 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', - 'Wait', 'Empty', 'CreateLaunchDialog', '$state', - function ($location, $stateParams, LaunchJob, PromptForPasswords, - Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) { - return function (params) { - var id = params.project_id, - scope = params.scope.$new(), - new_job_id, - html, - url; - - // this is used to cancel a running adhoc command from - // the jobs page - if (scope.removeCancelJob) { - scope.removeCancelJob(); - } - scope.removeCancelJob = scope.$on('CancelJob', function() { - // Delete the job - Wait('start'); - Rest.setUrl(GetBasePath('ad_hoc_commands') + new_job_id + '/'); - Rest.destroy() - .success(function() { - Wait('stop'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, - null, { hdr: 'Error!', - msg: 'Call to ' + url + - ' failed. DELETE returned status: ' + - status }); - }); - }); - - if (scope.removeStartAdhocRun) { - scope.removeStartAdhocRun(); - } - - scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() { - var password, - postData={}; - for (password in scope.passwords) { - postData[scope.passwords[password]] = scope[ - scope.passwords[password] - ]; - } - // Re-launch the adhoc job - Rest.setUrl(url); - Rest.post(postData) - .success(function (data) { - Wait('stop'); - if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') { - $state.go('adHocJobStdout', {id: data.id}); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, { - hdr: 'Error!', - msg: 'Failed to launch adhoc command. POST ' + - 'returned status: ' + status }); - }); - }); - - // start routine only if passwords need to be prompted - if (scope.removeCreateLaunchDialog) { - scope.removeCreateLaunchDialog(); - } - scope.removeCreateLaunchDialog = scope.$on('CreateLaunchDialog', - function(e, html, url) { - CreateLaunchDialog({ - scope: scope, - html: html, - url: url, - callback: 'StartAdhocRun' - }); - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', - function(e, passwords_needed_to_start,html, url) { - PromptForPasswords({ - scope: scope, - passwords: passwords_needed_to_start, - callback: 'CreateLaunchDialog', - html: html, - url: url - }); - }); // end password prompting routine - - // start the adhoc relaunch routine - Wait('start'); - url = GetBasePath('ad_hoc_commands') + id + '/relaunch/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - new_job_id = data.id; - - scope.passwords_needed_to_start = data.passwords_needed_to_start; - if (!Empty(data.passwords_needed_to_start) && - data.passwords_needed_to_start.length > 0) { - // go through the password prompt routine before - // starting the adhoc run - scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); - } - else { - // no prompting of passwords needed - scope.$emit('StartAdhocRun'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - }; - }]); diff --git a/awx/ui/client/src/helpers/ApiModel.js b/awx/ui/client/src/helpers/ApiModel.js deleted file mode 100644 index 20bed707f2..0000000000 --- a/awx/ui/client/src/helpers/ApiModel.js +++ /dev/null @@ -1,63 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:ApiModel - * @description Helper functions to convert singular/plural versions of our models to the opposite -*/ - -export default - angular.module('ApiModelHelper', ['Utilities']) - .factory('ModelToBasePathKey', [ - function () { - return function (model) { - // This function takes in the singular model string and returns the key needed - // to get the base path from $rootScope/local storage. - - var basePathKey; - - switch(model) { - case 'project': - basePathKey = 'projects'; - break; - case 'inventory': - basePathKey = 'inventory'; - break; - case 'job_template': - basePathKey = 'job_templates'; - break; - case 'credential': - basePathKey = 'credentials'; - break; - case 'user': - basePathKey = 'users'; - break; - case 'team': - basePathKey = 'teams'; - break; - case 'notification_template': - basePathKey = 'notification_templates'; - break; - case 'organization': - basePathKey = 'organizations'; - break; - case 'management_job': - basePathKey = 'management_jobs'; - break; - case 'custom_inventory_script': - basePathKey = 'inventory_scripts'; - break; - case 'workflow_job_template': - basePathKey = 'workflow_job_templates'; - break; - } - - return basePathKey; - - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Children.js b/awx/ui/client/src/helpers/Children.js deleted file mode 100644 index 7c8246dd62..0000000000 --- a/awx/ui/client/src/helpers/Children.js +++ /dev/null @@ -1,111 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Children - * @descriptionUsed in job_events to expand/collapse children by setting the - * 'show' attribute of each job_event in the set of job_events. - * See the filter in job_events.js list. -*/ - -export default - angular.module('ChildrenHelper', ['RestServices', 'Utilities']) - .factory('ToggleChildren', ['$location', 'Store', function ($location, Store) { - return function (params) { - - var scope = params.scope, - list = params.list, - id = params.id, - set = scope[list.name], - clicked, - //base = $location.path().replace(/^\//, '').split('/')[0], - path = $location.path(), - local_child_store; - - function updateExpand(key, expand) { - var found = false; - local_child_store.every(function(child, i) { - if (child.key === key) { - local_child_store[i].expand = expand; - found = true; - return false; - } - return true; - }); - if (!found) { - local_child_store.push({ key: key, expand: expand }); - } - } - - function updateShow(key, show) { - var found = false; - local_child_store.every(function(child, i) { - if (child.key === key) { - local_child_store[i].show = show; - found = true; - return false; - } - return true; - }); - if (!found) { - local_child_store.push({ key: key, show: show }); - } - } - - function expand(node) { - var i, has_children = false; - for (i = node + 1; i < set.length; i++) { - if (set[i].parent === set[node].id) { - updateShow(set[i].key, true); - set[i].show = true; - } - } - set[node].ngicon = (has_children) ? 'fa fa-minus-square-o node-toggle' : 'fa fa-minus-square-o node-toggle'; - } - - function collapse(node) { - var i, has_children = false; - for (i = node + 1; i < set.length; i++) { - if (set[i].parent === set[node].id) { - set[i].show = false; - has_children = true; - updateShow(set[i].key, false); - if (set[i].related.children) { - collapse(i); - } - } - } - set[node].ngicon = (has_children) ? 'fa fa-plus-square-o node-toggle' : 'fa fa-square-o node-toggle'; - } - - local_child_store = Store(path + '_children'); - if (!local_child_store) { - local_child_store = []; - } - - // Scan the array list and find the clicked element - set.every(function(row, i) { - if (row.id === id) { - clicked = i; - return false; - } - return true; - }); - - // Expand or collapse children based on clicked element's icon - if (/plus-square-o/.test(set[clicked].ngicon)) { - // Expand: lookup and display children - expand(clicked); - updateExpand(set[clicked].key, true); - } else if (/minus-square-o/.test(set[clicked].ngicon)) { - collapse(clicked); - updateExpand(set[clicked].key, false); - } - Store(path + '_children', local_child_store); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js deleted file mode 100644 index edb1e700ab..0000000000 --- a/awx/ui/client/src/helpers/Credentials.js +++ /dev/null @@ -1,432 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:Credentials - * @description Functions shared amongst Credential related controllers - */ - -export default -angular.module('CredentialsHelper', ['Utilities']) - -.factory('KindChange', ['Empty', 'i18n', - function (Empty, i18n) { - return function (params) { - var scope = params.scope, - reset = params.reset, - collapse, id; - - $('.popover').each(function() { - // remove lingering popover

. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - $('.tooltip').each( function() { - // close any lingering tool tipss - $(this).hide(); - }); - // Put things in a default state - scope.usernameLabel = i18n._('Username'); - scope.aws_required = false; - scope.email_required = false; - scope.rackspace_required = false; - scope.sshKeyDataLabel = i18n._('Private Key'); - scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) - scope.key_required = false; // JT -- doing the same for key and project - scope.project_required = false; - scope.subscription_required = false; - scope.key_description = i18n.sprintf(i18n._("Paste the contents of the SSH private key file.%s or click to close%s"), "
Esc", "
"); - scope.host_required = false; - scope.password_required = false; - scope.hostLabel = ''; - scope.passwordLabel = i18n._('Password'); - - $('.popover').each(function() { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - $('.tooltip').each( function() { - // close any lingering tool tipss - $(this).hide(); - }); - // Put things in a default state - scope.usernameLabel = i18n._('Username'); - scope.aws_required = false; - scope.email_required = false; - scope.rackspace_required = false; - scope.sshKeyDataLabel = i18n._('Private Key'); - scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) - scope.key_required = false; // JT -- doing the same for key and project - scope.project_required = false; - scope.domain_required = false; - scope.subscription_required = false; - scope.key_description = i18n._("Paste the contents of the SSH private key file."); - scope.host_required = false; - scope.password_required = false; - scope.hostLabel = ''; - scope.projectLabel = ''; - scope.domainLabel = ''; - scope.project_required = false; - scope.passwordLabel = i18n._('Password (API Key)'); - scope.projectPopOver = "

" + i18n._("The project value") + "

"; - scope.hostPopOver = "

" + i18n._("The host value") + "

"; - scope.ssh_key_data_api_error = ''; - - if (!Empty(scope.kind)) { - // Apply kind specific settings - switch (scope.kind.value) { - case 'aws': - scope.aws_required = true; - break; - case 'rax': - scope.rackspace_required = true; - scope.username_required = true; - break; - case 'ssh': - scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' - scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); - scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); - break; - case 'scm': - scope.sshKeyDataLabel = i18n._('SCM Private Key'); - scope.passwordLabel = i18n._('Password'); - break; - case 'gce': - scope.usernameLabel = i18n._('Service Account Email Address'); - scope.sshKeyDataLabel = i18n._('RSA Private Key'); - scope.email_required = true; - scope.key_required = true; - scope.project_required = true; - scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); - scope.projectLabel = i18n._("Project"); - scope.project_required = false; - scope.projectPopOver = "

" + i18n._("The Project ID is the " + - "GCE assigned identification. It is constructed as " + - "two words followed by a three digit number. Such " + - "as: ") + "

adjective-noun-000

"; - break; - case 'azure': - scope.sshKeyDataLabel = i18n._('Management Certificate'); - scope.subscription_required = true; - scope.key_required = true; - scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); - break; - case 'azure_rm': - scope.usernameLabel = i18n._("Username"); - scope.subscription_required = true; - scope.passwordLabel = i18n._('Password'); - scope.azure_rm_required = true; - break; - case 'vmware': - scope.username_required = true; - scope.host_required = true; - scope.password_required = true; - scope.hostLabel = "vCenter Host"; - scope.passwordLabel = i18n._('Password'); - scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); - break; - case 'openstack': - scope.hostLabel = i18n._("Host (Authentication URL)"); - scope.projectLabel = i18n._("Project (Tenant Name)"); - scope.domainLabel = i18n._("Domain Name"); - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "

" + i18n._("This is the tenant name. " + - " This value is usually the same " + - " as the username.") + "

"; - scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + - "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); - break; - case 'satellite6': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("Satellite 6 URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + - "Red Hat Satellite 6 server. %s" + - "For example, %s"), "
", "
", "https://satellite.example.org"); - break; - case 'cloudforms': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("CloudForms URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + - "corresponds to your CloudForm instance. %s" + - "For example, %s"), "
", "
", "https://cloudforms.example.org"); - break; - case 'net': - scope.username_required = true; - scope.password_required = false; - scope.passwordLabel = i18n._('Password'); - scope.sshKeyDataLabel = i18n._('SSH Key'); - break; - } - } - - // Reset all the field values related to Kind. - if (reset) { - scope.access_key = null; - scope.secret_key = null; - scope.api_key = null; - scope.username = null; - scope.password = null; - scope.password_confirm = null; - scope.ssh_key_data = null; - scope.ssh_key_unlock = null; - scope.ssh_key_unlock_confirm = null; - scope.become_username = null; - scope.become_password = null; - scope.authorize = false; - scope.authorize_password = null; - } - - // Collapse or open help widget based on whether scm value is selected - collapse = $('#credential_kind').parent().find('.panel-collapse').first(); - id = collapse.attr('id'); - if (!Empty(scope.kind) && scope.kind.value !== '') { - if ($('#' + id + '-icon').hasClass('icon-minus')) { - scope.accordionToggle('#' + id); - } - } else { - if ($('#' + id + '-icon').hasClass('icon-plus')) { - scope.accordionToggle('#' + id); - } - } - - }; - } -]) - -.factory('BecomeMethodChange', ['Empty', 'i18n', - function (Empty, i18n) { - return function (params) { - console.log('become method has changed'); - var scope = params.scope; - - if (!Empty(scope.kind)) { - // Apply kind specific settings - switch (scope.kind.value) { - case 'aws': - scope.aws_required = true; - break; - case 'rax': - scope.rackspace_required = true; - scope.username_required = true; - break; - case 'ssh': - scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' - scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); - scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); - break; - case 'scm': - scope.sshKeyDataLabel = i18n._('SCM Private Key'); - scope.passwordLabel = i18n._('Password'); - break; - case 'gce': - scope.usernameLabel = i18n._('Service Account Email Address'); - scope.sshKeyDataLabel = i18n._('RSA Private Key'); - scope.email_required = true; - scope.key_required = true; - scope.project_required = true; - scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); - scope.projectLabel = i18n._("Project"); - scope.project_required = false; - scope.projectPopOver = "

" + i18n._("The Project ID is the " + - "GCE assigned identification. It is constructed as " + - "two words followed by a three digit number. Such " + - "as: ") + "

adjective-noun-000

"; - break; - case 'azure': - scope.sshKeyDataLabel = i18n._('Management Certificate'); - scope.subscription_required = true; - scope.key_required = true; - scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); - break; - case 'azure_rm': - scope.usernameLabel = i18n._("Username"); - scope.subscription_required = true; - scope.passwordLabel = i18n._('Password'); - scope.azure_rm_required = true; - break; - case 'vmware': - scope.username_required = true; - scope.host_required = true; - scope.password_required = true; - scope.hostLabel = "vCenter Host"; - scope.passwordLabel = i18n._('Password'); - scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); - break; - case 'openstack': - scope.hostLabel = i18n._("Host (Authentication URL)"); - scope.projectLabel = i18n._("Project (Tenant Name)"); - scope.domainLabel = i18n._("Domain Name"); - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "

" + i18n._("This is the tenant name. " + - " This value is usually the same " + - " as the username.") + "

"; - scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + - "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); - break; - case 'satellite6': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("Satellite 6 URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + - "Red Hat Satellite 6 server. %s" + - "For example, %s"), "
", "
", "https://satellite.example.org"); - break; - case 'cloudforms': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("CloudForms URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + - "corresponds to your CloudForm instance. %s" + - "For example, %s"), "
", "
", "https://cloudforms.example.org"); - break; - case 'net': - scope.username_required = true; - scope.password_required = false; - scope.passwordLabel = i18n._('Password'); - scope.sshKeyDataLabel = i18n._('SSH Key'); - break; - } - } - }; - } -]) - - -.factory('OwnerChange', [ - function () { - return function (params) { - var scope = params.scope, - owner = scope.owner; - if (owner === 'team') { - scope.team_required = true; - scope.user_required = false; - scope.user = null; - scope.user_username = null; - } else { - scope.team_required = false; - scope.user_required = true; - scope.team = null; - scope.team_name = null; - } - }; -} -]) - -.factory('FormSave', ['$rootScope', '$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', '$state', 'i18n', - function ($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) { - return function (params) { - var scope = params.scope, - mode = params.mode, - form = CredentialForm, - data = {}, fld, url; - - for (fld in form.fields) { - if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' && - fld !== 'ssh_password') { - if (fld === "organization" && !scope[fld]) { - data.user = $rootScope.current_user.id; - } else if (scope[fld] === null) { - data[fld] = ""; - } else { - data[fld] = scope[fld]; - } - } - } - - data.kind = scope.kind.value; - if (scope.become_method === null || typeof scope.become_method === 'undefined') { - data.become_method = ""; - data.become_username = ""; - data.become_password = ""; - } else { - data.become_method = (scope.become_method.value) ? scope.become_method.value : ""; - } - switch (data.kind) { - case 'ssh': - data.password = scope.ssh_password; - break; - case 'aws': - data.username = scope.access_key; - data.password = scope.secret_key; - break; - case 'rax': - data.password = scope.api_key; - break; - case 'gce': - data.username = scope.email_address; - data.project = scope.project; - break; - case 'azure': - data.username = scope.subscription; - } - - Wait('start'); - if (mode === 'add') { - url = GetBasePath("credentials"); - Rest.setUrl(url); - Rest.post(data) - .success(function (data) { - scope.addedItem = data.id; - Wait('stop'); - var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'credentials') { - $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); - } - else { - ReturnToCaller(1); - } - }) - .error(function (data, status) { - Wait('stop'); - // TODO: hopefully this conditional error handling will to away in a future version of tower. The reason why we cannot - // simply pass this error to ProcessErrors is because it will actually match the form element 'ssh_key_unlock' and show - // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the - // error is never shown. In the future, the API will hopefully either behave or respond differently. - if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) { - scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported."); - } - else { - ProcessErrors(scope, data, status, form, { - hdr: i18n._('Error!'), - msg: i18n._('Failed to create new Credential. POST status: ') + status - }); - } - }); - } else { - url = GetBasePath('credentials') + scope.id + '/'; - Rest.setUrl(url); - Rest.put(data) - .success(function () { - Wait('stop'); - $state.go($state.current, {}, {reload: true}); - }) - .error(function (data, status) { - Wait('stop'); - ProcessErrors(scope, data, status, form, { - hdr: i18n._('Error!'), - msg: i18n._('Failed to update Credential. PUT status: ') + status - }); - }); - } - }; - } -]); diff --git a/awx/ui/client/src/helpers/Events.js b/awx/ui/client/src/helpers/Events.js deleted file mode 100644 index be58e72661..0000000000 --- a/awx/ui/client/src/helpers/Events.js +++ /dev/null @@ -1,237 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Events - * @description EventView - show the job_events form in a modal dialog -*/ - -export default - angular.module('EventsHelper', ['RestServices', 'Utilities', 'JobEventDataDefinition', 'JobEventsFormDefinition']) - - .factory('EventView', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'JobEventDataForm', 'Empty', 'JobEventsForm', - function ($rootScope, $location, $log, $stateParams, Rest, Alert, GenerateForm, Prompt, ProcessErrors, GetBasePath, - FormatDate, JobEventDataForm, Empty, JobEventsForm) { - return function (params) { - - var event_id = params.event_id, - generator = GenerateForm, - form = angular.copy(JobEventsForm), - scope, - defaultUrl = GetBasePath('base') + 'job_events/' + event_id + '/'; - - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl); - Rest.get() - .success(function (data) { - var i, n, fld, rows, txt, cDate; - - // If event_data is not available, remove fields that depend on it - if ($.isEmptyObject(data.event_data) || !data.event_data.res || typeof data.event_data.res === 'string') { - for (fld in form.fields) { - switch (fld) { - case 'start': - case 'end': - case 'delta': - case 'msg': - case 'stdout': - case 'stderr': - case 'msg': - case 'results': - case 'module_name': - case 'module_args': - case 'rc': - delete form.fields[fld]; - break; - } - } - } - - if ($.isEmptyObject(data.event_data) || !data.event_data.res || typeof data.event_data.res !== 'string') { - delete form.fields.traceback; - } - - // Remove remaining form fields that do not have a corresponding data value - for (fld in form.fields) { - switch (fld) { - case 'start': - case 'end': - case 'delta': - case 'msg': - case 'stdout': - case 'stderr': - case 'msg': - case 'rc': - if (data.event_data && data.event_data.res && Empty(data.event_data.res[fld])) { - delete form.fields[fld]; - } else { - if (form.fields[fld].type === 'textarea') { - n = data.event_data.res[fld].match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - rows = (rows < 3) ? 3 : rows; - form.fields[fld].rows = rows; - } - } - break; - case 'results': - if (data.event_data && data.event_data.res && data.event_data.res[fld] === undefined) { - // not defined - delete form.fields[fld]; - } else if (!Array.isArray(data.event_data.res[fld]) || data.event_data.res[fld].length === 0) { - // defined, but empty - delete form.fields[fld]; - } else { - // defined and not empty, so attempt to size the textarea field - txt = ''; - for (i = 0; i < data.event_data.res[fld].length; i++) { - txt += data.event_data.res[fld][i]; - } - if (txt === '') { - // there's an array, but the actual text is empty - delete form.fields[fld]; - } else { - n = txt.match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - rows = (rows < 3) ? 3 : rows; - form.fields[fld].rows = rows; - } - } - break; - case 'module_name': - case 'module_args': - if (data.event_data && data.event_data.res) { - if (data.event_data.res.invocation === undefined || - data.event_data.res.invocation[fld] === undefined) { - delete form.fields[fld]; - } - } - break; - } - } - - // load the form - scope = generator.inject(form, { - mode: 'edit', - modal: true, - related: false - }); - generator.reset(); - scope.formModalAction = function () { - $('#form-modal').modal("hide"); - }; - scope.formModalActionLabel = 'OK'; - scope.formModalCancelShow = false; - scope.formModalInfo = 'View JSON'; - $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none'); - $('#form-modal').addClass('skinny-modal'); - scope.formModalHeader = data.event_display.replace(/^\u00a0*/g, ''); - - // Respond to View JSON button - scope.formModalInfoAction = function () { - var generator = GenerateForm, - scope = generator.inject(JobEventDataForm, { - mode: 'edit', - modal: true, - related: false, - modal_selector: '#form-modal2', - modal_body_id: 'form-modal2-body', - modal_title_id: 'formModal2Header' - }); - generator.reset(); - scope.formModal2Header = data.event_display.replace(/^\u00a0*/g, ''); - scope.event_data = JSON.stringify(data.event_data, null, '\t'); - scope.formModal2ActionLabel = 'OK'; - scope.formModal2CancelShow = false; - scope.formModal2Info = false; - scope.formModalInfo = 'View JSON'; - scope.formModal2Action = function () { - $('#form-modal2').modal("hide"); - }; - $('#form-modal2 .btn-success').removeClass('btn-success').addClass('btn-none'); - }; - - if (typeof data.event_data.res === 'string') { - scope.traceback = data.event_data.res; - } - - for (fld in form.fields) { - switch (fld) { - case 'status': - if (data.failed) { - scope.status = 'error'; - } else if (data.changed) { - scope.status = 'changed'; - } else { - scope.status = 'success'; - } - break; - case 'created': - cDate = new Date(data.created); - scope.created = FormatDate(cDate); - break; - case 'host': - if (data.summary_fields && data.summary_fields.host) { - scope.host = data.summary_fields.host.name; - } - break; - case 'id': - case 'task': - case 'play': - scope[fld] = data[fld]; - break; - case 'start': - case 'end': - if (data.event_data && data.event_data.res && !Empty(data.event_data.res[fld])) { - scope[fld] = data.event_data.res[fld]; - } - - break; - case 'results': - if (Array.isArray(data.event_data.res[fld]) && data.event_data.res[fld].length > 0) { - txt = ''; - for (i = 0; i < data.event_data.res[fld].length; i++) { - txt += data.event_data.res[fld][i]; - } - if (txt !== '') { - scope[fld] = txt; - } - } - break; - case 'msg': - case 'stdout': - case 'stderr': - case 'delta': - case 'rc': - if (data.event_data && data.event_data.res && data.event_data.res[fld] !== undefined) { - scope[fld] = data.event_data.res[fld]; - } - break; - case 'module_name': - case 'module_args': - if (data.event_data.res && data.event_data.res.invocation) { - scope[fld] = data.event_data.res.invocation[fld]; - } - break; - } - } - - if (!scope.$$phase) { - scope.$digest(); - } - - }) - .error(function (data, status) { - $('#form-modal').modal("hide"); - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to retrieve event: ' + event_id + '. GET status: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Groups.js b/awx/ui/client/src/helpers/Groups.js deleted file mode 100644 index 295af244a1..0000000000 --- a/awx/ui/client/src/helpers/Groups.js +++ /dev/null @@ -1,1028 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -'use strict'; - -/** - * @ngdoc function - * @name helpers.function:Groups - * @description inventory tree widget add/edit/delete -*/ - -import listGenerator from '../shared/list-generator/main'; - -export default -angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'GroupListDefinition', listGenerator.name, 'GroupsHelper', 'InventoryHelper', 'SelectionHelper', - 'JobSubmissionHelper', 'PromptDialog', 'CredentialsListDefinition', - 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'StandardOutHelper', - 'SchedulesHelper' -]) - -/** - * - * Lookup options for group source and build an array of drop-down choices - * - */ -.factory('GetSourceTypeOptions', ['Rest', 'ProcessErrors', 'GetBasePath', - function (Rest, ProcessErrors, GetBasePath) { - return function (params) { - var scope = params.scope, - variable = params.variable; - - if (scope[variable] === undefined) { - scope[variable] = []; - Rest.setUrl(GetBasePath('inventory_sources')); - Rest.options() - .success(function (data) { - var i, choices = data.actions.GET.source.choices; - for (i = 0; i < choices.length; i++) { - if (choices[i][0] !== 'file') { - scope[variable].push({ - label: choices[i][1], - value: choices[i][0] - }); - } - } - scope.cloudCredentialRequired = false; - scope.$emit('sourceTypeOptionsReady'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status - }); - }); - } - }; - } -]) - -/** - * - * TODO: Document - * - */ -.factory('ViewUpdateStatus', ['$state', 'Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find', - function ($state, Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find) { - return function (params) { - - var scope = params.scope, - group_id = params.group_id, - group = Find({ list: scope.groups, key: 'id', val: group_id }); - - if (scope.removeSourceReady) { - scope.removeSourceReady(); - } - scope.removeSourceReady = scope.$on('SourceReady', function(e, source) { - - // Get the ID from the correct summary field - var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id; - - $state.go('inventorySyncStdout', {id: update_id}); - - }); - - if (group) { - if (Empty(group.source)) { - // do nothing - } else if (Empty(group.status) || group.status === "never updated") { - Alert('No Status Available', '

An inventory sync has not been performed for the selected group. Start the process by ' + - 'clicking the button.
', 'alert-info', null, null, null, null, true); - } else { - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('SourceReady', data); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + - ' GET returned status: ' + status }); - }); - } - } - - }; - } -]) - -/** - * - * TODO: Document - * - */ -.factory('GetHostsStatusMsg', [ - function () { - return function (params) { - - var active_failures = params.active_failures, - total_hosts = params.total_hosts, - tip, failures, html_class; - - // Return values for use on host status indicator - - if (active_failures > 0) { - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; - html_class = 'error'; - failures = true; - } else { - failures = false; - if (total_hosts === 0) { - // no hosts - tip = "Contains 0 hosts."; - html_class = 'none'; - } else { - // many hosts with 0 failures - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; - html_class = 'success'; - } - } - - return { - tooltip: tip, - failures: failures, - 'class': html_class - }; - }; -} -]) - -/** - * - * TODO: Document - * - */ -.factory('GetSyncStatusMsg', [ 'Empty', - function (Empty) { - return function (params) { - - var status = params.status, - source = params.source, - has_inventory_sources = params.has_inventory_sources, - launch_class = '', - launch_tip = 'Start sync process', - schedule_tip = 'Schedule future inventory syncs', - stat, stat_class, status_tip; - - stat = status; - stat_class = stat; - - switch (status) { - case 'never updated': - stat = 'never'; - stat_class = 'na'; - status_tip = 'Sync not performed. Click to start it now.'; - break; - case 'none': - case 'ok': - case '': - launch_class = 'btn-disabled'; - stat = 'n/a'; - stat_class = 'na'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - break; - case 'canceled': - status_tip = 'Sync canceled. Click to view log.'; - break; - case 'failed': - status_tip = 'Sync failed. Click to view log.'; - break; - case 'successful': - status_tip = 'Sync completed. Click to view log.'; - break; - case 'pending': - status_tip = 'Sync pending.'; - launch_class = "btn-disabled"; - launch_tip = "Sync pending"; - break; - case 'updating': - case 'running': - launch_class = "btn-disabled"; - launch_tip = "Sync running"; - status_tip = "Sync running. Click to view log."; - break; - } - - if (has_inventory_sources && Empty(source)) { - // parent has a source, therefore this group should not have a source - launch_class = "btn-disabled"; - status_tip = 'Managed by an external cloud source.'; - launch_tip = 'Can only be updated by running a sync on the parent group.'; - } - - if (has_inventory_sources === false && Empty(source)) { - launch_class = 'btn-disabled'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - } - - return { - "class": stat_class, - "tooltip": status_tip, - "status": stat, - "launch_class": launch_class, - "launch_tip": launch_tip, - "schedule_tip": schedule_tip - }; - }; - } -]) - -/** - * - * Cancel a pending or running inventory sync - * - */ -.factory('GroupsCancelUpdate', ['Empty', 'Rest', 'ProcessErrors', 'Alert', 'Wait', 'Find', - function (Empty, Rest, ProcessErrors, Alert, Wait, Find) { - return function (params) { - - var scope = params.scope, - id = params.id, - group = params.group; - - if (scope.removeCancelUpdate) { - scope.removeCancelUpdate(); - } - scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { - // Cancel the update process - Rest.setUrl(url); - Rest.post() - .success(function () { - Wait('stop'); - //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + - // 'Click the button to monitor the status.', 'alert-info'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. POST status: ' + status - }); - }); - }); - - if (scope.removeCheckCancel) { - scope.removeCheckCancel(); - } - scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { - // Check that we have access to cancelling an update - var url = (current_update) ? current_update : last_update; - url += 'cancel/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - if (data.can_cancel) { - scope.$emit('CancelUpdate', url); - //} else { - // Wait('stop'); - // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + - // 'the latest status.', 'alert-info'); - } - else { - Wait('stop'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET status: ' + status - }); - }); - }); - - // Cancel the update process - if (Empty(group)) { - group = Find({ list: scope.groups, key: 'id', val: id }); - scope.selected_group_id = group.id; - } - - if (group && (group.status === 'running' || group.status === 'pending')) { - // We found the group, and there is a running update - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status - }); - }); - } - }; - } -]) - - -/** - * - * Deprecated factory that used to support /#/home/groups/ - * - */ -.factory('GroupsEdit', ['$filter', '$rootScope', '$location', '$log', '$stateParams', '$compile', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate', - 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find', - 'ParseVariableString', 'ToJSON', 'GroupsScheduleListInit', 'SetSchedulesInnerDialogSize', 'CreateSelect2', - function ($filter, $rootScope, $location, $log, $stateParams, $compile, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, Empty, Wait, - GetChoices, UpdateGroup, SourceChange, Find, ParseVariableString, ToJSON, GroupsScheduleListInit, - SetSchedulesInnerDialogSize, CreateSelect2) { - return function (params) { - - var parent_scope = params.scope, - group_id = params.group_id, - mode = params.mode, // 'add' or 'edit' - inventory_id = params.inventory_id, - generator = GenerateForm, - group_created = false, - defaultUrl, - master = {}, - choicesReady, - modal_scope = parent_scope.$new(), - properties_scope = parent_scope.$new(), - sources_scope = parent_scope.$new(), - elem, x, y, ww, wh, maxrows, - group, - schedules_url = ''; - - if (mode === 'edit') { - defaultUrl = GetBasePath('groups') + group_id + '/'; - } - else { - defaultUrl = (group_id !== null) ? GetBasePath('groups') + group_id + '/children/' : - GetBasePath('inventory') + inventory_id + '/groups/'; - } - - $('#properties-tab').empty(); - $('#sources-tab').empty(); - $('#schedules-list').empty(); - $('#schedules-form').empty(); - $('#schedules-detail').empty(); - - elem = document.getElementById('group-modal-dialog'); - $compile(elem)(modal_scope); - - var form_scope = - generator.inject(GroupForm, { mode: mode, id: 'properties-tab', related: false, scope: properties_scope }); - var source_form_scope = - generator.inject(GroupForm, { mode: mode, id: 'sources-tab', related: false, scope: sources_scope }); - - //generator.reset(); - - GetSourceTypeOptions({ scope: sources_scope, variable: 'source_type_options' }); - sources_scope.source = GroupForm.fields.source['default']; - sources_scope.sourcePathRequired = false; - sources_scope[GroupForm.fields.source_vars.parseTypeName] = 'yaml'; - sources_scope.update_cache_timeout = 0; - properties_scope.parseType = 'yaml'; - - function waitStop() { Wait('stop'); } - - // Attempt to create the largest textarea field that will fit on the window. Minimum - // height is 6 rows, so on short windows you will see vertical scrolling - function textareaResize(textareaID) { - var textArea, formHeight, model, windowHeight, offset, rows; - textArea = $('#' + textareaID); - if (properties_scope.codeMirror) { - model = textArea.attr('ng-model'); - properties_scope[model] = properties_scope.codeMirror.getValue(); - properties_scope.codeMirror.destroy(); - } - textArea.attr('rows', 1); - formHeight = $('#group_form').height(); - windowHeight = $('#group-modal-dialog').height() - 20; //leave a margin of 20px - offset = Math.floor(windowHeight - formHeight); - rows = Math.floor(offset / 24); - rows = (rows < 6) ? 6 : rows; - textArea.attr('rows', rows); - while(rows > 6 && $('#group_form').height() > $('#group-modal-dialog').height()) { - rows--; - textArea.attr('rows', rows); - } - ParseTypeChange({ scope: properties_scope, field_id: textareaID, onReady: waitStop }); - } - - function initSourceChange() { - parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false; - SourceChange({ scope: sources_scope, form: GroupForm }); - } - - // Set modal dimensions based on viewport width - ww = $(document).width(); - wh = $('body').height(); - if (ww > 1199) { - // desktop - x = 675; - y = (800 > wh) ? wh - 15 : 800; - maxrows = 18; - } else if (ww <= 1199 && ww >= 768) { - x = 550; - y = (770 > wh) ? wh - 15 : 770; - maxrows = 12; - } else { - x = (ww - 20); - y = (770 > wh) ? wh - 15 : 770; - maxrows = 10; - } - - // Create the modal - $('#group-modal-dialog').dialog({ - buttons: { - 'Save': function () { - modal_scope.saveGroup(); - }, - 'Cancel': function() { - modal_scope.cancelModal(); - } - }, - modal: true, - width: x, - height: y, - autoOpen: false, - minWidth: 440, - title: (mode === 'edit') ? 'Edit Group' : 'Add Group', - closeOnEscape: false, - create: function () { - $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x'); - $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-buttonset button').each(function () { - var c, h, i, l; - l = $(this).text(); - if (l === 'Cancel') { - h = "fa-times"; - c = "btn btn-default"; - i = "group-close-button"; - $(this).attr({ - 'class': c, - 'id': i - }).html(" Cancel"); - } else if (l === 'Save') { - h = "fa-check"; - c = "btn btn-primary"; - i = "group-save-button"; - $(this).attr({ - 'class': c, - 'id': i - }).html(" Save"); - } - }); - }, - resizeStop: function () { - // for some reason, after resizing dialog the form and fields (the content) doesn't expand to 100% - var dialog = $('.ui-dialog[aria-describedby="group-modal-dialog"]'), - titleHeight = dialog.find('.ui-dialog-titlebar').outerHeight(), - buttonHeight = dialog.find('.ui-dialog-buttonpane').outerHeight(), - content = dialog.find('#group-modal-dialog'), - w; - content.width(dialog.width() - 28); - content.css({ height: (dialog.height() - titleHeight - buttonHeight - 10) }); - - if ($('#group_tabs .active a').text() === 'Properties') { - textareaResize('group_variables', properties_scope); - } - else if ($('#group_tabs .active a').text() === 'Schedule') { - w = $('#group_tabs').width() - 18; - $('#schedules-overlay').width(w); - $('#schedules-form-container').width(w); - SetSchedulesInnerDialogSize(); - } - }, - close: function () { - // Destroy on close - $('.tooltip').each(function () { - // Remove any lingering tooltip
elements - $(this).remove(); - }); - $('.popover').each(function () { - // remove lingering popover
elements - $(this).remove(); - }); - if (properties_scope.codeMirror) { - properties_scope.codeMirror.destroy(); - } - if (sources_scope.codeMirror) { - sources_scope.codeMirror.destroy(); - } - $('#properties-tab').empty(); - $('#sources-tab').empty(); - $('#schedules-list').empty(); - $('#schedules-form').empty(); - $('#schedules-detail').empty(); - $('#group-modal-dialog').hide(); - $('#group-modal-dialog').dialog('destroy'); - modal_scope.cancelModal(); - }, - open: function () { - function updateButtonStatus(isValid) { - $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.btn-primary').prop('disabled', !isValid); - } - form_scope.$watch('group_form.$valid', updateButtonStatus); - source_form_scope.$watch('source_form.$valid', updateButtonStatus); - $('#group_name').focus(); - Wait('stop'); - } - }); - - $('#group_tabs a[data-toggle="tab"]').on('show.bs.tab', function (e) { - if ($(e.target).text() === 'Properties') { - Wait('start'); - setTimeout(function(){ textareaResize('group_variables'); }, 300); - } - else if ($(e.target).text() === 'Source') { - if (sources_scope.source && (sources_scope.source.value === 'ec2')) { - Wait('start'); - ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: GroupForm.fields.source_vars.parseTypeName, - field_id: 'source_source_vars', onReady: waitStop }); - } else if (sources_scope.source && (sources_scope.source.value === 'vmware' || - sources_scope.source.value === 'openstack')) { - Wait('start'); - ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: GroupForm.fields.inventory_variables.parseTypeName, - field_id: 'source_inventory_variables', onReady: waitStop }); - } - else if (sources_scope.source && (sources_scope.source.value === 'custom')) { - Wait('start'); - ParseTypeChange({ scope: sources_scope, variable: 'extra_vars', parse_variable: GroupForm.fields.extra_vars.parseTypeName, - field_id: 'source_extra_vars', onReady: waitStop }); - } - } - else if ($(e.target).text() === 'Schedule') { - $('#schedules-overlay').hide(); - } - }); - - if (modal_scope.groupVariablesLoadedRemove) { - modal_scope.groupVariablesLoadedRemove(); - } - modal_scope.groupVariablesLoadedRemove = modal_scope.$on('groupVariablesLoaded', function () { - if (mode === 'edit' && - group.has_inventory_sources && - Empty(group.summary_fields.inventory_source.source) && - sources_scope.source && - sources_scope.source.value !== 'manual') { - modal_scope.showSourceTab = false; - } else { - modal_scope.showSourceTab = true; - } - modal_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=='manual') ? true : false; - if (mode === 'edit' && modal_scope.showSourceTab) { - // the use has access to the source tab, so they may create a schedule - GroupsScheduleListInit({ scope: modal_scope, url: schedules_url }); - } - $('#group_tabs a:first').tab('show'); - Wait('start'); - $('#group-modal-dialog').dialog('open'); - setTimeout(function() { textareaResize('group_variables', properties_scope); }, 300); - }); - - // JT -- this gets called after the properties & properties variables are loaded, and is emitted from (groupLoaded) - if (modal_scope.removeLoadSourceData) { - modal_scope.removeLoadSourceData(); - } - modal_scope.removeLoadSourceData = modal_scope.$on('LoadSourceData', function () { - if (sources_scope.source_url) { - // get source data - Rest.setUrl(sources_scope.source_url); - Rest.get() - .success(function (data) { - var fld, i, j, flag, found, set, opts, list, form; - form = GroupForm; - for (fld in form.fields) { - if (fld === 'checkbox_group') { - for (i = 0; i < form.fields[fld].fields.length; i++) { - flag = form.fields[fld].fields[i]; - if (data[flag.name] !== undefined) { - sources_scope[flag.name] = data[flag.name]; - master[flag.name] = sources_scope[flag.name]; - } - } - } - if (fld === 'source') { - found = false; - data.source = (data.source === "" ) ? "manual" : data.source; - for (i = 0; i < sources_scope.source_type_options.length; i++) { - if (sources_scope.source_type_options[i].value === data.source) { - sources_scope.source = sources_scope.source_type_options[i]; - found = true; - } - } - if (!found || sources_scope.source.value === "manual") { - sources_scope.groupUpdateHide = true; - } else { - sources_scope.groupUpdateHide = false; - } - master.source = sources_scope.source; - } else if (fld === 'source_vars') { - // Parse source_vars, converting to YAML. - sources_scope.source_vars = ParseVariableString(data.source_vars); - master.source_vars = sources_scope.variables; - } - else if(fld === "inventory_script"){ - // the API stores it as 'source_script', we call it inventory_script - data.summary_fields.inventory_script = data.summary_fields.source_script; - sources_scope.inventory_script = data.source_script; - master.inventory_script = sources_scope.inventory_script; - } else if (fld === "source_regions") { - if (data[fld] === "") { - sources_scope[fld] = data[fld]; - master[fld] = sources_scope[fld]; - } else { - sources_scope[fld] = data[fld].split(","); - master[fld] = sources_scope[fld]; - } - } else if (data[fld] !== undefined) { - sources_scope[fld] = data[fld]; - master[fld] = sources_scope[fld]; - } - - if (form.fields[fld].sourceModel && data.summary_fields && - data.summary_fields[form.fields[fld].sourceModel]) { - sources_scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - } - } - - initSourceChange(); - - if (data.source_regions) { - if (data.source === 'ec2' || - data.source === 'rax' || - data.source === 'gce' || - data.source === 'azure') { - if (data.source === 'ec2') { - set = sources_scope.ec2_regions; - } else if (data.source === 'rax') { - set = sources_scope.rax_regions; - } else if (data.source === 'gce') { - set = sources_scope.gce_regions; - } else if (data.source === 'azure') { - set = sources_scope.azure_regions; - } - opts = []; - list = data.source_regions.split(','); - for (i = 0; i < list.length; i++) { - for (j = 0; j < set.length; j++) { - if (list[i] === set[j].value) { - opts.push({ - id: set[j].value, - text: set[j].label - }); - } - } - } - master.source_regions = opts; - CreateSelect2({ - element: "#source_source_regions", - opts: opts - }); - - } - } else { - // If empty, default to all - master.source_regions = [{ - id: 'all', - text: 'All' - }]; - } - if (data.group_by && data.source === 'ec2') { - set = sources_scope.ec2_group_by; - opts = []; - list = data.group_by.split(','); - for (i = 0; i < list.length; i++) { - for (j = 0; j < set.length; j++) { - if (list[i] === set[j].value) { - opts.push({ - id: set[j].value, - text: set[j].label - }); - } - } - } - master.group_by = opts; - CreateSelect2({ - element: "#source_group_by", - opts: opts - }); - } - sources_scope.group_update_url = data.related.update; - modal_scope.$emit('groupVariablesLoaded'); // JT-- "groupVariablesLoaded" is where the schedule info is loaded, so I make a call after the sources_scope.source has been loaded - //Wait('stop'); - }) - .error(function (data, status) { - sources_scope.source = ""; - ProcessErrors(modal_scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source. GET status: ' + status }); - }); - } - else { - modal_scope.$emit('groupVariablesLoaded'); // JT-- "groupVariablesLoaded" is where the schedule info is loaded, so I make a call after the sources_scope.source has been loaded - } - }); - - if (sources_scope.removeScopeSourceTypeOptionsReady) { - sources_scope.removeScopeSourceTypeOptionsReady(); - } - sources_scope.removeScopeSourceTypeOptionsReady = sources_scope.$on('sourceTypeOptionsReady', function() { - if (mode === 'add') { - sources_scope.source = Find({ - list: sources_scope.source_type_options, - key: 'value', - val: '' - }); - modal_scope.showSchedulesTab = false; - } - }); - - if (modal_scope.removeChoicesComplete) { - modal_scope.removeChoicesComplete(); - } - modal_scope.removeChoicesComplete = modal_scope.$on('choicesCompleteGroup', function () { - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl); - Rest.get() - .success(function (data) { - group = data; - for (var fld in GroupForm.fields) { - if (data[fld]) { - properties_scope[fld] = data[fld]; - master[fld] = properties_scope[fld]; - } - } - schedules_url = data.related.inventory_source + 'schedules/'; - properties_scope.variable_url = data.related.variable_data; - sources_scope.source_url = data.related.inventory_source; - modal_scope.$emit('LoadSourceData'); - }) - .error(function (data, status) { - ProcessErrors(modal_scope, data, status, { hdr: 'Error!', - msg: 'Failed to retrieve group: ' + defaultUrl + '. GET status: ' + status }); - }); - }); - - choicesReady = 0; - - if (sources_scope.removeChoicesReady) { - sources_scope.removeChoicesReady(); - } - sources_scope.removeChoicesReady = sources_scope.$on('choicesReadyGroup', function () { - CreateSelect2({ - element: '#source_source', - multiple: false - }); - choicesReady++; - if (choicesReady === 2) { - if (mode === 'edit') { - modal_scope.$emit('choicesCompleteGroup'); - } - else { - properties_scope.variables = "---"; - master.variables = properties_scope.variables; - modal_scope.$emit('groupVariablesLoaded'); - } - } - }); - - // Load options for source regions - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'rax_regions', - choice_name: 'rax_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'ec2_regions', - choice_name: 'ec2_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'gce_regions', - choice_name: 'gce_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'azure_regions', - choice_name: 'azure_region_choices', - callback: 'choicesReadyGroup' - }); - - // Load options for group_by - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'group_by', - variable: 'ec2_group_by', - choice_name: 'ec2_group_by_choices', - callback: 'choicesReadyGroup' - }); - - Wait('start'); - - if (parent_scope.removeAddTreeRefreshed) { - parent_scope.removeAddTreeRefreshed(); - } - parent_scope.removeAddTreeRefreshed = parent_scope.$on('GroupTreeRefreshed', function() { - Wait('stop'); - try { - $('#group-modal-dialog').dialog('close'); - } - catch(e) { - // ignore - } - }); - - if (modal_scope.removeSaveComplete) { - modal_scope.removeSaveComplete(); - } - modal_scope.removeSaveComplete = modal_scope.$on('SaveComplete', function (e, error) { - if (!error) { - modal_scope.cancelModal(); - } - }); - - if (modal_scope.removeFormSaveSuccess) { - modal_scope.removeFormSaveSuccess(); - } - modal_scope.removeFormSaveSuccess = modal_scope.$on('formSaveSuccess', function () { - - // Source data gets stored separately from the group. Validate and store Source - // related fields, then call SaveComplete to wrap things up. - - var parseError = false, - regions, r, i, - group_by, - data = { - group: group_id, - source: ((sources_scope.source && sources_scope.source.value!=='manual') ? sources_scope.source.value : ''), - source_path: sources_scope.source_path, - credential: sources_scope.credential, - overwrite: sources_scope.overwrite, - overwrite_vars: sources_scope.overwrite_vars, - source_script: sources_scope.inventory_script, - update_on_launch: sources_scope.update_on_launch, - update_cache_timeout: (sources_scope.update_cache_timeout || 0) - }; - - // Create a string out of selected list of regions - if(sources_scope.source_regions){ - regions = $('#source_source_regions').select2("data"); - r = []; - for (i = 0; i < regions.length; i++) { - r.push(regions[i].id); - } - data.source_regions = r.join(); - } - - if (sources_scope.source && (sources_scope.source.value === 'ec2')) { - data.instance_filters = sources_scope.instance_filters; - // Create a string out of selected list of regions - group_by = $('#source_group_by').select2("data"); - r = []; - for (i = 0; i < group_by.length; i++) { - r.push(group_by[i].id); - } - data.group_by = r.join(); - } - - if (sources_scope.source && (sources_scope.source.value === 'ec2' )) { - // for ec2, validate variable data - data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true); - } - - if (sources_scope.source && (sources_scope.source.value === 'custom')) { - data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true); - } - - if (sources_scope.source && (sources_scope.source.value === 'vmware' || - sources_scope.source.value === 'openstack')) { - data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true); - } - - // the API doesn't expect the credential to be passed with a custom inv script - if(sources_scope.source && sources_scope.source.value === 'custom'){ - delete(data.credential); - } - - if (!parseError) { - Rest.setUrl(sources_scope.source_url); - Rest.put(data) - .success(function () { - modal_scope.$emit('SaveComplete', false); - }) - .error(function (data, status) { - $('#group_tabs a:eq(1)').tab('show'); - ProcessErrors(sources_scope, data, status, GroupForm, { hdr: 'Error!', - msg: 'Failed to update group inventory source. PUT status: ' + status }); - }); - } - }); - - // Cancel - modal_scope.cancelModal = function () { - try { - $('#group-modal-dialog').dialog('close'); - } - catch(e) { - //ignore - } - Wait('stop'); - }; - - // Save - modal_scope.saveGroup = function () { - Wait('start'); - var fld, data, json_data; - - try { - - json_data = ToJSON(properties_scope.parseType, properties_scope.variables, true); - - data = {}; - for (fld in GroupForm.fields) { - data[fld] = properties_scope[fld]; - } - - data.inventory = inventory_id; - - Rest.setUrl(defaultUrl); - if (mode === 'edit' || (mode === 'add' && group_created)) { - Rest.put(data) - .success(function () { - modal_scope.$emit('formSaveSuccess'); - }) - .error(function (data, status) { - $('#group_tabs a:eq(0)').tab('show'); - ProcessErrors(properties_scope, data, status, GroupForm, { hdr: 'Error!', - msg: 'Failed to update group: ' + group_id + '. PUT status: ' + status - }); - }); - } - else { - Rest.post(data) - .success(function (data) { - group_created = true; - group_id = data.id; - sources_scope.source_url = data.related.inventory_source; - modal_scope.$emit('formSaveSuccess'); - }) - .error(function (data, status) { - $('#group_tabs a:eq(0)').tab('show'); - ProcessErrors(properties_scope, data, status, GroupForm, { hdr: 'Error!', - msg: 'Failed to create group: ' + group_id + '. POST status: ' + status - }); - }); - } - } - catch(e) { - // ignore. ToJSON will have already alerted the user - } - }; - - // Start the update process - modal_scope.updateGroup = function () { - if (sources_scope.source === "manual" || sources_scope.source === null) { - Alert('Missing Configuration', 'The selected group is not configured for updates. You must first edit the group, provide Source settings, ' + - 'and then run an update.', 'alert-info'); - } else if (sources_scope.status === 'updating') { - Alert('Update in Progress', '
The inventory update process is currently running for group ' + - $filter('sanitize')(sources_scope.summary_fields.group.name) + '. Use the Refresh button to monitor the status.
', 'alert-info', null, null, null, null, true); - } else { - InventoryUpdate({ - scope: parent_scope, - group_id: group_id, - url: properties_scope.group_update_url, - group_name: properties_scope.name, - group_source: sources_scope.source.value - }); - } - }; - - // Change the lookup and regions when the source changes - sources_scope.sourceChange = function () { - sources_scope.credential_name = ""; - sources_scope.credential = ""; - if (sources_scope.credential_name_api_error) { - delete sources_scope.credential_name_api_error; - } - initSourceChange(); - }; - - }; - } -]); diff --git a/awx/ui/client/src/helpers/Hosts.js b/awx/ui/client/src/helpers/Hosts.js deleted file mode 100644 index 627c3a76eb..0000000000 --- a/awx/ui/client/src/helpers/Hosts.js +++ /dev/null @@ -1,406 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - -/* jshint loopfunc: true */ -/** - * @ngdoc function - * @name helpers.function:Hosts - * @description Routines that handle host add/edit/delete on the Inventory detail page. - */ - -'use strict'; - -import listGenerator from '../shared/list-generator/main'; - -export default -angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'HostListDefinition', - listGenerator.name, 'HostsHelper', - 'InventoryHelper', 'InventoryFormDefinition', 'SelectionHelper', - 'HostGroupsFormDefinition', 'VariablesHelper', 'ModalDialog', 'StandardOutHelper', - 'GroupListDefinition' -]) - -.factory('SetEnabledMsg', [ function() { - return function(host) { - if (host.has_inventory_sources) { - // Inventory sync managed, so not clickable - host.enabledToolTip = (host.enabled) ? 'Host is available' : 'Host is not available'; - } - else { - // Clickable - host.enabledToolTip = (host.enabled) ? 'Host is available. Click to toggle.' : 'Host is not available. Click to toggle.'; - } - }; -}]) - -.factory('SetHostStatus', ['SetEnabledMsg', function(SetEnabledMsg) { - return function(host) { - // Set status related fields on a host object - host.activeFailuresLink = '/#/hosts/' + host.id + '/job_host_summaries/?inventory=' + host.inventory + - '&host_name=' + encodeURI(host.name); - if (host.has_active_failures === true) { - host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; - host.active_failures = 'failed'; - } - else if (host.has_active_failures === false && host.last_job === null) { - host.has_active_failures = 'none'; - host.badgeToolTip = "No job data available."; - host.active_failures = 'n/a'; - } - else if (host.has_active_failures === false && host.last_job !== null) { - host.badgeToolTip = "Most recent job successful. Click to view jobs."; - host.active_failures = 'success'; - } - - host.enabled_flag = host.enabled; - SetEnabledMsg(host); - - }; -}]) - -.factory('SetStatus', ['$filter', 'SetEnabledMsg', 'Empty', function($filter, SetEnabledMsg, Empty) { - return function(params) { - - var scope = params.scope, - host = params.host, - i, html, title; - - function ellipsis(a) { - if (a.length > 25) { - return a.substr(0,25) + '...'; - } - return a; - } - - function noRecentJobs() { - title = 'No job data'; - html = "

No recent job data available for this host.

\n"; - } - - function setMsg(host) { - var j, job, jobs; - - if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) { - if (host.has_active_failures === true) { - host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; - host.active_failures = 'error'; - } - else { - host.badgeToolTip = "Most recent job successful. Click to view jobs."; - host.active_failures = 'successful'; - } - if (host.summary_fields.recent_jobs.length > 0) { - // build html table of job status info - jobs = host.summary_fields.recent_jobs.sort( - function(a,b) { - // reverse numerical order - return -1 * (a - b); - }); - title = "Recent Jobs"; - html = "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - for (j=0; j < jobs.length; j++) { - job = jobs[j]; - html += "\n"; - - // SmartStatus-tooltips are named --success whereas icon-job uses successful - var iconStatus = (job.status === 'successful') ? 'success' : 'failed'; - - html += "\n"; - - html += "\n"; - - html += "\n"; - - html += "\n"; - } - html += "\n"; - html += "
StatusFinishedName
" + ($filter('longDate')(job.finished)).replace(/ /,'
') + "
" + ellipsis(job.name) + "
\n"; - } - else { - noRecentJobs(); - } - } - else if (host.has_active_failures === false && host.last_job === null) { - host.badgeToolTip = "No job data available."; - host.active_failures = 'none'; - noRecentJobs(); - } - host.job_status_html = html; - host.job_status_title = title; - } - - if (!Empty(host)) { - // update single host - setMsg(host); - SetEnabledMsg(host); - } - else { - // update all hosts - for (i=0; i < scope.hosts.length; i++) { - setMsg(scope.hosts[i]); - SetEnabledMsg(scope.hosts[i]); - } - } - - }; -}]) - -.factory('HostsReload', [ '$stateParams', 'Empty', 'InventoryHosts', 'GetBasePath', 'Wait', - 'SetHostStatus', 'SetStatus', 'ApplyEllipsis', - function($stateParams, Empty, InventoryHosts, GetBasePath, Wait, SetHostStatus, SetStatus, - ApplyEllipsis) { - return function(params) { - - var scope = params.scope, - parent_scope = params.parent_scope; - - if (scope.removeHostsReloadPostRefresh) { - scope.removeHostsReloadPostRefresh(); - } - scope.removeHostsReloadPostRefresh = scope.$on('PostRefresh', function(e, set) { - if (set === 'hosts') { - for (var i=0; i < scope.hosts.length; i++) { - //Set tooltip for host enabled flag - scope.hosts[i].enabled_flag = scope.hosts[i].enabled; - } - SetStatus({ scope: scope }); - setTimeout(function() { ApplyEllipsis('#hosts_table .host-name a'); }, 2500); - Wait('stop'); - if (parent_scope) { - parent_scope.$emit('HostReloadComplete'); - } - } - }); - }; - }]) - -.factory('HostsCopy', ['$compile', 'Rest', 'ProcessErrors', 'CreateDialog', 'GetBasePath', 'Wait', 'generateList', 'GroupList', -function($compile, Rest, ProcessErrors, CreateDialog, GetBasePath, Wait, GenerateList, GroupList) { -return function(params) { - - var host_id = params.host_id, - group_scope = params.group_scope, - parent_scope = params.host_scope, - parent_group = group_scope.selected_group_id, - scope = parent_scope.$new(), - buttonSet, url, host; - - buttonSet = [{ - label: "Cancel", - onClick: function() { - scope.cancel(); - }, - icon: "fa-times", - "class": "btn btn-default", - "id": "host-copy-cancel-button" - },{ - label: "OK", - onClick: function() { - scope.performCopy(); - }, - icon: "fa-check", - "class": "btn btn-primary", - "id": "host-copy-ok-button" - }]; - - if (scope.removeHostCopyPostRefresh) { - scope.removeHostCopyPostRefresh(); - } - scope.removeHostCopyPostRefresh = scope.$on('PostRefresh', function() { - scope.copy_groups.forEach(function(row, i) { - scope.copy_groups[i].checked = '0'; - }); - Wait('stop'); - $('#host-copy-dialog').dialog('open'); - $('#host-copy-ok-button').attr('disabled','disabled'); - - // prevent backspace from navigation when not in input or textarea field - $(document).on("keydown", function (e) { - if (e.which === 8 && !$(e.target).is('input[type="text"], textarea')) { - e.preventDefault(); - } - }); - }); - - if (scope.removeHostCopyDialogReady) { - scope.removeHostCopyDialogReady(); - } - scope.removeCopyDialogReady = scope.$on('HostCopyDialogReady', function() { - GenerateList.inject(GroupList, { - mode: 'lookup', - id: 'copy-host-select-container', - scope: scope - }); - }); - - if (scope.removeShowDialog) { - scope.removeShowDialog(); - } - scope.removeShowDialog = scope.$on('ShowDialog', function() { - var d; - scope.name = host.name; - scope.copy_choice = "copy"; - d = angular.element(document.getElementById('host-copy-dialog')); - $compile(d)(scope); - CreateDialog({ - id: 'host-copy-dialog', - scope: scope, - buttons: buttonSet, - width: 650, - height: 650, - minWidth: 600, - title: 'Copy or Move Host', - callback: 'HostCopyDialogReady', - onClose: function() { - scope.cancel(); - } - }); - }); - - Wait('start'); - - url = GetBasePath('hosts') + host_id + '/'; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - host = data; - scope.$emit('ShowDialog'); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET returned: ' + status }); - }); - - - scope.cancel = function() { - $(document).off("keydown"); - try { - $('#host-copy-dialog').dialog('close'); - } - catch(e) { - // ignore - } - scope.$destroy(); - }; - - scope['toggle_' + GroupList.iterator] = function (id) { - var count = 0, - list = GroupList; - scope[list.name].forEach( function(row, i) { - if (row.id === id) { - if (row.checked) { - scope[list.name][i].success_class = 'success'; - } - else { - scope[list.name][i].success_class = ''; - } - } else { - scope[list.name][i].checked = 0; - scope[list.name][i].success_class = ''; - } - }); - // Check if any rows are checked - scope[list.name].forEach(function(row) { - if (row.checked) { - count++; - } - }); - if (count === 0) { - $('#host-copy-ok-button').attr('disabled','disabled'); - } - else { - $('#host-copy-ok-button').removeAttr('disabled'); - } - }; - - scope.performCopy = function() { - var list = GroupList, - target, - url; - - Wait('start'); - - if (scope.use_root_group) { - target = null; - } - else { - scope[list.name].every(function(row) { - if (row.checked === 1) { - target = row; - return false; - } - return true; - }); - } - - if (scope.copy_choice === 'move') { - // Respond to move - - // disassociate the host from the original parent - if (scope.removeHostRemove) { - scope.removeHostRemove(); - } - scope.removeHostRemove = scope.$on('RemoveHost', function () { - if (parent_group > 0) { - // Only remove a host from a parent when the parent is a group and not the inventory root - url = GetBasePath('groups') + parent_group + '/hosts/'; - Rest.setUrl(url); - Rest.post({ id: host.id, disassociate: 1 }) - .success(function () { - scope.cancel(); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to remove ' + host.name + ' from group ' + parent_group + '. POST returned: ' + status }); - }); - } else { - scope.cancel(); - } - }); - - // add the new host to the target - url = GetBasePath('groups') + target.id + '/hosts/'; - Rest.setUrl(url); - Rest.post(host) - .success(function () { - scope.$emit('RemoveHost'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to add ' + host.name + ' to ' + target.name + '. POST returned: ' + status }); - }); - } - else { - // Respond to copy by adding the new host to the target - url = GetBasePath('groups') + target.id + '/hosts/'; - Rest.setUrl(url); - Rest.post(host) - .success(function () { - scope.cancel(); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to add ' + host.name + ' to ' + target.name + '. POST returned: ' + status - }); - }); - } - }; - - -}; -}]); diff --git a/awx/ui/client/src/helpers/JobSubmission.js b/awx/ui/client/src/helpers/JobSubmission.js deleted file mode 100644 index c4fd62434d..0000000000 --- a/awx/ui/client/src/helpers/JobSubmission.js +++ /dev/null @@ -1,337 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -'use strict'; - -export default -angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', -'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) - -.factory('CreateLaunchDialog', ['$compile', 'CreateDialog', 'Wait', 'ParseTypeChange', -function($compile, CreateDialog, Wait, ParseTypeChange) { - return function(params) { - var buttons, - scope = params.scope, - html = params.html, - // job_launch_data = {}, - callback = params.callback || 'PlaybookLaunchFinished', - // url = params.url, - e; - - // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; - html+=''; - $('#password-modal').empty().html(html); - $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); - e = angular.element(document.getElementById('password-modal')); - $compile(e)(scope); - - if(scope.prompt_for_vars===true){ - ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); - } - - buttons = [{ - label: "Cancel", - onClick: function() { - $('#password-modal').dialog('close'); - // scope.$emit('CancelJob'); - // scope.$destroy(); - }, - icon: "fa-times", - "class": "btn btn-default", - "id": "password-cancel-button" - },{ - label: "Launch", - onClick: function() { - scope.$emit(callback); - $('#password-modal').dialog('close'); - }, - icon: "fa-check", - "class": "btn btn-primary", - "id": "password-accept-button" - }]; - - CreateDialog({ - id: 'password-modal', - scope: scope, - buttons: buttons, - width: 620, - height: 700, //(scope.passwords.length > 1) ? 700 : 500, - minWidth: 500, - title: 'Launch Configuration', - callback: 'DialogReady', - onOpen: function(){ - Wait('stop'); - } - }); - - if (scope.removeDialogReady) { - scope.removeDialogReady(); - } - scope.removeDialogReady = scope.$on('DialogReady', function() { - $('#password-modal').dialog('open'); - $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); - e = angular.element(document.getElementById('password-accept-button')); - $compile(e)(scope); - }); - }; - - }]) - - .factory('PromptForPasswords', ['CredentialForm', - function(CredentialForm) { - return function(params) { - var scope = params.scope, - callback = params.callback || 'PasswordsAccepted', - url = params.url, - form = CredentialForm, - fld, field, - html=params.html || ""; - - scope.passwords = params.passwords; - - html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; - - scope.passwords.forEach(function(password) { - // Prompt for password - field = form.fields[password]; - fld = password; - scope[fld] = ''; - html += "
\n"; - html += "\n"; - html += "Please enter a password.
\n"; - html += "
\n"; - html += "
\n"; - - // Add the related confirm field - if (field.associated) { - fld = field.associated; - field = form.fields[field.associated]; - scope[fld] = ''; - html += "
\n"; - html += "\n"; - html += "Please confirm the password.\n"; - html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; - html += "
\n"; - html += "
\n"; - } - }); - - scope.$emit(callback, html, url); - - // Password change - scope.clearPWConfirm = function (fld) { - // If password value changes, make sure password_confirm must be re-entered - scope[fld] = ''; - scope.job_launch_form[fld].$setValidity('awpassmatch', false); - scope.checkStatus(); - }; - - scope.checkStatus = function() { - if (!scope.job_launch_form.$invalid) { - $('#password-accept-button').removeAttr('disabled'); - } - else { - $('#password-accept-button').attr({ "disabled": "disabled" }); - } - }; - }; - }]) - - .factory('CheckPasswords', ['Rest', 'GetBasePath', 'ProcessErrors', 'Empty', - function(Rest, GetBasePath, ProcessErrors, Empty) { - return function(params) { - var scope = params.scope, - callback = params.callback, - credential = params.credential; - - var passwords = []; - if (!Empty(credential)) { - Rest.setUrl(GetBasePath('credentials')+credential); - Rest.get() - .success(function (data) { - if(data.kind === "ssh"){ - if(data.password === "ASK" ){ - passwords.push("ssh_password"); - } - if(data.ssh_key_unlock === "ASK"){ - passwords.push("ssh_key_unlock"); - } - if(data.become_password === "ASK"){ - passwords.push("become_password"); - } - if(data.vault_password === "ASK"){ - passwords.push("vault_password"); - } - } - scope.$emit(callback, passwords); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - } - - }; - }]) - - // Submit SCM Update request - .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', - function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { - return function (params) { - var scope = params.scope, - project_id = params.project_id, - url = GetBasePath('projects') + project_id + '/update/', - project; - - if (scope.removeUpdateSubmitted) { - scope.removeUpdateSubmitted(); - } - scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { - // Refresh the project list after update request submitted - Wait('stop'); - if (/\d$/.test($location.path())) { - //Request submitted from projects/N page. Navigate back to the list so user can see status - $location.path('/projects'); - } - if (scope.socketStatus === 'error') { - Alert('Update Started', '
The request to start the SCM update process was submitted. ' + - 'To monitor the update status, refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); - if (scope.refresh) { - scope.refresh(); - } - } - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { - PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); - }); - - if (scope.removeStartTheUpdate) { - scope.removeStartTheUpdate(); - } - scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { - LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); - }); - - // Check to see if we have permission to perform the update and if any passwords are needed - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - project = data; - if (project.can_update) { - if (project.passwords_needed_to_updated) { - Wait('stop'); - scope.$emit('PromptForPasswords'); - } - else { - scope.$emit('StartTheUpdate', {}); - } - } - else { - Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', - 'alert-danger'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); - }); - }; - } - ]) - - // Submit Inventory Update request - .factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', - function (PromptForPasswords, LaunchJob, Rest, GetBasePath, ProcessErrors, Alert, Wait) { - return function (params) { - - var scope = params.scope, - url = params.url, - inventory_source; - - if (scope.removeUpdateSubmitted) { - scope.removeUpdateSubmitted(); - } - scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { - Wait('stop'); - if (scope.socketStatus === 'error') { - Alert('Sync Started', '
The request to start the inventory sync process was submitted. ' + - 'To monitor the status refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); - if (scope.refreshGroups) { - // inventory detail page - scope.refreshGroups(); - } - else if (scope.refresh) { - scope.refresh(); - } - } - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { - PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); - }); - - if (scope.removeStartTheUpdate) { - scope.removeStartTheUpdate(); - } - scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { - LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); - }); - - // Check to see if we have permission to perform the update and if any passwords are needed - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - inventory_source = data; - if (data.can_update) { - if (data.passwords_needed_to_update) { - Wait('stop'); - scope.$emit('PromptForPasswords'); - } - else { - scope.$emit('StartTheUpdate', {}); - } - } else { - Wait('stop'); - Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', - 'alert-danger'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js deleted file mode 100644 index 35d80e78f6..0000000000 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ /dev/null @@ -1,173 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:JobTemplatesHelper - * @description Routines shared by job related controllers - */ - -export default -angular.module('JobTemplatesHelper', ['Utilities']) - -/* - * Add bits to $scope for handling callback url help - * - */ - -.factory('CallbackHelpInit', ['$location', 'GetBasePath', 'Rest', 'JobTemplateForm', 'GenerateForm', '$stateParams', 'ProcessErrors', 'ParseTypeChange', - 'ParseVariableString', 'Empty', 'InventoryList', 'CredentialList','ProjectList', 'Wait', - function($location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors, ParseTypeChange, - ParseVariableString, Empty, InventoryList, CredentialList, ProjectList, Wait) { - return function(params) { - - var scope = params.scope, - defaultUrl = GetBasePath('job_templates'), - // generator = GenerateForm, - form = JobTemplateForm(), - // loadingFinishedCount = 0, - // base = $location.path().replace(/^\//, '').split('/')[0], - master = {}, - id = $stateParams.job_template_id; - // checkSCMStatus, getPlaybooks, callback, - // choicesCount = 0; - - CredentialList = _.cloneDeep(CredentialList); - - // The form uses awPopOverWatch directive to 'watch' scope.callback_help for changes. Each time the - // popover is activated, a function checks the value of scope.callback_help before constructing the content. - scope.setCallbackHelp = function() { - scope.callback_help = "

With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + - "template. The request from the host must be a POST. Here is an example using curl:

\n" + - "
curl --data \"host_config_key=" + scope.example_config_key + "\" " +
-                                  scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/
\n" + - "

Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + - "locate the host, the request will be denied.

" + - "

Successful requests create an entry on the Jobs page, where results and history can be viewed.

"; - }; - - // The md5 helper emits NewMD5Generated whenever a new key is available - if (scope.removeNewMD5Generated) { - scope.removeNewMD5Generated(); - } - scope.removeNewMD5Generated = scope.$on('NewMD5Generated', function() { - scope.configKeyChange(); - }); - - // Fired when user enters a key value - scope.configKeyChange = function() { - scope.example_config_key = scope.host_config_key; - scope.setCallbackHelp(); - }; - - // Set initial values and construct help text - scope.callback_server_path = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : ''); - scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; - scope.example_template_id = 'N'; - scope.setCallbackHelp(); - - // this fills the job template form both on copy of the job template - // and on edit - scope.fillJobTemplate = function(){ - // id = id || $rootScope.copy.id; - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl + id); - Rest.get() - .success(function (data) { - scope.job_template_obj = data; - scope.name = data.name; - var fld, i; - for (fld in form.fields) { - if (fld !== 'variables' && fld !== 'survey' && data[fld] !== null && data[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 (data[fld] === scope[fld + '_options'][i].value) { - scope[fld] = scope[fld + '_options'][i]; - } - } - } else { - scope[fld] = data[fld]; - } - } else { - scope[fld] = data[fld]; - if(!Empty(data.summary_fields.survey)) { - scope.survey_exists = true; - } - } - master[fld] = scope[fld]; - } - if (fld === 'variables') { - // Parse extra_vars, converting to YAML. - scope.variables = ParseVariableString(data.extra_vars); - master.variables = scope.variables; - } - if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } - if (form.fields[fld].type === 'checkbox_group') { - for(var j=0; j" + i18n._("Submit the request to cancel?") + "
"; - var deleteBody = "
" + i18n._("Are you sure you want to delete the job below?") + "
#" + id + " " + $filter('sanitize')(job.name) + "
"; - Prompt({ - hdr: hdr, - body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody, - action: action, - actionText: (action_label === 'cancel' || job.status === 'new') ? "OK" : "DELETE" - }); - }); - - if (action_label === 'cancel') { - Rest.setUrl(url); - Rest.get() - .success(function(data) { - if (data.can_cancel) { - scope.$emit('CancelJob'); - } - else { - scope.$emit('CancelNotAllowed'); - } - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - ' failed. GET returned: ' + status }); - }); - } - else { - scope.$emit('CancelJob'); - } - - }; - }]) - - .factory('RelaunchInventory', ['Find', 'Wait', 'Rest', 'InventoryUpdate', 'ProcessErrors', 'GetBasePath', - function(Find, Wait, Rest, InventoryUpdate, ProcessErrors, GetBasePath) { - return function(params) { - var scope = params.scope, - id = params.id, - url = GetBasePath('inventory_sources') + id + '/'; - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - InventoryUpdate({ - scope: scope, - url: data.related.update, - group_name: data.summary_fields.group.name, - group_source: data.source, - tree_id: null, - group_id: data.group - }); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + - url + ' GET returned: ' + status }); - }); - }; - }]) - - .factory('RelaunchPlaybook', ['InitiatePlaybookRun', function(InitiatePlaybookRun) { - return function(params) { - var scope = params.scope, - id = params.id, - job_type = params.job_type; - InitiatePlaybookRun({ scope: scope, id: id, relaunch: true, job_type: job_type }); - }; - }]) - - .factory('RelaunchSCM', ['ProjectUpdate', function(ProjectUpdate) { - return function(params) { - var scope = params.scope, - id = params.id; - ProjectUpdate({ scope: scope, project_id: id }); - }; - }]) - - .factory('RelaunchAdhoc', ['AdhocRun', function(AdhocRun) { - return function(params) { - var scope = params.scope, - id = params.id; - AdhocRun({ scope: scope, project_id: id, relaunch: true }); - }; - }]); diff --git a/awx/ui/client/src/helpers/Parse.js b/awx/ui/client/src/helpers/Parse.js deleted file mode 100644 index a2715f6efd..0000000000 --- a/awx/ui/client/src/helpers/Parse.js +++ /dev/null @@ -1,108 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Parse - * @description - * Show the CodeMirror variable editor and allow - * toggle between JSON and YAML - * - */ - -import 'codemirror/lib/codemirror.js'; -import 'codemirror/mode/javascript/javascript.js'; -import 'codemirror/mode/yaml/yaml.js'; -import 'codemirror/addon/lint/lint.js'; -import 'angular-codemirror/lib/yaml-lint.js'; -import 'codemirror/addon/edit/closebrackets.js'; -import 'codemirror/addon/edit/matchbrackets.js'; -import 'codemirror/addon/selection/active-line.js'; - - -export default - angular.module('ParseHelper', ['Utilities', 'AngularCodeMirrorModule']) - .factory('ParseTypeChange', ['Alert', 'AngularCodeMirror', - function (Alert, AngularCodeMirror) { - return function (params) { - - var scope = params.scope, - field_id = params.field_id, - fld = (params.variable) ? params.variable : 'variables', - pfld = (params.parse_variable) ? params.parse_variable : 'parseType', - onReady = params.onReady, - onChange = params.onChange, - readOnly = params.readOnly; - - function removeField(fld) { - //set our model to the last change in CodeMirror and then destroy CodeMirror - scope[fld] = scope[fld + 'codeMirror'].getValue(); - $('#cm-' + fld + '-container > .CodeMirror').empty().remove(); - } - - function createField(onChange, onReady, fld) { - //hide the textarea and show a fresh CodeMirror with the current mode (json or yaml) - - scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly); - scope[fld + 'codeMirror'].addModes(global.$AnsibleConfig.variable_edit_modes); - scope[fld + 'codeMirror'].showTextArea({ - scope: scope, - model: fld, - element: field_id, - lineNumbers: true, - mode: scope[pfld], - onReady: onReady, - onChange: onChange - }); - } - - // Hide the textarea and show a CodeMirror editor - createField(onChange, onReady, fld); - - - // Toggle displayed variable string between JSON and YAML - scope.parseTypeChange = function(model, fld) { - var json_obj; - if (scope[model] === 'json') { - // converting yaml to json - try { - removeField(fld); - json_obj = jsyaml.load(scope[fld]); - if ($.isEmptyObject(json_obj)) { - scope[fld] = "{}"; - } - else { - scope[fld] = JSON.stringify(json_obj, null, " "); - } - createField(onReady, onChange, fld); - } - catch (e) { - Alert('Parse Error', 'Failed to parse valid YAML. ' + e.message); - setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(onReady, onChange, fld); }); }, 500); - } - } - else { - // convert json to yaml - try { - removeField(fld); - json_obj = JSON.parse(scope[fld]); - if ($.isEmptyObject(json_obj)) { - scope[fld] = '---'; - } - else { - scope[fld] = jsyaml.safeDump(json_obj); - } - createField(onReady, onChange, fld); - } - catch (e) { - Alert('Parse Error', 'Failed to parse valid JSON. ' + e.message); - setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(onReady, onChange, fld); }); }, 500 ); - } - } - }; - }; - } - ]); diff --git a/awx/ui/client/src/helpers/ProjectPath.js b/awx/ui/client/src/helpers/ProjectPath.js deleted file mode 100644 index 83753be443..0000000000 --- a/awx/ui/client/src/helpers/ProjectPath.js +++ /dev/null @@ -1,91 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:ProjectPath - * @description - * Use GetProjectPath({ scope: , master: }) to - * load scope.project_local_paths (array of options for drop-down) and - * scope.base_dir (readonly field). - * - */ - -export default - angular.module('ProjectPathHelper', ['RestServices', 'Utilities']) - .factory('GetProjectPath', ['Alert', 'Rest', 'GetBasePath', 'ProcessErrors', - function (Alert, Rest, GetBasePath, ProcessErrors) { - return function (params) { - - var scope = params.scope, - master = params.master; - - function arraySort(data) { - //Sort nodes by name - var i, j, names = [], - newData = []; - for (i = 0; i < data.length; i++) { - names.push(data[i].value); - } - names.sort(); - for (j = 0; j < names.length; j++) { - for (i = 0; i < data.length; i++) { - if (data[i].value === names[j]) { - newData.push(data[i]); - } - } - } - return newData; - } - - scope.showMissingPlaybooksAlert = false; - - Rest.setUrl(GetBasePath('config')); - Rest.get() - .success(function (data) { - var opts = [], i; - if (data.project_local_paths) { - for (i = 0; i < data.project_local_paths.length; i++) { - opts.push({ - label: data.project_local_paths[i], - value: data.project_local_paths[i] - }); - } - } - if (scope.local_path) { - // List only includes paths not assigned to projects, so add the - // path assigned to the current project. - opts.push({ - label: scope.local_path, - value: scope.local_path - }); - } - scope.project_local_paths = arraySort(opts); - if (scope.local_path) { - for (i = 0; scope.project_local_paths.length; i++) { - if (scope.project_local_paths[i].value === scope.local_path) { - scope.local_path = scope.project_local_paths[i]; - break; - } - } - } - scope.base_dir = data.project_base_dir; - master.local_path = scope.local_path; - master.base_dir = scope.base_dir; // Keep in master object so that it doesn't get - // wiped out on form reset. - if (opts.length === 0) { - // trigger display of alert block when scm_type == manual - scope.showMissingPlaybooksAlert = true; - } - scope.$emit('pathsReady'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to access API config. GET status: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Projects.js b/awx/ui/client/src/helpers/Projects.js deleted file mode 100644 index 613dac0c86..0000000000 --- a/awx/ui/client/src/helpers/Projects.js +++ /dev/null @@ -1,84 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Projects - * @description - * Use GetProjectPath({ scope: , master: }) to - * load scope.project_local_paths (array of options for drop-down) and - * scope.base_dir (readonly field). - * - */ - - -export default - angular.module('ProjectsHelper', ['RestServices', 'Utilities', 'ProjectStatusDefinition', 'ProjectFormDefinition']) - - .factory('GetProjectIcon', [ function() { - return function(status) { - var result = ''; - switch (status) { - case 'n/a': - case 'ok': - case 'never updated': - result = 'none'; - break; - case 'pending': - case 'waiting': - case 'new': - result = 'none'; - break; - case 'updating': - case 'running': - result = 'running'; - break; - case 'successful': - result = 'success'; - break; - case 'failed': - case 'missing': - case 'canceled': - result = 'error'; - } - return result; - }; - }]) - - .factory('GetProjectToolTip', ['i18n', function(i18n) { - return function(status) { - var result = ''; - switch (status) { - case 'n/a': - case 'ok': - case 'never updated': - result = i18n._('No SCM updates have run for this project'); - break; - case 'pending': - case 'waiting': - case 'new': - result = i18n._('Queued. Click for details'); - break; - case 'updating': - case 'running': - result = i18n._('Running! Click for details'); - break; - case 'successful': - result = i18n._('Success! Click for details'); - break; - case 'failed': - result = i18n._('Failed. Click for details'); - break; - case 'missing': - result = i18n._('Missing. Click for details'); - break; - case 'canceled': - result = i18n._('Canceled. Click for details'); - break; - } - return result; - }; - }]); diff --git a/awx/ui/client/src/helpers/Schedules.js b/awx/ui/client/src/helpers/Schedules.js deleted file mode 100644 index 90c1589e5b..0000000000 --- a/awx/ui/client/src/helpers/Schedules.js +++ /dev/null @@ -1,506 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Schedules - * @description - * Schedules Helper - * - * Display the scheduler widget in a dialog - * - */ - -import listGenerator from '../shared/list-generator/main'; - -export default - angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', listGenerator.name, 'ModalDialog', - 'GeneratorHelpers']) - - .factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest', - 'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state', - function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors, - GetBasePath, SchedulePost, $state) { - return function(params) { - var scope = params.scope, - id = params.id, - callback = params.callback, - schedule, scheduler, - url = GetBasePath('schedules') + id + '/'; - - delete scope.isFactCleanup; - delete scope.cleanupJob; - - function setGranularity(){ - var a,b, prompt_for_days, - keep_unit, - granularity, - granularity_keep_unit; - - if(scope.cleanupJob){ - scope.schedulerPurgeDays = Number(schedule.extra_data.days); - // scope.scheduler_form.schedulerPurgeDays.$setViewValue( Number(schedule.extra_data.days)); - } - else if(scope.isFactCleanup){ - scope.keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.granularity_keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - // the API returns something like 20w or 1y - a = schedule.extra_data.older_than; // "20y" - b = schedule.extra_data.granularity; // "1w" - prompt_for_days = Number(_.initial(a,1).join('')); // 20 - keep_unit = _.last(a); // "y" - granularity = Number(_.initial(b,1).join('')); // 1 - granularity_keep_unit = _.last(b); // "w" - - scope.keep_amount = prompt_for_days; - scope.granularity_keep_amount = granularity; - scope.keep_unit = _.find(scope.keep_unit_choices, function(i){ - return i.value === keep_unit; - }); - scope.granularity_keep_unit =_.find(scope.granularity_keep_unit_choices, function(i){ - return i.value === granularity_keep_unit; - }); - } - } - - if (scope.removeScheduleFound) { - scope.removeScheduleFound(); - } - scope.removeScheduleFound = scope.$on('ScheduleFound', function() { - $('#form-container').empty(); - scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); - scheduler.inject('form-container', false); - scheduler.injectDetail('occurrences', false); - - if (!/DTSTART/.test(schedule.rrule)) { - schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); - } - schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); - schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); - scope.$on("htmlDetailReady", function() { - scheduler.setRRule(schedule.rrule); - scheduler.setName(schedule.name); - $rootScope.$broadcast("ScheduleFormCreated", scope); - }); - scope.showRRuleDetail = false; - - scheduler.setRRule(schedule.rrule); - scheduler.setName(schedule.name); - if(scope.isFactCleanup || scope.cleanupJob){ - setGranularity(); - } - }); - - - if (scope.removeScheduleSaved) { - scope.removeScheduleSaved(); - } - scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { - Wait('stop'); - if (callback) { - scope.$emit(callback, data); - } - $state.go("^"); - }); - scope.saveSchedule = function() { - schedule.extra_data = scope.extraVars; - SchedulePost({ - scope: scope, - url: url, - scheduler: scheduler, - callback: 'ScheduleSaved', - mode: 'edit', - schedule: schedule - }); - }; - - Wait('start'); - - // Get the existing record - Rest.setUrl(url); - Rest.get() - .success(function(data) { - schedule = data; - try { - schedule.extra_data = JSON.parse(schedule.extra_data); - } catch(e) { - // do nothing - } - scope.extraVars = data.extra_data === '' ? '---' : '---\n' + jsyaml.safeDump(data.extra_data); - - if(schedule.extra_data.hasOwnProperty('granularity')){ - scope.isFactCleanup = true; - } - if (schedule.extra_data.hasOwnProperty('days')){ - scope.cleanupJob = true; - } - - scope.schedule_obj = data; - - scope.$emit('ScheduleFound'); - }) - .error(function(data,status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); - }); - }; - }]) - - .factory('AddSchedule', ['$location', '$rootScope', '$stateParams', - 'SchedulerInit', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', '$state', 'Rest', 'ProcessErrors', - function($location, $rootScope, $stateParams, SchedulerInit, - Wait, GetBasePath, Empty, SchedulePost, $state, Rest, - ProcessErrors) { - return function(params) { - var scope = params.scope, - callback= params.callback, - base = params.base || $location.path().replace(/^\//, '').split('/')[0], - url = params.url || null, - scheduler, - job_type; - - job_type = scope.parentObject.job_type; - if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !url) { - url = GetBasePath(base) + $stateParams.id + '/schedules/'; - } - else if(base === "inventories"){ - if (!params.url){ - url = GetBasePath('groups') + $stateParams.id + '/'; - Rest.setUrl(url); - Rest.get(). - then(function (data) { - url = data.data.related.inventory_source + 'schedules/'; - }).catch(function (response) { - ProcessErrors(null, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get inventory group info. GET returned status: ' + - response.status - }); - }); - } - else { - url = params.url; - } - } - else if (base === 'system_job_templates') { - url = GetBasePath(base) + $stateParams.id + '/schedules/'; - if(job_type === "cleanup_facts"){ - scope.isFactCleanup = true; - scope.keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.granularity_keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.prompt_for_days_facts_form.keep_amount.$setViewValue(30); - scope.prompt_for_days_facts_form.granularity_keep_amount.$setViewValue(1); - scope.keep_unit = scope.keep_unit_choices[0]; - scope.granularity_keep_unit = scope.granularity_keep_unit_choices[1]; - } - else { - scope.cleanupJob = true; - } - } - - Wait('start'); - $('#form-container').empty(); - scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); - if(scope.schedulerUTCTime) { - // The UTC time is already set - scope.processSchedulerEndDt(); - } - else { - // We need to wait for it to be set by angular-scheduler because the following function depends - // on it - var schedulerUTCTimeWatcher = scope.$watch('schedulerUTCTime', function(newVal) { - if(newVal) { - // Remove the watcher - schedulerUTCTimeWatcher(); - scope.processSchedulerEndDt(); - } - }); - } - scheduler.inject('form-container', false); - scheduler.injectDetail('occurrences', false); - scheduler.clear(); - scope.$on("htmlDetailReady", function() { - $rootScope.$broadcast("ScheduleFormCreated", scope); - }); - scope.showRRuleDetail = false; - - if (scope.removeScheduleSaved) { - scope.removeScheduleSaved(); - } - scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { - Wait('stop'); - if (callback) { - scope.$emit(callback, data); - } - $state.go("^", null, {reload: true}); - }); - scope.saveSchedule = function() { - SchedulePost({ - scope: scope, - url: url, - scheduler: scheduler, - callback: 'ScheduleSaved', - mode: 'add' - }); - }; - - $('#scheduler-tabs li a').on('shown.bs.tab', function(e) { - if ($(e.target).text() === 'Details') { - if (!scheduler.isValid()) { - $('#scheduler-tabs a:first').tab('show'); - } - } - }); - }; - }]) - - .factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait', - function(Rest, ProcessErrors, RRuleToAPI, Wait) { - return function(params) { - var scope = params.scope, - url = params.url, - scheduler = params.scheduler, - mode = params.mode, - schedule = (params.schedule) ? params.schedule : {}, - callback = params.callback, - newSchedule, rrule, extra_vars; - if (scheduler.isValid()) { - Wait('start'); - newSchedule = scheduler.getValue(); - rrule = scheduler.getRRule(); - schedule.name = newSchedule.name; - schedule.rrule = RRuleToAPI(rrule.toString()); - schedule.description = (/error/.test(rrule.toText())) ? '' : rrule.toText(); - - if (scope.isFactCleanup) { - extra_vars = { - "older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value, - "granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value - }; - schedule.extra_data = JSON.stringify(extra_vars); - } else if (scope.cleanupJob) { - extra_vars = { - "days" : scope.scheduler_form.schedulerPurgeDays.$viewValue - }; - schedule.extra_data = JSON.stringify(extra_vars); - } - else if(scope.extraVars){ - schedule.extra_data = scope.parseType === 'yaml' ? - (scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars; - } - Rest.setUrl(url); - if (mode === 'add') { - Rest.post(schedule) - .success(function(){ - if (callback) { - scope.$emit(callback); - } - else { - Wait('stop'); - } - }) - .error(function(data, status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'POST to ' + url + ' returned: ' + status }); - }); - } - else { - Rest.put(schedule) - .success(function(){ - if (callback) { - scope.$emit(callback, schedule); - } - else { - Wait('stop'); - } - }) - .error(function(data, status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'POST to ' + url + ' returned: ' + status }); - }); - } - } - else { - return false; - } - }; - }]) - - /** - * Flip a schedule's enable flag - * - * ToggleSchedule({ - * scope: scope, - * id: schedule.id to update - * callback: scope.$emit label to call when update completes - * }); - * - */ - .factory('ToggleSchedule', ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', '$state', - function(Wait, GetBasePath, ProcessErrors, Rest, $state) { - return function(params) { - var scope = params.scope, - id = params.id, - url = GetBasePath('schedules') + id +'/'; - - // Perform the update - if (scope.removeScheduleFound) { - scope.removeScheduleFound(); - } - scope.removeScheduleFound = scope.$on('ScheduleFound', function(e, data) { - data.enabled = (data.enabled) ? false : true; - Rest.put(data) - .success( function() { - Wait('stop'); - $state.go('.', null, {reload: true}); - }) - .error( function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to update schedule ' + id + ' PUT returned: ' + status }); - }); - }); - - Wait('start'); - - // Get the schedule - Rest.setUrl(url); - Rest.get() - .success(function(data) { - scope.$emit('ScheduleFound', data); - }) - .error(function(data,status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); - }); - }; - }]) - - /** - * Delete a schedule. Prompts user to confirm delete - * - * DeleteSchedule({ - * scope: $scope containing list of schedules - * id: id of schedule to delete - * callback: $scope.$emit label to call when delete is completed - * }) - * - */ - .factory('DeleteSchedule', ['GetBasePath','Rest', 'Wait', '$state', - 'ProcessErrors', 'Prompt', 'Find', '$location', '$filter', - function(GetBasePath, Rest, Wait, $state, ProcessErrors, Prompt, Find, - $location, $filter) { - return function(params) { - - var scope = params.scope, - id = params.id, - callback = params.callback, - action, schedule, list, url, hdr; - - if (scope.schedules) { - list = scope.schedules; - } - else if (scope.scheduled_jobs) { - list = scope.scheduled_jobs; - } - - url = GetBasePath('schedules') + id + '/'; - schedule = Find({list: list, key: 'id', val: id }); - hdr = 'Delete Schedule'; - - action = function () { - Wait('start'); - Rest.setUrl(url); - Rest.destroy() - .success(function () { - $('#prompt-modal').modal('hide'); - scope.$emit(callback, id); - if (new RegExp('/' + id + '$').test($location.$$url)) { - $location.url($location.url().replace(/[/][0-9]+$/, "")); // go to list view - } - else{ - $state.go('.', null, {reload: true}); - } - }) - .error(function (data, status) { - try { - $('#prompt-modal').modal('hide'); - } - catch(e) { - // ignore - } - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - ' failed. DELETE returned: ' + status }); - }); - }; - - Prompt({ - hdr: hdr, - body: '
Are you sure you want to delete the schedule below?
' + $filter('sanitize')(schedule.name) + '
', - action: action, - actionText: 'DELETE', - backdrop: false - }); - - }; - }]) - - /** - * Convert rrule string to an API agreeable format - * - */ - .factory('RRuleToAPI', [ function() { - return function(rrule) { - var response; - response = rrule.replace(/(^.*(?=DTSTART))(DTSTART=.*?;)(.*$)/, function(str, p1, p2, p3) { - return p2.replace(/\;/,'').replace(/=/,':') + ' ' + 'RRULE:' + p1 + p3; - }); - return response; - }; - }]); diff --git a/awx/ui/client/src/helpers/Selection.js b/awx/ui/client/src/helpers/Selection.js deleted file mode 100644 index 3fa4fa826b..0000000000 --- a/awx/ui/client/src/helpers/Selection.js +++ /dev/null @@ -1,177 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Selection - * @description - * SelectionHelper - * Used in list controllers where the list might also be used as a selection list. - * - * SelectionInit( { - * scope: , - * list: - * }) - */ - - -export default - angular.module('SelectionHelper', ['Utilities', 'RestServices']) - - .factory('SelectionInit', ['Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'Wait', - function (Rest, Alert, ProcessErrors, ReturnToCaller, Wait) { - return function (params) { - - var scope = params.scope, - list = params.list, - target_url = params.url, - returnToCaller = params.returnToCaller, - selected; - - if (params.selected !== undefined) { - selected = params.selected; - } else { - selected = []; //array of selected row IDs - } - - scope.formModalActionDisabled = true; - scope.disableSelectBtn = true; - - // toggle row selection - scope['toggle_' + list.iterator] = function (id, ischeckbox) { - var i, j, found; - for (i = 0; i < scope[list.name].length; i++) { - if (scope[list.name][i].id === id) { - var control = scope[list.name][i]; - if (ischeckbox && control.checked) { - scope[list.name][i].success_class = 'success'; - // add selected object to the array - found = false; - for (j = 0; j < selected.length; j++) { - if (selected[j].id === id) { - found = true; - break; - } - } - if (!found) { - selected.push(scope[list.name][i]); - } - } else if (ischeckbox) { - scope[list.name][i].success_class = ''; - - // remove selected object from the array - for (j = 0; j < selected.length; j++) { - if (selected[j].id === id) { - selected.splice(j, 1); - break; - } - } - } - } - } - if (selected.length > 0) { - scope.formModalActionDisabled = false; - scope.disableSelectBtn = false; - } else { - scope.formModalActionDisabled = true; - scope.disableSelectBtn = true; - } - }; - - // Add the selections - scope.finishSelection = function () { - Rest.setUrl(target_url); - - var queue = [], j; - - scope.formModalActionDisabled = true; - scope.disableSelectBtn = true; - - Wait('start'); - - function finished() { - selected = []; - if (returnToCaller !== undefined) { - ReturnToCaller(returnToCaller); - } else { - $('#form-modal').modal('hide'); - scope.$emit('modalClosed'); - } - } - - function postIt(data) { - Rest.post(data) - .success(function (data, status) { - queue.push({ result: 'success', data: data, status: status }); - scope.$emit('callFinished'); - }) - .error(function (data, status, headers) { - queue.push({ result: 'error', data: data, status: status, headers: headers }); - scope.$emit('callFinished'); - }); - } - - if (scope.callFinishedRemove) { - scope.callFinishedRemove(); - } - scope.callFinishedRemove = scope.$on('callFinished', function () { - // We call the API for each selected item. We need to hang out until all the api - // calls are finished. - var i, errors=0; - if (queue.length === selected.length) { - Wait('stop'); - for (i = 0; i < queue.length; i++) { - if (queue[i].result === 'error') { - ProcessErrors(scope, queue[i].data, queue[i].status, null, { hdr: 'POST Failure', - msg: 'Failed to add ' + list.iterator + '. POST returned status: ' + queue[i].status }); - errors++; - } - } - if (errors === 0) { - finished(); - } - } - }); - - if (selected.length > 0) { - for (j = 0; j < selected.length; j++) { - postIt(selected[j]); - } - } else { - finished(); - } - }; - - scope.formModalAction = scope.finishSelection; - - // Initialize our data set after a refresh (page change or search) - if (scope.SelectPostRefreshRemove) { - scope.SelectPostRefreshRemove(); - } - scope.SelectPostRefreshRemove = scope.$on('PostRefresh', function () { - var i, j, found; - if (scope[list.name]) { - for (i = 0; i < scope[list.name].length; i++) { - found = false; - for (j = 0; j < selected.length; j++) { - if (selected[j].id === scope[list.name][i].id) { - found = true; - break; - } - } - if (found) { - scope[list.name][i].checked = '1'; - scope[list.name][i].success_class = 'success'; - } else { - scope[list.name][i].checked = '0'; - scope[list.name][i].success_class = ''; - } - } - } - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Users.js b/awx/ui/client/src/helpers/Users.js deleted file mode 100644 index 7f09b486c3..0000000000 --- a/awx/ui/client/src/helpers/Users.js +++ /dev/null @@ -1,46 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Users - * @description - * UserHelper - * Routines shared amongst the user controllers - * - */ - - -export default - angular.module('UserHelper', ['UserFormDefinition']) - .factory('ResetForm', ['UserForm', - function (UserForm) { - return function () { - // Restore form to default conditions. Run before applying LDAP configuration. - // LDAP may manage some or all of these fields in which case the user cannot - // make changes to their values in AWX. - - UserForm.fields.first_name.readonly = false; - UserForm.fields.first_name.editRequired = true; - UserForm.fields.last_name.readonly = false; - UserForm.fields.last_name.editRequired = true; - UserForm.fields.email.readonly = false; - UserForm.fields.email.editRequired = true; - UserForm.fields.organization.awRequiredWhen = { - reqExpression: "orgrequired", - init: true - }; - UserForm.fields.organization.readonly = false; - UserForm.fields.username.awRequiredWhen = { - reqExpression: "not_ldap_user", - init: true - }; - UserForm.fields.username.readonly = false; - UserForm.fields.password.editRequired = false; - UserForm.fields.password.addRrequired = true; - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Variables.js b/awx/ui/client/src/helpers/Variables.js deleted file mode 100644 index 8364c4c15b..0000000000 --- a/awx/ui/client/src/helpers/Variables.js +++ /dev/null @@ -1,180 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Variables - * @description - * VariablesHelper - * - * - */ - -export default -angular.module('VariablesHelper', ['Utilities']) - - /** - variables: string containing YAML or JSON | a JSON object. - - If JSON string, convert to JSON object and run through jsyaml.safeDump() to create a YAML document. If YAML, - will attempt to load via jsyaml.safeLoad() and return a YAML document using jsyaml.safeDump(). In all cases - a YAML document is returned. - **/ - .factory('ParseVariableString', ['$log', 'ProcessErrors', 'SortVariables', function ($log, ProcessErrors, SortVariables) { - return function (variables) { - var result = "---", json_obj; - if (typeof variables === 'string') { - if (variables === "{}" || variables === "null" || variables === "" || variables === "\"\"") { - // String is empty, return --- - } else { - try { - json_obj = JSON.parse(variables); - json_obj = SortVariables(json_obj); - result = jsyaml.safeDump(json_obj); - - } - catch (e) { - $log.debug('Attempt to parse extra_vars as JSON failed. Check that the variables parse as yaml. Set the raw string as the result.'); - try { - // do safeLoad, which well error if not valid yaml - json_obj = jsyaml.safeLoad(variables); - // but just send the variables - result = variables; - } - catch(e2) { - ProcessErrors(null, variables, e2.message, null, { hdr: 'Error!', - msg: 'Attempts to parse variables as JSON and YAML failed. Last attempt returned: ' + e2.message }); - } - } - } - } - else { - if ($.isEmptyObject(variables) || variables === null) { - // Empty object, return --- - } - else { - // convert object to yaml - try { - json_obj = SortVariables(variables); - result = jsyaml.safeDump(json_obj); - // result = variables; - } - catch(e3) { - ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!', - msg: 'Attempt to convert JSON object to YAML document failed: ' + e3.message }); - } - } - } - return result; - }; - }]) - - /** - parseType: 'json' | 'yaml' - variables: string containing JSON or YAML - stringify: optional, boolean - - Parse the given string according to the parseType to a JSON object. If stringify true, - stringify the object and return the string. Otherwise, return the JSON object. - - **/ - .factory('ToJSON', ['$log', 'ProcessErrors', function($log, ProcessErrors) { - return function(parseType, variables, stringify, reviver) { - var json_data, - result, - tmp; - // bracketVar, - // key, - // lines, i, newVars = []; - if (parseType === 'json') { - try { - // perform a check to see if the user cleared the field completly - if(variables.trim() === "" || variables.trim() === "{" || variables.trim() === "}" ){ - variables = "{}"; - } - //parse a JSON string - if (reviver) { - json_data = JSON.parse(variables, reviver); - } - else { - json_data = JSON.parse(variables); - } - } - catch(e) { - json_data = {}; - $log.error('Failed to parse JSON string. Parser returned: ' + e.message); - ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', - msg: 'Failed to parse JSON string. Parser returned: ' + e.message }); - throw 'Parse error. Failed to parse variables.'; - } - } else { - try { - if(variables.trim() === "" || variables.trim() === "-" || variables.trim() === "--"){ - variables = '---'; - } - json_data = jsyaml.safeLoad(variables); - if(json_data!==null){ - // unparsing just to make sure no weird characters are included. - tmp = jsyaml.dump(json_data); - if(tmp.indexOf('[object Object]')!==-1){ - throw "Failed to parse YAML string. Parser returned' + key + ' : ' +value + '.' "; - } - } - } - catch(e) { - json_data = undefined; // {}; - // $log.error('Failed to parse YAML string. Parser returned undefined'); - ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', - msg: 'Failed to parse YAML string. Parser returned undefined'}); - } - } - // Make sure our JSON is actually an object - if (typeof json_data !== 'object') { - ProcessErrors(null, variables, null, null, { hdr: 'Error!', - msg: 'Failed to parse variables. Attempted to parse ' + parseType + '. Parser did not return an object.' }); - // setTimeout( function() { - throw 'Parse error. Failed to parse variables.'; - // }, 1000); - } - result = json_data; - if (stringify) { - if(json_data === undefined){ - result = undefined; - } - else if ($.isEmptyObject(json_data)) { - result = ""; - } else { - // utilize the parsing to get here - // but send the raw variable string - result = variables; - } - } - return result; - }; - }]) - - .factory('SortVariables', [ function() { - return function(variableObj) { - var newObj; - function sortIt(objToSort) { - var i, - keys = Object.keys(objToSort), - newObj = {}; - keys = keys.sort(); - for (i=0; i < keys.length; i++) { - if (typeof objToSort[keys[i]] === 'object' && objToSort[keys[i]] !== null && !Array.isArray(objToSort[keys[i]])) { - newObj[keys[i]] = sortIt(objToSort[keys[i]]); - } - else { - newObj[keys[i]] = objToSort[keys[i]]; - } - } - return newObj; - } - newObj = sortIt(variableObj); - return newObj; - }; - }]); diff --git a/awx/ui/client/src/helpers/api-defaults.js b/awx/ui/client/src/helpers/api-defaults.js deleted file mode 100644 index bf9308b0da..0000000000 --- a/awx/ui/client/src/helpers/api-defaults.js +++ /dev/null @@ -1,94 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:api-defaults - * @description this could use more discussion -*/ - -export default - angular.module('APIDefaults', ['RestServices', 'Utilities']) - .factory('GetAPIDefaults', ['Alert', 'Rest', '$rootScope', - function (Alert, Rest, $rootScope) { - return function (key) { - - //Reload a related collection on pagination or search change - - var result = {}, cnt = 0, url; - - function lookup(key) { - var id, result = {}; - for (id in $rootScope.apiDefaults) { - if (id === key || id.iterator === key) { - result[id] = $rootScope.apiDefaults[id]; - break; - } - } - return result; - } - - function wait() { - if ($.isEmptyObject(result) && cnt < 5) { - cnt++; - setTimeout(1000, wait()); - } else if (result.status === 'success') { - return lookup(key); - } - } - - if ($rootScope.apiDefaults === null || $rootScope.apiDefaults === undefined) { - url = '/api/v1/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - var id, defaults = data; - for (id in defaults) { - switch (id) { - case 'organizations': - defaults[id].iterator = 'organization'; - break; - case 'jobs': - defaults[id].iterator = 'job'; - break; - case 'users': - defaults[id].iterator = 'user'; - break; - case 'teams': - defaults[id].iterator = 'team'; - break; - case 'hosts': - defaults[id].iterator = 'host'; - break; - case 'groups': - defaults[id].iterator = 'group'; - break; - case 'projects': - defaults[id].iterator = 'project'; - break; - case 'inventories': - defaults[id].iterator = 'inventory'; - break; - } - } - $rootScope.apiDefaults = defaults; - result = { - status: 'success' - }; - }) - .error(function (data, status) { - result = { - status: 'error', - msg: 'Call to ' + url + ' failed. GET returned status: ' + status - }; - }); - return wait(); - } else { - return lookup(key); - } - }; - } - ]); diff --git a/awx/ui/client/src/helpers/inventory.js b/awx/ui/client/src/helpers/inventory.js deleted file mode 100644 index 1310899fa0..0000000000 --- a/awx/ui/client/src/helpers/inventory.js +++ /dev/null @@ -1,84 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Inventory - * @description InventoryHelper - * Routines for building the tree. Everything related to the tree is here except - * for the menu piece. The routine for building the menu is in InventoriesEdit controller - * (controllers/Inventories.js) -*/ - -import listGenerator from '../shared/list-generator/main'; - -export default - angular.module('InventoryHelper', ['RestServices', 'Utilities', 'OrganizationListDefinition', listGenerator.name, - 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'VariablesHelper', - ]) - - .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'OrganizationList', - 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', - function (InventoryForm, Rest, Alert, ProcessErrors, OrganizationList, GetBasePath, ParseTypeChange, Wait, - ToJSON) { - return function (params) { - - // Save inventory property modifications - - var scope = params.scope, - form = InventoryForm, - defaultUrl = GetBasePath('inventory'), - fld, json_data, data; - - Wait('start'); - - // Make sure we have valid variable data - json_data = ToJSON(scope.parseType, scope.variables); - - data = {}; - for (fld in form.fields) { - if (fld !== 'variables') { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = scope[fld]; - } else { - data[fld] = scope[fld]; - } - } - } - - if (scope.removeUpdateInventoryVariables) { - scope.removeUpdateInventoryVariables(); - } - scope.removeUpdateInventoryVariables = scope.$on('UpdateInventoryVariables', function(e, data) { - Rest.setUrl(data.related.variable_data); - Rest.put(json_data) - .success(function () { - Wait('stop'); - scope.$emit('InventorySaved'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to update inventory varaibles. PUT returned status: ' + status - }); - }); - }); - - Rest.setUrl(defaultUrl + scope.inventory_id + '/'); - Rest.put(data) - .success(function (data) { - if (scope.variables) { - scope.$emit('UpdateInventoryVariables', data); - } else { - scope.$emit('InventorySaved'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to update inventory. PUT returned status: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/md5.js b/awx/ui/client/src/helpers/md5.js deleted file mode 100644 index 26590be8fc..0000000000 --- a/awx/ui/client/src/helpers/md5.js +++ /dev/null @@ -1,46 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:md5 - * @description - * Run md5Setup({ scope: , master:, check_field:, default_val: }) - * to initialize md5 fields (checkbox and text field). - * discussion - */ - - -export default - angular.module('md5Helper', ['RestServices', 'Utilities', require('angular-md5')]) - .factory('md5Setup', ['md5', function (md5) { - return function (params) { - - var scope = params.scope, - master = params.master, - check_field = params.check_field, - default_val = params.default_val; - - scope[check_field] = default_val; - master[check_field] = default_val; - - scope.genMD5 = function (fld) { - var now = new Date(); - scope[fld] = md5.createHash('AnsibleWorks' + now.getTime()); - scope.$emit('NewMD5Generated'); - }; - - scope.toggleCallback = function (fld) { - if (scope.allow_callbacks === false) { - scope[fld] = ''; - } - }; - - scope.selectAll = function (fld) { - $('input[name="' + fld + '"]').focus().select(); - }; - }; - }]); diff --git a/awx/ui/client/src/helpers/teams.js b/awx/ui/client/src/helpers/teams.js deleted file mode 100644 index 936023f003..0000000000 --- a/awx/ui/client/src/helpers/teams.js +++ /dev/null @@ -1,73 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:teams - * @description - * TeamHelper - * Routines shared amongst the team controllers - */ - -import listGenerator from '../shared/list-generator/main'; - -export default - angular.module('TeamHelper', ['RestServices', 'Utilities', 'OrganizationListDefinition', listGenerator.name - ]) - .factory('SetTeamListeners', ['Alert', 'Rest', - function (Alert, Rest) { - return function (params) { - - var scope = params.scope, - set = params.set; - - // Listeners to perform lookups after main inventory list loads - - scope.$on('TeamResultFound', function (e, results, lookup_results) { - var i, j, key, property; - if (lookup_results.length === results.length) { - key = 'organization'; - property = 'organization_name'; - for (i = 0; i < results.length; i++) { - for (j = 0; j < lookup_results.length; j++) { - if (results[i][key] === lookup_results[j].id) { - results[i][property] = lookup_results[j].value; - } - } - } - - scope[set] = results; - } - }); - - scope.$on('TeamRefreshFinished', function (e, results) { - // Loop through the result set (sent to us by the search helper) and - // lookup the id and name of each organization. After each lookup - // completes, call resultFound. - - var i, lookup_results = [], url; - - function getOrganization(url) { - Rest.setUrl(url); - Rest.get() - .success(function (data) { - lookup_results.push({ id: data.id, value: data.name }); - scope.$emit('TeamResultFound', results, lookup_results); - }) - .error(function () { - lookup_results.push({ id: 'error' }); - scope.$emit('TeamResultFound', results, lookup_results); - }); - } - - for (i = 0; i < results.length; i++) { - url = '/api/v1/organizations/' + results[i].organization + '/'; - getOrganization(url); - } - }); - }; - } - ]); diff --git a/awx/ui/client/src/home/dashboard/lists/job-templates/main.js b/awx/ui/client/src/home/dashboard/lists/job-templates/main.js index 1a47bb600b..9825630182 100644 --- a/awx/ui/client/src/home/dashboard/lists/job-templates/main.js +++ b/awx/ui/client/src/home/dashboard/lists/job-templates/main.js @@ -1,6 +1,5 @@ import JobTemplatesListDirective from './job-templates-list.directive'; import systemStatus from '../../../../smart-status/main'; -import jobSubmissionHelper from '../../../../helpers/JobSubmission'; -export default angular.module('JobTemplatesList', [systemStatus.name, jobSubmissionHelper.name]) +export default angular.module('JobTemplatesList', [systemStatus.name]) .directive('jobTemplatesList', JobTemplatesListDirective); diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js new file mode 100644 index 0000000000..19a846c414 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js @@ -0,0 +1,33 @@ +export default + function GetHostsStatusMsg() { + return function(params) { + var active_failures = params.active_failures, + total_hosts = params.total_hosts, + tip, failures, html_class; + + // Return values for use on host status indicator + + if (active_failures > 0) { + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; + html_class = 'error'; + failures = true; + } else { + failures = false; + if (total_hosts === 0) { + // no hosts + tip = "Contains 0 hosts."; + html_class = 'none'; + } else { + // many hosts with 0 failures + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; + html_class = 'success'; + } + } + + return { + tooltip: tip, + failures: failures, + 'class': html_class + }; + }; + } diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js new file mode 100644 index 0000000000..befef8a499 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js @@ -0,0 +1,37 @@ +export default + function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + variable = params.variable; + + if (scope[variable] === undefined) { + scope[variable] = []; + Rest.setUrl(GetBasePath('inventory_sources')); + Rest.options() + .success(function (data) { + var i, choices = data.actions.GET.source.choices; + for (i = 0; i < choices.length; i++) { + if (choices[i][0] !== 'file') { + scope[variable].push({ + label: choices[i][1], + value: choices[i][0] + }); + } + } + scope.cloudCredentialRequired = false; + scope.$emit('sourceTypeOptionsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status + }); + }); + } + }; + } + +GetSourceTypeOptions.$inject = + [ 'Rest', + 'ProcessErrors', + 'GetBasePath' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js new file mode 100644 index 0000000000..2541abcc27 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js @@ -0,0 +1,77 @@ +export default + function GetSyncStatusMsg(Empty) { + return function(params) { + var status = params.status, + source = params.source, + has_inventory_sources = params.has_inventory_sources, + launch_class = '', + launch_tip = 'Start sync process', + schedule_tip = 'Schedule future inventory syncs', + stat, stat_class, status_tip; + + stat = status; + stat_class = stat; + + switch (status) { + case 'never updated': + stat = 'never'; + stat_class = 'na'; + status_tip = 'Sync not performed. Click to start it now.'; + break; + case 'none': + case 'ok': + case '': + launch_class = 'btn-disabled'; + stat = 'n/a'; + stat_class = 'na'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + break; + case 'canceled': + status_tip = 'Sync canceled. Click to view log.'; + break; + case 'failed': + status_tip = 'Sync failed. Click to view log.'; + break; + case 'successful': + status_tip = 'Sync completed. Click to view log.'; + break; + case 'pending': + status_tip = 'Sync pending.'; + launch_class = "btn-disabled"; + launch_tip = "Sync pending"; + break; + case 'updating': + case 'running': + launch_class = "btn-disabled"; + launch_tip = "Sync running"; + status_tip = "Sync running. Click to view log."; + break; + } + + if (has_inventory_sources && Empty(source)) { + // parent has a source, therefore this group should not have a source + launch_class = "btn-disabled"; + status_tip = 'Managed by an external cloud source.'; + launch_tip = 'Can only be updated by running a sync on the parent group.'; + } + + if (has_inventory_sources === false && Empty(source)) { + launch_class = 'btn-disabled'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + } + + return { + "class": stat_class, + "tooltip": status_tip, + "status": stat, + "launch_class": launch_class, + "launch_tip": launch_tip, + "schedule_tip": schedule_tip + }; + }; + } + +GetSyncStatusMsg.$inject = + [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js new file mode 100644 index 0000000000..1447d0aa1c --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js @@ -0,0 +1,81 @@ +export default + function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { + return function(params) { + var scope = params.scope, + id = params.id, + group = params.group; + + if (scope.removeCancelUpdate) { + scope.removeCancelUpdate(); + } + scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { + // Cancel the update process + Rest.setUrl(url); + Rest.post() + .success(function () { + Wait('stop'); + //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + + // 'Click the button to monitor the status.', 'alert-info'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. POST status: ' + status + }); + }); + }); + + if (scope.removeCheckCancel) { + scope.removeCheckCancel(); + } + scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { + // Check that we have access to cancelling an update + var url = (current_update) ? current_update : last_update; + url += 'cancel/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + if (data.can_cancel) { + scope.$emit('CancelUpdate', url); + //} else { + // Wait('stop'); + // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + + // 'the latest status.', 'alert-info'); + } + else { + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET status: ' + status + }); + }); + }); + + // Cancel the update process + if (Empty(group)) { + group = Find({ list: scope.groups, key: 'id', val: id }); + scope.selected_group_id = group.id; + } + + if (group && (group.status === 'running' || group.status === 'pending')) { + // We found the group, and there is a running update + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status + }); + }); + } + }; + } + +GroupsCancelUpdate.$inject = + [ 'Empty', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js new file mode 100644 index 0000000000..da186e77aa --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js @@ -0,0 +1,46 @@ +export default + function ViewUpdateStatus($state, Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find) { + return function(params) { + var scope = params.scope, + group_id = params.group_id, + group = Find({ list: scope.groups, key: 'id', val: group_id }); + + if (scope.removeSourceReady) { + scope.removeSourceReady(); + } + scope.removeSourceReady = scope.$on('SourceReady', function(e, source) { + + // Get the ID from the correct summary field + var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id; + + $state.go('inventorySyncStdout', {id: update_id}); + + }); + + if (group) { + if (Empty(group.source)) { + // do nothing + } else if (Empty(group.status) || group.status === "never updated") { + Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + + 'clicking the button.
', 'alert-info', null, null, null, null, true); + } else { + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('SourceReady', data); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + ' GET returned status: ' + status }); + }); + } + } + }; + } + +ViewUpdateStatus.$inject = + [ '$state', 'Rest', 'ProcessErrors', + 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/main.js b/awx/ui/client/src/inventories/manage/groups/main.js index 35e7b80c59..e1c2c16ddb 100644 --- a/awx/ui/client/src/inventories/manage/groups/main.js +++ b/awx/ui/client/src/inventories/manage/groups/main.js @@ -6,8 +6,18 @@ import GroupAddController from './groups-add.controller'; import GroupEditController from './groups-edit.controller'; +import GetHostsStatusMsg from './factories/get-hosts-status-msg.factory'; +import GetSourceTypeOptions from './factories/get-source-type-options.factory'; +import GetSyncStatusMsg from './factories/get-sync-status-msg.factory'; +import GroupsCancelUpdate from './factories/groups-cancel-update.factory'; +import ViewUpdateStatus from './factories/view-update-status.factory'; export default angular.module('manageGroups', []) + .factory('GetHostsStatusMsg', GetHostsStatusMsg) + .factory('GetSourceTypeOptions', GetSourceTypeOptions) + .factory('GetSyncStatusMsg', GetSyncStatusMsg) + .factory('GroupsCancelUpdate', GroupsCancelUpdate) + .factory('ViewUpdateStatus', ViewUpdateStatus) .controller('GroupAddController', GroupAddController) .controller('GroupEditController', GroupEditController); diff --git a/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js b/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js new file mode 100644 index 0000000000..7c60174611 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js @@ -0,0 +1,13 @@ +export default + function SetEnabledMsg() { + return function(host) { + if (host.has_inventory_sources) { + // Inventory sync managed, so not clickable + host.enabledToolTip = (host.enabled) ? 'Host is available' : 'Host is not available'; + } + else { + // Clickable + host.enabledToolTip = (host.enabled) ? 'Host is available. Click to toggle.' : 'Host is not available. Click to toggle.'; + } + }; + } diff --git a/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js b/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js new file mode 100644 index 0000000000..c03468ee1f --- /dev/null +++ b/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js @@ -0,0 +1,102 @@ +export default + function SetStatus($filter, SetEnabledMsg, Empty) { + return function(params) { + var scope = params.scope, + host = params.host, + i, html, title; + + function ellipsis(a) { + if (a.length > 25) { + return a.substr(0,25) + '...'; + } + return a; + } + + function noRecentJobs() { + title = 'No job data'; + html = "

No recent job data available for this host.

\n"; + } + + function setMsg(host) { + var j, job, jobs; + + if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) { + if (host.has_active_failures === true) { + host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; + host.active_failures = 'error'; + } + else { + host.badgeToolTip = "Most recent job successful. Click to view jobs."; + host.active_failures = 'successful'; + } + if (host.summary_fields.recent_jobs.length > 0) { + // build html table of job status info + jobs = host.summary_fields.recent_jobs.sort( + function(a,b) { + // reverse numerical order + return -1 * (a - b); + }); + title = "Recent Jobs"; + html = "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + for (j=0; j < jobs.length; j++) { + job = jobs[j]; + html += "\n"; + + // SmartStatus-tooltips are named --success whereas icon-job uses successful + var iconStatus = (job.status === 'successful') ? 'success' : 'failed'; + + html += "\n"; + + html += "\n"; + + html += "\n"; + + html += "\n"; + } + html += "\n"; + html += "
StatusFinishedName
" + ($filter('longDate')(job.finished)).replace(/ /,'
') + "
" + ellipsis(job.name) + "
\n"; + } + else { + noRecentJobs(); + } + } + else if (host.has_active_failures === false && host.last_job === null) { + host.badgeToolTip = "No job data available."; + host.active_failures = 'none'; + noRecentJobs(); + } + host.job_status_html = html; + host.job_status_title = title; + } + + if (!Empty(host)) { + // update single host + setMsg(host); + SetEnabledMsg(host); + } + else { + // update all hosts + for (i=0; i < scope.hosts.length; i++) { + setMsg(scope.hosts[i]); + SetEnabledMsg(scope.hosts[i]); + } + } + }; + } + +SetStatus.$inject = + [ '$filter', + 'SetEnabledMsg', + 'Empty' + ]; diff --git a/awx/ui/client/src/inventories/manage/hosts/main.js b/awx/ui/client/src/inventories/manage/hosts/main.js index 6dd1f334a7..1b22bd3d35 100644 --- a/awx/ui/client/src/inventories/manage/hosts/main.js +++ b/awx/ui/client/src/inventories/manage/hosts/main.js @@ -6,8 +6,12 @@ import HostsAddController from './hosts-add.controller'; import HostsEditController from './hosts-edit.controller'; +import SetStatus from './factories/set-status.factory'; +import SetEnabledMsg from './factories/set-enabled-msg.factory'; export default angular.module('manageHosts', []) + .factory('SetStatus', SetStatus) + .factory('SetEnabledMsg', SetEnabledMsg) .controller('HostsAddController', HostsAddController) .controller('HostEditController', HostsEditController); diff --git a/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js new file mode 100644 index 0000000000..bcc688936f --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js @@ -0,0 +1,162 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name helpers.function:Adhoc + * @description These routines are shared by adhoc command related controllers. + * The content here is very similar to the JobSubmission helper, and in fact, + * certain services are pulled from that helper. This leads to an important + * point: if you need to create functionality that is shared between the command + * and playbook run process, put that code in the JobSubmission helper and make + * it into a reusable step (by specifying a callback parameter in the factory). + * For a good example of this, please see how the AdhocLaunch factory in this + * file utilizes the CheckPasswords factory from the JobSubmission helper. + * + * #AdhocRelaunch Step 1: preparing the GET to ad_hoc_commands/n/relaunch + * The adhoc relaunch process is called from the JobSubmission helper. It is a + * separate process from the initial adhoc run becuase of the way the API + * endpoints work. For AdhocRelaunch, we have access to the original run and + * we can pull the related relaunch URL by knowing the original Adhoc runs ID. + * + * #AdhocRelaunch Step 2: If we got passwords back, add them + * The relaunch URL gives us back the passwords we need to prompt for (if any). + * We'll go to step 3 if there are passwords, and step 4 if not. + * + * #AdhocRelaunch Step 3: PromptForPasswords and the CreateLaunchDialog + * + * #AdhocRelaunch Step 5: StartAdhocRun + * + * #AdhocRelaunch Step 6: LaunchJob and navigate to the standard out page. + + * **If you are + * TODO: once the API endpoint is figured out for running an adhoc command + * from the form is figured out, the rest work should probably be excised from + * the controller and moved into here. See the todo statements in the + * controller for more information about this. + */ + + export default + function AdhocRun($location, $stateParams, LaunchJob, PromptForPasswords, + Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) { + return function(params) { + var id = params.project_id, + scope = params.scope.$new(), + new_job_id, + html, + url; + + // this is used to cancel a running adhoc command from + // the jobs page + if (scope.removeCancelJob) { + scope.removeCancelJob(); + } + scope.removeCancelJob = scope.$on('CancelJob', function() { + // Delete the job + Wait('start'); + Rest.setUrl(GetBasePath('ad_hoc_commands') + new_job_id + '/'); + Rest.destroy() + .success(function() { + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, + null, { hdr: 'Error!', + msg: 'Call to ' + url + + ' failed. DELETE returned status: ' + + status }); + }); + }); + + if (scope.removeStartAdhocRun) { + scope.removeStartAdhocRun(); + } + + scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() { + var password, + postData={}; + for (password in scope.passwords) { + postData[scope.passwords[password]] = scope[ + scope.passwords[password] + ]; + } + // Re-launch the adhoc job + Rest.setUrl(url); + Rest.post(postData) + .success(function (data) { + Wait('stop'); + if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') { + $state.go('adHocJobStdout', {id: data.id}); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, { + hdr: 'Error!', + msg: 'Failed to launch adhoc command. POST ' + + 'returned status: ' + status }); + }); + }); + + // start routine only if passwords need to be prompted + if (scope.removeCreateLaunchDialog) { + scope.removeCreateLaunchDialog(); + } + scope.removeCreateLaunchDialog = scope.$on('CreateLaunchDialog', + function(e, html, url) { + CreateLaunchDialog({ + scope: scope, + html: html, + url: url, + callback: 'StartAdhocRun' + }); + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', + function(e, passwords_needed_to_start,html, url) { + PromptForPasswords({ + scope: scope, + passwords: passwords_needed_to_start, + callback: 'CreateLaunchDialog', + html: html, + url: url + }); + }); // end password prompting routine + + // start the adhoc relaunch routine + Wait('start'); + url = GetBasePath('ad_hoc_commands') + id + '/relaunch/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + new_job_id = data.id; + + scope.passwords_needed_to_start = data.passwords_needed_to_start; + if (!Empty(data.passwords_needed_to_start) && + data.passwords_needed_to_start.length > 0) { + // go through the password prompt routine before + // starting the adhoc run + scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); + } + else { + // no prompting of passwords needed + scope.$emit('StartAdhocRun'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + }; + } + + AdhocRun.$inject = + [ '$location','$stateParams', 'LaunchJob', + 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', + 'Wait', 'Empty', 'CreateLaunchDialog', '$state' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js new file mode 100644 index 0000000000..4f6089f7e2 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js @@ -0,0 +1,43 @@ +export default + function CheckPasswords(Rest, GetBasePath, ProcessErrors, Empty) { + return function(params) { + var scope = params.scope, + callback = params.callback, + credential = params.credential; + + var passwords = []; + if (!Empty(credential)) { + Rest.setUrl(GetBasePath('credentials')+credential); + Rest.get() + .success(function (data) { + if(data.kind === "ssh"){ + if(data.password === "ASK" ){ + passwords.push("ssh_password"); + } + if(data.ssh_key_unlock === "ASK"){ + passwords.push("ssh_key_unlock"); + } + if(data.become_password === "ASK"){ + passwords.push("become_password"); + } + if(data.vault_password === "ASK"){ + passwords.push("vault_password"); + } + } + scope.$emit(callback, passwords); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + } + + }; + } + +CheckPasswords.$inject = + [ 'Rest', + 'GetBasePath', + 'ProcessErrors', + 'Empty' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/create-launch-dialog.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/create-launch-dialog.factory.js new file mode 100644 index 0000000000..9620807621 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/create-launch-dialog.factory.js @@ -0,0 +1,75 @@ +export default + function CreateLaunchDialog($compile, CreateDialog, Wait, ParseTypeChange) { + return function(params) { + var buttons, + scope = params.scope, + html = params.html, + // job_launch_data = {}, + callback = params.callback || 'PlaybookLaunchFinished', + // url = params.url, + e; + + // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; + html+=''; + $('#password-modal').empty().html(html); + $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); + e = angular.element(document.getElementById('password-modal')); + $compile(e)(scope); + + if(scope.prompt_for_vars===true){ + ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); + } + + buttons = [{ + label: "Cancel", + onClick: function() { + $('#password-modal').dialog('close'); + // scope.$emit('CancelJob'); + // scope.$destroy(); + }, + icon: "fa-times", + "class": "btn btn-default", + "id": "password-cancel-button" + },{ + label: "Launch", + onClick: function() { + scope.$emit(callback); + $('#password-modal').dialog('close'); + }, + icon: "fa-check", + "class": "btn btn-primary", + "id": "password-accept-button" + }]; + + CreateDialog({ + id: 'password-modal', + scope: scope, + buttons: buttons, + width: 620, + height: 700, //(scope.passwords.length > 1) ? 700 : 500, + minWidth: 500, + title: 'Launch Configuration', + callback: 'DialogReady', + onOpen: function(){ + Wait('stop'); + } + }); + + if (scope.removeDialogReady) { + scope.removeDialogReady(); + } + scope.removeDialogReady = scope.$on('DialogReady', function() { + $('#password-modal').dialog('open'); + $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); + e = angular.element(document.getElementById('password-accept-button')); + $compile(e)(scope); + }); + }; + } + +CreateLaunchDialog.$inject = + [ '$compile', + 'CreateDialog', + 'Wait', + 'ParseTypeChange' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/inventory-update.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/inventory-update.factory.js new file mode 100644 index 0000000000..1c3ba53d03 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/inventory-update.factory.js @@ -0,0 +1,76 @@ +export default + function InventoryUpdate(PromptForPasswords, LaunchJob, Rest, GetBasePath, ProcessErrors, Alert, Wait) { + return function (params) { + + var scope = params.scope, + url = params.url, + inventory_source; + + if (scope.removeUpdateSubmitted) { + scope.removeUpdateSubmitted(); + } + scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { + Wait('stop'); + if (scope.socketStatus === 'error') { + Alert('Sync Started', '
The request to start the inventory sync process was submitted. ' + + 'To monitor the status refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); + if (scope.refreshGroups) { + // inventory detail page + scope.refreshGroups(); + } + else if (scope.refresh) { + scope.refresh(); + } + } + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { + PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); + }); + + if (scope.removeStartTheUpdate) { + scope.removeStartTheUpdate(); + } + scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { + LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); + }); + + // Check to see if we have permission to perform the update and if any passwords are needed + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + inventory_source = data; + if (data.can_update) { + if (data.passwords_needed_to_update) { + Wait('stop'); + scope.$emit('PromptForPasswords'); + } + else { + scope.$emit('StartTheUpdate', {}); + } + } else { + Wait('stop'); + Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', + 'alert-danger'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); + }); + }; + } + +InventoryUpdate.$inject = + [ 'PromptForPasswords', + 'LaunchJob', + 'Rest', + 'GetBasePath', + 'ProcessErrors', + 'Alert', + 'Wait' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/project-update.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/project-update.factory.js new file mode 100644 index 0000000000..b089127600 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/project-update.factory.js @@ -0,0 +1,78 @@ +export default + function ProjectUpdate(PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { + return function (params) { + var scope = params.scope, + project_id = params.project_id, + url = GetBasePath('projects') + project_id + '/update/', + project; + + if (scope.removeUpdateSubmitted) { + scope.removeUpdateSubmitted(); + } + scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { + // Refresh the project list after update request submitted + Wait('stop'); + if (/\d$/.test($location.path())) { + //Request submitted from projects/N page. Navigate back to the list so user can see status + $location.path('/projects'); + } + if (scope.socketStatus === 'error') { + Alert('Update Started', '
The request to start the SCM update process was submitted. ' + + 'To monitor the update status, refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); + if (scope.refresh) { + scope.refresh(); + } + } + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { + PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); + }); + + if (scope.removeStartTheUpdate) { + scope.removeStartTheUpdate(); + } + scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { + LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); + }); + + // Check to see if we have permission to perform the update and if any passwords are needed + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + project = data; + if (project.can_update) { + if (project.passwords_needed_to_updated) { + Wait('stop'); + scope.$emit('PromptForPasswords'); + } + else { + scope.$emit('StartTheUpdate', {}); + } + } + else { + Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', + 'alert-danger'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); + }); + }; + } + +ProjectUpdate.$inject = + [ 'PromptForPasswords', + 'LaunchJob', + 'Rest', + '$location', + 'GetBasePath', + 'ProcessErrors', + 'Alert', + 'Wait' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/prompt-for-passwords.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/prompt-for-passwords.factory.js new file mode 100644 index 0000000000..7efc827919 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/prompt-for-passwords.factory.js @@ -0,0 +1,82 @@ +export default + function PromptForPasswords(CredentialForm) { + return function(params) { + var scope = params.scope, + callback = params.callback || 'PasswordsAccepted', + url = params.url, + form = CredentialForm, + fld, field, + html=params.html || ""; + + scope.passwords = params.passwords; + + html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; + + scope.passwords.forEach(function(password) { + // Prompt for password + field = form.fields[password]; + fld = password; + scope[fld] = ''; + html += "
\n"; + html += "\n"; + html += "Please enter a password.
\n"; + html += "
\n"; + html += "
\n"; + + // Add the related confirm field + if (field.associated) { + fld = field.associated; + field = form.fields[field.associated]; + scope[fld] = ''; + html += "
\n"; + html += "\n"; + html += "Please confirm the password.\n"; + html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; + html += "
\n"; + html += "
\n"; + } + }); + + scope.$emit(callback, html, url); + + // Password change + scope.clearPWConfirm = function (fld) { + // If password value changes, make sure password_confirm must be re-entered + scope[fld] = ''; + scope.job_launch_form[fld].$setValidity('awpassmatch', false); + scope.checkStatus(); + }; + + scope.checkStatus = function() { + if (!scope.job_launch_form.$invalid) { + $('#password-accept-button').removeAttr('disabled'); + } + else { + $('#password-accept-button').attr({ "disabled": "disabled" }); + } + }; + }; + } + +PromptForPasswords.$inject = + [ 'CredentialForm' ]; diff --git a/awx/ui/client/src/job-submission/main.js b/awx/ui/client/src/job-submission/main.js index 71c4c5c0de..1204613560 100644 --- a/awx/ui/client/src/job-submission/main.js +++ b/awx/ui/client/src/job-submission/main.js @@ -7,6 +7,12 @@ import InitiatePlaybookRun from './job-submission-factories/initiateplaybookrun.factory'; import LaunchJob from './job-submission-factories/launchjob.factory'; import GetSurveyQuestions from './job-submission-factories/getsurveyquestions.factory'; +import AdhocRun from './job-submission-factories/adhoc-run.factory.js'; +import CheckPasswords from './job-submission-factories/check-passwords.factory'; +import CreateLaunchDialog from './job-submission-factories/create-launch-dialog.factory'; +import InventoryUpdate from './job-submission-factories/inventory-update.factory'; +import ProjectUpdate from './job-submission-factories/project-update.factory'; +import PromptForPasswords from './job-submission-factories/prompt-for-passwords.factory'; import submitJob from './job-submission.directive'; import credentialList from './lists/credential/job-sub-cred-list.directive'; import inventoryList from './lists/inventory/job-sub-inv-list.directive'; @@ -16,6 +22,12 @@ export default .factory('InitiatePlaybookRun', InitiatePlaybookRun) .factory('LaunchJob', LaunchJob) .factory('GetSurveyQuestions', GetSurveyQuestions) + .factory('AdhocRun', AdhocRun) + .factory('CheckPasswords', CheckPasswords) + .factory('CreateLaunchDialog', CreateLaunchDialog) + .factory('InventoryUpdate', InventoryUpdate) + .factory('ProjectUpdate', ProjectUpdate) + .factory('PromptForPasswords', PromptForPasswords) .directive('submitJob', submitJob) .directive('jobSubCredList', credentialList) .directive('jobSubInvList', inventoryList); diff --git a/awx/ui/client/src/jobs/factories/delete-job.factory.js b/awx/ui/client/src/jobs/factories/delete-job.factory.js new file mode 100644 index 0000000000..d82afe70fc --- /dev/null +++ b/awx/ui/client/src/jobs/factories/delete-job.factory.js @@ -0,0 +1,137 @@ +export default + function DeleteJob($state, Find, GetBasePath, Rest, Wait, ProcessErrors, Prompt, Alert, + $filter, i18n) { + return function(params) { + var scope = params.scope, + id = params.id, + job = params.job, + callback = params.callback, + action, jobs, url, action_label, hdr; + + if (!job) { + if (scope.completed_jobs) { + jobs = scope.completed_jobs; + } + else if (scope.running_jobs) { + jobs = scope.running_jobs; + } + else if (scope.queued_jobs) { + jobs = scope.queued_jobs; + } + else if (scope.all_jobs) { + jobs = scope.all_jobs; + } + else if (scope.jobs) { + jobs = scope.jobs; + } + job = Find({list: jobs, key: 'id', val: id }); + } + + if (job.status === 'pending' || job.status === 'running' || job.status === 'waiting') { + url = job.related.cancel; + action_label = 'cancel'; + hdr = i18n._('Cancel'); + } else { + url = job.url; + action_label = 'delete'; + hdr = i18n._('Delete'); + } + + action = function () { + Wait('start'); + Rest.setUrl(url); + if (action_label === 'cancel') { + Rest.post() + .success(function () { + $('#prompt-modal').modal('hide'); + if (callback) { + scope.$emit(callback, action_label); + } + else { + $state.reload(); + Wait('stop'); + } + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + if (status === 403) { + Alert('Error', obj.detail); + } + // Ignore the error. The job most likely already finished. + // ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + // ' failed. POST returned status: ' + status }); + }); + } else { + Rest.destroy() + .success(function () { + $('#prompt-modal').modal('hide'); + if (callback) { + scope.$emit(callback, action_label); + } + else { + $state.reload(); + Wait('stop'); + } + }) + .error(function (obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + if (status === 403) { + Alert('Error', obj.detail); + } + // Ignore the error. The job most likely already finished. + //ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + // ' failed. DELETE returned status: ' + status }); + }); + } + }; + + if (scope.removeCancelNotAllowed) { + scope.removeCancelNotAllowed(); + } + scope.removeCancelNotAllowed = scope.$on('CancelNotAllowed', function() { + Wait('stop'); + Alert('Job Completed', 'The request to cancel the job could not be submitted. The job already completed.', 'alert-info'); + }); + + if (scope.removeCancelJob) { + scope.removeCancelJob(); + } + scope.removeCancelJob = scope.$on('CancelJob', function() { + var cancelBody = "
" + i18n._("Submit the request to cancel?") + "
"; + var deleteBody = "
" + i18n._("Are you sure you want to delete the job below?") + "
#" + id + " " + $filter('sanitize')(job.name) + "
"; + Prompt({ + hdr: hdr, + body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody, + action: action, + actionText: (action_label === 'cancel' || job.status === 'new') ? "OK" : "DELETE" + }); + }); + + if (action_label === 'cancel') { + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.can_cancel) { + scope.$emit('CancelJob'); + } + else { + scope.$emit('CancelNotAllowed'); + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + ' failed. GET returned: ' + status }); + }); + } + else { + scope.$emit('CancelJob'); + } + }; + } + +DeleteJob.$inject = + [ '$state', 'Find', 'GetBasePath', 'Rest', 'Wait', + 'ProcessErrors', 'Prompt', 'Alert', '$filter', 'i18n' + ]; diff --git a/awx/ui/client/src/jobs/factories/job-status-tool-tip.factory.js b/awx/ui/client/src/jobs/factories/job-status-tool-tip.factory.js new file mode 100644 index 0000000000..126c9977d3 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/job-status-tool-tip.factory.js @@ -0,0 +1,31 @@ +export default + function JobStatusToolTip() { + return function(status) { + var toolTip; + switch (status) { + case 'successful': + case 'success': + toolTip = 'There were no failed tasks.'; + break; + case 'failed': + toolTip = 'Some tasks encountered errors.'; + break; + case 'canceled': + toolTip = 'Stopped by user request.'; + break; + case 'new': + toolTip = 'In queue, waiting on task manager.'; + break; + case 'waiting': + toolTip = 'SCM Update or Inventory Update is executing.'; + break; + case 'pending': + toolTip = 'Not in queue, waiting on task manager.'; + break; + case 'running': + toolTip = 'Playbook tasks executing.'; + break; + } + return toolTip; + }; + } diff --git a/awx/ui/client/src/jobs/factories/jobs-list-update.factory.js b/awx/ui/client/src/jobs/factories/jobs-list-update.factory.js new file mode 100644 index 0000000000..5f950e458c --- /dev/null +++ b/awx/ui/client/src/jobs/factories/jobs-list-update.factory.js @@ -0,0 +1,49 @@ +export default + function JobsListUpdate() { + return function(params) { + var scope = params.scope, + parent_scope = params.parent_scope, + list = params.list; + + scope[list.name].forEach(function(item, item_idx) { + var fld, field, + itm = scope[list.name][item_idx]; + + //if (item.type === 'inventory_update') { + // itm.name = itm.name.replace(/^.*?:/,'').replace(/^: /,''); + //} + + // Set the item type label + if (list.fields.type) { + parent_scope.type_choices.forEach(function(choice) { + if (choice.value === item.type) { + itm.type_label = choice.label; + } + }); + } + // Set the job status label + parent_scope.status_choices.forEach(function(status) { + if (status.value === item.status) { + itm.status_label = status.label; + } + }); + + if (list.name === 'completed_jobs' || list.name === 'running_jobs') { + itm.status_tip = itm.status_label + '. Click for details.'; + } + else if (list.name === 'queued_jobs') { + itm.status_tip = 'Pending'; + } + + // Copy summary_field values + for (field in list.fields) { + fld = list.fields[field]; + if (fld.sourceModel) { + if (itm.summary_fields[fld.sourceModel]) { + itm[field] = itm.summary_fields[fld.sourceModel][fld.sourceField]; + } + } + } + }); + }; + } diff --git a/awx/ui/client/src/jobs/factories/relaunch-adhoc.factory.js b/awx/ui/client/src/jobs/factories/relaunch-adhoc.factory.js new file mode 100644 index 0000000000..462501f097 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-adhoc.factory.js @@ -0,0 +1,11 @@ +export default + function RelaunchAdhoc(AdhocRun) { + return function(params) { + var scope = params.scope, + id = params.id; + AdhocRun({ scope: scope, project_id: id, relaunch: true }); + }; + } + +RelaunchAdhoc.$inject = + [ 'AdhocRun' ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-inventory.factory.js b/awx/ui/client/src/jobs/factories/relaunch-inventory.factory.js new file mode 100644 index 0000000000..eccd06a291 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-inventory.factory.js @@ -0,0 +1,30 @@ +export default + function RelaunchInventory(Find, Wait, Rest, InventoryUpdate, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + id = params.id, + url = GetBasePath('inventory_sources') + id + '/'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + InventoryUpdate({ + scope: scope, + url: data.related.update, + group_name: data.summary_fields.group.name, + group_source: data.source, + tree_id: null, + group_id: data.group + }); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + + url + ' GET returned: ' + status }); + }); + }; + } + +RelaunchInventory.$inject = + [ 'Find', 'Wait', 'Rest', + 'InventoryUpdate', 'ProcessErrors', 'GetBasePath' + ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-job.factory.js b/awx/ui/client/src/jobs/factories/relaunch-job.factory.js new file mode 100644 index 0000000000..0190cf9c79 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-job.factory.js @@ -0,0 +1,28 @@ +export default + function RelaunchJob(RelaunchInventory, RelaunchPlaybook, RelaunchSCM, RelaunchAdhoc) { + return function(params) { + var scope = params.scope, + id = params.id, + type = params.type, + name = params.name; + if (type === 'inventory_update') { + RelaunchInventory({ scope: scope, id: id}); + } + else if (type === 'ad_hoc_command') { + RelaunchAdhoc({ scope: scope, id: id, name: name }); + } + else if (type === 'job' || type === 'system_job' || type === 'workflow_job') { + RelaunchPlaybook({ scope: scope, id: id, name: name, job_type: type }); + } + else if (type === 'project_update') { + RelaunchSCM({ scope: scope, id: id }); + } + }; + } + +RelaunchJob.$inject = + [ 'RelaunchInventory', + 'RelaunchPlaybook', + 'RelaunchSCM', + 'RelaunchAdhoc' + ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-playbook.factory.js b/awx/ui/client/src/jobs/factories/relaunch-playbook.factory.js new file mode 100644 index 0000000000..78431b17be --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-playbook.factory.js @@ -0,0 +1,12 @@ +export default + function RelaunchPlaybook(InitiatePlaybookRun) { + return function(params) { + var scope = params.scope, + id = params.id, + job_type = params.job_type; + InitiatePlaybookRun({ scope: scope, id: id, relaunch: true, job_type: job_type }); + }; + } + +RelaunchPlaybook.$inject = + [ 'InitiatePlaybookRun' ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-scm.factory.js b/awx/ui/client/src/jobs/factories/relaunch-scm.factory.js new file mode 100644 index 0000000000..b37e90e9ca --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-scm.factory.js @@ -0,0 +1,11 @@ +export default + function RelaunchSCM(ProjectUpdate) { + return function(params) { + var scope = params.scope, + id = params.id; + ProjectUpdate({ scope: scope, project_id: id }); + }; + } + +RelaunchSCM.$inject = + [ 'ProjectUpdate' ]; diff --git a/awx/ui/client/src/jobs/main.js b/awx/ui/client/src/jobs/main.js index b1782cead6..6045000ef0 100644 --- a/awx/ui/client/src/jobs/main.js +++ b/awx/ui/client/src/jobs/main.js @@ -6,10 +6,26 @@ import jobsList from './jobs-list.controller'; import jobsRoute from './jobs.route'; +import DeleteJob from './factories/delete-job.factory'; +import JobStatusToolTip from './factories/job-status-tool-tip.factory'; +import JobsListUpdate from './factories/jobs-list-update.factory'; +import RelaunchAdhoc from './factories/relaunch-adhoc.factory'; +import RelaunchInventory from './factories/relaunch-inventory.factory'; +import RelaunchJob from './factories/relaunch-job.factory'; +import RelaunchPlaybook from './factories/relaunch-playbook.factory'; +import RelaunchSCM from './factories/relaunch-scm.factory'; export default angular.module('JobsModule', []) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(jobsRoute); }]) - .controller('JobsList', jobsList); + .controller('JobsList', jobsList) + .factory('DeleteJob', DeleteJob) + .factory('JobStatusToolTip', JobStatusToolTip) + .factory('JobsListUpdate', JobsListUpdate) + .factory('RelaunchAdhoc', RelaunchAdhoc) + .factory('RelaunchInventory', RelaunchInventory) + .factory('RelaunchJob', RelaunchJob) + .factory('RelaunchPlaybook', RelaunchPlaybook) + .factory('RelaunchSCM', RelaunchSCM); diff --git a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js index 0a59ddbaef..5db0ffeac2 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js @@ -12,9 +12,9 @@ */ export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath', 'generateList', -'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', 'QuerySet', 'UserList', +'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', 'QuerySet', 'UserList', function($scope, $rootScope, ProcessErrors, GetBasePath, generateList, - SelectionInit, templateUrl, $state, Rest, $q, Wait, $window, qs, UserList) { + templateUrl, $state, Rest, $q, Wait, $window, qs, UserList) { $scope.$on("linkLists", function() { if ($state.current.name.split(".")[1] === "users") { diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js index 6018b53d85..5f112f47c0 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -7,11 +7,11 @@ export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'Prompt', 'ReturnToCaller', 'ClearScope', 'OrgProjectList', 'OrgProjectDataset', - 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'ProjectUpdate', + 'ProcessErrors', 'GetBasePath', 'ProjectUpdate', 'Wait', 'GetChoices', 'Empty', 'Find', 'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state', function($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, Prompt, ReturnToCaller, ClearScope, OrgProjectList, Dataset, - ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, + ProcessErrors, GetBasePath, ProjectUpdate, Wait, GetChoices, Empty, Find, GetProjectIcon, GetProjectToolTip, $filter, $state) { var list = OrgProjectList, diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js index 654ed8ee59..3589328bb3 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js @@ -6,12 +6,12 @@ export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'OrgTeamList', 'Rest', 'Alert', 'Prompt', 'OrgTeamsDataset', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors', 'SetTeamListeners', 'GetBasePath', - 'SelectionInit', 'Wait', '$state', + 'ProcessErrors', 'GetBasePath', + 'Wait', '$state', function($scope, $rootScope, $location, $log, $stateParams, OrgTeamList, Rest, Alert, Prompt, Dataset, ReturnToCaller, ClearScope, - ProcessErrors, SetTeamListeners, GetBasePath, - SelectionInit, Wait, $state) { + ProcessErrors, GetBasePath, + Wait, $state) { var list = OrgTeamList, orgBase = GetBasePath('organizations'); diff --git a/awx/ui/client/src/projects/factories/get-project-icon.factory.js b/awx/ui/client/src/projects/factories/get-project-icon.factory.js new file mode 100644 index 0000000000..5234041e38 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-icon.factory.js @@ -0,0 +1,30 @@ +export default + function GetProjectIcon() { + return function(status) { + var result = ''; + switch (status) { + case 'n/a': + case 'ok': + case 'never updated': + result = 'none'; + break; + case 'pending': + case 'waiting': + case 'new': + result = 'none'; + break; + case 'updating': + case 'running': + result = 'running'; + break; + case 'successful': + result = 'success'; + break; + case 'failed': + case 'missing': + case 'canceled': + result = 'error'; + } + return result; + }; + } diff --git a/awx/ui/client/src/projects/factories/get-project-path.factory.js b/awx/ui/client/src/projects/factories/get-project-path.factory.js new file mode 100644 index 0000000000..1a48772ff9 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-path.factory.js @@ -0,0 +1,78 @@ +export default + function GetProjectPath(Alert, Rest, GetBasePath, ProcessErrors) { + return function(params) { + var scope = params.scope, + master = params.master; + + function arraySort(data) { + //Sort nodes by name + var i, j, names = [], + newData = []; + for (i = 0; i < data.length; i++) { + names.push(data[i].value); + } + names.sort(); + for (j = 0; j < names.length; j++) { + for (i = 0; i < data.length; i++) { + if (data[i].value === names[j]) { + newData.push(data[i]); + } + } + } + return newData; + } + + scope.showMissingPlaybooksAlert = false; + + Rest.setUrl(GetBasePath('config')); + Rest.get() + .success(function (data) { + var opts = [], i; + if (data.project_local_paths) { + for (i = 0; i < data.project_local_paths.length; i++) { + opts.push({ + label: data.project_local_paths[i], + value: data.project_local_paths[i] + }); + } + } + if (scope.local_path) { + // List only includes paths not assigned to projects, so add the + // path assigned to the current project. + opts.push({ + label: scope.local_path, + value: scope.local_path + }); + } + scope.project_local_paths = arraySort(opts); + if (scope.local_path) { + for (i = 0; scope.project_local_paths.length; i++) { + if (scope.project_local_paths[i].value === scope.local_path) { + scope.local_path = scope.project_local_paths[i]; + break; + } + } + } + scope.base_dir = data.project_base_dir; + master.local_path = scope.local_path; + master.base_dir = scope.base_dir; // Keep in master object so that it doesn't get + // wiped out on form reset. + if (opts.length === 0) { + // trigger display of alert block when scm_type == manual + scope.showMissingPlaybooksAlert = true; + } + scope.$emit('pathsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to access API config. GET status: ' + status }); + }); + }; + } + +GetProjectPath.$inject = + [ 'Alert', + 'Rest', + 'GetBasePath', + 'ProcessErrors' + ]; diff --git a/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js new file mode 100644 index 0000000000..6db397bed0 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js @@ -0,0 +1,38 @@ +export default + function GetProjectToolTip(i18n) { + return function(status) { + var result = ''; + switch (status) { + case 'n/a': + case 'ok': + case 'never updated': + result = i18n._('No SCM updates have run for this project'); + break; + case 'pending': + case 'waiting': + case 'new': + result = i18n._('Queued. Click for details'); + break; + case 'updating': + case 'running': + result = i18n._('Running! Click for details'); + break; + case 'successful': + result = i18n._('Success! Click for details'); + break; + case 'failed': + result = i18n._('Failed. Click for details'); + break; + case 'missing': + result = i18n._('Missing. Click for details'); + break; + case 'canceled': + result = i18n._('Canceled. Click for details'); + break; + } + return result; + }; + } + +GetProjectToolTip.$inject = + [ 'i18n' ]; diff --git a/awx/ui/client/src/projects/main.js b/awx/ui/client/src/projects/main.js index 05addaf1fe..2bb0c3cdcc 100644 --- a/awx/ui/client/src/projects/main.js +++ b/awx/ui/client/src/projects/main.js @@ -8,12 +8,18 @@ import ProjectsList from './list/projects-list.controller'; import ProjectsAdd from './add/projects-add.controller'; import ProjectsEdit from './edit/projects-edit.controller'; import { N_ } from '../i18n'; +import GetProjectPath from './factories/get-project-path.factory'; +import GetProjectIcon from './factories/get-project-icon.factory'; +import GetProjectToolTip from './factories/get-project-tool-tip.factory'; export default angular.module('Projects', []) .controller('ProjectsList', ProjectsList) .controller('ProjectsAdd', ProjectsAdd) .controller('ProjectsEdit', ProjectsEdit) + .factory('GetProjectPath', GetProjectPath) + .factory('GetProjectIcon', GetProjectIcon) + .factory('GetProjectToolTip', GetProjectToolTip) .config(['$stateProvider', 'stateDefinitionsProvider', function($stateProvider, stateDefinitionsProvider) { let stateDefinitions = stateDefinitionsProvider.$get(); diff --git a/awx/ui/client/src/scheduler/factories/add-schedule.factory.js b/awx/ui/client/src/scheduler/factories/add-schedule.factory.js new file mode 100644 index 0000000000..c1870dd469 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/add-schedule.factory.js @@ -0,0 +1,135 @@ +export default + function AddSchedule($location, $rootScope, $stateParams, SchedulerInit, + Wait, GetBasePath, Empty, SchedulePost, $state, Rest, + ProcessErrors) { + return function(params) { + var scope = params.scope, + callback= params.callback, + base = params.base || $location.path().replace(/^\//, '').split('/')[0], + url = params.url || null, + scheduler, + job_type; + + job_type = scope.parentObject.job_type; + if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !url) { + url = GetBasePath(base) + $stateParams.id + '/schedules/'; + } + else if(base === "inventories"){ + if (!params.url){ + url = GetBasePath('groups') + $stateParams.id + '/'; + Rest.setUrl(url); + Rest.get(). + then(function (data) { + url = data.data.related.inventory_source + 'schedules/'; + }).catch(function (response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get inventory group info. GET returned status: ' + + response.status + }); + }); + } + else { + url = params.url; + } + } + else if (base === 'system_job_templates') { + url = GetBasePath(base) + $stateParams.id + '/schedules/'; + if(job_type === "cleanup_facts"){ + scope.isFactCleanup = true; + scope.keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + scope.granularity_keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + scope.prompt_for_days_facts_form.keep_amount.$setViewValue(30); + scope.prompt_for_days_facts_form.granularity_keep_amount.$setViewValue(1); + scope.keep_unit = scope.keep_unit_choices[0]; + scope.granularity_keep_unit = scope.granularity_keep_unit_choices[1]; + } + else { + scope.cleanupJob = true; + } + } + + Wait('start'); + $('#form-container').empty(); + scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); + if(scope.schedulerUTCTime) { + // The UTC time is already set + scope.processSchedulerEndDt(); + } + else { + // We need to wait for it to be set by angular-scheduler because the following function depends + // on it + var schedulerUTCTimeWatcher = scope.$watch('schedulerUTCTime', function(newVal) { + if(newVal) { + // Remove the watcher + schedulerUTCTimeWatcher(); + scope.processSchedulerEndDt(); + } + }); + } + scheduler.inject('form-container', false); + scheduler.injectDetail('occurrences', false); + scheduler.clear(); + scope.$on("htmlDetailReady", function() { + $rootScope.$broadcast("ScheduleFormCreated", scope); + }); + scope.showRRuleDetail = false; + + if (scope.removeScheduleSaved) { + scope.removeScheduleSaved(); + } + scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { + Wait('stop'); + if (callback) { + scope.$emit(callback, data); + } + $state.go("^", null, {reload: true}); + }); + scope.saveSchedule = function() { + SchedulePost({ + scope: scope, + url: url, + scheduler: scheduler, + callback: 'ScheduleSaved', + mode: 'add' + }); + }; + + $('#scheduler-tabs li a').on('shown.bs.tab', function(e) { + if ($(e.target).text() === 'Details') { + if (!scheduler.isValid()) { + $('#scheduler-tabs a:first').tab('show'); + } + } + }); + }; + } + +AddSchedule.$inject = + [ '$location', '$rootScope', '$stateParams', + 'SchedulerInit', 'Wait', 'GetBasePath', + 'Empty', 'SchedulePost', '$state', + 'Rest', 'ProcessErrors' + ]; diff --git a/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js b/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js new file mode 100644 index 0000000000..7c75882966 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js @@ -0,0 +1,61 @@ +export default + function DeleteSchedule(GetBasePath, Rest, Wait, $state, + ProcessErrors, Prompt, Find, $location, $filter) { + return function(params) { + var scope = params.scope, + id = params.id, + callback = params.callback, + action, schedule, list, url, hdr; + + if (scope.schedules) { + list = scope.schedules; + } + else if (scope.scheduled_jobs) { + list = scope.scheduled_jobs; + } + + url = GetBasePath('schedules') + id + '/'; + schedule = Find({list: list, key: 'id', val: id }); + hdr = 'Delete Schedule'; + + action = function () { + Wait('start'); + Rest.setUrl(url); + Rest.destroy() + .success(function () { + $('#prompt-modal').modal('hide'); + scope.$emit(callback, id); + if (new RegExp('/' + id + '$').test($location.$$url)) { + $location.url($location.url().replace(/[/][0-9]+$/, "")); // go to list view + } + else{ + $state.go('.', null, {reload: true}); + } + }) + .error(function (data, status) { + try { + $('#prompt-modal').modal('hide'); + } + catch(e) { + // ignore + } + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + ' failed. DELETE returned: ' + status }); + }); + }; + + Prompt({ + hdr: hdr, + body: '
Are you sure you want to delete the schedule below?
' + $filter('sanitize')(schedule.name) + '
', + action: action, + actionText: 'DELETE', + backdrop: false + }); + }; + } + +DeleteSchedule.$inject = + [ 'GetBasePath','Rest', 'Wait', '$state', + 'ProcessErrors', 'Prompt', 'Find', '$location', + '$filter' + ]; diff --git a/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js b/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js new file mode 100644 index 0000000000..de591aee08 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js @@ -0,0 +1,154 @@ +export default + function EditSchedule(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors, + GetBasePath, SchedulePost, $state) { + return function(params) { + var scope = params.scope, + id = params.id, + callback = params.callback, + schedule, scheduler, + url = GetBasePath('schedules') + id + '/'; + + delete scope.isFactCleanup; + delete scope.cleanupJob; + + function setGranularity(){ + var a,b, prompt_for_days, + keep_unit, + granularity, + granularity_keep_unit; + + if(scope.cleanupJob){ + scope.schedulerPurgeDays = Number(schedule.extra_data.days); + // scope.scheduler_form.schedulerPurgeDays.$setViewValue( Number(schedule.extra_data.days)); + } + else if(scope.isFactCleanup){ + scope.keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + scope.granularity_keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + // the API returns something like 20w or 1y + a = schedule.extra_data.older_than; // "20y" + b = schedule.extra_data.granularity; // "1w" + prompt_for_days = Number(_.initial(a,1).join('')); // 20 + keep_unit = _.last(a); // "y" + granularity = Number(_.initial(b,1).join('')); // 1 + granularity_keep_unit = _.last(b); // "w" + + scope.keep_amount = prompt_for_days; + scope.granularity_keep_amount = granularity; + scope.keep_unit = _.find(scope.keep_unit_choices, function(i){ + return i.value === keep_unit; + }); + scope.granularity_keep_unit =_.find(scope.granularity_keep_unit_choices, function(i){ + return i.value === granularity_keep_unit; + }); + } + } + + if (scope.removeScheduleFound) { + scope.removeScheduleFound(); + } + scope.removeScheduleFound = scope.$on('ScheduleFound', function() { + $('#form-container').empty(); + scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); + scheduler.inject('form-container', false); + scheduler.injectDetail('occurrences', false); + + if (!/DTSTART/.test(schedule.rrule)) { + schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); + } + schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); + schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); + scope.$on("htmlDetailReady", function() { + scheduler.setRRule(schedule.rrule); + scheduler.setName(schedule.name); + $rootScope.$broadcast("ScheduleFormCreated", scope); + }); + scope.showRRuleDetail = false; + + scheduler.setRRule(schedule.rrule); + scheduler.setName(schedule.name); + if(scope.isFactCleanup || scope.cleanupJob){ + setGranularity(); + } + }); + + + if (scope.removeScheduleSaved) { + scope.removeScheduleSaved(); + } + scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { + Wait('stop'); + if (callback) { + scope.$emit(callback, data); + } + $state.go("^"); + }); + scope.saveSchedule = function() { + schedule.extra_data = scope.extraVars; + SchedulePost({ + scope: scope, + url: url, + scheduler: scheduler, + callback: 'ScheduleSaved', + mode: 'edit', + schedule: schedule + }); + }; + + Wait('start'); + + // Get the existing record + Rest.setUrl(url); + Rest.get() + .success(function(data) { + schedule = data; + try { + schedule.extra_data = JSON.parse(schedule.extra_data); + } catch(e) { + // do nothing + } + scope.extraVars = data.extra_data === '' ? '---' : '---\n' + jsyaml.safeDump(data.extra_data); + + if(schedule.extra_data.hasOwnProperty('granularity')){ + scope.isFactCleanup = true; + } + if (schedule.extra_data.hasOwnProperty('days')){ + scope.cleanupJob = true; + } + + scope.schedule_obj = data; + + scope.$emit('ScheduleFound'); + }) + .error(function(data,status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); + }); + }; + } + +EditSchedule.$inject = + [ 'SchedulerInit', '$rootScope', 'Wait', 'Rest', + 'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state' + ]; diff --git a/awx/ui/client/src/scheduler/factories/r-rule-to-api.factory.js b/awx/ui/client/src/scheduler/factories/r-rule-to-api.factory.js new file mode 100644 index 0000000000..1e3b251d8a --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/r-rule-to-api.factory.js @@ -0,0 +1,10 @@ +export default + function RRuleToAPI() { + return function(rrule) { + var response; + response = rrule.replace(/(^.*(?=DTSTART))(DTSTART=.*?;)(.*$)/, function(str, p1, p2, p3) { + return p2.replace(/\;/,'').replace(/=/,':') + ' ' + 'RRULE:' + p1 + p3; + }); + return response; + }; + } diff --git a/awx/ui/client/src/scheduler/factories/schedule-post.factory.js b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js new file mode 100644 index 0000000000..2c636f8b89 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js @@ -0,0 +1,78 @@ +export default + function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait) { + return function(params) { + var scope = params.scope, + url = params.url, + scheduler = params.scheduler, + mode = params.mode, + schedule = (params.schedule) ? params.schedule : {}, + callback = params.callback, + newSchedule, rrule, extra_vars; + if (scheduler.isValid()) { + Wait('start'); + newSchedule = scheduler.getValue(); + rrule = scheduler.getRRule(); + schedule.name = newSchedule.name; + schedule.rrule = RRuleToAPI(rrule.toString()); + schedule.description = (/error/.test(rrule.toText())) ? '' : rrule.toText(); + + if (scope.isFactCleanup) { + extra_vars = { + "older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value, + "granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value + }; + schedule.extra_data = JSON.stringify(extra_vars); + } else if (scope.cleanupJob) { + extra_vars = { + "days" : scope.scheduler_form.schedulerPurgeDays.$viewValue + }; + schedule.extra_data = JSON.stringify(extra_vars); + } + else if(scope.extraVars){ + schedule.extra_data = scope.parseType === 'yaml' ? + (scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars; + } + Rest.setUrl(url); + if (mode === 'add') { + Rest.post(schedule) + .success(function(){ + if (callback) { + scope.$emit(callback); + } + else { + Wait('stop'); + } + }) + .error(function(data, status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'POST to ' + url + ' returned: ' + status }); + }); + } + else { + Rest.put(schedule) + .success(function(){ + if (callback) { + scope.$emit(callback, schedule); + } + else { + Wait('stop'); + } + }) + .error(function(data, status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'POST to ' + url + ' returned: ' + status }); + }); + } + } + else { + return false; + } + }; + } + +SchedulePost.$inject = + [ 'Rest', + 'ProcessErrors', + 'RRuleToAPI', + 'Wait' + ]; diff --git a/awx/ui/client/src/scheduler/factories/toggle-schedule.factory.js b/awx/ui/client/src/scheduler/factories/toggle-schedule.factory.js new file mode 100644 index 0000000000..423c92227a --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/toggle-schedule.factory.js @@ -0,0 +1,46 @@ +export default + function ToggleSchedule(Wait, GetBasePath, ProcessErrors, Rest, $state) { + return function(params) { + var scope = params.scope, + id = params.id, + url = GetBasePath('schedules') + id +'/'; + + // Perform the update + if (scope.removeScheduleFound) { + scope.removeScheduleFound(); + } + scope.removeScheduleFound = scope.$on('ScheduleFound', function(e, data) { + data.enabled = (data.enabled) ? false : true; + Rest.put(data) + .success( function() { + Wait('stop'); + $state.go('.', null, {reload: true}); + }) + .error( function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to update schedule ' + id + ' PUT returned: ' + status }); + }); + }); + + Wait('start'); + + // Get the schedule + Rest.setUrl(url); + Rest.get() + .success(function(data) { + scope.$emit('ScheduleFound', data); + }) + .error(function(data,status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); + }); + }; + } + +ToggleSchedule.$inject = + [ 'Wait', + 'GetBasePath', + 'ProcessErrors', + 'Rest', + '$state' + ]; diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js index 05960b13d6..81ad13d3c3 100644 --- a/awx/ui/client/src/scheduler/main.js +++ b/awx/ui/client/src/scheduler/main.js @@ -10,12 +10,24 @@ import editController from './schedulerEdit.controller'; import {templateUrl} from '../shared/template-url/template-url.factory'; import schedulerDatePicker from './schedulerDatePicker.directive'; import { N_ } from '../i18n'; +import AddSchedule from './factories/add-schedule.factory'; +import DeleteSchedule from './factories/delete-schedule.factory'; +import EditSchedule from './factories/edit-schedule.factory'; +import RRuleToAPI from './factories/r-rule-to-api.factory'; +import SchedulePost from './factories/schedule-post.factory'; +import ToggleSchedule from './factories/toggle-schedule.factory'; export default angular.module('scheduler', []) .controller('schedulerListController', listController) .controller('schedulerAddController', addController) .controller('schedulerEditController', editController) + .factory('AddSchedule', AddSchedule) + .factory('DeleteSchedule', DeleteSchedule) + .factory('EditSchedule', EditSchedule) + .factory('RRuleToAPI', RRuleToAPI) + .factory('SchedulePost', SchedulePost) + .factory('ToggleSchedule', ToggleSchedule) .directive('schedulerDatePicker', schedulerDatePicker) .run(['$stateExtender', function($stateExtender) { // Inventory sync schedule states registered in: awx/ui/client/src/inventories/manage/groups/main.js diff --git a/awx/ui/client/src/shared/Modal.js b/awx/ui/client/src/shared/Modal.js index e6890ac367..86c2da80a9 100644 --- a/awx/ui/client/src/shared/Modal.js +++ b/awx/ui/client/src/shared/Modal.js @@ -17,7 +17,7 @@ export default -angular.module('ModalDialog', ['Utilities', 'ParseHelper']) +angular.module('ModalDialog', ['Utilities']) /** * @ngdoc method diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index 1dcfb7ac62..9c09f2f4ed 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -14,7 +14,7 @@ */ export default -angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) +angular.module('AWDirectives', ['RestServices', 'Utilities']) // awpassmatch: Add to password_confirm field. Will test if value // matches that of 'input[name="password"]' diff --git a/awx/ui/client/src/helpers/LoadConfig.js b/awx/ui/client/src/shared/load-config/load-config.factory.js similarity index 82% rename from awx/ui/client/src/helpers/LoadConfig.js rename to awx/ui/client/src/shared/load-config/load-config.factory.js index 93d381079b..6394acd288 100644 --- a/awx/ui/client/src/helpers/LoadConfig.js +++ b/awx/ui/client/src/shared/load-config/load-config.factory.js @@ -1,30 +1,5 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:LoadConfig - * @description Attempts to load local_config.js. If not found, loads config.js. Then evaluates the loaded - * javascript, putting the result in $AnsibleConfig. - * LoadConfigHelper - * - * - * - */ - -/*jshint evil:true */ - - - export default -angular.module('LoadConfigHelper', ['Utilities']) - -.factory('LoadConfig', ['$log', '$rootScope', '$http', '$location', 'GetBasePath', - 'ProcessErrors', 'Rest', 'Store', - function($log, $rootScope, $http, $location, GetBasePath, ProcessErrors, Rest, Store) { + function LoadConfig($log, $rootScope, $http, $location, GetBasePath, ProcessErrors, Rest, Store) { return function() { // These ettings used to be found in config.js, hardcoded now. @@ -105,4 +80,8 @@ angular.module('LoadConfigHelper', ['Utilities']) }; } -]); + +LoadConfig.$inject = + [ '$log', '$rootScope', '$http', '$location', + 'GetBasePath', 'ProcessErrors', 'Rest', 'Store' + ]; diff --git a/awx/ui/client/src/shared/load-config/main.js b/awx/ui/client/src/shared/load-config/main.js new file mode 100644 index 0000000000..3accf42ffa --- /dev/null +++ b/awx/ui/client/src/shared/load-config/main.js @@ -0,0 +1,5 @@ +import LoadConfig from './load-config.factory'; + +export default + angular.module('loadconfig', []) + .factory('LoadConfig', LoadConfig); diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 5b68027dde..c1c1b606f9 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -20,6 +20,10 @@ import templateUrl from './template-url/main'; import RestServices from '../rest/main'; import stateDefinitions from './stateDefinitions.factory'; import apiLoader from './api-loader'; +import variables from './variables/main'; +import parse from './parse/main'; +import loadconfig from './load-config/main'; +import Modal from './Modal'; import 'angular-duration-format'; export default @@ -36,6 +40,10 @@ angular.module('shared', [listGenerator.name, templateUrl.name, RestServices.name, apiLoader.name, + variables.name, + parse.name, + loadconfig.name, + Modal.name, require('angular-cookies'), 'angular-duration-format' ]) diff --git a/awx/ui/client/src/shared/parse/main.js b/awx/ui/client/src/shared/parse/main.js new file mode 100644 index 0000000000..962bf14bcf --- /dev/null +++ b/awx/ui/client/src/shared/parse/main.js @@ -0,0 +1,5 @@ +import ParseTypeChange from './parse-type-change.factory'; + +export default + angular.module('parse', []) + .factory('ParseTypeChange', ParseTypeChange); diff --git a/awx/ui/client/src/shared/parse/parse-type-change.factory.js b/awx/ui/client/src/shared/parse/parse-type-change.factory.js new file mode 100644 index 0000000000..678c91abe9 --- /dev/null +++ b/awx/ui/client/src/shared/parse/parse-type-change.factory.js @@ -0,0 +1,93 @@ +import 'codemirror/lib/codemirror.js'; +import 'codemirror/mode/javascript/javascript.js'; +import 'codemirror/mode/yaml/yaml.js'; +import 'codemirror/addon/lint/lint.js'; +import 'angular-codemirror/lib/yaml-lint.js'; +import 'codemirror/addon/edit/closebrackets.js'; +import 'codemirror/addon/edit/matchbrackets.js'; +import 'codemirror/addon/selection/active-line.js'; + +export default + function ParseTypeChange(Alert, AngularCodeMirror) { + return function(params) { + var scope = params.scope, + field_id = params.field_id, + fld = (params.variable) ? params.variable : 'variables', + pfld = (params.parse_variable) ? params.parse_variable : 'parseType', + onReady = params.onReady, + onChange = params.onChange, + readOnly = params.readOnly; + + function removeField(fld) { + //set our model to the last change in CodeMirror and then destroy CodeMirror + scope[fld] = scope[fld + 'codeMirror'].getValue(); + $('#cm-' + fld + '-container > .CodeMirror').empty().remove(); + } + + function createField(onChange, onReady, fld) { + //hide the textarea and show a fresh CodeMirror with the current mode (json or yaml) + + scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly); + scope[fld + 'codeMirror'].addModes(global.$AnsibleConfig.variable_edit_modes); + scope[fld + 'codeMirror'].showTextArea({ + scope: scope, + model: fld, + element: field_id, + lineNumbers: true, + mode: scope[pfld], + onReady: onReady, + onChange: onChange + }); + } + + // Hide the textarea and show a CodeMirror editor + createField(onChange, onReady, fld); + + + // Toggle displayed variable string between JSON and YAML + scope.parseTypeChange = function(model, fld) { + var json_obj; + if (scope[model] === 'json') { + // converting yaml to json + try { + removeField(fld); + json_obj = jsyaml.load(scope[fld]); + if ($.isEmptyObject(json_obj)) { + scope[fld] = "{}"; + } + else { + scope[fld] = JSON.stringify(json_obj, null, " "); + } + createField(onReady, onChange, fld); + } + catch (e) { + Alert('Parse Error', 'Failed to parse valid YAML. ' + e.message); + setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(onReady, onChange, fld); }); }, 500); + } + } + else { + // convert json to yaml + try { + removeField(fld); + json_obj = JSON.parse(scope[fld]); + if ($.isEmptyObject(json_obj)) { + scope[fld] = '---'; + } + else { + scope[fld] = jsyaml.safeDump(json_obj); + } + createField(onReady, onChange, fld); + } + catch (e) { + Alert('Parse Error', 'Failed to parse valid JSON. ' + e.message); + setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(onReady, onChange, fld); }); }, 500 ); + } + } + }; + }; + } + +ParseTypeChange.$inject = + [ 'Alert', + 'AngularCodeMirror' + ]; diff --git a/awx/ui/client/src/shared/variables/main.js b/awx/ui/client/src/shared/variables/main.js new file mode 100644 index 0000000000..29942a7ff9 --- /dev/null +++ b/awx/ui/client/src/shared/variables/main.js @@ -0,0 +1,9 @@ +import ParseVariableString from './parse-variable-string.factory'; +import SortVariables from './sort-variables.factory'; +import ToJSON from './to-json.factory'; + +export default + angular.module('variables', []) + .factory('ParseVariableString', ParseVariableString) + .factory('SortVariables', SortVariables) + .factory('ToJSON', ToJSON); diff --git a/awx/ui/client/src/shared/variables/parse-variable-string.factory.js b/awx/ui/client/src/shared/variables/parse-variable-string.factory.js new file mode 100644 index 0000000000..71e145897f --- /dev/null +++ b/awx/ui/client/src/shared/variables/parse-variable-string.factory.js @@ -0,0 +1,55 @@ +export default + function ParseVariableString($log, ProcessErrors, SortVariables) { + return function (variables) { + var result = "---", json_obj; + if (typeof variables === 'string') { + if (variables === "{}" || variables === "null" || variables === "" || variables === "\"\"") { + // String is empty, return --- + } else { + try { + json_obj = JSON.parse(variables); + json_obj = SortVariables(json_obj); + result = jsyaml.safeDump(json_obj); + + } + catch (e) { + $log.debug('Attempt to parse extra_vars as JSON failed. Check that the variables parse as yaml. Set the raw string as the result.'); + try { + // do safeLoad, which well error if not valid yaml + json_obj = jsyaml.safeLoad(variables); + // but just send the variables + result = variables; + } + catch(e2) { + ProcessErrors(null, variables, e2.message, null, { hdr: 'Error!', + msg: 'Attempts to parse variables as JSON and YAML failed. Last attempt returned: ' + e2.message }); + } + } + } + } + else { + if ($.isEmptyObject(variables) || variables === null) { + // Empty object, return --- + } + else { + // convert object to yaml + try { + json_obj = SortVariables(variables); + result = jsyaml.safeDump(json_obj); + // result = variables; + } + catch(e3) { + ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!', + msg: 'Attempt to convert JSON object to YAML document failed: ' + e3.message }); + } + } + } + return result; + }; + } + +ParseVariableString.$inject = + [ '$log', + 'ProcessErrors', + 'SortVariables' + ]; diff --git a/awx/ui/client/src/shared/variables/sort-variables.factory.js b/awx/ui/client/src/shared/variables/sort-variables.factory.js new file mode 100644 index 0000000000..a9cb3a363b --- /dev/null +++ b/awx/ui/client/src/shared/variables/sort-variables.factory.js @@ -0,0 +1,23 @@ +export default + function SortVariables() { + return function(variableObj) { + var newObj; + function sortIt(objToSort) { + var i, + keys = Object.keys(objToSort), + newObj = {}; + keys = keys.sort(); + for (i=0; i < keys.length; i++) { + if (typeof objToSort[keys[i]] === 'object' && objToSort[keys[i]] !== null && !Array.isArray(objToSort[keys[i]])) { + newObj[keys[i]] = sortIt(objToSort[keys[i]]); + } + else { + newObj[keys[i]] = objToSort[keys[i]]; + } + } + return newObj; + } + newObj = sortIt(variableObj); + return newObj; + }; + } diff --git a/awx/ui/client/src/shared/variables/to-json.factory.js b/awx/ui/client/src/shared/variables/to-json.factory.js new file mode 100644 index 0000000000..70996d489e --- /dev/null +++ b/awx/ui/client/src/shared/variables/to-json.factory.js @@ -0,0 +1,80 @@ +export default + function ToJSON($log, ProcessErrors) { + return function(parseType, variables, stringify, reviver) { + var json_data, + result, + tmp; + // bracketVar, + // key, + // lines, i, newVars = []; + if (parseType === 'json') { + try { + // perform a check to see if the user cleared the field completly + if(variables.trim() === "" || variables.trim() === "{" || variables.trim() === "}" ){ + variables = "{}"; + } + //parse a JSON string + if (reviver) { + json_data = JSON.parse(variables, reviver); + } + else { + json_data = JSON.parse(variables); + } + } + catch(e) { + json_data = {}; + $log.error('Failed to parse JSON string. Parser returned: ' + e.message); + ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', + msg: 'Failed to parse JSON string. Parser returned: ' + e.message }); + throw 'Parse error. Failed to parse variables.'; + } + } else { + try { + if(variables.trim() === "" || variables.trim() === "-" || variables.trim() === "--"){ + variables = '---'; + } + json_data = jsyaml.safeLoad(variables); + if(json_data!==null){ + // unparsing just to make sure no weird characters are included. + tmp = jsyaml.dump(json_data); + if(tmp.indexOf('[object Object]')!==-1){ + throw "Failed to parse YAML string. Parser returned' + key + ' : ' +value + '.' "; + } + } + } + catch(e) { + json_data = undefined; // {}; + // $log.error('Failed to parse YAML string. Parser returned undefined'); + ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', + msg: 'Failed to parse YAML string. Parser returned undefined'}); + } + } + // Make sure our JSON is actually an object + if (typeof json_data !== 'object') { + ProcessErrors(null, variables, null, null, { hdr: 'Error!', + msg: 'Failed to parse variables. Attempted to parse ' + parseType + '. Parser did not return an object.' }); + // setTimeout( function() { + throw 'Parse error. Failed to parse variables.'; + // }, 1000); + } + result = json_data; + if (stringify) { + if(json_data === undefined){ + result = undefined; + } + else if ($.isEmptyObject(json_data)) { + result = ""; + } else { + // utilize the parsing to get here + // but send the raw variable string + result = variables; + } + } + return result; + }; + } + +ToJSON.$inject = + [ '$log', + 'ProcessErrors' + ]; diff --git a/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js new file mode 100644 index 0000000000..891d391f95 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js @@ -0,0 +1,156 @@ +export default + function CallbackHelpInit($location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors, ParseTypeChange, + ParseVariableString, Empty, InventoryList, CredentialList, ProjectList, Wait) { + return function(params) { + var scope = params.scope, + defaultUrl = GetBasePath('job_templates'), + // generator = GenerateForm, + form = JobTemplateForm(), + // loadingFinishedCount = 0, + // base = $location.path().replace(/^\//, '').split('/')[0], + master = {}, + id = $stateParams.job_template_id; + // checkSCMStatus, getPlaybooks, callback, + // choicesCount = 0; + + CredentialList = _.cloneDeep(CredentialList); + + // The form uses awPopOverWatch directive to 'watch' scope.callback_help for changes. Each time the + // popover is activated, a function checks the value of scope.callback_help before constructing the content. + scope.setCallbackHelp = function() { + scope.callback_help = "

With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + + "template. The request from the host must be a POST. Here is an example using curl:

\n" + + "
curl --data \"host_config_key=" + scope.example_config_key + "\" " +
+                    scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/
\n" + + "

Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + + "locate the host, the request will be denied.

" + + "

Successful requests create an entry on the Jobs page, where results and history can be viewed.

"; + }; + + // The md5 helper emits NewMD5Generated whenever a new key is available + if (scope.removeNewMD5Generated) { + scope.removeNewMD5Generated(); + } + scope.removeNewMD5Generated = scope.$on('NewMD5Generated', function() { + scope.configKeyChange(); + }); + + // Fired when user enters a key value + scope.configKeyChange = function() { + scope.example_config_key = scope.host_config_key; + scope.setCallbackHelp(); + }; + + // Set initial values and construct help text + scope.callback_server_path = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : ''); + scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; + scope.example_template_id = 'N'; + scope.setCallbackHelp(); + + // this fills the job template form both on copy of the job template + // and on edit + scope.fillJobTemplate = function(){ + // id = id || $rootScope.copy.id; + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + id); + Rest.get() + .success(function (data) { + scope.job_template_obj = data; + scope.name = data.name; + var fld, i; + for (fld in form.fields) { + if (fld !== 'variables' && fld !== 'survey' && data[fld] !== null && data[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 (data[fld] === scope[fld + '_options'][i].value) { + scope[fld] = scope[fld + '_options'][i]; + } + } + } else { + scope[fld] = data[fld]; + } + } else { + scope[fld] = data[fld]; + if(!Empty(data.summary_fields.survey)) { + scope.survey_exists = true; + } + } + master[fld] = scope[fld]; + } + if (fld === 'variables') { + // Parse extra_vars, converting to YAML. + scope.variables = ParseVariableString(data.extra_vars); + master.variables = scope.variables; + } + if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; + } + if (form.fields[fld].type === 'checkbox_group') { + for(var j=0; j { let $controller; let workflowResults; let $rootScope; - let ParseVariableString; let workflowResultsService; let $interval; @@ -17,10 +16,8 @@ describe('Controller: workflowResults', () => { } }; - beforeEach(angular.mock.module('VariablesHelper')); - beforeEach(angular.mock.module('workflowResults', ($provide) => { - ['PromptDialog', 'Prompt', 'Wait', 'Rest', '$state', 'ProcessErrors', + ['PromptDialog', 'Prompt', 'Wait', 'Rest', '$state', 'ProcessErrors', 'InitiatePlaybookRun', 'jobLabels', 'workflowNodes', 'count', ].forEach((item) => { $provide.value(item, {}); @@ -30,8 +27,9 @@ describe('Controller: workflowResults', () => { $provide.value('workflowData', workflow_job_json); $provide.value('workflowDataOptions', workflow_job_options_json); $provide.value('ParseTypeChange', function() {}); + $provide.value('ParseVariableString', function() {}); $provide.value('i18n', { '_': (a) => { return a; } }); - $provide.provider('$stateProvider', { '$get': function() { return function() {} } }); + $provide.provider('$stateProvider', { '$get': function() { return function() {}; } }); $provide.service('WorkflowService', function($q) { return { buildTree: function() { @@ -39,14 +37,13 @@ describe('Controller: workflowResults', () => { deferred.resolve(treeData); return deferred.promise; } - } + }; }); })); - beforeEach(angular.mock.inject(function(_$controller_, _$rootScope_, _ParseVariableString_, _workflowResultsService_, _$interval_){ + beforeEach(angular.mock.inject(function(_$controller_, _$rootScope_, _workflowResultsService_, _$interval_){ $controller = _$controller_; $rootScope = _$rootScope_; - ParseVariableString = _ParseVariableString_; workflowResultsService = _workflowResultsService_; $interval = _$interval_; @@ -102,7 +99,7 @@ describe('Controller: workflowResults', () => { describe('job waiting', () => { beforeEach(() => { jobWaitingWorkflowResultsControllerFixture(null, 'waiting'); - }); + }); it('should not start elapsed timer', () => { expect(workflowResultsService.createOneSecondTimer).not.toHaveBeenCalled(); @@ -133,4 +130,4 @@ describe('Controller: workflowResults', () => { }); }); }); -}); \ No newline at end of file +});