diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index cdc39200f3..d7c42b13fd 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -56,6 +56,7 @@ import setupMenu from './setup-menu/main'; import mainMenu from './main-menu/main'; import breadCrumb from './bread-crumb/main'; import browserData from './browser-data/main'; +import configuration from './configuration/main'; import dashboard from './dashboard/main'; import moment from './shared/moment/main'; import login from './login/main'; @@ -100,6 +101,7 @@ var tower = angular.module('Tower', [ license.name, RestServices.name, browserData.name, + configuration.name, systemTracking.name, inventories.name, inventoryScripts.name, diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js new file mode 100644 index 0000000000..3ba3d2fa72 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -0,0 +1,235 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default [ + '$scope', + '$state', + '$stateParams', + '$timeout', + '$q', + 'configurationGithubForm', + 'configurationGithubOrgForm', + 'configurationGithubTeamForm', + 'configurationGoogleForm', + 'configurationLdapForm', + 'configurationRadiusForm', + 'configurationSamlForm', + 'ConfigurationService', + 'ConfigurationUtils', + 'CreateSelect2', + 'GenerateForm', + 'ParseTypeChange', + 'Wait', + function( + $scope, + $state, + $stateParams, + $timeout, + $q, + configurationGithubForm, + configurationGithubOrgForm, + configurationGithubTeamForm, + configurationGoogleForm, + configurationLdapForm, + configurationRadiusForm, + configurationSamlForm, + ConfigurationService, + ConfigurationUtils, + CreateSelect2, + GenerateForm, + ParseTypeChange, + Wait + ) { + var authVm = this; + + var generator = GenerateForm; + var formTracker = $scope.$parent.vm.formTracker; + var dropdownValue = 'github'; + var activeAuthForm = 'github'; + + // Default active form + if ($stateParams.currentTab === '' || $stateParams.currentTab === 'auth') { + formTracker.setCurrentAuth(activeAuthForm); + } + + var activeForm = function() { + if(!$scope.$parent[formTracker.currentFormName()].$dirty) { + authVm.activeAuthForm = authVm.dropdownValue; + formTracker.setCurrentAuth(authVm.activeAuthForm); + } else { + var msg = 'You have unsaved changes. Would you like to proceed without saving?'; + var title = 'Warning: Unsaved Changes'; + var buttons = [{ + label: "Discard changes", + "class": "btn Form-cancelButton", + "id": "formmodal-cancel-button", + onClick: function() { + $scope.$parent.vm.populateFromApi(); + $scope.$parent[formTracker.currentFormName()].$setPristine(); + authVm.activeAuthForm = authVm.dropdownValue; + formTracker.setCurrentAuth(authVm.activeAuthForm); + $('#FormModal-dialog').dialog('close'); + } + }, { + label: "Save changes", + onClick: function() { + $scope.$parent.vm.formSave() + .then(function() { + $scope.$parent[formTracker.currentFormName()].$setPristine(); + $scope.$parent.vm.populateFromApi(); + authVm.activeAuthForm = authVm.dropdownValue; + formTracker.setCurrentAuth(authVm.activeAuthForm); + $('#FormModal-dialog').dialog('close'); + }); + }, + "class": "btn btn-primary", + "id": "formmodal-save-button" + }]; + $scope.$parent.vm.triggerModal(msg, title, buttons); + } + formTracker.setCurrentAuth(authVm.activeAuthForm); + }; + + var dropdownOptions = [ + {label: 'Github', value: 'github'}, + {label: 'Github Org', value: 'github_org'}, + {label: 'Github Team', value: 'github_team'}, + {label: 'Google OAuth2', value: 'google_oauth'}, + {label: 'LDAP', value: 'ldap'}, + {label: 'RADIUS', value: 'radius'}, + {label: 'SAML', value: 'saml'} + ]; + + CreateSelect2({ + element: '#configure-dropdown-nav', + multiple: false, + }); + + var authForms = [{ + formDef: configurationGithubForm, + id: 'auth-github-form' + }, { + formDef: configurationGithubOrgForm, + id: 'auth-github-org-form' + }, { + formDef: configurationGithubTeamForm, + id: 'auth-github-team-form' + }, { + formDef: configurationGoogleForm, + id: 'auth-google-form' + }, { + formDef: configurationLdapForm, + id: 'auth-ldap-form' + }, { + formDef: configurationRadiusForm, + id: 'auth-radius-form' + }, { + formDef: configurationSamlForm, + id: 'auth-saml-form' + }, ]; + + var forms = _.pluck(authForms, 'formDef'); + + _.each(forms, function(form) { + var keys = _.keys(form.fields); + _.each(keys, function(key) { + if($scope.$parent.configDataResolve[key].type === 'choice') { + // Create options for dropdowns + var optionsGroup = key + '_options'; + $scope.$parent[optionsGroup] = []; + _.each($scope.$parent.configDataResolve[key].choices, function(choice){ + $scope.$parent[optionsGroup].push({ + name: choice[0], + label: choice[1], + value: choice[0] + }); + }); + } + addFieldInfo(form, key); + }); + }); + + function addFieldInfo(form, key) { + _.extend(form.fields[key], { + awPopOver: $scope.$parent.configDataResolve[key].help_text, + label: $scope.$parent.configDataResolve[key].label, + name: key, + toggleSource: key, + dataPlacement: 'top', + placeholder: ConfigurationUtils.formatPlaceholder($scope.$parent.configDataResolve[key].placeholder, key) || null, + dataTitle: $scope.$parent.configDataResolve[key].label + }); + } + + $scope.$parent.parseType = 'json'; + + _.each(authForms, function(form) { + // Generate the forms + generator.inject(form.formDef, { + id: form.id, + mode: 'edit', + scope: $scope.$parent, + related: true + }); + }); + + // Flag to avoid re-rendering and breaking Select2 dropdowns on tab switching + var dropdownRendered = false; + + $scope.$on('populated', function() { + // Attach codemirror to fields that need it + _.each(authForms, function(form) { + _.each(form.formDef.fields, function(field) { + // Codemirror balks at empty values so give it one + if($scope.$parent[field.name] === null && field.codeMirror) { + $scope.$parent[field.name] = '{}'; + } + if(field.codeMirror) { + ParseTypeChange({ + scope: $scope.$parent, + variable: field.name, + parse_variable: 'parseType', + field_id: form.formDef.name + '_' + field.name + }); + } + }); + }); + + // Create Select2 fields + var opts = []; + if($scope.$parent.AUTH_LDAP_GROUP_TYPE !== null) { + opts.push({ + id: $scope.$parent.AUTH_LDAP_GROUP_TYPE, + text: $scope.$parent.AUTH_LDAP_GROUP_TYPE + }); + } + + if(!dropdownRendered) { + dropdownRendered = true; + CreateSelect2({ + element: '#configuration_ldap_template_AUTH_LDAP_GROUP_TYPE', + multiple: false, + placeholder: 'Select group types', + opts: opts + }); + // Fix for bug where adding selected opts causes form to be $dirty and triggering modal + // TODO Find better solution for this bug + $timeout(function(){ + $scope.$parent.configuration_ldap_template_form.$setPristine(); + }, 1000); + } + + }); + + angular.extend(authVm, { + activeForm: activeForm, + activeAuthForm: activeAuthForm, + authForms: authForms, + dropdownOptions: dropdownOptions, + dropdownValue: dropdownValue + }); + } +]; diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html new file mode 100644 index 0000000000..d80652f124 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html @@ -0,0 +1,53 @@ +
+ +
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js new file mode 100644 index 0000000000..6bc58773e3 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-org.form.js @@ -0,0 +1,43 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + name: 'configuration_github_org_template', + showActions: true, + showHeader: false, + + fields: { + SOCIAL_AUTH_GITHUB_ORG_KEY: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_ORG_KEY' + }, + SOCIAL_AUTH_GITHUB_ORG_SECRET: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_ORG_SECRET' + }, + SOCIAL_AUTH_GITHUB_ORG_NAME: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_ORG_NAME' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js new file mode 100644 index 0000000000..bad0c95627 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github-team.form.js @@ -0,0 +1,43 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + name: 'configuration_github_team_template', + showActions: true, + showHeader: false, + + fields: { + SOCIAL_AUTH_GITHUB_TEAM_KEY: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_TEAM_KEY' + }, + SOCIAL_AUTH_GITHUB_TEAM_SECRET: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_TEAM_SECRET' + }, + SOCIAL_AUTH_GITHUB_TEAM_ID: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_TEAM_ID' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js new file mode 100644 index 0000000000..ee46c53fb7 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-github.form.js @@ -0,0 +1,39 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + name: 'configuration_github_template', + showActions: true, + showHeader: false, + + fields: { + SOCIAL_AUTH_GITHUB_KEY: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_KEY' + }, + SOCIAL_AUTH_GITHUB_SECRET: { + type: 'text', + reset: 'SOCIAL_AUTH_GITHUB_SECRET' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js new file mode 100644 index 0000000000..b00748da55 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-google-oauth2.form.js @@ -0,0 +1,51 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + name: 'configuration_google_oauth_template', + showActions: true, + showHeader: false, + + fields: { + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: { + type: 'text', + reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY' + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: { + type: 'text', + reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET' + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: { + type: 'textarea', + reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS', + rows: 6 + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: { + type: 'textarea', + reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS', + codeMirror: true, + rows: 6, + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js new file mode 100644 index 0000000000..2ef6b8b5c4 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-ldap.form.js @@ -0,0 +1,99 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + // editTitle: 'Authorization Configuration', + name: 'configuration_ldap_template', + showActions: true, + showHeader: false, + + fields: { + AUTH_LDAP_SERVER_URI: { + type: 'text', + reset: 'AUTH_LDAP_SERVER_URI' + }, + AUTH_LDAP_BIND_DN: { + type: 'text', + reset: 'AUTH_LDAP_BIND_DN' + }, + AUTH_LDAP_BIND_PASSWORD: { + type: 'password' + }, + AUTH_LDAP_USER_SEARCH: { + type: 'textarea', + reset: 'AUTH_LDAP_USER_SEARCH' + }, + AUTH_LDAP_USER_DN_TEMPLATE: { + type: 'text', + reset: 'AUTH_LDAP_USER_DN_TEMPLATE' + }, + AUTH_LDAP_USER_ATTR_MAP: { + type: 'textarea', + reset: 'AUTH_LDAP_USER_ATTR_MAP', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + }, + AUTH_LDAP_GROUP_SEARCH: { + type: 'textarea', + reset: 'AUTH_LDAP_GROUP_SEARCH' + }, + AUTH_LDAP_GROUP_TYPE: { + type: 'select', + reset: 'AUTH_LDAP_GROUP_TYPE', + ngOptions: 'group.label for group in AUTH_LDAP_GROUP_TYPE_options track by group.value', + }, + AUTH_LDAP_REQUIRE_GROUP: { + type: 'text', + reset: 'AUTH_LDAP_REQUIRE_GROUP' + }, + AUTH_LDAP_DENY_GROUP: { + type: 'text', + reset: 'AUTH_LDAP_DENY_GROUP' + }, + AUTH_LDAP_START_TLS: { + type: 'toggleSwitch' + }, + AUTH_LDAP_USER_FLAGS_BY_GROUP: { + type: 'textarea', + reset: 'AUTH_LDAP_USER_FLAGS_BY_GROUP', + codeMirror: true, + rows: 6, + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + }, + AUTH_LDAP_ORGANIZATION_MAP: { + type: 'textarea', + reset: 'AUTH_LDAP_ORGANIZATION_MAP', + codeMirror: true, + rows: 6, + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + }, + AUTH_LDAP_TEAM_MAP: { + type: 'textarea', + reset: 'AUTH_LDAP_TEAM_MAP', + codeMirror: true, + rows: 6, + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js new file mode 100644 index 0000000000..cd8dc68352 --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-radius.form.js @@ -0,0 +1,44 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + // editTitle: 'Authorization Configuration', + name: 'configuration_radius_template', + showActions: true, + showHeader: false, + + fields: { + RADIUS_SERVER: { + type: 'text', + reset: 'RADIUS_SERVER' + }, + RADIUS_PORT: { + type: 'text', + reset: 'RADIUS_PORT' + }, + RADIUS_SECRET: { + type: 'text', + reset: 'RADIUS_SECRET' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js new file mode 100644 index 0000000000..462e1373fd --- /dev/null +++ b/awx/ui/client/src/configuration/auth-form/sub-forms/auth-saml.form.js @@ -0,0 +1,71 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + name: 'configuration_saml_template', + showActions: true, + showHeader: false, + + fields: { + SOCIAL_AUTH_SAML_SP_ENTITY_ID: { + type: 'text', + reset: 'SOCIAL_AUTH_SAML_SP_ENTITY_ID' + }, + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: { + type: 'text', + reset: 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT' + }, + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: { + type: 'text', + reset: 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY' + }, + SOCIAL_AUTH_SAML_ORG_INFO: { + type: 'textarea', + reset: 'SOCIAL_AUTH_SAML_ORG_INFO', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth' + }, + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: { + type: 'textarea', + reset: 'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth' + }, + SOCIAL_AUTH_SAML_SUPPORT_CONTACT: { + type: 'textarea', + reset: 'SOCIAL_AUTH_SAML_SUPPORT_CONTACT', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth' + }, + SOCIAL_AUTH_SAML_ENABLED_IDPS: { + type: 'textarea', + reset: 'SOCIAL_AUTH_SAML_ENABLED_IDPS', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/configuration.block.less b/awx/ui/client/src/configuration/configuration.block.less new file mode 100644 index 0000000000..9c6c855274 --- /dev/null +++ b/awx/ui/client/src/configuration/configuration.block.less @@ -0,0 +1,27 @@ +.Form-resetValue { + float: right; + text-transform: uppercase; + font-weight: normal; + cursor: pointer; + font-size: 12px; +} + +.Form-tab { + min-width: 77px; +} + +.Form-button--left { + margin-right: auto; + margin-left: 0; +} + +.Form-nav--dropdown { + width: 175px; + margin-top: -52px; + margin-bottom: 22px; + margin-left: auto; +} + +.Form-tabRow { + width: 80%; +} diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js new file mode 100644 index 0000000000..1214c994d3 --- /dev/null +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -0,0 +1,405 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default [ + '$scope', '$state', '$stateParams', '$timeout', '$q', 'Alert', 'ClearScope', + 'ConfigurationService', 'ConfigurationUtils', 'CreateDialog', 'CreateSelect2', 'ParseTypeChange', 'ProcessErrors', + 'Wait', 'configDataResolve', + //Form definitions + 'configurationGithubForm', + 'configurationGithubOrgForm', + 'configurationGithubTeamForm', + 'configurationGoogleForm', + 'configurationLdapForm', + 'configurationRadiusForm', + 'configurationSamlForm', + 'ConfigurationJobsForm', + 'ConfigurationSystemForm', + 'ConfigurationUiForm', + function( + $scope, $state, $stateParams, $timeout, $q, Alert, ClearScope, + ConfigurationService, ConfigurationUtils, CreateDialog, CreateSelect2, ParseTypeChange, ProcessErrors, + Wait, configDataResolve, + //Form definitions + configurationGithubForm, + configurationGithubOrgForm, + configurationGithubTeamForm, + configurationGoogleForm, + configurationLdapForm, + configurationRadiusForm, + configurationSamlForm, + ConfigurationJobsForm, + ConfigurationSystemForm, + ConfigurationUiForm + ) { + var vm = this; + + var formDefs = { + 'github': configurationGithubForm, + 'github_org': configurationGithubOrgForm, + 'github_team': configurationGithubTeamForm, + 'google_oauth': configurationGoogleForm, + 'ldap': configurationLdapForm, + 'radius': configurationRadiusForm, + 'saml': configurationSamlForm, + 'jobs': ConfigurationJobsForm, + 'system': ConfigurationSystemForm, + 'ui': ConfigurationUiForm + }; + + var populateFromApi = function() { + ConfigurationService.getCurrentValues() + .then(function(data) { + var currentKeys = _.keys(data); + _.each(currentKeys, function(key) { + if (data[key] !== null && typeof data[key] === 'object') { + if (Array.isArray(data[key])) { + //handle arrays + $scope[key] = ConfigurationUtils.arrayToList(data[key], key); + } else { + //handle nested objects + if(ConfigurationUtils.isEmpty(data[key])) { + $scope[key] = '{}'; + } else { + $scope[key] = JSON.stringify(data[key]); + } + } + } else { + $scope[key] = data[key]; + } + }); + $scope.$broadcast('populated', data); + }); + }; + + populateFromApi(); + + var formTracker = { + lastForm: '', + currentForm: '', + currentAuth: '', + setCurrent: function(form) { + this.lastForm = this.currentForm; + this.currentForm = form; + }, + setCurrentAuth: function(form) { + this.currentAuth = form; + this.setCurrent(this.currentAuth); + }, + getCurrent: function() { + return this.currentForm; + }, + currentFormName: function() { + return 'configuration_' + this.currentForm + '_template_form'; + } + }; + + // Default to auth form and tab + if ($stateParams.currentTab === '') { + $state.go('configuration', { + currentTab: 'auth' + }, { + location: true, + inherit: false, + notify: false, + reload: false + }); + } + + var currentForm = ''; + var currentTab = function() { + if ($stateParams.currentTab === '' || $stateParams.currentTab === 'auth') { + return 'auth'; + } else if ($stateParams.currentTab !== '' && $stateParams.currentTab !== 'auth') { + formTracker.setCurrent($stateParams.currentTab); + return $stateParams.currentTab; + } + }; + var activeTab = currentTab(); + + $scope.configDataResolve = configDataResolve; + + function activeTabCheck(setForm) { + if(!$scope[formTracker.currentFormName()].$dirty) { + active(setForm); + } else { + var msg = 'You have unsaved changes. Would you like to proceed without saving?'; + var title = 'Warning: Unsaved Changes'; + var buttons = [{ + label: "Discard changes", + "class": "btn Form-cancelButton", + "id": "formmodal-cancel-button", + onClick: function() { + clearApiErrors(); + populateFromApi(); + $scope[formTracker.currentFormName()].$setPristine(); + $('#FormModal-dialog').dialog('close'); + active(setForm); + } + }, { + label: "Save changes", + onClick: function() { + vm.formSave(); + $scope[formTracker.currentFormName()].$setPristine(); + $('#FormModal-dialog').dialog('close'); + active(setForm); + }, + "class": "btn btn-primary", + "id": "formmodal-save-button" + }]; + triggerModal(msg, title, buttons); + } + } + + function active(setForm) { + if (setForm === 'auth') { + // Default to 'github' on first load + if (formTracker.currentAuth === '') { + formTracker.setCurrentAuth('github'); + } else { + // If returning to auth tab reset current form to previously viewed + formTracker.setCurrentAuth(formTracker.currentAuth); + } + } else { + formTracker.setCurrent(setForm); + } + vm.activeTab = setForm; + $state.go('configuration', { + currentTab: setForm + }, { + location: true, + inherit: false, + notify: false, + reload: false + }); + } + + var formCancel = function() { + if ($scope[formTracker.currentFormName()].$dirty === true) { + var msg = 'You have unsaved changes. Would you like to proceed without saving?'; + var title = 'Warning: Unsaved Changes'; + var buttons = [{ + label: "Discard changes", + "class": "btn Form-cancelButton", + "id": "formmodal-cancel-button", + onClick: function() { + $('#FormModal-dialog').dialog('close'); + $state.go('setup'); + } + }, { + label: "Save changes", + onClick: function() { + $scope.formSave(); + $('#FormModal-dialog').dialog('close'); + $state.go('setup'); + }, + "class": "btn btn-primary", + "id": "formmodal-save-button" + }]; + triggerModal(msg, title, buttons); + } else { + $state.go('setup'); + } + }; + + + $scope.resetValue = function(key) { + Wait('start'); + var payload = {}; + payload[key] = $scope.configDataResolve[key].default; + + ConfigurationService.patchConfiguration(payload) + .then(function(data) { + $scope[key] = $scope.configDataResolve[key].default; + }) + .catch(function(error) { + ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + { + hdr: 'Error!', + msg: 'There was an error resetting value. Returned status: ' + error.detail + }); + + }) + .finally(function() { + Wait('stop'); + }); + }; + + var triggerModal = function(msg, title, buttons) { + if ($scope.removeModalReady) { + $scope.removeModalReady(); + } + $scope.removeModalReady = $scope.$on('ModalReady', function() { + // $('#lookup-save-button').attr('disabled', 'disabled'); + $('#FormModal-dialog').dialog('open'); + }); + + $('#FormModal-dialog').html(msg); + + CreateDialog({ + scope: $scope, + buttons: buttons, + width: 600, + height: 200, + minWidth: 500, + title: title, + id: 'FormModal-dialog', + resizable: false, + callback: 'ModalReady' + }); + }; + + function clearApiErrors() { + var currentForm = formDefs[formTracker.getCurrent()]; + for (var fld in currentForm.fields) { + if (currentForm.fields[fld].sourceModel) { + $scope[currentForm.fields[fld].sourceModel + '_' + currentForm.fields[fld].sourceField + '_api_error'] = ''; + $('[name="' + currentForm.fields[fld].sourceModel + '_' + currentForm.fields[fld].sourceField + '"]').removeClass('ng-invalid'); + } else if (currentForm.fields[fld].realName) { + $scope[currentForm.fields[fld].realName + '_api_error'] = ''; + $('[name="' + currentForm.fields[fld].realName + '"]').removeClass('ng-invalid'); + } else { + $scope[fld + '_api_error'] = ''; + $('[name="' + fld + '"]').removeClass('ng-invalid'); + } + } + if (!$scope.$$phase) { + $scope.$digest(); + } + } + + // Some dropdowns are listed as "list" type in the API even though they're a dropdown: + var multiselectDropdowns = ['AD_HOC_COMMANDS']; + var formSave = function() { + var saveDeferred = $q.defer(); + var keys = _.keys(formDefs[formTracker.getCurrent()].fields); + var payload = {}; + clearApiErrors(); + _.each(keys, function(key) { + if($scope.configDataResolve[key].type === 'choice' || multiselectDropdowns.indexOf(key) !== -1) { + //Parse dropdowns and dropdowns labeled as lists + if($scope[key] === null) { + payload[key] = null; + } else if($scope[key][0] && $scope[key][0].value !== undefined) { + payload[key] = _.map($scope[key], 'value').join(','); + } else { + payload[key] = $scope[key].value; + } + } else if($scope.configDataResolve[key].type === 'list' && $scope[key] !== null) { + // Parse lists + payload[key] = ConfigurationUtils.listToArray($scope[key], key); + } + else if($scope.configDataResolve[key].type === 'nested object') { + if($scope[key] === '') { + payload[key] = {}; + } else { + payload[key] = JSON.parse($scope[key]); + } + } + else { + // Everything else + payload[key] = $scope[key]; + } + }); + + Wait('start'); + ConfigurationService.patchConfiguration(payload) + .then(function(data) { + saveDeferred.resolve(data); + $scope[formTracker.currentFormName()].$setPristine(); + }) + .catch(function(error, status) { + ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + { + hdr: 'Error!', + msg: 'Failed to save settings. Returned status: ' + status + }); + saveDeferred.reject(error); + }) + .finally(function() { + Wait('stop'); + }); + + return saveDeferred.promise; + }; + + $scope.toggleForm = function(key) { + $scope[key] = !$scope[key]; + Wait('start'); + var payload = {}; + payload[key] = $scope[key]; + ConfigurationService.patchConfiguration(payload) + .then(function(results) { + //TODO consider updating form values with returned data here + }) + .catch(function(error, status) { + //Change back on unsuccessful update + $scope[key] = !$scope[key]; + ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + { + hdr: 'Error!', + msg: 'Failed to save toggle settings. Returned status: ' + error.detail + }); + }) + .finally(function() { + Wait('stop'); + }); + }; + + var resetAll = function() { + Wait('start'); + ConfigurationService.resetAll() + .then(function(results) { + populateFromApi(); + $scope[formTracker.currentFormName].$setPristine(); + }) + .catch(function(error) { + ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + { + hdr: 'Error!', + msg: 'There was an error resetting values. Returned status: ' + error.detail + }); + }) + .finally(function() { + Wait('stop'); + }); + }; + + var resetAllConfirm = function() { + var buttons = [{ + label: "Cancel", + "class": "btn btn-default", + "id": "formmodal-cancel-button", + onClick: function() { + $('#FormModal-dialog').dialog('close'); + } + }, { + label: "Confirm Reset", + onClick: function() { + resetAll(); + $('#FormModal-dialog').dialog('close'); + }, + "class": "btn btn-primary", + "id": "formmodal-reset-button" + }]; + var msg = 'This will reset all configuration values to their factory defaults. Are you sure you want to proceed?'; + var title = 'Confirm factory reset'; + triggerModal(msg, title, buttons); + }; + + angular.extend(vm, { + activeTab: activeTab, + activeTabCheck: activeTabCheck, + currentForm: currentForm, + formCancel: formCancel, + formTracker: formTracker, + formSave: formSave, + populateFromApi: populateFromApi, + resetAllConfirm: resetAllConfirm, + triggerModal: triggerModal + }); + } +]; diff --git a/awx/ui/client/src/configuration/configuration.partial.html b/awx/ui/client/src/configuration/configuration.partial.html new file mode 100644 index 0000000000..4a35e4bf10 --- /dev/null +++ b/awx/ui/client/src/configuration/configuration.partial.html @@ -0,0 +1,22 @@ +
+
+
+
Configure Tower
+
+
+
+
+
Authorization
+
Jobs
+
System
+
UI
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/configuration/configuration.route.js b/awx/ui/client/src/configuration/configuration.route.js new file mode 100644 index 0000000000..0026ec9e3c --- /dev/null +++ b/awx/ui/client/src/configuration/configuration.route.js @@ -0,0 +1,56 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + import {templateUrl} from '../shared/template-url/template-url.factory'; + import ConfigurationController from './configuration.controller'; + +// Import form controllers + import ConfigurationAuthController from './auth-form/configuration-auth.controller'; + import ConfigurationJobsController from './jobs-form/configuration-jobs.controller'; + import ConfigurationSystemController from './system-form/configuration-system.controller'; + import ConfigurationUiController from './ui-form/configuration-ui.controller'; + + export default { + name: 'configuration', + route: '/configuration/:currentTab', + + ncyBreadcrumb: { + label: "Edit Configuration" + }, + controller: ConfigurationController, + resolve: { + configDataResolve: ['ConfigurationService', function(ConfigurationService){ + return ConfigurationService.getConfigurationOptions(); + }] + }, + views: { + '': { + templateUrl: templateUrl('configuration/configuration'), + controller: ConfigurationController, + controllerAs: 'vm' + }, + 'auth@configuration': { + templateUrl: templateUrl('configuration/auth-form/configuration-auth'), + controller: ConfigurationAuthController, + controllerAs: 'authVm' + }, + 'jobs@configuration': { + templateUrl: templateUrl('configuration/jobs-form/configuration-jobs'), + controller: ConfigurationJobsController, + controllerAs: 'jobsVm' + }, + 'system@configuration': { + templateUrl: templateUrl('configuration/system-form/configuration-system'), + controller: ConfigurationSystemController, + controllerAs: 'systemVm' + }, + 'ui@configuration': { + templateUrl: templateUrl('configuration/ui-form/configuration-ui'), + controller: ConfigurationUiController, + controllerAs: 'uiVm' + } + }, + }; diff --git a/awx/ui/client/src/configuration/configuration.service.js b/awx/ui/client/src/configuration/configuration.service.js new file mode 100644 index 0000000000..86bb4ecdf5 --- /dev/null +++ b/awx/ui/client/src/configuration/configuration.service.js @@ -0,0 +1,74 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['GetBasePath', 'ProcessErrors', '$q', '$http', 'Rest', '$rootScope', '$timeout', 'Wait', + function(GetBasePath, ProcessErrors, $q, $http, Rest, $rootScope, $timeout, Wait) { + var url = GetBasePath('settings'); + + return { + getConfigurationOptions: function() { + var deferred = $q.defer(); + Rest.setUrl(url + '/all'); + Rest.options() + .success(function(data) { + var returnData = data.actions.PUT; + //LICENSE is read only, returning here explicitly for display + returnData.LICENSE = data.actions.GET.LICENSE; + deferred.resolve(returnData); + }) + .error(function(error) { + deferred.reject(error); + }); + + return deferred.promise; + }, + + patchConfiguration: function(body) { + var deferred = $q.defer(); + + Rest.setUrl(url + 'all'); + Rest.patch(body) + .success(function(data) { + deferred.resolve(data); + }) + .error(function(error) { + deferred.reject(error); + }); + + return deferred.promise; + }, + + getCurrentValues: function() { + var deferred = $q.defer(); + Rest.setUrl(url + '/all'); + Rest.get() + .success(function(data) { + deferred.resolve(data); + }) + .error(function(error) { + deferred.reject(error); + }); + + return deferred.promise; + }, + + resetAll: function() { + var deferred = $q.defer(); + + Rest.setUrl(url + '/all'); + Rest.delete() + .success(function(data) { + deferred.resolve(data); + }) + .error(function(error) { + deferred.reject(error); + }); + + return deferred.promise; + } + }; + } +]; diff --git a/awx/ui/client/src/configuration/configurationUtils.service.js b/awx/ui/client/src/configuration/configurationUtils.service.js new file mode 100644 index 0000000000..ba6069e3f3 --- /dev/null +++ b/awx/ui/client/src/configuration/configurationUtils.service.js @@ -0,0 +1,67 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default [ + function() { + + return { + listToArray: function(input, key) { + if (input.indexOf('\n') !== -1) { + //Parse multiline input + return input.replace(/^\s+|\s+$/g, "").split('\n'); + } else { + return input.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/); + } + }, + + arrayToList: function(input, key) { + var multiLineInput = false; + _.each(input, function(statement) { + if (statement.indexOf(',') !== -1) { + multiLineInput = true; + } + }); + if (multiLineInput === false) { + return input.join(', '); + } else { + return input.join('\n'); + } + }, + + isEmpty: function(map) { + for (var key in map) { + if (map.hasOwnProperty(key)) { + return false; + } + } + return true; + }, + + formatPlaceholder: function(input, key) { + if(input !== null && typeof input === 'object') { + if(Array.isArray(input)) { + var multiLineInput = false; + _.each(input, function(statement) { + if(statement.indexOf(',') !== -1) { + multiLineInput = true; + } + }); + if(multiLineInput === false) { + return input.join(', '); + } else { + return input.join('\n'); + } + } else { + return JSON.stringify(input); + } + } else { + return input; + } + } + + }; + } +]; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js new file mode 100644 index 0000000000..b0b4631adf --- /dev/null +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js @@ -0,0 +1,94 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default [ + '$scope', + '$state', + '$timeout', + 'ConfigurationJobsForm', + 'ConfigurationService', + 'ConfigurationUtils', + 'CreateSelect2', + 'GenerateForm', + function( + $scope, + $state, + $timeout, + ConfigurationJobsForm, + ConfigurationService, + ConfigurationUtils, + CreateSelect2, + GenerateForm + ) { + var jobsVm = this; + var generator = GenerateForm; + var form = ConfigurationJobsForm; + $scope.$parent.AD_HOC_COMMANDS_options = []; + _.each($scope.$parent.configDataResolve.AD_HOC_COMMANDS.default, function(command) { + $scope.$parent.AD_HOC_COMMANDS_options.push({ + name: command, + label: command, + value: command + }); + }); + + var keys = _.keys(form.fields); + _.each(keys, function(key) { + addFieldInfo(form, key); + }); + + function addFieldInfo(form, key) { + _.extend(form.fields[key], { + awPopOver: $scope.$parent.configDataResolve[key].help_text, + label: $scope.$parent.configDataResolve[key].label, + name: key, + toggleSource: key, + dataPlacement: 'top', + dataTitle: $scope.$parent.configDataResolve[key].label + }); + } + + generator.inject(form, { + id: 'configure-jobs-form', + mode: 'edit', + scope: $scope.$parent, + related: false + }); + + // Flag to avoid re-rendering and breaking Select2 dropdowns on tab switching + var dropdownRendered = false; + + $scope.$on('populated', function() { + var opts = []; + _.each(ConfigurationUtils.listToArray($scope.$parent.AD_HOC_COMMANDS), function(command) { + opts.push({ + id: command, + text: command + }); + }); + + if(!dropdownRendered) { + dropdownRendered = true; + CreateSelect2({ + element: '#configuration_jobs_template_AD_HOC_COMMANDS', + multiple: true, + placeholder: 'Select commands', + opts: opts + }); + } + + }); + // Fix for bug where adding selected opts causes form to be $dirty and triggering modal + // TODO Find better solution for this bug + $timeout(function(){ + $scope.$parent.configuration_jobs_template_form.$setPristine(); + }, 1000); + + angular.extend(jobsVm, { + }); + + } +]; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js new file mode 100644 index 0000000000..d4a80ad056 --- /dev/null +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js @@ -0,0 +1,67 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default function() { + return { + showHeader: false, + name: 'configuration_jobs_template', + showActions: true, + + fields: { + AD_HOC_COMMANDS: { + type: 'select', + ngOptions: 'command.label for command in AD_HOC_COMMANDS_options track by command.value', + reset: 'AD_HOC_COMMANDS', + multiSelect: true + }, + STDOUT_MAX_BYTES_DISPLAY: { + type: 'number', + reset: 'STDOUT_MAX_BYTES_DISPLAY' + }, + AWX_PROOT_BASE_PATH: { + type: 'text', + reset: 'AWX_PROOT_BASE_PATH', + }, + SCHEDULE_MAX_JOBS: { + type: 'number', + reset: 'SCHEDULE_MAX_JOBS' + }, + AWX_PROOT_SHOW_PATHS: { + type: 'textarea', + reset: 'AWX_PROOT_SHOW_PATHS', + rows: 6 + }, + AWX_ANSIBLE_CALLBACK_PLUGINS: { + type: 'textarea', + reset: 'AWX_ANSIBLE_CALLBACK_PLUGINS', + rows: 6 + }, + AWX_PROOT_HIDE_PATHS: { + type: 'textarea', + reset: 'AWX_PROOT_HIDE_PATHS', + rows: 6 + }, + AWX_PROOT_ENABLED: { + type: 'toggleSwitch', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + // ngDisabled: true + } + } + }; + } diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.partial.html b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.partial.html new file mode 100644 index 0000000000..20b85355df --- /dev/null +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.partial.html @@ -0,0 +1,10 @@ +
+ +
+
+
+
+
+
+ diff --git a/awx/ui/client/src/configuration/main.js b/awx/ui/client/src/configuration/main.js new file mode 100644 index 0000000000..554690aa71 --- /dev/null +++ b/awx/ui/client/src/configuration/main.js @@ -0,0 +1,44 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import configurationService from './configuration.service'; +import ConfigurationUtils from './configurationUtils.service'; +import configurationRoute from './configuration.route'; +import configurationController from './configuration.controller.js'; + +// Import forms +//authorization sub-forms +import configurationGithubForm from './auth-form/sub-forms/auth-github.form.js'; +import configurationGithubOrgForm from './auth-form/sub-forms/auth-github-org.form'; +import configurationGithubTeamForm from './auth-form/sub-forms/auth-github-team.form'; +import configurationGoogleForm from './auth-form/sub-forms/auth-google-oauth2.form'; +import configurationLdapForm from './auth-form/sub-forms/auth-ldap.form.js'; +import configurationRadiusForm from './auth-form/sub-forms/auth-radius.form.js'; +import configurationSamlForm from './auth-form/sub-forms/auth-saml.form'; + +import configurationJobsForm from './jobs-form/configuration-jobs.form'; +import configurationSystemForm from './system-form/configuration-system.form'; +import configurationUiForm from './ui-form/configuration-ui.form'; + +export default +angular.module('configuration', []) + .controller('ConfigurationController', configurationController) + //auth forms + .factory('configurationGithubForm', configurationGithubForm) + .factory('configurationGithubOrgForm', configurationGithubOrgForm) + .factory('configurationGithubTeamForm', configurationGithubTeamForm) + .factory('configurationGoogleForm', configurationGoogleForm) + .factory('configurationLdapForm', configurationLdapForm) + .factory('configurationRadiusForm', configurationRadiusForm) + .factory('configurationSamlForm', configurationSamlForm) + .factory('ConfigurationJobsForm', configurationJobsForm) + .factory('ConfigurationSystemForm', configurationSystemForm) + .factory('ConfigurationUiForm', configurationUiForm) + .factory('ConfigurationUtils', ConfigurationUtils) + .service('ConfigurationService', configurationService) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(configurationRoute); + }]); diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js new file mode 100644 index 0000000000..99091e7af1 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -0,0 +1,68 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default [ + '$scope', '$state', 'AngularCodeMirror', 'ConfigurationSystemForm', 'ConfigurationService', 'ConfigurationUtils', 'GenerateForm', 'ParseTypeChange', + function( + $scope, $state, AngularCodeMirror, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm, ParseTypeChange + ) { + var systemVm = this; + var generator = GenerateForm; + var form = ConfigurationSystemForm; + var keys = _.keys(form.fields); + + _.each(keys, function(key) { + addFieldInfo(form, key); + }); + + function addFieldInfo(form, key) { + _.extend(form.fields[key], { + awPopOver: $scope.$parent.configDataResolve[key].help_text, + label: $scope.$parent.configDataResolve[key].label, + name: key, + toggleSource: key, + dataPlacement: 'top', + dataTitle: $scope.$parent.configDataResolve[key].label + }); + } + + generator.inject(form, { + id: 'configure-system-form', + mode: 'edit', + scope: $scope.$parent, + related: true + }); + + + $scope.$on('populated', function() { + + // var fld = 'LICENSE'; + // var readOnly = true; + // $scope.$parent[fld + 'codeMirror'] = AngularCodeMirror(readOnly); + // $scope.$parent[fld + 'codeMirror'].addModes($AnsibleConfig.variable_edit_modes); + // $scope.$parent[fld + 'codeMirror'].showTextArea({ + // scope: $scope.$parent, + // model: fld, + // element: "configuration_system_template_LICENSE", + // lineNumbers: true, + // mode: 'json', + // }); + + $scope.$parent.parseType = 'json'; + ParseTypeChange({ + scope: $scope.$parent, + variable: 'LICENSE', + parse_variable: 'parseType', + field_id: 'configuration_system_template_LICENSE', + readOnly: true + }); + }); + + angular.extend(systemVm, { + + }); + } +]; diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.form.js b/awx/ui/client/src/configuration/system-form/configuration-system.form.js new file mode 100644 index 0000000000..c7ef1a2cd8 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/configuration-system.form.js @@ -0,0 +1,55 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default function() { + return { + showHeader: false, + name: 'configuration_system_template', + showActions: true, + + fields: { + TOWER_URL_BASE: { + type: 'text', + reset: 'TOWER_URL_BASE', + addRequired: false, + editRequird: false, + }, + TOWER_ADMIN_ALERTS: { + type: 'toggleSwitch', + }, + ACTIVITY_STREAM_ENABLED: { + type: 'toggleSwitch', + }, + ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: { + type: 'toggleSwitch' + }, + ORG_ADMINS_CAN_SEE_ALL_USERS: { + type: 'toggleSwitch', + }, + LICENSE: { + type: 'textarea', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html new file mode 100644 index 0000000000..7ff91b4441 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html @@ -0,0 +1,10 @@ +
+ +
+
+
+
+
+
+ diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js new file mode 100644 index 0000000000..d50239fce5 --- /dev/null +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.controller.js @@ -0,0 +1,92 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default [ + '$scope', + '$state', + '$timeout', + 'ConfigurationUiForm', + 'ConfigurationService', + 'CreateSelect2', + 'GenerateForm', + function( + $scope, + $state, + $timeout, + ConfigurationUiForm, + ConfigurationService, + CreateSelect2, + GenerateForm + ) { + var uiVm = this; + var generator = GenerateForm; + var form = ConfigurationUiForm; + + + var keys = _.keys(form.fields); + _.each(keys, function(key) { + if($scope.$parent.configDataResolve[key].type === 'choice') { + // Create options for dropdowns + var optionsGroup = key + '_options'; + $scope.$parent[optionsGroup] = []; + _.each($scope.$parent.configDataResolve[key].choices, function(choice){ + $scope.$parent[optionsGroup].push({ + name: choice[0], + label: choice[1], + value: choice[0] + }); + }); + } + addFieldInfo(form, key); + }); + + function addFieldInfo(form, key) { + _.extend(form.fields[key], { + awPopOver: $scope.$parent.configDataResolve[key].help_text, + label: $scope.$parent.configDataResolve[key].label, + name: key, + toggleSource: key, + dataPlacement: 'top', + dataTitle: $scope.$parent.configDataResolve[key].label + }); + } + + generator.inject(form, { + id: 'configure-ui-form', + mode: 'edit', + scope: $scope.$parent, + related: true + }); + + // Flag to avoid re-rendering and breaking Select2 dropdowns on tab switching + var dropdownRendered = false; + + $scope.$on('populated', function(){ + if(!dropdownRendered) { + dropdownRendered = true; + CreateSelect2({ + element: '#configuration_ui_template_PENDO_TRACKING_STATE', + multiple: false, + placeholder: 'Select commands', + opts: [{ + id: $scope.$parent.PENDO_TRACKING_STATE, + text: $scope.$parent.PENDO_TRACKING_STATE + }] + }); + // Fix for bug where adding selected opts causes form to be $dirty and triggering modal + // TODO Find better solution for this bug + $timeout(function(){ + $scope.$parent.configuration_ui_template_form.$setPristine(); + }, 1000); + } + }); + + angular.extend(uiVm, { + + }); + + } + ]; diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js b/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js new file mode 100644 index 0000000000..6b222f5805 --- /dev/null +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.form.js @@ -0,0 +1,37 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default function() { + return { + showHeader: false, + name: 'configuration_ui_template', + showActions: true, + + fields: { + PENDO_TRACKING_STATE: { + type: 'select', + ngChange: 'changedPendo()', + ngOptions: 'choice.label for choice in PENDO_TRACKING_STATE_options track by choice.value', + reset: 'PENDO_TRACKING_STATE' + }, + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: 'Reset All', + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; + } diff --git a/awx/ui/client/src/configuration/ui-form/configuration-ui.partial.html b/awx/ui/client/src/configuration/ui-form/configuration-ui.partial.html new file mode 100644 index 0000000000..d880be7025 --- /dev/null +++ b/awx/ui/client/src/configuration/ui-form/configuration-ui.partial.html @@ -0,0 +1,10 @@ +
+ +
+
+
+
+
+
+ diff --git a/awx/ui/client/src/setup-menu/setup-menu.partial.html b/awx/ui/client/src/setup-menu/setup-menu.partial.html index f8f6276cec..8281a68a29 100644 --- a/awx/ui/client/src/setup-menu/setup-menu.partial.html +++ b/awx/ui/client/src/setup-menu/setup-menu.partial.html @@ -49,6 +49,12 @@ View and edit your license information.

+ +

Configure Tower

+

+ Edit Tower's configuration. +

+

About Tower

diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 5a4d0ce4dc..9bc18dfd13 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -228,7 +228,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) scope[field + '_api_error'] = data[form.fields[field]][0]; //scope[form.name + '_form'][form.fields[field].realName].$setValidity('apiError', false); $('[name="' + form.fields[field].realName + '"]').addClass('ng-invalid'); - $('[name="' + form.fields[field].realName + '"]').ScrollTo({ "onlyIfOutside": true, "offsetTop": 100 }); + $('html, body').animate({scrollTop: $('[name="' + form.fields[field].realName + '"]').offset().top}, 0); fieldErrors = true; } } @@ -246,7 +246,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) scope[field + '_api_error'] = data[field][0]; //scope[form.name + '_form'][field].$setValidity('apiError', false); $('[name="' + field + '"]').addClass('ng-invalid'); - $('[name="' + field + '"]').ScrollTo({ "onlyIfOutside": true, "offsetTop": 100 }); + $('html, body').animate({scrollTop: $('[name="' + field + '"]').offset().top}, 0); fieldErrors = true; } } diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index f3ea41ea07..6211a95ec0 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -478,6 +478,12 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat let cls = action["class"] || ""; html += `${action.label}`; } + + if(field.reset) { + var resetValue = "'" + field.reset+ "'"; + html+= `Reset`; + } + html += "\n\t\n"; } return html; @@ -535,6 +541,16 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += (field.awFeature) ? "aw-feature=\"" + field.awFeature + "\" " : ""; html += ">\n"; + // toggle switches + if(field.type === 'toggleSwitch') { + html += label(); + html += `

+
ON
+
OFF
+
`; + } + //text fields if (field.type === 'text' || field.type === 'password' || field.type === 'email') { html += label();