diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index c953778157..bf10688c22 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -2152,6 +2152,7 @@ tr td button i { cursor: not-allowed; opacity: 100; background: @egrey; + border-radius: 5px; } .select2-container--default .select2-selection--single { @@ -2337,6 +2338,7 @@ input[disabled].ui-spinner-input { margin-right: 0; overflow: auto !important; overflow-y: auto !important; + border-radius: 5px; } .CodeMirror-lines { diff --git a/awx/ui/client/legacy-styles/codemirror.less b/awx/ui/client/legacy-styles/codemirror.less index a317585460..8aed0b7e54 100644 --- a/awx/ui/client/legacy-styles/codemirror.less +++ b/awx/ui/client/legacy-styles/codemirror.less @@ -39,11 +39,9 @@ // Disabled textarea[disabled="disabled"] + div[id*="-container"]{ - pointer-events: none; - cursor: not-allowed; - .CodeMirror { cursor: not-allowed; + pointer-events: none; } .CodeMirror.cm-s-default, @@ -68,4 +66,10 @@ textarea[disabled="disabled"] + div[id*="-container"]{ .CodeMirror-cursors { display: none; } + + .CodeMirror.cm-s-default { + min-height: initial !important; + max-height: initial !important; + height: initial !important; + } } diff --git a/awx/ui/client/legacy-styles/forms.less b/awx/ui/client/legacy-styles/forms.less index 3e5f7f79bf..a16654ba13 100644 --- a/awx/ui/client/legacy-styles/forms.less +++ b/awx/ui/client/legacy-styles/forms.less @@ -283,13 +283,13 @@ } .Button-primary--hollow { - border: 1px solid @default-link; + border: 1px solid @default-link; color: @default-link; background: @default-bg; } .Button-primary--hollow:hover { color: @default-link-hov; - border: 1px solid @default-link-hov; + border: 1px solid @default-link-hov; } .ui-spinner{ @@ -495,7 +495,8 @@ input[type='radio']:checked:before { display: block !important; } -.Form-inputLabelContainer[for=variables] { +.Form-inputLabelContainer[for=variables], +.Form-inputLabelContainer--codeMirror { width: auto; display: inline-block !important; } @@ -506,9 +507,10 @@ input[type='radio']:checked:before { .FormToggle {} .FormToggle-container { + float: right; margin: 0 0 0 10px; - padding-bottom: 5px; display: initial; + padding-bottom: 5px; label { &:first-child { @@ -529,6 +531,10 @@ input[type='radio']:checked:before { } } +#credential_type_form .FormToggle-container { + float: initial; +} + .Form-inputLabelContainer { width: 100%; display: block !important; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 4b3c5e6ed8..0649023b54 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -41,6 +41,7 @@ import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; import inventories from './inventories/main'; import inventoryScripts from './inventory-scripts/main'; +import credentialTypes from './credential-types/main'; import organizations from './organizations/main'; import managementJobs from './management-jobs/main'; import workflowResults from './workflow-results/main'; @@ -99,6 +100,7 @@ var tower = angular.module('Tower', [ systemTracking.name, inventories.name, inventoryScripts.name, + credentialTypes.name, organizations.name, managementJobs.name, setupMenu.name, diff --git a/awx/ui/client/src/credential-types/add/add.controller.js b/awx/ui/client/src/credential-types/add/add.controller.js new file mode 100644 index 0000000000..d5302bdf61 --- /dev/null +++ b/awx/ui/client/src/credential-types/add/add.controller.js @@ -0,0 +1,106 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['Rest', 'Wait', + 'CredentialTypesForm', 'ProcessErrors', 'GetBasePath', + 'GenerateForm', '$scope', '$state', 'Alert', 'GetChoices', 'ParseTypeChange', 'ToJSON', 'CreateSelect2', + function(Rest, Wait, + CredentialTypesForm, ProcessErrors, GetBasePath, + GenerateForm, $scope, $state, Alert, GetChoices, ParseTypeChange, ToJSON, CreateSelect2 + ) { + var form = CredentialTypesForm, + url = GetBasePath('credential_types'); + + init(); + + function init() { + // Load the list of options for Kind + GetChoices({ + scope: $scope, + url: url, + field: 'kind', + variable: 'credential_kind_options' + }); + + Rest.setUrl(url); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a credential type.', 'alert-info'); + } + }); + + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + // @issue @jmitchell - this setting probably collides with new RBAC can* implementation? + $scope.canEdit = true; + + var callback = function() { + // Make sure the form controller knows there was a change + $scope[form.name + '_form'].$setDirty(); + }; + $scope.parseTypeInputs = 'yaml'; + $scope.parseTypeInjectors = 'yaml'; + ParseTypeChange({ + scope: $scope, + field_id: 'credential_type_inputs', + variable: 'inputs', + onChange: callback, + parse_variable: 'parseTypeInputs' + }); + ParseTypeChange({ + scope: $scope, + field_id: 'credential_type_injectors', + variable: 'injectors', + onChange: callback, + parse_variable: 'parseTypeInjectors' + }); + + CreateSelect2({ + element: '#credential_type_kind', + multiple: false, + }); + } + + // Save + $scope.formSave = function() { + GenerateForm.clearApiErrors($scope); + Wait('start'); + Rest.setUrl(url); + var inputs = ToJSON($scope.parseTypeInputs, $scope.inputs); + var injectors = ToJSON($scope.parseTypeInjectors, $scope.injectors); + if (inputs === null) { + inputs = {}; + } + if (injectors === null) { + injectors = {}; + } + Rest.post({ + name: $scope.name, + description: $scope.description, + kind: $scope.kind.value, + inputs: inputs, + injectors: injectors + }) + .success(function(data) { + $state.go('credentialTypes.edit', { credential_type_id: data.id }, { reload: true }); + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to add new credential type. PUT returned status: ' + status + }); + }); + }; + + $scope.formCancel = function() { + $state.go('^'); + }; + } +]; diff --git a/awx/ui/client/src/credential-types/add/main.js b/awx/ui/client/src/credential-types/add/main.js new file mode 100644 index 0000000000..9344da0e98 --- /dev/null +++ b/awx/ui/client/src/credential-types/add/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './add.controller'; + +export default + angular.module('credentialTypesAdd', []) + .controller('CredentialTypesAddController', controller); diff --git a/awx/ui/client/src/credential-types/credential-types.form.js b/awx/ui/client/src/credential-types/credential-types.form.js new file mode 100644 index 0000000000..f1fba41b84 --- /dev/null +++ b/awx/ui/client/src/credential-types/credential-types.form.js @@ -0,0 +1,113 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:CredentialType + * @description This form is for adding/editing a credential type +*/ + +export default ['i18n', function(i18n) { + return { + + addTitle: i18n._('NEW CREDENTIAL TYPE'), + editTitle: '{{ name }}', + name: 'credential_type', + basePath: 'credential_types', + stateTree: 'credentialTypes', + breadcrumbName: i18n._('CREDENTIAL TYPE'), + showActions: true, + + // TODO: update fields to be the schema for credential types instead of inventory scripts + fields: { + name: { + label: i18n._('Name'), + type: 'text', + ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)', + required: true, + capitalize: false + }, + description: { + label: i18n._('Description'), + type: 'text', + ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)' + }, + kind: { + label: i18n._('Kind'), + excludeModal: true, + type: 'select', + ngOptions: 'kind.label for kind in credential_kind_options track by kind.value', + required: true, + awPopOver: '
\n' + + '
' + i18n._('Machine') + '
\n' + + '
' + i18n._('Authentication for remote machine access. This can include SSH keys, usernames, passwords, ' + + 'and sudo information. Machine credentials are used when submitting jobs to run playbooks against ' + + 'remote hosts.') + '
' + + '
' + i18n._('Network') + '
\n' + + '
' + i18n._('Authentication for network device access. This can include SSH keys, usernames, passwords, ' + + 'and authorize information. Network credentials are used when submitting jobs to run playbooks against ' + + 'network devices.') + '
' + + '
' + i18n._('Source Control') + '
\n' + + '
' + i18n._('Used to check out and synchronize playbook repositories with a remote source control ' + + 'management system such as Git, Subversion (svn), or Mercurial (hg). These credentials are ' + + 'used by Projects.') + '
\n' + + '
' + i18n._('Cloud') + '
\n' + + '
' + i18n._('Usernames, passwords, and access keys for authenticating to the specified cloud or infrastructure ' + + 'provider. These are used for dynamic inventory sources and for cloud provisioning and deployment ' + + 'in playbook runs.') + '
\n' + + '
\n', + dataTitle: i18n._('Kind'), + dataPlacement: 'right', + dataContainer: "body", + ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)' + }, + inputs: { + label: i18n._('Input Configuration'), + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + type: 'textarea', + rows: 6, + default: '---', + showParseTypeToggle: true, + parseTypeName: 'parseTypeInputs', + awPopOver: '

TODO: input config helper text

', + dataTitle: i18n._('Input Configuration'), + dataPlacement: 'right', + dataContainer: "body", + ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)' + }, + injectors: { + label: i18n._('Injector Configuration'), + class: 'Form-textAreaLabel Form-formGroup--fullWidth', + type: 'textarea', + rows: 6, + default: '---', + showParseTypeToggle: true, + parseTypeName: 'parseTypeInjectors', + awPopOver: '

TODO: injector config helper text

', + dataTitle: i18n._('Injector Configuration'), + dataPlacement: 'right', + dataContainer: "body", + ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)' + }, + }, + + buttons: { //for now always generates