diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less index a9f9d2bdde..c7baad45e4 100644 --- a/awx/ui/client/lib/theme/index.less +++ b/awx/ui/client/lib/theme/index.less @@ -112,6 +112,7 @@ @import '../../src/workflow-results/standard-out.block.less'; @import '../../src/templates/prompt/prompt.block.less'; @import '../../src/templates/job_templates/multi-credential/multi-credential.block.less'; +@import '../../src/templates/job_templates/webhook-credential/webhook-credential.block.less'; @import '../../src/templates/labels/labelsList.block.less'; @import '../../src/templates/survey-maker/survey-maker.block.less'; @import '../../src/templates/survey-maker/survey-maker.block.less'; diff --git a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js index 218f3353f7..3e8dc913b7 100644 --- a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js +++ b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js @@ -10,14 +10,14 @@ 'ProcessErrors', 'GetBasePath', 'hashSetup', 'ParseTypeChange', 'Wait', 'Empty', 'ToJSON', 'CallbackHelpInit', 'GetChoices', '$state', 'availableLabels', 'CreateSelect2', '$q', 'i18n', 'Inventory', 'Project', 'InstanceGroupsService', - 'MultiCredentialService', 'ConfigData', 'resolvedModels', + 'MultiCredentialService', 'ConfigData', 'resolvedModels', '$compile', function( $filter, $scope, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, ProcessErrors, GetBasePath, hashSetup, ParseTypeChange, Wait, Empty, ToJSON, CallbackHelpInit, GetChoices, $state, availableLabels, CreateSelect2, $q, i18n, Inventory, Project, InstanceGroupsService, - MultiCredentialService, ConfigData, resolvedModels + MultiCredentialService, ConfigData, resolvedModels, $compile ) { // Inject dynamic view @@ -45,6 +45,113 @@ $scope.credentialNotPresent = false; $scope.canGetAllRelatedResources = true; + // + // webhook credential - all handlers, dynamic state, etc. live here + // + + $scope.webhookCredential = { + id: null, + name: null, + isModalOpen: false, + isModalReady: false, + modalTitle: i18n._('Select Webhook Credential'), + modalBaseParams: { + order_by: 'name', + page_size: 5, + credential_type__namespace: null, + }, + modalSelectedId: null, + modalSelectedName: null, + }; + + $scope.handleWebhookCredentialLookupClick = () => { + $scope.webhookCredential.modalSelectedId = $scope.webhookCredential.id; + $scope.webhookCredential.isModalOpen = true; + }; + + $scope.handleWebhookCredentialTagDelete = () => { + $scope.webhookCredential.id = null; + $scope.webhookCredential.name = null; + }; + + $scope.handleWebhookCredentialModalClose = () => { + $scope.webhookCredential.isModalOpen = false; + $scope.webhookCredential.isModalReady = false; + }; + + $scope.handleWebhookCredentialModalReady = () => { + $scope.webhookCredential.isModalReady = true; + }; + + $scope.handleWebhookCredentialModalItemSelect = (item) => { + $scope.webhookCredential.modalSelectedId = item.id; + $scope.webhookCredential.modalSelectedName = item.name; + }; + + $scope.handleWebhookCredentialModalCancel = () => { + $scope.webhookCredential.isModalOpen = false; + $scope.webhookCredential.isModalReady = false; + $scope.webhookCredential.modalSelectedId = null; + $scope.webhookCredential.modalSelectedName = null; + + }; + + $scope.handleWebhookCredentialSelect = () => { + $scope.webhookCredential.isModalOpen = false; + $scope.webhookCredential.isModalReady = false; + $scope.webhookCredential.id = $scope.webhookCredential.modalSelectedId; + $scope.webhookCredential.name = $scope.webhookCredential.modalSelectedName; + $scope.webhookCredential.modalSelectedId = null; + $scope.webhookCredential.modalSelectedName = null; + }; + + $('#content-container').append($compile(` + + + + + ${i18n._('CANCEL')} + + + ${i18n._('SELECT')} + + + `)($scope)); + + $scope.$watch('webhook_service', (newValue, oldValue) => { + const newServiceValue = newValue && typeof newValue === 'object' ? newValue.value : newValue; + const oldServiceValue = oldValue && typeof oldValue === 'object' ? oldValue.value : oldValue; + if (newServiceValue !== oldServiceValue || newServiceValue === newValue) { + $scope.webhook_service = { value: newServiceValue }; + sync_webhook_service_select2(); + $scope.webhookCredential.modalBaseParams.credential_type__namespace = newServiceValue ? + `${newServiceValue}_token` + : null; + if (newServiceValue !== newValue || newValue === null) { + $scope.webhookCredential.id = null; + $scope.webhookCredential.name = null; + } + } + }); + hashSetup({ scope: $scope, master: master, @@ -182,6 +289,17 @@ }); } + function sync_webhook_service_select2() { + CreateSelect2({ + element:'#webhook-service-select', + addNew: false, + multiple: false, + scope: $scope, + options: 'webhook_service_options', + model: 'webhook_service' + }); + } + $scope.toggleForm = function(key) { $scope[key] = !$scope[key]; }; @@ -344,6 +462,10 @@ delete data.credential; delete data.vault_credential; delete data.webhook_url; + data.webhook_credential = $scope.webhookCredential.id; + if (!data.webhook_credential) { + data.webhook_service = null; + } data.extra_vars = ToJSON($scope.parseType, $scope.extra_vars, true); diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index 2c2a20ecb3..456ec4855f 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -19,7 +19,7 @@ export default 'initSurvey', '$state', 'CreateSelect2', 'isNotificationAdmin', 'ToggleNotification','$q', 'InstanceGroupsService', 'InstanceGroupsData', 'MultiCredentialService', 'availableLabels', 'projectGetPermissionDenied', - 'inventoryGetPermissionDenied', 'jobTemplateData', 'ParseVariableString', 'ConfigData', + 'inventoryGetPermissionDenied', 'jobTemplateData', 'ParseVariableString', 'ConfigData', '$compile', function( $filter, $scope, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, @@ -29,7 +29,7 @@ export default SurveyControllerInit, $state, CreateSelect2, isNotificationAdmin, ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData, MultiCredentialService, availableLabels, projectGetPermissionDenied, - inventoryGetPermissionDenied, jobTemplateData, ParseVariableString, ConfigData + inventoryGetPermissionDenied, jobTemplateData, ParseVariableString, ConfigData, $compile ) { $scope.$watch('job_template_obj.summary_fields.user_capabilities.edit', function(val) { @@ -63,7 +63,7 @@ export default $scope.playbook_options = null; $scope.webhook_service_options = null; $scope.playbook = null; - $scope.webhook_service = null; + $scope.webhook_service = jobTemplateData.webhook_service; $scope.webhook_url = ''; $scope.mode = 'edit'; $scope.parseType = 'yaml'; @@ -77,6 +77,119 @@ export default $scope.custom_virtualenvs_options = virtualEnvs; $scope.webhook_url_help = i18n._('Webhook services can launch jobs with this job template by making a POST request to this URL.'); + // + // webhook credential - all handlers, dynamic state, etc. live here + // + + $scope.webhookCredential = { + id: _.get(jobTemplateData, ['summary_fields', 'webhook_credential', 'id']), + name: _.get(jobTemplateData, ['summary_fields', 'webhook_credential', 'name']), + isModalOpen: false, + isModalReady: false, + modalTitle: i18n._('Select Webhook Credential'), + modalBaseParams: { + order_by: 'name', + page_size: 5, + credential_type__namespace: `${jobTemplateData.webhook_service}_token`, + }, + modalSelectedId: null, + modalSelectedName: null, + }; + + $scope.handleWebhookCredentialLookupClick = () => { + $scope.webhookCredential.modalSelectedId = $scope.webhookCredential.id; + $scope.webhookCredential.isModalOpen = true; + }; + + $scope.handleWebhookCredentialTagDelete = () => { + $scope.webhookCredential.id = null; + $scope.webhookCredential.name = null; + }; + + $scope.handleWebhookCredentialModalClose = () => { + $scope.webhookCredential.isModalOpen = false; + $scope.webhookCredential.isModalReady = false; + }; + + $scope.handleWebhookCredentialModalReady = () => { + $scope.webhookCredential.isModalReady = true; + }; + + $scope.handleWebhookCredentialModalItemSelect = (item) => { + $scope.webhookCredential.modalSelectedId = item.id; + $scope.webhookCredential.modalSelectedName = item.name; + }; + + $scope.handleWebhookCredentialModalCancel = () => { + $scope.webhookCredential.isModalOpen = false; + $scope.webhookCredential.isModalReady = false; + $scope.webhookCredential.modalSelectedId = null; + $scope.webhookCredential.modalSelectedName = null; + + }; + + $scope.handleWebhookCredentialSelect = () => { + $scope.webhookCredential.isModalOpen = false; + $scope.webhookCredential.isModalReady = false; + $scope.webhookCredential.id = $scope.webhookCredential.modalSelectedId; + $scope.webhookCredential.name = $scope.webhookCredential.modalSelectedName; + $scope.webhookCredential.modalSelectedId = null; + $scope.webhookCredential.modalSelectedName = null; + }; + + $('#content-container').append($compile(` + + + + + ${i18n._('CANCEL')} + + + ${i18n._('SELECT')} + + + `)($scope)); + + $scope.$watch('webhook_service', (newValue, oldValue) => { + const newServiceValue = newValue && typeof newValue === 'object' ? newValue.value : newValue; + const oldServiceValue = oldValue && typeof oldValue === 'object' ? oldValue.value : oldValue; + if (newServiceValue) { + $scope.webhook_url = `${$scope.callback_server_path}${jobTemplateData.url}${newServiceValue}`; + } else { + $scope.webhook_url = ''; + } + if (newServiceValue !== oldServiceValue || newServiceValue === newValue) { + $scope.webhook_service = { value: newServiceValue }; + sync_webhook_service_select2(); + $scope.webhookCredential.modalBaseParams.credential_type__namespace = newServiceValue ? + `${newServiceValue}_token` : null; + if (newServiceValue !== newValue || newValue === null) { + $scope.webhookCredential.id = null; + $scope.webhookCredential.name = null; + } + } + }); + + $scope.$watch('verbosity', sync_verbosity_select2); + SurveyControllerInit({ scope: $scope, parent_scope: $scope, @@ -178,24 +291,6 @@ export default } } }); - - // watch for changes to 'verbosity', ensure we keep our select2 in sync when it changes. - $scope.$watch('verbosity', sync_verbosity_select2); - $scope.$watch('webhook_service', (newValue) => { - if (newValue) { - // TODO: We'll need the host from the server. - const baseURL = window.location.origin; - if (typeof newValue === 'string') { - $scope.webhook_url = `${baseURL}${jobTemplateData.url}${newValue}`; - $scope.webhook_service = { value: newValue }; - } else { - $scope.webhook_url = `${baseURL}${jobTemplateData.url}${newValue.value}`; - } - } else { - $scope.webhook_url = ''; - } - sync_webhook_service_select2(); - }); } callback = function() { @@ -229,7 +324,7 @@ export default scope: $scope, options: 'webhook_service_options', model: 'webhook_service' - })); + })); } function jobTemplateLoadFinished(){ @@ -775,7 +870,12 @@ export default data.job_tags = (Array.isArray($scope.job_tags)) ? _.uniq($scope.job_tags).join() : ""; data.skip_tags = (Array.isArray($scope.skip_tags)) ? _.uniq($scope.skip_tags).join() : ""; + delete data.webhook_url; + data.webhook_credential = $scope.webhookCredential.id; + if (!data.webhook_credential) { + data.webhook_service = null; + } Rest.setUrl(defaultUrl + $state.params.job_template_id); Rest.patch(data) diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index cb416484e2..f6c4c4d5b9 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -419,6 +419,23 @@ function(NotificationsList, i18n) { dataContainer: "body", ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' }, + webhook_credential: { + label: i18n._('Webhook Credential'), + type: 'custom', + control: ` + `, + awPopOver: "

" + i18n._("Select the credential to use with the webhook service.") + "

", + dataTitle: i18n._('Webhook Credential'), + dataPlacement: 'right', + dataContainer: "body", + ngDisabled: 'canAddJobTemplate', + required: false, + }, extra_vars: { label: i18n._('Extra Variables'), type: 'textarea', diff --git a/awx/ui/client/src/templates/job_templates/main.js b/awx/ui/client/src/templates/job_templates/main.js index 0c096c0134..ee9538a3dd 100644 --- a/awx/ui/client/src/templates/job_templates/main.js +++ b/awx/ui/client/src/templates/job_templates/main.js @@ -1,13 +1,13 @@ import jobTemplateAdd from './add-job-template/main'; import jobTemplateEdit from './edit-job-template/main'; import multiCredential from './multi-credential/main'; +import webhookCredential from './webhook-credential'; import hashSetup from './factories/hash-setup.factory'; import CallbackHelpInit from './factories/callback-help-init.factory'; import JobTemplateForm from './job-template.form'; export default - angular.module('jobTemplates', [jobTemplateAdd.name, jobTemplateEdit.name, - multiCredential.name]) - .factory('hashSetup', hashSetup) - .factory('CallbackHelpInit', CallbackHelpInit) - .factory('JobTemplateForm', JobTemplateForm); + angular.module('jobTemplates', [jobTemplateAdd.name, jobTemplateEdit.name, multiCredential.name, webhookCredential.name]) + .factory('hashSetup', hashSetup) + .factory('CallbackHelpInit', CallbackHelpInit) + .factory('JobTemplateForm', JobTemplateForm); diff --git a/awx/ui/client/src/templates/job_templates/webhook-credential/index.js b/awx/ui/client/src/templates/job_templates/webhook-credential/index.js new file mode 100644 index 0000000000..97b634463a --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/webhook-credential/index.js @@ -0,0 +1,4 @@ +import webhookCredentialInput from './webhook-credential-input.component'; + +export default angular.module('webhookCredential', []) + .component('webhookCredentialInput', webhookCredentialInput); diff --git a/awx/ui/client/src/templates/job_templates/webhook-credential/main.js b/awx/ui/client/src/templates/job_templates/webhook-credential/main.js new file mode 100644 index 0000000000..a91d79dbc6 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/webhook-credential/main.js @@ -0,0 +1,9 @@ +import webhookCredential from './webhook-credential.directive'; +import webhookCredentialModal from './webhook-credential-modal.directive'; +import webhookCredentialService from './webhook-credential.service'; + +export default + angular.module('webhookCredential', []) + .directive('webhookCredential', webhookCredential) + .directive('webhookCredentialModal', webhookCredentialModal) + .service('WebhookCredentialService', webhookCredentialService); diff --git a/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential-input.component.js b/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential-input.component.js new file mode 100644 index 0000000000..8f5d752948 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential-input.component.js @@ -0,0 +1,11 @@ +const templateUrl = require('~src/templates/job_templates/webhook-credential/webhook-credential-input.partial.html'); +export default { + templateUrl, + controllerAs: 'vm', + bindings: { + isFieldDisabled: '<', + tagName: '<', + onLookupClick: '<', + onTagDelete: '<', + }, +}; diff --git a/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential-input.partial.html b/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential-input.partial.html new file mode 100644 index 0000000000..268fb05f6e --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential-input.partial.html @@ -0,0 +1,43 @@ +
+ + + + +
+
+
+
+
+ +
+
+ +
+
+ + {{ vm.tagName }} + +
+
+ +
+
+
+
+
+
+
diff --git a/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential.block.less b/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential.block.less new file mode 100644 index 0000000000..24467c7d0f --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/webhook-credential/webhook-credential.block.less @@ -0,0 +1,113 @@ +.WebhookCredential-tags { + padding-left: 0px; +} + +.WebhookCredential-flexContainer { + display: flex; + width: 100%; + flex-wrap: wrap; +} + +.WebhookCredential-tagContainer { + display: flex; + max-width: 100%; + background-color: @default-link; + color: @default-bg; + border-radius: 5px; + padding: 0px 0px 0px 10px; + margin: 3px 10px 3px 0px; +} + +.WebhookCredential-tagContainer--disabled { + background-color: @default-icon; +} + +.WebhookCredential-tag { + font-size: 12px; + margin-right: 10px; + max-width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + padding: 2px 0px 2px 15px; +} + +.WebhookCredential-tag--disabled { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + padding-left: 10px; +} + +.WebhookCredential-tag--deletable { + margin-right: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-right: 0; + max-width: ~"calc(100% - 23px)"; + padding-left: 10px; +} + +.WebhookCredential-deleteContainer { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + padding: 2px 5px; + align-items: center; + display: flex; + cursor: pointer; +} + +.WebhookCredential-tagDelete { + font-size: 11px; +} + +.WebhookCredential-iconContainer { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + padding: 0px 5px; + margin: 3px 0px; + margin-left: -3px; + align-items: center; + display: flex; +} + +.WebhookCredential-iconContainer--disabled { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + padding: 0 5px; + padding-left: 10px; + margin: 3px 0px; + align-items: center; + display: flex; +} + + +.WebhookCredential-tagIcon { + margin: 0px 0px; + font-size: 12px; +} + +.WebhookCredential-name { + flex: initial; + font-size: 12px; + max-width: 100%; +} + +.WebhookCredential-name--label { + color: @default-list-header-bg; + font-size: 12px; + margin-left: -8px; + margin-right: 5px; +} + +.WebhookCredential-tag--deletable > .WebhookCredential-name { + max-width: ~"calc(100% - 23px)"; +} + +.WebhookCredential-deleteContainer:hover { + border-color: @default-err; + background-color: @default-err!important; +} + +.WebhookCredential-deleteContainer:hover > .WebhookCredential-tagDelete { + color: @default-bg; +}