1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-28 02:25:27 +03:00

Merge pull request #1096 from mabashian/169-v1

UI support for prompting on job template schedules
This commit is contained in:
Jake McDermott 2018-02-09 11:34:25 -05:00 committed by GitHub
commit b2b519e48d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 2813 additions and 607 deletions

View File

@ -42,4 +42,3 @@
</at-panel>
<div ng-if="$state.current.name.includes('permissions.add')" ui-view="modal"></div>

View File

@ -1,4 +1,4 @@
function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strings, $state, $scope, rbacUiControlService, Dataset, $filter, Alert, InitiatePlaybookRun, Prompt, Wait, ProcessErrors, TemplateCopyService) {
function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strings, $state, $scope, rbacUiControlService, Dataset, $filter, Alert, InitiatePlaybookRun, Prompt, Wait, ProcessErrors, TemplateCopyService, $q, Empty, i18n, PromptService) {
const vm = this || {},
unifiedJobTemplate = model,
jobTemplate = new JobTemplate(),
@ -85,28 +85,157 @@ function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strin
// TODO: edit indicator doesn't update when you enter edit route after initial load right now
vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id);
// TODO: update to new way of launching job after mike opens his pr
vm.submitJob = function(template) {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'job_template' });
let jobTemplate = new JobTemplate();
$q.all([jobTemplate.optionsLaunch(template.id), jobTemplate.getLaunch(template.id)])
.then((responses) => {
if(jobTemplate.canLaunchWithoutPrompt()) {
jobTemplate.postLaunch({id: template.id})
.then((launchRes) => {
$state.go('jobResult', { id: launchRes.data.job }, { reload: true });
});
} else {
if(responses[1].data.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions(template.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec
});
$scope.promptData = {
launchConf: responses[1].data,
launchOptions: responses[0].data,
surveyQuestions: processed.surveyQuestions,
template: template.id,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
triggerModalOpen: true
};
});
}
else {
$scope.promptData = {
launchConf: responses[1].data,
launchOptions: responses[0].data,
template: template.id,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
triggerModalOpen: true
};
}
}
});
}
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' });
}
else {
var alertStrings = {
header: 'Error: Unable to determine template type',
body: 'We were unable to determine this template\'s type while launching.'
// Something went wrong - Let the user know that we're unable to launch because we don't know
// what type of job template this is
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.');
}
}
else {
Alert('Error: Unable to launch template', 'Template parameter is missing');
}
};
$scope.launchJob = () => {
let jobLaunchData = {
extra_vars: $scope.promptData.extraVars
};
let jobTemplate = new JobTemplate();
if($scope.promptData.launchConf.ask_tags_on_launch){
jobLaunchData.job_tags = $scope.promptData.prompts.tags.value.map(a => a.value).join();
}
if($scope.promptData.launchConf.ask_skip_tags_on_launch){
jobLaunchData.skip_tags = $scope.promptData.prompts.skipTags.value.map(a => a.value).join();
}
if($scope.promptData.launchConf.ask_limit_on_launch && _.has($scope, 'promptData.prompts.limit.value')){
jobLaunchData.limit = $scope.promptData.prompts.limit.value;
}
if($scope.promptData.launchConf.ask_job_type_on_launch && _.has($scope, 'promptData.prompts.jobType.value.value')) {
jobLaunchData.job_type = $scope.promptData.prompts.jobType.value.value;
}
if($scope.promptData.launchConf.ask_verbosity_on_launch && _.has($scope, 'promptData.prompts.verbosity.value.value')) {
jobLaunchData.verbosity = $scope.promptData.prompts.verbosity.value.value;
}
if($scope.promptData.launchConf.ask_inventory_on_launch && !Empty($scope.promptData.prompts.inventory.value.id)){
jobLaunchData.inventory_id = $scope.promptData.prompts.inventory.value.id;
}
if($scope.promptData.launchConf.ask_credential_on_launch){
jobLaunchData.credentials = [];
$scope.promptData.prompts.credentials.value.forEach((credential) => {
jobLaunchData.credentials.push(credential.id);
});
}
if($scope.promptData.launchConf.ask_diff_mode_on_launch && _.has($scope, 'promptData.prompts.diffMode.value')) {
jobLaunchData.diff_mode = $scope.promptData.prompts.diffMode.value;
}
if($scope.promptData.prompts.credentials.passwords) {
_.forOwn($scope.promptData.prompts.credentials.passwords, (val, key) => {
if(!jobLaunchData.credential_passwords) {
jobLaunchData.credential_passwords = {};
}
Alert(strings.get('ALERT', alertStrings));
if(key === "ssh_key_unlock") {
jobLaunchData.credential_passwords.ssh_key_unlock = val.value;
} else if(key !== "vault") {
jobLaunchData.credential_passwords[`${key}_password`] = val.value;
} else {
_.each(val, (vaultCred) => {
jobLaunchData.credential_passwords[vaultCred.vault_id ? `${key}_password.${vaultCred.vault_id}` : `${key}_password`] = vaultCred.value;
});
}
});
}
// If the extra_vars dict is empty, we don't want to include it if we didn't prompt for anything.
if(_.isEmpty(jobLaunchData.extra_vars) && !($scope.promptData.launchConf.ask_variables_on_launch && $scope.promptData.launchConf.survey_enabled && $scope.promptData.surveyQuestions.length > 0)){
delete jobLaunchData.extra_vars;
}
jobTemplate.postLaunch({
id: $scope.promptData.template,
launchData: jobLaunchData
}).then((launchRes) => {
$state.go('jobResult', { id: launchRes.data.job }, { reload: true });
}).catch(({data, status}) => {
ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'),
msg: i18n.sprintf(i18n._('Failed to launch job template. POST returned: %d'), $scope.promptData.template, status) });
});
};
vm.scheduleJob = (template) => {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
$state.go('jobTemplateSchedules', {id: template.id});
}
} else {
var alertStrings = {
header: 'Error: Unable to launch template',
body: 'Template parameter is missing'
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
$state.go('workflowJobTemplateSchedules', {id: template.id});
}
Alert(strings.get('ALERT', alertStrings));
else {
// Something went wrong Let the user know that we're unable to redirect to schedule because we don't know
// what type of job template this is
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to schedule.');
}
}
else {
Alert('Error: Unable to schedule job', 'Template parameter is missing');
}
};
@ -334,7 +463,11 @@ ListTemplatesController.$inject = [
'Prompt',
'Wait',
'ProcessErrors',
'TemplateCopyService'
'TemplateCopyService',
'$q',
'Empty',
'i18n',
'PromptService'
];
export default ListTemplatesController;

View File

@ -118,4 +118,5 @@
query-set="querySet">
</paginate>
</at-panel-body>
<prompt prompt-data="promptData" open-modal="openModal" on-finish="launchJob()"></launch>
</at-panel>

View File

@ -6,7 +6,7 @@ function TemplatesStrings (BaseString) {
ns.state = {
LIST_BREADCRUMB_LABEL: t.s('TEMPLATES')
}
};
ns.list = {
PANEL_TITLE: t.s('TEMPLATES'),
@ -19,7 +19,44 @@ function TemplatesStrings (BaseString) {
ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'),
ROW_ITEM_LABEL_MODIFIED: t.s('Last Modified'),
ROW_ITEM_LABEL_RAN: t.s('Last Ran'),
}
};
ns.prompt = {
INVENTORY: t.s('Inventory'),
CREDENTIAL: t.s('Credential'),
OTHER_PROMPTS: t.s('Other Prompts'),
SURVEY: t.s('Survey'),
PREVIEW: t.s('Preview'),
LAUNCH: t.s('LAUNCH'),
SELECTED: t.s('SELECTED'),
NO_CREDENTIALS_SELECTED: t.s('No credentials selected'),
NO_INVENTORY_SELECTED: t.s('No inventory selected'),
REVERT: t.s('REVERT'),
CREDENTIAL_TYPE: t.s('Credential Type'),
PASSWORDS_REQUIRED_HELP: t.s('Launching this job requires the passwords listed below. Enter and confirm each password before continuing.'),
PLEASE_ENTER_PASSWORD: t.s('Please enter a password.'),
credential_passwords: {
SSH_PASSWORD: t.s('SSH Password'),
PRIVATE_KEY_PASSPHRASE: t.s('Private Key Passphrase'),
PRIVILEGE_ESCALATION_PASSWORD: t.s('Privilege Escalation Password'),
VAULT_PASSWORD: t.s('Vault Password')
},
SHOW_CHANGES: t.s('Show Changes'),
SKIP_TAGS: t.s('Skip Tags'),
JOB_TAGS: t.s('Job Tags'),
LIMIT: t.s('Limit'),
JOB_TYPE: t.s('Job Type'),
VERBOSITY: t.s('Verbosity'),
CHOOSE_JOB_TYPE: t.s('Choose a job type'),
CHOOSE_VERBOSITY: t.s('Choose a verbosity'),
EXTRA_VARIABLES: t.s('Extra Variables'),
PLEASE_ENTER_ANSWER: t.s('Please enter an answer.'),
VALID_INTEGER: t.s('Please enter an answer that is a valid integer.'),
VALID_DECIMAL: t.s('Please enter an answer that is a decimal number.'),
PLAYBOOK_RUN: t.s('Playbook Run'),
CHECK: t.s('Check'),
NO_CREDS_MATCHING_TYPE: t.s('No Credentials Matching This Type Have Been Created')
};
}
TemplatesStrings.$inject = ['BaseStringService'];

View File

@ -626,11 +626,20 @@ input[type='radio']:checked:before {
background-color: @submit-button-bg-dis;
}
.Form-saveButton--disabled {
background-color: @submit-button-bg-dis;
cursor: not-allowed;
}
.Form-saveButton:hover, .Form-launchButton:hover {
background-color: @submit-button-bg-hov;
color: @submit-button-text;
}
.Form-saveButton--disabled:hover {
background-color: @submit-button-bg-dis;
}
.Form-cancelButton {
background-color: @default-bg;
color: @btn-txt;
@ -668,6 +677,10 @@ input[type='radio']:checked:before {
background-color: @default-link;
}
.Form-primaryButton--noMargin {
margin-right: 0px;
}
.Form-formGroup--singleColumn {
width: 100% !important;
padding-right: 0px;

View File

@ -16,14 +16,12 @@ function AtModalController (eventService, strings) {
const vm = this;
let overlay;
let modal;
let listeners;
vm.strings = strings;
vm.init = (scope, el) => {
overlay = el[0]; // eslint-disable-line prefer-destructuring
modal = el.find('.at-Modal-window')[0]; // eslint-disable-line prefer-destructuring
vm.modal = scope[scope.ns].modal;
vm.modal.show = vm.show;
@ -35,7 +33,7 @@ function AtModalController (eventService, strings) {
vm.modal.message = message;
listeners = eventService.addListeners([
[window, 'click', vm.clickToHide]
[overlay, 'click', vm.clickToHide]
]);
overlay.style.display = 'block';
@ -53,22 +51,10 @@ function AtModalController (eventService, strings) {
};
vm.clickToHide = event => {
if (vm.clickIsOutsideModal(event)) {
if ($(event.target).hasClass('at-Modal')) {
vm.hide();
}
};
vm.clickIsOutsideModal = e => {
const m = modal.getBoundingClientRect();
const cx = e.clientX;
const cy = e.clientY;
if (cx < m.left || cx > m.right || cy > m.bottom || cy < m.top) {
return true;
}
return false;
};
}
AtModalController.$inject = [

View File

@ -10,6 +10,12 @@ function AtTabGroupController () {
vm.tabs.push(tab);
};
vm.clearActive = () => {
vm.tabs.forEach((tab) => {
tab.state._active = false;
});
};
}
function atTabGroup () {

View File

@ -25,7 +25,12 @@ function AtTabController ($state) {
return;
}
$state.go(scope.state._go, scope.state._params, { reload: true });
if (scope.state._go) {
$state.go(scope.state._go, scope.state._params, { reload: true });
} else {
group.clearActive();
scope.state._active = true;
}
};
}

View File

@ -0,0 +1,21 @@
let Base;
function JobModel (method, resource, config) {
Base.call(this, 'jobs');
this.Constructor = JobModel;
return this.create(method, resource, config);
}
function JobModelLoader (BaseModel) {
Base = BaseModel;
return JobModel;
}
JobModelLoader.$inject = [
'BaseModel'
];
export default JobModelLoader;

View File

@ -1,5 +1,69 @@
let Base;
let WorkflowJobTemplateNode;
let $http;
function optionsLaunch (id) {
const req = {
method: 'OPTIONS',
url: `${this.path}${id}/launch/`
};
return $http(req);
}
function getLaunch (id) {
const req = {
method: 'GET',
url: `${this.path}${id}/launch/`
};
return $http(req)
.then(res => {
this.model.launch.GET = res.data;
return res;
});
}
function postLaunch (params) {
const req = {
method: 'POST',
url: `${this.path}${params.id}/launch/`
};
if (params.launchData) {
req.data = params.launchData;
}
return $http(req);
}
function getSurveyQuestions (id) {
const req = {
method: 'GET',
url: `${this.path}${id}/survey_spec/`
};
return $http(req);
}
function canLaunchWithoutPrompt () {
const launchData = this.model.launch.GET;
return (
launchData.can_start_without_user_input &&
!launchData.ask_inventory_on_launch &&
!launchData.ask_credential_on_launch &&
!launchData.ask_verbosity_on_launch &&
!launchData.ask_job_type_on_launch &&
!launchData.ask_limit_on_launch &&
!launchData.ask_tags_on_launch &&
!launchData.ask_skip_tags_on_launch &&
!launchData.ask_variables_on_launch &&
!launchData.ask_diff_mode_on_launch &&
!launchData.survey_enabled
);
}
function setDependentResources (id) {
this.dependentResources = [
@ -17,20 +81,30 @@ function JobTemplateModel (method, resource, config) {
this.Constructor = JobTemplateModel;
this.setDependentResources = setDependentResources.bind(this);
this.optionsLaunch = optionsLaunch.bind(this);
this.getLaunch = getLaunch.bind(this);
this.postLaunch = postLaunch.bind(this);
this.getSurveyQuestions = getSurveyQuestions.bind(this);
this.canLaunchWithoutPrompt = canLaunchWithoutPrompt.bind(this);
this.model.launch = {};
return this.create(method, resource, config);
}
function JobTemplateModelLoader (BaseModel, WorkflowJobTemplateNodeModel) {
function JobTemplateModelLoader (BaseModel, WorkflowJobTemplateNodeModel, _$http_) {
Base = BaseModel;
WorkflowJobTemplateNode = WorkflowJobTemplateNodeModel;
$http = _$http_;
return JobTemplateModel;
}
JobTemplateModelLoader.$inject = [
'BaseModel',
'WorkflowJobTemplateNodeModel'
'WorkflowJobTemplateNodeModel',
'$http',
'$state'
];
export default JobTemplateModelLoader;

View File

@ -0,0 +1,21 @@
let Base;
function WorkflowJobModel (method, resource, config) {
Base.call(this, 'workflow_jobs');
this.Constructor = WorkflowJobModel;
return this.create(method, resource, config);
}
function WorkflowJobModelLoader (BaseModel) {
Base = BaseModel;
return WorkflowJobModel;
}
WorkflowJobModelLoader.$inject = [
'BaseModel'
];
export default WorkflowJobModelLoader;

View File

@ -14,6 +14,8 @@ import InstanceGroup from '~models/InstanceGroup';
import InventorySource from '~models/InventorySource';
import Inventory from '~models/Inventory';
import InventoryScript from '~models/InventoryScript';
import Job from '~models/Job';
import WorkflowJob from '~models/WorkflowJob';
import WorkflowJobTemplate from '~models/WorkflowJobTemplate';
import ModelsStrings from '~models/models.strings';
@ -41,6 +43,9 @@ angular
.service('InventoryScriptModel', InventoryScript)
.service('WorkflowJobTemplateModel', WorkflowJobTemplate)
.service('ModelsStrings', ModelsStrings)
.service('UnifiedJobTemplateModel', UnifiedJobTemplate);
.service('UnifiedJobTemplateModel', UnifiedJobTemplate)
.service('JobModel', Job)
.service('WorkflowJobModel', WorkflowJob)
.service('WorkflowJobTemplateModel', WorkflowJobTemplate);
export default MODULE_NAME;

View File

@ -60,8 +60,14 @@ function BaseStringService (namespace) {
this.CANCEL = t.s('CANCEL');
this.SAVE = t.s('SAVE');
this.OK = t.s('OK');
this.NEXT = t.s('NEXT');
this.SHOW = t.s('SHOW');
this.HIDE = t.s('HIDE');
this.ON = t.s('ON');
this.OFF = t.s('OFF');
this.YAML = t.s('YAML');
this.JSON = t.s('JSON');
this.deleteResource = {
HEADER: t.s('Delete'),
USED_BY: resourceType => t.s('The {{ resourceType }} is currently being used by other resources.', { resourceType }),

View File

@ -122,6 +122,7 @@
@import '../../src/system-tracking/fact-module-filter.block.less';
@import '../../src/system-tracking/fact-module-pickers.block.less';
@import '../../src/system-tracking/system-tracking-container.block.less';
@import '../../src/templates/prompt/prompt.block.less';
@import '../../src/templates/job_templates/multi-credential/multi-credential.block.less';
@import '../../src/templates/labels/labelsList.block.less';
@import '../../src/templates/survey-maker/survey-maker.block.less';

View File

@ -1,12 +1,12 @@
<div class="LabelList-tagContainer" ng-repeat="related_group in related_groups">
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && host.summary_fields.user_capabilities.edit)}">
<span class="LabelList-name">{{ related_group.name }}</span>
</div>
<div class="LabelList-deleteContainer"
ng-click="deleteLabel(host, related_group)"
ng-show="showDelete && host.summary_fields.user_capabilities.edit">
<i class="fa fa-times LabelList-tagDelete"></i>
</div>
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && host.summary_fields.user_capabilities.edit)}">
<span class="LabelList-name">{{ related_group.name }}</span>
</div>
</div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && seeMoreInactive"
ng-click="seeMore()" translate>View More</div>

View File

@ -394,20 +394,9 @@ export default
}
};
$scope.toggle_inventory = function(id) {
$scope.inventories.forEach(function(row, i) {
if (row.id === id) {
$scope.selected_inventory = angular.copy(row);
$scope.inventories[i].checked = 1;
} else {
$scope.inventories[i].checked = 0;
}
});
};
$scope.toggle_credential = function(id) {
$scope.toggle_credential = function(cred) {
$scope.credentials.forEach(function(row, i) {
if (row.id === id) {
if (row.id === cred.id) {
$scope.selected_credentials.machine = angular.copy(row);
$scope.credentials[i].checked = 1;
} else {

View File

@ -31,12 +31,12 @@
<div class="JobSubmission-previewTags--outer">
<div class="JobSubmission-previewTags--inner">
<div class="JobSubmission-previewTagContainer">
<div class="JobSubmission-previewTagContainerDelete" ng-click="deleteSelectedInventory()">
<i class="fa fa-times JobSubmission-previewTagContainerTagDelete"></i>
</div>
<div class="JobSubmission-previewTag JobSubmission-previewTag--deletable">
<span>{{selected_inventory.name}}</span>
</div>
<div class="JobSubmission-previewTagContainerDelete" ng-click="deleteSelectedInventory()">
<i class="fa fa-times JobSubmission-previewTagContainerTagDelete"></i>
</div>
</div>
</div>
</div>
@ -58,22 +58,22 @@
<div class="JobSubmission-previewTags--outer">
<div class="JobSubmission-previewTags--inner">
<div class="JobSubmission-previewTagContainer u-wordwrap" ng-show="selected_credentials.machine">
<div class="JobSubmission-previewTagContainerDelete" ng-click="deleteMachineCred()" ng-show="ask_credential_on_launch">
<i class="fa fa-times JobSubmission-previewTagContainerTagDelete"></i>
</div>
<div class="JobSubmission-previewTag" ng-class="{'JobSubmission-previewTag--deletable': ask_credential_on_launch}">
<span><span class="JobSubmission-previewTagLabel" ng-class="{'JobSubmission-previewTagLabel--deletable': ask_credential_on_launch}">
MACHINE</span>:&nbsp;{{selected_credentials.machine.name}}</span>
</div>
</div>
<div class="JobSubmission-previewTagContainer" ng-repeat="extraCredential in selected_credentials.extra">
<div class="JobSubmission-previewTagContainerDelete" ng-click="deleteExtraCred($index)" ng-show="ask_credential_on_launch">
<div class="JobSubmission-previewTagContainerDelete" ng-click="deleteMachineCred()" ng-show="ask_credential_on_launch">
<i class="fa fa-times JobSubmission-previewTagContainerTagDelete"></i>
</div>
</div>
<div class="JobSubmission-previewTagContainer" ng-repeat="extraCredential in selected_credentials.extra">
<div class="JobSubmission-previewTag" ng-class="{'JobSubmission-previewTag--deletable': ask_credential_on_launch}">
<span><span class="JobSubmission-previewTagLabel" ng-class="{'JobSubmission-previewTagLabel--deletable': ask_credential_on_launch}">
{{credential_types[extraCredential.credential_type].name | uppercase}}</span>:&nbsp;{{extraCredential.name}}</span>
</div>
<div class="JobSubmission-previewTagContainerDelete" ng-click="deleteExtraCred($index)" ng-show="ask_credential_on_launch">
<i class="fa fa-times JobSubmission-previewTagContainerTagDelete"></i>
</div>
</div>
<div class="JobSubmission-previewTagContainer JobSubmission-previewTagContainer--vault" ng-show="selected_credentials.vault">
<div class="JobSubmission-previewTag JobSubmission-previewTag--vault" ng-class="{'JobSubmission-previewTag--deletable': ask_credential_on_launch}">

View File

@ -1,135 +0,0 @@
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'
];

View File

@ -1,154 +0,0 @@
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()
.then(({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');
})
.catch(({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'
];

View File

@ -1,13 +1,14 @@
export default
function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait) {
function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait, $q) {
return function(params) {
var scope = params.scope,
url = params.url,
scheduler = params.scheduler,
mode = params.mode,
schedule = (params.schedule) ? params.schedule : {},
callback = params.callback,
promptData = params.promptData,
newSchedule, rrule, extra_vars;
let deferred = $q.defer();
if (scheduler.isValid()) {
Wait('start');
newSchedule = scheduler.getValue();
@ -32,41 +33,101 @@ export default
schedule.extra_data = scope.parseType === 'yaml' ?
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
}
if(promptData) {
if(promptData.launchConf.survey_enabled){
for (var i=0; i < promptData.surveyQuestions.length; i++){
var fld = promptData.surveyQuestions[i].variable;
// grab all survey questions that have answers
if(promptData.surveyQuestions[i].required || (promptData.surveyQuestions[i].required === false && promptData.surveyQuestions[i].model.toString()!=="")) {
if(!schedule.extra_data) {
schedule.extra_data = {};
}
schedule.extra_data[fld] = promptData.surveyQuestions[i].model;
}
if(promptData.surveyQuestions[i].required === false && _.isEmpty(promptData.surveyQuestions[i].model)) {
switch (promptData.surveyQuestions[i].type) {
// for optional text and text-areas, submit a blank string if min length is 0
// -- this is confusing, for an explanation see:
// http://docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html#optional-survey-questions
//
case "text":
case "textarea":
if (promptData.surveyQuestions[i].min === 0) {
schedule.extra_data[fld] = "";
}
break;
}
}
}
}
if(_.has(promptData, 'prompts.jobType.value.value') && _.get(promptData, 'launchConf.ask_job_type_on_launch')) {
schedule.job_type = promptData.prompts.jobType.templateDefault === promptData.prompts.jobType.value.value ? null : promptData.prompts.jobType.value.value;
}
if(_.has(promptData, 'prompts.tags.value') && _.get(promptData, 'launchConf.ask_tags_on_launch')){
let templateDefaultJobTags = promptData.prompts.tags.templateDefault.split(',');
schedule.job_tags = (_.isEqual(templateDefaultJobTags.sort(), promptData.prompts.tags.value.map(a => a.value).sort())) ? null : promptData.prompts.tags.value.map(a => a.value).join();
}
if(_.has(promptData, 'prompts.skipTags.value') && _.get(promptData, 'launchConf.ask_skip_tags_on_launch')){
let templateDefaultSkipTags = promptData.prompts.skipTags.templateDefault.split(',');
schedule.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : promptData.prompts.skipTags.value.map(a => a.value).join();
}
if(_.has(promptData, 'prompts.limit.value') && _.get(promptData, 'launchConf.ask_limit_on_launch')){
schedule.limit = promptData.prompts.limit.templateDefault === promptData.prompts.limit.value ? null : promptData.prompts.limit.value;
}
if(_.has(promptData, 'prompts.verbosity.value.value') && _.get(promptData, 'launchConf.ask_verbosity_on_launch')){
schedule.verbosity = promptData.prompts.verbosity.templateDefault === promptData.prompts.verbosity.value.value ? null : promptData.prompts.verbosity.value.value;
}
if(_.has(promptData, 'prompts.inventory.value') && _.get(promptData, 'launchConf.ask_inventory_on_launch')){
schedule.inventory = promptData.prompts.inventory.templateDefault.id === promptData.prompts.inventory.value.id ? null : promptData.prompts.inventory.value.id;
}
if(_.has(promptData, 'prompts.diffMode.value') && _.get(promptData, 'launchConf.ask_diff_mode_on_launch')){
schedule.diff_mode = promptData.prompts.diffMode.templateDefault === promptData.prompts.diffMode.value ? null : promptData.prompts.diffMode.value;
}
// Credentials gets POST'd to a separate endpoint
// if($scope.promptData.launchConf.ask_credential_on_launch){
// jobLaunchData.credentials = [];
// promptData.credentials.value.forEach((credential) => {
// jobLaunchData.credentials.push(credential.id);
// });
// }
}
Rest.setUrl(url);
if (mode === 'add') {
Rest.post(schedule)
.then(() => {
if (callback) {
scope.$emit(callback);
}
else {
Wait('stop');
}
Wait('stop');
deferred.resolve();
})
.catch(({data, status}) => {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'POST to ' + url + ' returned: ' + status });
deferred.reject();
});
}
else {
Rest.put(schedule)
.then(() => {
if (callback) {
scope.$emit(callback, schedule);
}
else {
Wait('stop');
}
Wait('stop');
deferred.resolve(schedule);
})
.catch(({data, status}) => {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'POST to ' + url + ' returned: ' + status });
deferred.reject();
});
}
}
else {
return false;
deferred.reject();
}
return deferred.promise;
};
}
@ -74,5 +135,6 @@ SchedulePost.$inject =
[ 'Rest',
'ProcessErrors',
'RRuleToAPI',
'Wait'
'Wait',
'$q'
];

View File

@ -10,9 +10,7 @@ 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';
@ -24,9 +22,7 @@ export default
.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)
@ -47,10 +43,10 @@ export default
activityStreamTarget: 'job_template',
activityStreamId: 'id'
},
ncyBreadcrumb: {
parent: 'templates.editJobTemplate({job_template_id: parentObject.id})',
label: N_('SCHEDULES')
},
// ncyBreadcrumb: {
// parent: 'templates.editJobTemplate({job_template_id: parentObject.id})',
// label: N_('SCHEDULES')
// },
resolve: {
Dataset: ['ScheduleList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {

View File

@ -4,12 +4,22 @@
* All Rights Reserved
*************************************************/
export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait',
export default ['$filter', '$state', '$stateParams', 'Wait',
'$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', 'GetBasePath',
'Rest', 'ParentObject',
function($filter, $state, $stateParams, AddSchedule, Wait, $scope,
$rootScope, CreateSelect2, ParseTypeChange, GetBasePath, Rest, ParentObject) {
$scope.processSchedulerEndDt = function(){
'Rest', 'ParentObject', 'JobTemplateModel', '$q', 'Empty', 'SchedulePost',
'ProcessErrors', 'SchedulerInit', '$location', 'PromptService',
function($filter, $state, $stateParams, Wait,
$scope, $rootScope, CreateSelect2, ParseTypeChange, GetBasePath,
Rest, ParentObject, JobTemplate, $q, Empty, SchedulePost,
ProcessErrors, SchedulerInit, $location, PromptService) {
var base = $scope.base || $location.path().replace(/^\//, '').split('/')[0],
scheduler,
job_type;
var schedule_url = ParentObject.related.schedules || `${ParentObject.related.inventory_source}schedules`;
let processSchedulerEndDt = function(){
// set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight
var dt = new Date($scope.schedulerUTCTime);
// increment date by 1 day
@ -19,12 +29,6 @@ export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait',
$scope.$parent.schedulerEndDt = month + '/' + day + '/' + dt.getFullYear();
};
// initial end @ midnight values
$scope.schedulerEndHour = "00";
$scope.schedulerEndMinute = "00";
$scope.schedulerEndSecond = "00";
$scope.parentObject = ParentObject;
/*
* This is a workaround for the angular-scheduler library inserting `ll` into fields after an
* invalid entry and never unsetting them. Presumably null is being truncated down to 2 chars
@ -50,9 +54,255 @@ export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait',
$scope.scheduleTimeChange();
};
$scope.$on("ScheduleFormCreated", function(e, scope) {
$scope.saveSchedule = function() {
SchedulePost({
scope: $scope,
url: schedule_url,
scheduler: scheduler,
promptData: $scope.promptData,
mode: 'add'
}).then(() => {
Wait('stop');
$state.go("^", null, {reload: true});
});
};
$scope.prompt = () => {
$scope.promptData.triggerModalOpen = true;
};
$scope.formCancel = function() {
$state.go("^");
};
// initial end @ midnight values
$scope.schedulerEndHour = "00";
$scope.schedulerEndMinute = "00";
$scope.schedulerEndSecond = "00";
$scope.parentObject = ParentObject;
$scope.hideForm = true;
// extra_data field is not manifested in the UI when scheduling a Management Job
if ($state.current.name === 'jobTemplateSchedules.add'){
$scope.parseType = 'yaml';
$scope.extraVars = '---';
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars'
});
let jobTemplate = new JobTemplate();
$q.all([jobTemplate.optionsLaunch(ParentObject.id), jobTemplate.getLaunch(ParentObject.id)])
.then((responses) => {
let launchConf = responses[1].data;
let watchForPromptChanges = () => {
let promptValuesToWatch = [
'promptData.prompts.inventory.value',
'promptData.prompts.verbosity.value',
'missingSurveyValue'
];
$scope.$watchGroup(promptValuesToWatch, function() {
let missingPromptValue = false;
if($scope.missingSurveyValue) {
missingPromptValue = true;
} else if(!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) {
missingPromptValue = true;
}
$scope.promptModalMissingReqFields = missingPromptValue;
});
};
if(!launchConf.ask_variables_on_launch) {
$scope.noVars = true;
}
if(!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.survey_enabled &&
!launchConf.credential_needed_to_start &&
!launchConf.inventory_needed_to_start &&
launchConf.passwords_needed_to_start.length === 0 &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
} else {
$scope.showPromptButton = true;
// Ignore the fact that variables might be promptable on launch
// Promptable variables will happen in the schedule form
launchConf.ignore_ask_variables = true;
if(launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) {
$scope.promptModalMissingReqFields = true;
}
if(launchConf.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions(ParentObject.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.promptData = {
launchConf: responses[1].data,
launchOptions: responses[0].data,
surveyQuestions: processed.surveyQuestions,
template: ParentObject.id,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
};
$scope.$watch('promptData.surveyQuestions', () => {
let missingSurveyValue = false;
_.each($scope.promptData.surveyQuestions, (question) => {
if(question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
});
$scope.missingSurveyValue = missingSurveyValue;
}, true);
watchForPromptChanges();
});
}
else {
$scope.promptData = {
launchConf: responses[1].data,
launchOptions: responses[0].data,
template: ParentObject.id,
prompts: PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data
}),
};
watchForPromptChanges();
}
}
});
}
else if ($state.current.name === 'workflowJobTemplateSchedules.add'){
$scope.parseType = 'yaml';
// grab any existing extra_vars from parent workflow_job_template
let defaultUrl = GetBasePath('workflow_job_templates') + $stateParams.id + '/';
Rest.setUrl(defaultUrl);
Rest.get().then(function(res){
var data = res.data.extra_vars;
$scope.extraVars = data === '' ? '---' : data;
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars'
});
});
}
else if ($state.current.name === 'projectSchedules.add'){
$scope.noVars = true;
}
else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.add'){
$scope.noVars = true;
}
job_type = $scope.parentObject.job_type;
if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !schedule_url) {
schedule_url = GetBasePath(base) + $stateParams.id + '/schedules/';
}
else if(base === "inventories"){
if (!schedule_url){
Rest.setUrl(GetBasePath('groups') + $stateParams.id + '/');
Rest.get()
.then(function (data) {
schedule_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 if (base === 'system_job_templates') {
schedule_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
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();
processSchedulerEndDt();
}
});
}
scheduler.inject('form-container', false);
scheduler.injectDetail('occurrences', false);
scheduler.clear();
$scope.$on("htmlDetailReady", function() {
$scope.hideForm = false;
$scope = angular.extend($scope, scope);
$scope.$on("formUpdated", function() {
$rootScope.$broadcast("loadSchedulerDetailPane");
});
@ -91,72 +341,18 @@ export default ['$filter', '$state', '$stateParams', 'AddSchedule', 'Wait',
Wait('stop');
});
$scope.showRRuleDetail = false;
$scope.hideForm = true;
var schedule_url = ParentObject.related.schedules || `${ParentObject.related.inventory_source}schedules`;
$scope.formCancel = function() {
$state.go("^");
};
// extra_data field is not manifested in the UI when scheduling a Management Job
if ($state.current.name === 'jobTemplateSchedules.add'){
$scope.parseType = 'yaml';
// grab any existing extra_vars from parent job_template
let defaultUrl = GetBasePath('job_templates') + $stateParams.id + '/';
Rest.setUrl(defaultUrl);
Rest.get().then(function(res){
var data = res.data.extra_vars;
$scope.extraVars = data === '' ? '---' : data;
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars'
});
});
}
else if ($state.current.name === 'workflowJobTemplateSchedules.add'){
$scope.parseType = 'yaml';
// grab any existing extra_vars from parent workflow_job_template
let defaultUrl = GetBasePath('workflow_job_templates') + $stateParams.id + '/';
Rest.setUrl(defaultUrl);
Rest.get().then(function(res){
var data = res.data.extra_vars;
$scope.extraVars = data === '' ? '---' : data;
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars'
});
});
}
else if ($state.current.name === 'projectSchedules.add'){
$scope.noVars = true;
}
else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.add'){
$scope.noVars = true;
}
AddSchedule({
scope: $scope,
callback: 'SchedulesRefresh',
base: $scope.base ? $scope.base : null,
url: schedule_url
$('#scheduler-tabs li a').on('shown.bs.tab', function(e) {
if ($(e.target).text() === 'Details') {
if (!scheduler.isValid()) {
$('#scheduler-tabs a:first').tab('show');
}
}
});
var callSelect2 = function() {
CreateSelect2({
element: '.MakeSelect2',
multiple: false
});
};
$scope.$on("updateSchedulerSelects", function() {
callSelect2();
CreateSelect2({
element: '.MakeSelect2',
multiple: false
});
callSelect2();
}];

View File

@ -1,5 +1,20 @@
export default ['$filter', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', 'ParentObject',
function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange, ParentObject) {
export default ['$filter', '$state', '$stateParams', 'Wait', '$scope',
'$rootScope', 'CreateSelect2', 'ParseTypeChange', 'ParentObject', 'ProcessErrors', 'Rest',
'GetBasePath', 'SchedulerInit', 'SchedulePost', 'JobTemplateModel', '$q', 'Empty', 'PromptService',
function($filter, $state, $stateParams, Wait, $scope,
$rootScope, CreateSelect2, ParseTypeChange, ParentObject, ProcessErrors, Rest,
GetBasePath, SchedulerInit, SchedulePost, JobTemplate, $q, Empty, PromptService) {
let schedule, scheduler;
// initial end @ midnight values
$scope.schedulerEndHour = "00";
$scope.schedulerEndMinute = "00";
$scope.schedulerEndSecond = "00";
$scope.parentObject = ParentObject;
$scope.isEdit = true;
$scope.hideForm = true;
$scope.parseType = 'yaml';
$scope.processSchedulerEndDt = function(){
// set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight
@ -10,11 +25,10 @@ function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope,
day = $filter('schZeroPad')(dt.getDate(), 2);
$scope.$parent.schedulerEndDt = month + '/' + day + '/' + dt.getFullYear();
};
// initial end @ midnight values
$scope.schedulerEndHour = "00";
$scope.schedulerEndMinute = "00";
$scope.schedulerEndSecond = "00";
$scope.parentObject = ParentObject;
$scope.formCancel = function() {
$state.go("^");
};
/*
* This is a workaround for the angular-scheduler library inserting `ll` into fields after an
@ -41,85 +55,24 @@ function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope,
$scope.scheduleTimeChange();
};
$scope.$on("ScheduleFormCreated", function(e, scope) {
$scope.hideForm = false;
$scope = angular.extend($scope, scope);
$scope.$on("formUpdated", function() {
$rootScope.$broadcast("loadSchedulerDetailPane");
$scope.saveSchedule = function() {
schedule.extra_data = $scope.extraVars;
SchedulePost({
scope: $scope,
url: GetBasePath('schedules') + parseInt($stateParams.schedule_id) + '/',
scheduler: scheduler,
mode: 'edit',
schedule: schedule,
promptData: $scope.promptData
}).then(() => {
Wait('stop');
$state.go("^", null, {reload: true});
});
$scope.$watchGroup(["schedulerName",
"schedulerStartDt",
"schedulerStartHour",
"schedulerStartMinute",
"schedulerStartSecond",
"schedulerTimeZone",
"schedulerFrequency",
"schedulerInterval",
"monthlyRepeatOption",
"monthDay",
"monthlyOccurrence",
"monthlyWeekDay",
"yearlyRepeatOption",
"yearlyMonth",
"yearlyMonthDay",
"yearlyOccurrence",
"yearlyWeekDay",
"yearlyOtherMonth",
"schedulerEnd",
"schedulerOccurrenceCount",
"schedulerEndDt"
], function() {
$scope.$emit("formUpdated");
}, true);
$scope.$watch("weekDays", function() {
$scope.$emit("formUpdated");
}, true);
$rootScope.$broadcast("loadSchedulerDetailPane");
Wait('stop');
});
$scope.isEdit = true;
$scope.hideForm = true;
$scope.parseType = 'yaml';
$scope.formCancel = function() {
$state.go("^");
};
// extra_data field is not manifested in the UI when scheduling a Management Job
if ($state.current.name !== 'managementJobsList.schedule.add' && $state.current.name !== 'managementJobsList.schedule.edit'){
$scope.$on('ScheduleFound', function(){
if ($state.current.name === 'projectSchedules.edit'){
$scope.noVars = true;
}
else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.edit'){
$scope.noVars = true;
}
else {
let readOnly = !$scope.schedule_obj.summary_fields.user_capabilities
.edit;
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars',
readOnly: readOnly
});
}
});
}
EditSchedule({
scope: $scope,
id: parseInt($stateParams.schedule_id),
callback: 'SchedulesRefresh',
base: $scope.base ? $scope.base: null
});
$scope.prompt = () => {
$scope.promptData.triggerModalOpen = true;
};
var callSelect2 = function() {
CreateSelect2({
@ -132,5 +85,284 @@ function($filter, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope,
callSelect2();
});
Wait('start');
// Get the existing record
Rest.setUrl(GetBasePath('schedules') + parseInt($stateParams.schedule_id) + '/');
Rest.get()
.then(({data}) => {
schedule = data;
try {
schedule.extra_data = JSON.parse(schedule.extra_data);
} catch(e) {
// do nothing
}
$scope.extraVars = (data.extra_data === '' || _.isEmpty(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;
$('#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);
$scope.hideForm = false;
$scope.$watchGroup(["schedulerName",
"schedulerStartDt",
"schedulerStartHour",
"schedulerStartMinute",
"schedulerStartSecond",
"schedulerTimeZone",
"schedulerFrequency",
"schedulerInterval",
"monthlyRepeatOption",
"monthDay",
"monthlyOccurrence",
"monthlyWeekDay",
"yearlyRepeatOption",
"yearlyMonth",
"yearlyMonthDay",
"yearlyOccurrence",
"yearlyWeekDay",
"yearlyOtherMonth",
"schedulerEnd",
"schedulerOccurrenceCount",
"schedulerEndDt"
], function() {
$rootScope.$broadcast("loadSchedulerDetailPane");
}, true);
$scope.$watch("weekDays", function() {
$rootScope.$broadcast("loadSchedulerDetailPane");
}, true);
$rootScope.$broadcast("loadSchedulerDetailPane");
Wait('stop');
});
$scope.showRRuleDetail = false;
scheduler.setRRule(schedule.rrule);
scheduler.setName(schedule.name);
if($scope.isFactCleanup || $scope.cleanupJob){
var a,b, prompt_for_days,
keep_unit,
granularity,
granularity_keep_unit;
if($scope.cleanupJob){
$scope.schedulerPurgeDays = 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 ($state.current.name === 'jobTemplateSchedules.edit'){
let jobTemplate = new JobTemplate();
Rest.setUrl(data.related.credentials);
$q.all([jobTemplate.optionsLaunch(ParentObject.id), jobTemplate.getLaunch(ParentObject.id), Rest.get()])
.then((responses) => {
let launchOptions = responses[0].data,
launchConf = responses[1].data,
scheduleCredentials = responses[2].data;
let watchForPromptChanges = () => {
let promptValuesToWatch = [
// credential passwords...?
'promptData.prompts.inventory.value',
'promptData.prompts.verbosity.value',
'missingSurveyValue'
];
$scope.$watchGroup(promptValuesToWatch, function() {
let missingPromptValue = false;
if($scope.missingSurveyValue) {
missingPromptValue = true;
} else if(!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) {
missingPromptValue = true;
}
$scope.promptModalMissingReqFields = missingPromptValue;
});
};
let prompts = PromptService.processPromptValues({
launchConf: responses[1].data,
launchOptions: responses[0].data,
currentValues: data
});
prompts.credentials.value = scheduleCredentials.results.length > 0 ? scheduleCredentials.results : prompts.credentials.value;
if(!launchConf.ask_variables_on_launch) {
$scope.noVars = true;
}
if(!launchConf.survey_enabled &&
!launchConf.ask_inventory_on_launch &&
!launchConf.ask_credential_on_launch &&
!launchConf.ask_verbosity_on_launch &&
!launchConf.ask_job_type_on_launch &&
!launchConf.ask_limit_on_launch &&
!launchConf.ask_tags_on_launch &&
!launchConf.ask_skip_tags_on_launch &&
!launchConf.ask_diff_mode_on_launch &&
!launchConf.survey_enabled &&
!launchConf.credential_needed_to_start &&
!launchConf.inventory_needed_to_start &&
launchConf.passwords_needed_to_start.length === 0 &&
launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false;
} else {
$scope.showPromptButton = true;
// Ignore the fact that variables might be promptable on launch
// Promptable variables will happen in the schedule form
launchConf.ignore_ask_variables = true;
if(launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has(data, 'summary_fields.inventory')) {
$scope.promptModalMissingReqFields = true;
}
if(responses[1].data.survey_enabled) {
// go out and get the survey questions
jobTemplate.getSurveyQuestions(ParentObject.id)
.then((surveyQuestionRes) => {
let processed = PromptService.processSurveyQuestions({
surveyQuestions: surveyQuestionRes.data.spec,
extra_data: data.extra_data
});
$scope.missingSurveyValue = processed.missingSurveyValue;
$scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data);
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars',
readOnly: !$scope.schedule_obj.summary_fields.user_capabilities.edit
});
$scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
surveyQuestions: surveyQuestionRes.data.spec,
template: ParentObject.id
};
$scope.$watch('promptData.surveyQuestions', () => {
let missingSurveyValue = false;
_.each($scope.promptData.surveyQuestions, (question) => {
if(question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
});
$scope.missingSurveyValue = missingSurveyValue;
}, true);
watchForPromptChanges();
});
}
else {
$scope.promptData = {
launchConf: launchConf,
launchOptions: launchOptions,
prompts: prompts,
template: ParentObject.id
};
watchForPromptChanges();
}
}
});
}
// extra_data field is not manifested in the UI when scheduling a Management Job
if ($state.current.name !== 'managementJobsList.schedule.add' && $state.current.name !== 'managementJobsList.schedule.edit'){
if ($state.current.name === 'projectSchedules.edit'){
$scope.noVars = true;
}
else if ($state.current.name === 'inventories.edit.inventory_sources.edit.schedules.edit'){
$scope.noVars = true;
}
else {
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars',
readOnly: !$scope.schedule_obj.summary_fields.user_capabilities.edit
});
}
}
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve schedule ' + parseInt($stateParams.schedule_id) + ' GET returned: ' + status });
});
callSelect2();
}];

View File

@ -54,3 +54,15 @@
margin-right: 0px;
}
}
.SchedulerForm-promptSaveTooltip {
position: absolute;
height: 100%;
display: block;
margin-left: 20px;
width: ~"calc(100% - 20px)";
}
.SchedulerForm-promptSave {
position: relative;
}

View File

@ -661,23 +661,30 @@
</div>
</div>
</div>
<div class="buttons Form-buttons">
<button type="button"
class="btn btn-sm Form-cancelButton"
id="project_cancel_btn"
ng-show="!(schedule_obj.summary_fields.user_capabilities.edit || canAdd)"
ng-click="formCancel()">Close</button>
<button type="button"
class="btn btn-sm Form-cancelButton"
id="project_cancel_btn"
ng-show="(schedule_obj.summary_fields.user_capabilities.edit || canAdd)"
ng-click="formCancel()">Cancel</button>
<button type="button"
class="btn btn-sm Form-saveButton"
id="project_save_btn"
ng-click="saveSchedule()"
ng-show="(schedule_obj.summary_fields.user_capabilities.edit || canAdd)"
ng-disabled="!schedulerIsValid"> Save</button>
</div>
<div class="buttons Form-buttons">
<button type="button"
class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin"
id="schedule_prompt_btn"
ng-show="(schedule_obj.summary_fields.user_capabilities.edit || canAdd) && showPromptButton"
ng-click="prompt()">Prompt</button>
<button type="button"
class="btn btn-sm Form-cancelButton"
id="schedule_cancel_btn"
ng-show="!(schedule_obj.summary_fields.user_capabilities.edit || canAdd)"
ng-click="formCancel()">Close</button>
<button type="button"
class="btn btn-sm Form-cancelButton"
id="schedule_cancel_btn"
ng-show="(schedule_obj.summary_fields.user_capabilities.edit || canAdd)"
ng-click="formCancel()">Cancel</button>
<div class="SchedulerForm-promptSave" ng-show="(schedule_obj.summary_fields.user_capabilities.edit || canAdd)">
<div ng-if="promptModalMissingReqFields" class="SchedulerForm-promptSaveTooltip" aw-tool-tip="Additional information required in the Prompt area before saving" data-placement="top"></div>
<button type="button"
class="btn btn-sm Form-saveButton"
id="schedule_save_btn"
ng-click="saveSchedule()"
ng-disabled="!schedulerIsValid || promptModalMissingReqFields"> Save</button>
</div>
</div>
<prompt prompt-data="promptData" action-text="CONFIRM"></launch>
</div>

View File

@ -8,13 +8,13 @@
<span id="InstanceGroups" class="form-control Form-textInput Form-textInput--variableHeight input-medium lookup LabelList-lookupTags"
ng-disabled="fieldIsDisabled">
<div class="LabelList-tagContainer" ng-repeat="tag in instanceGroupsTags">
<div class="LabelList-tag LabelList-tag--deletable">
<span class="LabelList-name">{{ tag.name }}</span>
</div>
<div class="LabelList-deleteContainer"
ng-click="deleteTag(tag)">
<i class="fa fa-times LabelList-tagDelete"></i>
</div>
<div class="LabelList-tag LabelList-tag--deletable">
<span class="LabelList-name">{{ tag.name }}</span>
</div>
</div>
</span>
</div>

View File

@ -344,7 +344,7 @@ export default ['$compile', 'Attr', 'Icon',
}
else { // its assumed that options.input_type = checkbox
innerTable += "<td class=\"List-tableCell select-column List-staticColumn--smallStatus\"><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-true-value=\"1\" " +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ", true)\" ng-true-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_" + list.iterator + "_{{" + list.iterator + ".id}}\" /></td>";
}
}
@ -368,11 +368,11 @@ export default ['$compile', 'Attr', 'Icon',
if (options.mode === 'select') {
if (options.input_type === "radio") { //added by JT so that lookup forms can be either radio inputs or check box inputs
innerTable += "<td class=\"List-tableCell\"><input type=\"radio\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-value=\"1\" " +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ", true)\" ng-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
} else { // its assumed that options.input_type = checkbox
innerTable += "<td class=\"List-tableCell\"><input type=\"checkbox\" ng-model=\"" + list.iterator + ".checked\" name=\"check_{{" +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ".id, true)\" ng-true-value=\"1\" " +
list.iterator + ".id }}\" ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ", true)\" ng-true-value=\"1\" " +
"ng-false-value=\"0\" id=\"check_{{" + list.iterator + ".id}}\" /></td>";
}
} else if ((options.mode === 'edit' || options.mode === 'summary') && list.fieldActions) {

View File

@ -48,6 +48,7 @@ export default
scrollbarStyle: null
}
};
scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly);
scope[fld + 'codeMirror'].addModes(variableEditModes);
scope[fld + 'codeMirror'].showTextArea({
@ -95,6 +96,7 @@ export default
// convert json to yaml
try {
removeField(fld);
json_obj = JSON.parse(scope[fld]);
if ($.isEmptyObject(json_obj)) {
scope[fld] = '---';

View File

@ -21,12 +21,7 @@
<div class="MultiCredential-tags">
<div class="MultiCredential-tagSection">
<div class="MultiCredential-flexContainer">
<div class="MultiCredential-tagContainer ng-scope" ng-repeat="tag in tags track by $index">
<div class="MultiCredential-deleteContainer"
ng-click="vm.removeCredential(tag)"
ng-hide="fieldIsDisabled || tag.readOnly">
<i class="fa fa-times MultiCredential-tagDelete"></i>
</div>
<div class="MultiCredential-tagContainer ng-scope" ng-repeat="tag in tags track by $index" ng-class="{'MultiCredential-tagContainer--disabled': tag.readOnly}">
<div class="MultiCredential-iconContainer--disabled" ng-switch="tag.kind" ng-if="fieldIsDisabled || tag.readOnly">
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
<i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
@ -52,6 +47,11 @@
{{ tag.name }} | {{ tag.info }}
</span>
</div>
<div class="MultiCredential-deleteContainer"
ng-click="vm.removeCredential(tag)"
ng-hide="fieldIsDisabled || tag.readOnly">
<i class="fa fa-times MultiCredential-tagDelete"></i>
</div>
</div>
</div>
</div>

View File

@ -34,25 +34,28 @@
.MultiCredential-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;
}
.MultiCredential-tagContainer--disabled {
background-color: @default-icon;
}
.MultiCredential-tag {
border-radius: 5px;
padding: 2px 10px;
margin: 3px 0px;
font-size: 12px;
margin-right: 10px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
background-color: @default-link;
color: @default-bg;
padding-left: 15px;
padding: 2px 0px 2px 15px;
}
.MultiCredential-tag--disabled {
background-color: @default-icon;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
padding-left: 10px;
@ -60,22 +63,17 @@
.MultiCredential-tag--deletable {
margin-right: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right: 0;
max-width: ~"calc(100% - 23px)";
margin-right: 10px;
padding-left: 10px;
}
.MultiCredential-deleteContainer {
background-color: @default-link!important;
color: white;
background-color: @default-bg;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
padding: 0 5px;
margin: 3px 0px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding: 2px 5px;
align-items: center;
display: flex;
cursor: pointer;
@ -86,8 +84,6 @@
}
.MultiCredential-iconContainer {
background-color: @default-link!important;
color: @default-bg;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
padding: 0px 5px;
@ -98,8 +94,6 @@
}
.MultiCredential-iconContainer--disabled {
background-color: @default-icon;
color: @default-bg;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
padding: 0 5px;
@ -112,7 +106,7 @@
.MultiCredential-tagIcon {
margin: 0px 0px;
font-size: 10px;
font-size: 12px;
}
.MultiCredential-name {
@ -123,10 +117,9 @@
.MultiCredential-name--label {
color: @default-list-header-bg;
font-size: 10px;
font-size: 12px;
margin-left: -8px;
margin-right: 5px;
text-transform: uppercase;
}
.MultiCredential-tag--deletable > .MultiCredential-name {

View File

@ -16,12 +16,8 @@
<div class="MultiCredential-tagSection">
<div class="MultiCredential-flexContainer">
<div class="MultiCredential-tagContainer ng-scope"
ng-class="{'MultiCredential-tagContainer--disabled': tag.readOnly}"
ng-repeat="tag in tags track by $index">
<div class="MultiCredential-deleteContainer"
ng-click="vm.deselectCredential(tag)"
ng-hide="fieldIsDisabled || tag.readOnly">
<i class="fa fa-times MultiCredential-tagDelete"></i>
</div>
<div class="MultiCredential-iconContainer--disabled" ng-switch="tag.kind" ng-if="fieldIsDisabled || tag.readOnly">
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
<i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
@ -47,6 +43,11 @@
{{ tag.name }} | {{ tag.info }}
</span>
</div>
<div class="MultiCredential-deleteContainer"
ng-click="vm.deselectCredential(tag)"
ng-hide="fieldIsDisabled || tag.readOnly">
<i class="fa fa-times MultiCredential-tagDelete"></i>
</div>
</div>
</div>
</div>

View File

@ -39,27 +39,27 @@
color: @default-link-hov;
}
.LabelList-tag--deletable, .JobSubmission-previewTag--deletable {
.LabelList-tag--deletable, .JobSubmission-previewTag--deletable, .Prompt-previewTag--deletable {
color: @default-bg;
background-color: @default-link;
margin-right: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right: 0;
max-width: ~"calc(100% - 23px)";
margin-right: 5px;
}
.LabelList-deleteContainer, .JobSubmission-previewTagContainerDelete {
.LabelList-deleteContainer, .JobSubmission-previewTagContainerDelete, .Prompt-previewTagContainerDelete {
background-color: @default-link;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
color: @default-bg;
padding: 0 5px;
margin: 3px 0px;
align-items: center;
display: flex;
cursor: pointer;
margin-right: 5px;
}
.LabelList-tagDelete {
@ -76,12 +76,12 @@
max-width: ~"calc(100% - 23px)";
}
.LabelList-deleteContainer:hover, .JobSubmission-previewTagContainerDelete:hover {
.LabelList-deleteContainer:hover, .JobSubmission-previewTagContainerDelete:hover, .Prompt-previewTagContainerDelete:hover {
border-color: @default-err;
background-color: @default-err;
}
.LabelList-deleteContainer:hover > .LabelList-tagDelete, .JobSubmission-previewTagContainerDelete:hover > .JobSubmission-previewTagContainerTagDelete {
.LabelList-deleteContainer:hover > .LabelList-tagDelete, .JobSubmission-previewTagContainerDelete:hover > .JobSubmission-previewTagContainerTagDelete, .Prompt-previewTagContainerDelete:hover > .Prompt-previewTagContainerTagDelete {
color: @default-bg;
}

View File

@ -17,14 +17,14 @@
Labels
</div>
<div class="LabelList-tagContainer" ng-repeat="label in labels">
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && template.summary_fields.user_capabilities.edit)}">
<span class="LabelList-name">{{ label.name }}</span>
</div>
<div class="LabelList-deleteContainer"
ng-click="deleteLabel(template, label)"
ng-show="showDelete && template.summary_fields.user_capabilities.edit">
<i class="fa fa-times LabelList-tagDelete"></i>
</div>
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && template.summary_fields.user_capabilities.edit)}">
<span class="LabelList-name">{{ label.name }}</span>
</div>
</div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && seeMoreInactive"
ng-click="seeMore()">View More</div>

View File

@ -10,6 +10,7 @@ import jobTemplates from './job_templates/main';
import workflowAdd from './workflows/add-workflow/main';
import workflowEdit from './workflows/edit-workflow/main';
import labels from './labels/main';
import prompt from './prompt/main';
import workflowChart from './workflows/workflow-chart/main';
import workflowMaker from './workflows/workflow-maker/main';
import workflowControls from './workflows/workflow-controls/main';
@ -23,7 +24,7 @@ import TemplatesStrings from './templates.strings';
import listRoute from '~features/templates/list.route.js';
export default
angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, workflowAdd.name, workflowEdit.name,
angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, prompt.name, workflowAdd.name, workflowEdit.name,
workflowChart.name, workflowMaker.name, workflowControls.name
])
.service('TemplatesService', templatesService)

View File

@ -0,0 +1,17 @@
import promptDirective from './prompt.directive';
import promptInventory from './steps/inventory/prompt-inventory.directive';
import promptCredential from './steps/credential/prompt-credential.directive';
import promptOtherPrompts from './steps/other-prompts/prompt-other-prompts.directive';
import promptSurvey from './steps/survey/prompt-survey.directive';
import promptPreview from './steps/preview/prompt-preview.directive';
import promptService from './prompt.service';
export default
angular.module('prompt', [])
.directive('prompt', promptDirective)
.directive('promptInventory', promptInventory)
.directive('promptCredential', promptCredential)
.directive('promptOtherPrompts', promptOtherPrompts)
.directive('promptSurvey', promptSurvey)
.directive('promptPreview', promptPreview)
.service('PromptService', promptService);

View File

@ -0,0 +1,166 @@
.Prompt .modal-dialog {
width: 700px;
}
.Prompt-step {
margin-top: 20px;
}
.Prompt-footer {
display: flex;
flex: 0 0 auto;
margin-top: 15px;
justify-content: flex-end;
align-items: flex-end;
}
.Prompt-actionButton {
background-color: @submit-button-bg;
border: 1px solid @submit-button-bg;
color: @submit-button-text;
text-transform: uppercase;
border-radius: 5px;
height: 30px;
padding-left:15px;
padding-right: 15px;
min-width: 85px;
}
.Prompt-actionButton:disabled {
background-color: @d7grey;
border-color: @d7grey;
opacity: 0.65;
}
.Prompt-actionButton:enabled:hover,
.Prompt-actionButton:enabled:focus {
background-color: @submit-button-bg-hov;
border: 1px solid @submit-button-bg-hov;
}
.Prompt-defaultButton{
background-color: @default-bg;
color: @btn-txt;
text-transform: uppercase;
border-radius: 5px;
border: 1px solid @btn-bord;
padding-left:15px;
padding-right: 15px;
height: 30px;
min-width: 85px;
margin-right: 20px;
}
.Prompt-defaultButton:hover{
background-color: @btn-bg-hov;
color: @btn-txt;
}
.Prompt-revertLink {
font-size: 12px;
}
.Prompt-selectedItem {
display: flex;
flex: 1 0 auto;
margin-bottom: 15px;
align-items: baseline;
}
.Prompt-selectedItemInfo {
display: flex;
flex: 0 0 100%;
background-color: @default-no-items-bord;
border: 1px solid @default-border;
padding: 10px;
border-radius: 5px;
max-height: 120px;
overflow-y: scroll;
}
.Prompt-selectedItemRevert {
display: flex;
flex: 0 0 auto;
}
.Prompt-credentialSubSection {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 15px;
}
.Prompt-selectedItemLabel, .Prompt-label {
color: @default-interface-txt;
margin-right: 10px;
}
.Prompt-label {
line-height: 24px;
}
.Prompt-selectedItemNone {
color: @default-icon;
}
.Prompt-selectedItemContainer {
display: block;
width: 100%;
}
.Prompt-instructions {
color: @default-interface-txt;
margin-top: 25px;
margin-bottom: 15px;
}
.Prompt-passwordButton {
padding: 4px 13px;
}
.Prompt .List-noItems {
margin-top: auto;
}
.Prompt-selectedItemLabel {
flex: 0 0 80px;
line-height: 29px;
}
.Prompt-previewTags--outer {
flex: 1 0 auto;
max-width: ~"calc(100% - 140px)";
}
.Prompt-previewTags--inner {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.Prompt-previewTagLabel {
color: @default-interface-txt;
}
.Prompt-previewTagLabel--deletable{
color: @default-list-header-bg;
}
.Prompt-previewTagRevert {
flex: 0 0 60px;
line-height: 29px;
}
.Prompt-previewTagContainer {
display: flex;
}
.Prompt-previewRow--flex {
display: flex;
margin-bottom: 10px;
}
.Prompt-previewRow--noflex {
margin-bottom: 10px;
}
.Prompt-previewRowTitle {
width: 150px;
color: @default-interface-txt;
text-transform: uppercase;
}
.Prompt-previewRowValue {
flex: 1 0 auto;
}
.Prompt-noSelectedItem {
height: 30px;
line-height: 30px;
font-style: italic;
color: @default-interface-txt;
}
.Prompt-previewTag {
border-radius: 5px;
padding: 2px 10px;
margin: 3px 0px;
font-size: 12px;
color: @default-bg;
background-color: @default-link;
margin-right: 5px;
max-width: 100%;
display: inline-block;
}
.Prompt-credentialSubSection .select2 {
width: 50% !important;
}

View File

@ -0,0 +1,188 @@
export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', 'TemplatesStrings',
function (Rest, GetBasePath, ProcessErrors, CredentialType, strings) {
// strings.get('deleteResource.HEADER')
// ${strings.get('deleteResource.CONFIRM', 'template')}
const vm = this || {};
vm.strings = strings;
let scope;
let modal;
vm.init = (_scope_) => {
scope = _scope_;
({ modal } = scope[scope.ns]);
scope.$watch('vm.promptData.triggerModalOpen', () => {
if(vm.promptData && vm.promptData.triggerModalOpen) {
vm.steps = {
inventory: {
includeStep: false
},
credential: {
includeStep: false
},
other_prompts: {
includeStep: false
},
survey: {
includeStep: false
},
preview: {
includeStep: true,
tab: {
_active: false,
_disabled: true
}
}
};
let order = 1;
vm.actionText = vm.actionText ? vm.actionText : strings.get('prompt.LAUNCH');
vm.forms = {};
let credentialType = new CredentialType();
credentialType.http.get()
.then( (response) => {
vm.promptData.prompts.credentials.credentialTypes = {};
vm.promptData.prompts.credentials.credentialTypeOptions = [];
response.data.results.forEach((credentialTypeRow => {
vm.promptData.prompts.credentials.credentialTypes[credentialTypeRow.id] = credentialTypeRow.kind;
if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault)$/)) {
if(credentialTypeRow.kind === 'ssh') {
vm.promptData.prompts.credentials.credentialKind = credentialTypeRow.id.toString();
}
vm.promptData.prompts.credentials.credentialTypeOptions.push({
name: credentialTypeRow.name,
value: credentialTypeRow.id
});
}
}));
vm.promptData.prompts.inventory.templateDefault = _.has(vm, 'promptData.launchConf.defaults.inventory') ? vm.promptData.launchConf.defaults.inventory : null;
vm.promptData.prompts.credentials.templateDefault = _.has(vm, 'promptData.launchConf.defaults.credentials') ? angular.copy(vm.promptData.launchConf.defaults.credentials) : [];
vm.promptData.prompts.credentials.passwordsNeededToStart = vm.promptData.launchConf.passwords_needed_to_start;
vm.promptData.prompts.credentials.passwords = {};
vm.promptData.prompts.credentials.value.forEach((credential) => {
if (credential.passwords_needed && credential.passwords_needed.length > 0) {
credential.passwords_needed.forEach(passwordNeeded => {
let credPassObj = {
id: credential.id,
name: credential.name
};
if(passwordNeeded === "ssh_password") {
vm.promptData.prompts.credentials.passwords.ssh = credPassObj;
}
if(passwordNeeded === "become_password") {
vm.promptData.prompts.credentials.passwords.become = credPassObj;
}
if(passwordNeeded === "ssh_key_unlock") {
vm.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj;
}
if(passwordNeeded.startsWith("vault_password")) {
if(passwordNeeded.includes('.')) {
credPassObj.vault_id = passwordNeeded.split(/\.(.+)/)[1];
}
if(!vm.promptData.prompts.credentials.passwords.vault) {
vm.promptData.prompts.credentials.passwords.vault = [];
}
vm.promptData.prompts.credentials.passwords.vault.push(credPassObj);
}
});
}
});
vm.promptData.prompts.variables.ignore = vm.promptData.launchConf.ignore_ask_variables;
vm.promptData.prompts.verbosity.templateDefault = vm.promptData.launchConf.defaults.verbosity;
vm.promptData.prompts.jobType.templateDefault = vm.promptData.launchConf.defaults.job_type;
vm.promptData.prompts.limit.templateDefault = vm.promptData.launchConf.defaults.limit;
vm.promptData.prompts.tags.templateDefault = vm.promptData.launchConf.defaults.job_tags;
vm.promptData.prompts.skipTags.templateDefault = vm.promptData.launchConf.defaults.skip_tags;
vm.promptData.prompts.diffMode.templateDefault = vm.promptData.launchConf.defaults.diff_mode;
if(vm.promptData.launchConf.ask_inventory_on_launch) {
vm.steps.inventory.includeStep = true;
vm.steps.inventory.tab = {
_active: true,
order: order
};
order++;
}
if(vm.promptData.launchConf.ask_credential_on_launch || (vm.promptData.launchConf.passwords_needed_to_start && vm.promptData.launchConf.passwords_needed_to_start.length > 0)) {
vm.steps.credential.includeStep = true;
vm.steps.credential.tab = {
_active: order === 1 ? true : false,
_disabled: order === 1 ? false : true,
order: order
};
order++;
}
if(vm.promptData.launchConf.ask_verbosity_on_launch || vm.promptData.launchConf.ask_job_type_on_launch || vm.promptData.launchConf.ask_limit_on_launch || vm.promptData.launchConf.ask_tags_on_launch || vm.promptData.launchConf.ask_skip_tags_on_launch || (vm.promptData.launchConf.ask_variables_on_launch && !vm.promptData.launchConf.ignore_ask_variables) || vm.promptData.launchConf.ask_diff_mode_on_launch) {
vm.steps.other_prompts.includeStep = true;
vm.steps.other_prompts.tab = {
_active: order === 1 ? true : false,
_disabled: order === 1 ? false : true,
order: order
};
order++;
}
if(vm.promptData.launchConf.survey_enabled) {
vm.steps.survey.includeStep = true;
vm.steps.survey.tab = {
_active: order === 1 ? true : false,
_disabled: order === 1 ? false : true,
order: order
};
order++;
}
vm.steps.preview.tab.order = order;
modal.show('PROMPT');
vm.promptData.triggerModalOpen = false;
})
.catch(({data, status}) => {
ProcessErrors(scope, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get credential types. GET status: ' + status
});
});
}
}, true);
};
vm.next = (currentTab) => {
Object.keys(vm.steps).forEach(step => {
if(vm.steps[step].tab) {
if(vm.steps[step].tab.order === currentTab.order) {
vm.steps[step].tab._active = false;
} else if(vm.steps[step].tab.order === currentTab.order + 1) {
vm.steps[step].tab._active = true;
vm.steps[step].tab._disabled = false;
}
}
});
};
vm.finish = () => {
vm.promptData.triggerModalOpen = false;
if(vm.onFinish) {
vm.onFinish();
}
modal.hide();
};
vm.cancel = () => {
vm.promptData.triggerModalOpen = false;
modal.hide();
};
}];

View File

@ -0,0 +1,24 @@
import promptController from './prompt.controller';
export default [ 'templateUrl',
function(templateUrl) {
return {
scope: {
promptData: '=',
onFinish: '&',
actionText: '@actionText'
},
templateUrl: templateUrl('templates/prompt/prompt'),
replace: true,
transclude: true,
restrict: 'E',
controller: promptController,
controllerAs: 'vm',
bindToController: true,
link: function(scope, el, attrs, promptController) {
scope.ns = 'launch';
scope[scope.ns] = { modal: {} };
promptController.init(scope);
}
};
}];

View File

@ -0,0 +1,36 @@
<div class="Prompt">
<at-modal>
<at-tab-group>
<at-tab ng-if="vm.steps.inventory.tab" state="vm.steps.inventory.tab">{{:: vm.strings.get('prompt.INVENTORY') }}</at-tab>
<at-tab ng-if="vm.steps.credential.tab" state="vm.steps.credential.tab">{{:: vm.strings.get('prompt.CREDENTIAL') }}</at-tab>
<at-tab ng-if="vm.steps.other_prompts.tab" state="vm.steps.other_prompts.tab">{{:: vm.strings.get('prompt.OTHER_PROMPTS') }}</at-tab>
<at-tab ng-if="vm.steps.survey.tab" state="vm.steps.survey.tab">{{:: vm.strings.get('prompt.SURVEY') }}</at-tab>
<at-tab state="vm.steps.preview.tab">{{:: vm.strings.get('prompt.PREVIEW') }}</at-tab>
</at-tab-group>
<div class="Prompt-step">
<div ng-if="vm.steps.inventory.includeStep" ng-show="vm.steps.inventory.tab._active">
<prompt-inventory prompt-data="vm.promptData"></prompt-inventory>
</div>
<div ng-if="vm.steps.credential.includeStep" ng-show="vm.steps.credential.tab._active">
<prompt-credential prompt-data="vm.promptData" credential-passwords-form="vm.forms.credentialPasswords"></prompt-credential>
</div>
<div ng-if="vm.steps.other_prompts.includeStep" ng-show="vm.steps.other_prompts.tab._active">
<prompt-other-prompts prompt-data="vm.promptData" other-prompts-form="vm.forms.otherPrompts" is-active-step="vm.steps.other_prompts.tab._active"></prompt-other-prompts>
</div>
<div ng-if="vm.steps.survey.includeStep" ng-show="vm.steps.survey.tab._active">
<prompt-survey prompt-data="vm.promptData" survey-form="vm.forms.survey"></prompt-survey>
</div>
<div ng-if="vm.steps.preview.tab._active">
<prompt-preview prompt-data="vm.promptData"></prompt-preview>
</div>
</div>
<div class="Prompt-footer">
<button class="Prompt-defaultButton" ng-click="vm.cancel()">{{:: vm.strings.get('CANCEL') }}</button>
<button class="Prompt-actionButton" ng-show="vm.steps.inventory.tab._active" ng-click="vm.next(vm.steps.inventory.tab)" ng-disabled="!vm.promptData.prompts.inventory.value.id">{{:: vm.strings.get('NEXT') }}</button>
<button class="Prompt-actionButton" ng-show="vm.steps.credential.tab._active" ng-click="vm.next(vm.steps.credential.tab)" ng-disabled="!vm.forms.credentialPasswords.$valid">{{:: vm.strings.get('NEXT') }}</button>
<button class="Prompt-actionButton" ng-show="vm.steps.other_prompts.tab._active" ng-click="vm.next(vm.steps.other_prompts.tab)" ng-disabled="!vm.forms.otherPrompts.$valid">{{:: vm.strings.get('NEXT') }}</button>
<button class="Prompt-actionButton" ng-show="vm.steps.survey.tab._active" ng-click="vm.next(vm.steps.survey.tab)" ng-disabled="!vm.forms.survey.$valid">{{:: vm.strings.get('NEXT') }}</button>
<button class="Prompt-actionButton" ng-show="vm.steps.preview.tab._active" ng-click="vm.finish()" ng-bind="vm.actionText"></button>
</div>
</at-modal>
</div>

View File

@ -0,0 +1,124 @@
function PromptService (Empty, $filter) {
this.processPromptValues = (params) => {
let prompts = {
credentials: {},
inventory: {},
variables: {},
verbosity: {},
jobType: {},
limit: {},
tags: {},
skipTags: {},
diffMode: {}
};
prompts.credentials.value = _.has(params, 'launchConf.defaults.credentials') ? params.launchConf.defaults.credentials : [];
prompts.inventory.value = _.has(params, 'currentValues.summary_fields.inventory') ? params.currentValues.summary_fields.inventory : (_.has(params, 'launchConf.defaults.inventory') ? params.launchConf.defaults.inventory : null);
let skipTags = _.has(params, 'currentValues.skip_tags') && params.currentValues.skip_tags ? params.currentValues.skip_tags : (_.has(params, 'launchConf.defaults.skip_tags') ? params.launchConf.defaults.skip_tags : "");
let jobTags = _.has(params, 'currentValues.job_tags') && params.currentValues.job_tags ? params.currentValues.job_tags : (_.has(params, 'launchConf.defaults.job_tags') ? params.launchConf.defaults.job_tags : "");
prompts.variables.value = _.has(params, 'launchConf.defaults.extra_vars') ? params.launchConf.defaults.extra_vars : "---";
prompts.verbosity.choices = _.get(params, 'launchOptions.actions.POST.verbosity.choices', []).map(c => ({label: c[1], value: c[0]}));
prompts.verbosity.value = _.has(params, 'currentValues.verbosity') && params.currentValues.verbosity ? _.find(prompts.verbosity.choices, item => item.value === params.currentValues.verbosity) : _.find(prompts.verbosity.choices, item => item.value === params.launchConf.defaults.verbosity);
prompts.jobType.choices = _.get(params, 'launchOptions.actions.POST.job_type.choices', []).map(c => ({label: c[1], value: c[0]}));
prompts.jobType.value = _.has(params, 'currentValues.job_type') && params.currentValues.job_type ? _.find(prompts.jobType.choices, item => item.value === params.currentValues.job_type) : _.find(prompts.jobType.choices, item => item.value === params.launchConf.defaults.job_type);
prompts.limit.value = _.has(params, 'currentValues.limit') && params.currentValues.limit ? params.currentValues.limit : (_.has(params, 'launchConf.defaults.limit') ? params.launchConf.defaults.limit : "");
prompts.tags.options = prompts.tags.value = (jobTags && jobTags !== "") ? jobTags.split(',').map((i) => ({name: i, label: i, value: i})) : [];
prompts.skipTags.options = prompts.skipTags.value = (skipTags && skipTags !== "") ? skipTags.split(',').map((i) => ({name: i, label: i, value: i})) : [];
prompts.diffMode.value = _.has(params, 'currentValues.diff_mode') && typeof params.currentValues.diff_mode === 'boolean' ? params.currentValues.diff_mode : (_.has(params, 'launchConf.defaults.diff_mode') ? params.launchConf.defaults.diff_mode : false);
return prompts;
};
this.processSurveyQuestions = (params) => {
let missingSurveyValue = false;
for(let i=0; i<params.surveyQuestions.length; i++){
var question = params.surveyQuestions[i];
question.index = i;
question.question_name = $filter('sanitize')(question.question_name);
question.question_description = (question.question_description) ? $filter('sanitize')(question.question_description) : undefined;
if(question.type === "textarea" && (!Empty(question.default_textarea) || (params.extra_data && params.extra_data[question.variable]))) {
if(params.extra_data && params.extra_data[question.variable]) {
question.model = params.extra_data[question.variable];
delete params.extra_data[question.variable];
} else {
question.model = angular.copy(question.default_textarea);
}
}
else if(question.type === "multiselect") {
if(params.extra_data && params.extra_data[question.variable]) {
question.model = params.extra_data[question.variable];
delete params.extra_data[question.variable];
} else {
question.model = question.default.split(/\n/);
}
question.choices = question.choices.split(/\n/);
}
else if(question.type === "multiplechoice") {
if(params.extra_data && params.extra_data[question.variable]) {
question.model = params.extra_data[question.variable];
delete params.extra_data[question.variable];
} else {
question.model = question.default ? angular.copy(question.default) : "";
}
question.choices = question.choices.split(/\n/);
// Add a default empty string option to the choices array. If this choice is
// selected then the extra var will not be sent when we POST to the launch
// endpoint
if(!question.required) {
question.choices.unshift('');
}
}
else if(question.type === "float"){
if(params.extra_data && params.extra_data[question.variable]) {
question.model = !Empty(params.extra_data[question.variable]) ? params.extra_data[question.variable] : ((!Empty(question.default)) ? angular.copy(question.default) : (!Empty(question.default_float)) ? angular.copy(question.default_float) : "");
delete params.extra_data[question.variable];
} else {
question.model = (!Empty(question.default)) ? angular.copy(question.default) : (!Empty(question.default_float)) ? angular.copy(question.default_float) : "";
}
}
else {
if(params.extra_data && params.extra_data[question.variable]) {
question.model = params.extra_data[question.variable];
delete params.extra_data[question.variable];
} else {
question.model = question.default ? angular.copy(question.default) : "";
}
}
if(question.type === "text" || question.type === "textarea" || question.type === "password") {
question.minlength = (!Empty(question.min)) ? Number(question.min) : "";
question.maxlength = (!Empty(question.max)) ? Number(question.max) : "" ;
}
else if(question.type === "integer") {
question.minValue = (!Empty(question.min)) ? Number(question.min) : "";
question.maxValue = (!Empty(question.max)) ? Number(question.max) : "" ;
}
else if(question.type === "float") {
question.minValue = (!Empty(question.min)) ? question.min : "";
question.maxValue = (!Empty(question.max)) ? question.max : "" ;
}
if(question.required && (Empty(question.model) || question.model === [])) {
missingSurveyValue = true;
}
}
return {
surveyQuestions: params.surveyQuestions,
extra_data: params.extra_data,
missingSurveyValue: missingSurveyValue
};
};
}
PromptService.$inject = ['Empty', '$filter'];
export default PromptService;

View File

@ -0,0 +1,273 @@
/*************************************************
* Copyright (c) 2017 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[ 'CredentialList', 'QuerySet', 'GetBasePath', 'CreateSelect2', 'TemplatesStrings',
function(CredentialList, qs, GetBasePath, CreateSelect2, strings) {
const vm = this;
vm.strings = strings;
let scope;
let launch;
let updateSelectedRow = () => {
if(scope.credentials && scope.credentials.length > 0) {
scope.credentials.forEach((credential, i) => {
scope.credentials[i].checked = 0;
});
scope.promptData.prompts.credentials.value.forEach((selectedCredential) => {
if(selectedCredential.credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) {
scope.credentials.forEach((credential, i) => {
if(scope.credentials[i].id === selectedCredential.id) {
scope.credentials[i].checked = 1;
}
});
}
});
}
};
let wipePasswords = (cred) => {
if(cred.passwords_needed) {
cred.passwords_needed.forEach((passwordNeeded => {
if(passwordNeeded === 'ssh_password') {
delete scope.promptData.prompts.credentials.passwords.ssh;
} else if(passwordNeeded === 'become_password') {
delete scope.promptData.prompts.credentials.passwords.become;
} else if(passwordNeeded === 'ssh_key_unlock') {
delete scope.promptData.prompts.credentials.passwords.ssh_key_unlock;
} else if(passwordNeeded.startsWith("vault_password")) {
for (let i = scope.promptData.prompts.credentials.passwords.vault.length - 1; i >= 0; i--) {
if(cred.id === scope.promptData.prompts.credentials.passwords.vault[i].id) {
scope.promptData.prompts.credentials.passwords.vault.splice(i, 1);
}
}
}
}));
} else if(cred.inputs && !_.isEmpty(cred.inputs)) {
if(cred.inputs.password && cred.inputs.password === "ASK") {
delete scope.promptData.prompts.credentials.passwords.ssh;
} else if(cred.inputs.become_password && cred.inputs.become_password === "ASK") {
delete scope.promptData.prompts.credentials.passwords.become;
} else if(cred.inputs.ssh_key_unlock && cred.inputs.ssh_key_unlock === "ASK") {
delete scope.promptData.prompts.credentials.passwords.ssh_key_unlock;
} else if(cred.inputs.vault_password && cred.inputs.vault_password === "ASK") {
for (let i = scope.promptData.prompts.credentials.passwords.vault.length - 1; i >= 0; i--) {
if(cred.id === scope.promptData.prompts.credentials.passwords.vault[i].id) {
scope.promptData.prompts.credentials.passwords.vault.splice(i, 1);
}
}
}
}
};
let updateNeededPasswords = (cred) => {
if(cred.inputs) {
let credPassObj = {
id: cred.id,
name: cred.name
};
if(cred.inputs.password && cred.inputs.password === "ASK") {
scope.promptData.prompts.credentials.passwords.ssh = credPassObj;
} else if(cred.inputs.become_password && cred.inputs.become_password === "ASK") {
scope.promptData.prompts.credentials.passwords.become = credPassObj;
} else if(cred.inputs.ssh_key_unlock && cred.inputs.ssh_key_unlock === "ASK") {
scope.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj;
} else if(cred.inputs.vault_password && cred.inputs.vault_password === "ASK") {
credPassObj.vault_id = cred.inputs.vault_id;
if(!scope.promptData.prompts.credentials.passwords.vault) {
scope.promptData.prompts.credentials.passwords.vault = [];
}
scope.promptData.prompts.credentials.passwords.vault.push(credPassObj);
}
}
};
vm.init = (_scope_, _launch_) => {
scope = _scope_;
launch = _launch_;
scope.toggle_row = (selectedRow) => {
for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) {
if(scope.promptData.prompts.credentials.value[i].credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) {
wipePasswords(scope.promptData.prompts.credentials.value[i]);
scope.promptData.prompts.credentials.value.splice(i, 1);
}
}
scope.promptData.prompts.credentials.value.push(_.cloneDeep(selectedRow));
updateNeededPasswords(selectedRow);
};
scope.toggle_credential = (cred) => {
// This is a checkbox click. At the time of writing this the only
// multi-select credentials on launch are vault credentials so this
// logic should only get executed when a vault credential checkbox
// is clicked.
let uncheck = false;
let removeCredential = (credentialToRemove, index) => {
wipePasswords(credentialToRemove);
scope.promptData.prompts.credentials.value.splice(index, 1);
};
// Only one vault credential per vault_id is allowed so we need to check
// to see if one has already been selected and if so replace it.
for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) {
if(cred.credential_type === scope.promptData.prompts.credentials.value[i].credential_type) {
if(scope.promptData.prompts.credentials.value[i].id === cred.id) {
removeCredential(scope.promptData.prompts.credentials.value[i], i);
i = -1;
uncheck = true;
}
else if(scope.promptData.prompts.credentials.value[i].inputs) {
if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].inputs.vault_id) {
removeCredential(scope.promptData.prompts.credentials.value[i], i);
}
} else if(scope.promptData.prompts.credentials.value[i].vault_id) {
if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].vault_id) {
removeCredential(scope.promptData.prompts.credentials.value[i], i);
}
} else {
// The currently selected vault credential does not have a vault_id
if(!cred.inputs.vault_id || cred.inputs.vault_id === "") {
removeCredential(scope.promptData.prompts.credentials.value[i], i);
}
}
}
}
if(!uncheck) {
scope.promptData.prompts.credentials.value.push(cred);
updateNeededPasswords(cred);
}
};
scope.credential_dataset = [];
scope.credentials = [];
let credList = _.cloneDeep(CredentialList);
credList.emptyListText = strings.get('prompt.NO_CREDS_MATCHING_TYPE');
scope.list = credList;
scope.generateCredentialList(scope.promptData.prompts.credentials.credentialKind);
scope.credential_default_params = {
order_by: 'name',
page_size: 5
};
scope.credential_queryset = {
order_by: 'name',
page_size: 5
};
scope.$watch('promptData.prompts.credentials.credentialKind', (oldKind, newKind) => {
if (scope.promptData.prompts.credentials && scope.promptData.prompts.credentials.credentialKind) {
if(scope.promptData.prompts.credentials.credentialTypes[oldKind] === "vault" || scope.promptData.prompts.credentials.credentialTypes[newKind] === "vault") {
scope.generateCredentialList(scope.promptData.prompts.credentials.credentialKind);
}
scope.credential_queryset.page = 1;
scope.credential_default_params.credential_type = scope.credential_queryset.credential_type = parseInt(scope.promptData.prompts.credentials.credentialKind);
qs.search(GetBasePath('credentials'), scope.credential_default_params)
.then(res => {
scope.credential_dataset = res.data;
scope.credentials = scope.credential_dataset.results;
});
}
});
scope.$watchCollection('promptData.prompts.credentials.value', () => {
updateSelectedRow();
});
scope.$watchCollection('credentials', () => {
updateSelectedRow();
});
CreateSelect2({
element: '#launch-kind-select',
multiple: false
});
};
vm.deleteSelectedCredential = (credentialToDelete) => {
for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) {
if(scope.promptData.prompts.credentials.value[i].id === credentialToDelete.id) {
wipePasswords(credentialToDelete);
scope.promptData.prompts.credentials.value.splice(i, 1);
}
}
scope.credentials.forEach((credential, i) => {
if(credential.id === credentialToDelete.id) {
scope.credentials[i].checked = 0;
}
});
};
vm.revert = () => {
scope.promptData.prompts.credentials.value = scope.promptData.prompts.credentials.templateDefault;
scope.promptData.prompts.credentials.passwords = {
vault: []
};
scope.promptData.prompts.credentials.value.forEach((credential) => {
if (credential.passwords_needed && credential.passwords_needed.length > 0) {
credential.passwords_needed.forEach(passwordNeeded => {
let credPassObj = {
id: credential.id,
name: credential.name
};
if(passwordNeeded === "ssh_password") {
scope.promptData.prompts.credentials.passwords.ssh = credPassObj;
}
if(passwordNeeded === "become_password") {
scope.promptData.prompts.credentials.passwords.become = credPassObj;
}
if(passwordNeeded === "ssh_key_unlock") {
scope.promptData.prompts.credentials.passwords.ssh_key_unlock = credPassObj;
}
if(passwordNeeded.startsWith("vault_password")) {
credPassObj.vault_id = credential.vault_id;
scope.promptData.prompts.credentials.passwords.vault.push(credPassObj);
}
});
}
});
};
vm.showRevertCredentials = () => {
if(scope.promptData.launchConf.ask_credential_on_launch) {
if(scope.promptData.prompts.credentials.value && scope.promptData.prompts.credentials.templateDefault && (scope.promptData.prompts.credentials.value.length === scope.promptData.prompts.credentials.templateDefault.length)) {
let selectedIds = scope.promptData.prompts.credentials.value.map((x) => { return x.id; }).sort();
let defaultIds = scope.promptData.prompts.credentials.templateDefault.map((x) => { return x.id; }).sort();
return !selectedIds.every((e, i) => { return defaultIds.indexOf(e) === i; });
} else {
return true;
}
} else {
return false;
}
};
vm.togglePassword = (id) => {
var buttonId = id + "_show_input_button",
inputId = id;
if ($(inputId).attr("type") === "password") {
$(buttonId).html(strings.get('HIDE'));
$(inputId).attr("type", "text");
} else {
$(buttonId).html(strings.get('SHOW'));
$(inputId).attr("type", "password");
}
};
}
];

View File

@ -0,0 +1,56 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import promptCredentialController from './prompt-credential.controller';
export default [ 'templateUrl', '$compile', 'generateList',
(templateUrl, $compile, GenerateList) => {
return {
scope: {
promptData: '=',
credentialPasswordsForm: '='
},
templateUrl: templateUrl('templates/prompt/steps/credential/prompt-credential'),
controller: promptCredentialController,
controllerAs: 'vm',
require: ['^^prompt', 'promptCredential'],
restrict: 'E',
replace: true,
transclude: true,
link: (scope, el, attrs, controllers) => {
const launchController = controllers[0];
const promptCredentialController = controllers[1];
scope.generateCredentialList = (credKind) => {
let inputType = (credKind && scope.promptData.prompts.credentials.credentialTypes[credKind] === "vault") ? null : 'radio';
let list = _.cloneDeep(scope.list);
if(credKind && scope.promptData.prompts.credentials.credentialTypes[credKind] === "vault") {
list.fields.name.modalColumnClass = 'col-md-6';
list.fields.info = {
label: 'Vault ID',
ngBind: 'credential.inputs.vault_id',
key: false,
nosort: true,
modalColumnClass: 'col-md-6',
infoHeaderClass: '',
dataPlacement: 'top',
};
}
let html = GenerateList.build({
list: list,
input_type: inputType,
mode: 'lookup'
});
$('#prompt-credential').empty().append($compile(html)(scope));
};
promptCredentialController.init(scope, launchController);
}
};
}];

View File

@ -0,0 +1,113 @@
<div>
<div class="Prompt-selectedItem">
<div class="Prompt-selectedItemInfo" ng-hide="promptData.prompts.credentials.templateDefault.length === 0 && promptData.prompts.credentials.value.length === 0">
<div class="Prompt-selectedItemLabel">
<span>{{:: vm.strings.get('prompt.SELECTED') }}</span>
</div>
<div class="Prompt-previewTags--outer">
<div ng-show="promptData.prompts.credentials.templateDefault.length > 0 && promptData.prompts.credentials.value.length === 0" class="Prompt-noSelectedItem">{{:: vm.strings.get('prompt.NO_CREDENTIALS_SELECTED') }}</div>
<div class="Prompt-previewTags--inner">
<div class="MultiCredential-tagContainer"
ng-class="{'MultiCredential-tagContainer--disabled': !promptData.launchConf.ask_credential_on_launch}"
ng-repeat="credential in promptData.prompts.credentials.value">
<div class="MultiCredential-iconContainer" ng-switch="promptData.prompts.credentials.credentialTypes[credential.credential_type]">
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
<i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
<i class="fa fa-sitemap MultiCredential-tagIcon" ng-switch-when="net"></i>
<i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
<i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
<i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
</div>
<div class="MultiCredential-tag" ng-class="promptData.launchConf.ask_credential_on_launch ? 'MultiCredential-tag--deletable' : 'MultiCredential-tag--disabled'">
<span ng-if="!credential.inputs.vault_id && !credential.vault_id" class="MultiCredential-name--label">
{{ credential.name }}
</span>
<span ng-if="credential.inputs.vault_id || credential.vault_id" class="MultiCredential-name--label">
{{ credential.name }} | {{ credential.vault_id ? credential.vault_id : credential.inputs.vault_id }}
</span>
</div>
<div class="MultiCredential-deleteContainer"
ng-click="vm.deleteSelectedCredential(credential)"
ng-hide="!promptData.launchConf.ask_credential_on_launch">
<i class="fa fa-times MultiCredential-tagDelete"></i>
</div>
</div>
</div>
</div>
<div class="Prompt-previewTagRevert">
<a class="Prompt-revertLink" href="" ng-show="vm.showRevertCredentials()" ng-click="vm.revert()">{{:: vm.strings.get('prompt.REVERT') }}</a>
</div>
</div>
</div>
<span ng-show="promptData.launchConf.ask_credential_on_launch">
<div class="Prompt-credentialSubSection">
<span class="Prompt-label">{{:: vm.strings.get('prompt.CREDENTIAL_TYPE') }}:</span>
<select id="launch-kind-select" ng-model="promptData.prompts.credentials.credentialKind">
<option ng-repeat="option in promptData.prompts.credentials.credentialTypeOptions" value="{{option.value}}">{{option.name}}</option>
</select>
</div>
<div id="prompt-credential"></div>
</span>
<div ng-show="promptData.prompts.credentials.passwords.ssh || promptData.prompts.credentials.passwords.become || promptData.prompts.credentials.passwords.ssh_key_unlock || (promptData.prompts.credentials.passwords.vault && promptData.prompts.credentials.passwords.vault.length > 0)">
<div class="Prompt-instructions">{{:: vm.strings.get('prompt.PASSWORDS_REQUIRED_HELP') }}</div>
<form name="credentialPasswordsForm" autocomplete="off" novalidate>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.prompts.credentials.passwords.ssh">
<label for="ssh_password" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.credential_passwords.SSH_PASSWORD') }}</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-default show_input_button Prompt-passwordButton" id="prompt-ssh-password_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="vm.togglePassword('#prompt-ssh-password')" data-container="job-launch-modal" data-original-title="" title="">{{:: vm.strings.get('SHOW') }}</button>
</span>
<input id="prompt-ssh-password" type="password" ng-model="promptData.prompts.credentials.passwords.ssh.value" name="ssh_password" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="credentialPasswordsForm.ssh_password.$dirty && credentialPasswordsForm.ssh_password.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}</div>
<div class="error api-error" ng-bind="ssh_password_api_error"></div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.prompts.credentials.passwords.ssh_key_unlock">
<label for="ssh_key_unlock" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.credential_passwords.PRIVATE_KEY_PASSPHRASE') }}</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-default show_input_button Prompt-passwordButton" id="prompt-ssh-key-unlock_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="vm.togglePassword('#prompt-ssh-key-unlock')" data-container="job-launch-modal" data-original-title="" title="">{{:: vm.strings.get('SHOW') }}</button>
</span>
<input id="prompt-ssh-key-unlock" type="password" ng-model="promptData.prompts.credentials.passwords.ssh_key_unlock.value" name="ssh_key_unlock" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="credentialPasswordsForm.ssh_key_unlock.$dirty && credentialPasswordsForm.ssh_key_unlock.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}</div>
<div class="error api-error" ng-bind="ssh_key_unlock_api_error"></div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.prompts.credentials.passwords.become">
<label for="become_password" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.credential_passwords.PRIVILEGE_ESCALATION_PASSWORD') }}</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-default show_input_button Prompt-passwordButton" id="prompt-become-password_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="vm.togglePassword('#prompt-become-password')" data-container="job-launch-modal" data-original-title="" title="">{{:: vm.strings.get('SHOW') }}</button>
</span>
<input id="prompt-become-password" type="password" ng-model="promptData.prompts.credentials.passwords.become.value" name="become_password" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="credentialPasswordsForm.become_password.$dirty && credentialPasswordsForm.become_password.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}</div>
<div class="error api-error" ng-bind="become_password_api_error"></div>
</div>
<span ng-if="promptData.prompts.credentials.passwords.vault && promptData.prompts.credentials.passwords.vault.length > 0">
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-repeat="vault_credential in promptData.prompts.credentials.passwords.vault track by $index">
<label for="vault_password" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.credential_passwords.VAULT_PASSWORD') }} - {{vault_credential.name}}</span>
</label>
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-default show_input_button Prompt-passwordButton" id="prompt-vault-password_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="vm.togglePassword('#prompt-vault-password')" data-container="job-launch-modal" data-original-title="" title="">{{:: vm.strings.get('SHOW') }}</button>
</span>
<input id="prompt-vault-password" type="password" ng-model="promptData.prompts.credentials.passwords.vault[$index].value" name="vault_password" class="password-field form-control input-sm Form-textInput" required>
</div>
<div class="error" ng-show="credentialPasswordsForm.vault_password.$dirty && credentialPasswordsForm.vault_password.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_PASSWORD') }}</div>
<div class="error api-error" ng-bind="vault_password_api_error"></div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,37 @@
/*************************************************
* Copyright (c) 2017 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[ 'TemplatesStrings', function(strings) {
const vm = this;
vm.strings = strings;
let scope;
let launch;
vm.init = (_scope_, _launch_) => {
scope = _scope_;
launch = _launch_;
scope.toggle_row = (row) => {
scope.promptData.prompts.inventory.value = row;
};
};
vm.deleteSelectedInventory = () => {
scope.promptData.prompts.inventory.value = null;
scope.inventories.forEach((inventory) => {
inventory.checked = 0;
});
};
vm.revert = () => {
scope.promptData.prompts.inventory.value = scope.promptData.prompts.inventory.templateDefault;
};
}
];

View File

@ -0,0 +1,72 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import promptInventoryController from './prompt-inventory.controller';
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList',
(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) => {
return {
scope: {
promptData: '='
},
templateUrl: templateUrl('templates/prompt/steps/inventory/prompt-inventory'),
controller: promptInventoryController,
controllerAs: 'vm',
require: ['^^prompt', 'promptInventory'],
restrict: 'E',
replace: true,
transclude: true,
link: (scope, el, attrs, controllers) => {
const launchController = controllers[0];
const promptInventoryController = controllers[1];
promptInventoryController.init(scope, launchController);
scope.inventory_default_params = {
order_by: 'name',
page_size: 5
};
scope.inventory_queryset = {
order_by: 'name',
page_size: 5
};
// Fire off the initial search
qs.search(GetBasePath('inventory'), scope.inventory_default_params)
.then(res => {
scope.inventory_dataset = res.data;
scope.inventories = scope.inventory_dataset.results;
let invList = _.cloneDeep(InventoryList);
let html = GenerateList.build({
list: invList,
input_type: 'radio',
mode: 'lookup'
});
scope.list = invList;
$('#prompt-inventory').append($compile(html)(scope));
scope.$watch('promptData.prompts.inventory.value', () => {
if(scope.promptData.prompts.inventory.value && scope.promptData.prompts.inventory.value.id) {
// Loop across the inventories and see if one of them should be "checked"
scope.inventories.forEach((row, i) => {
if (row.id === scope.promptData.prompts.inventory.value.id) {
scope.inventories[i].checked = 1;
}
else {
scope.inventories[i].checked = 0;
}
});
}
});
});
}
};
}];

View File

@ -0,0 +1,26 @@
<div>
<div class="Prompt-selectedItem">
<div class="Prompt-selectedItemInfo" ng-hide="!promptData.prompts.inventory.value.id && !promptData.prompts.inventory.templateDefault.id">
<div class="Prompt-selectedItemLabel">
<span>{{:: vm.strings.get('prompt.SELECTED') }}</span>
</div>
<div class="Prompt-previewTags--outer">
<div ng-show="promptData.prompts.inventory.templateDefault.id && !promptData.prompts.inventory.value.id" class="Prompt-noSelectedItem">{{:: vm.strings.get('prompt.NO_INVENTORY_SELECTED') }}</div>
<div class="Prompt-previewTags--inner" ng-hide="!promptData.prompts.inventory.value.id">
<div class="Prompt-previewTagContainer">
<div class="Prompt-previewTag Prompt-previewTag--deletable">
<span>{{promptData.prompts.inventory.value.name}}</span>
</div>
<div class="Prompt-previewTagContainerDelete" ng-click="vm.deleteSelectedInventory()">
<i class="fa fa-times Prompt-previewTagContainerTagDelete"></i>
</div>
</div>
</div>
</div>
<div class="Prompt-previewTagRevert">
<a class="Prompt-revertLink" href="" ng-hide="promptData.prompts.inventory.value.id === promptData.prompts.inventory.templateDefault.id" ng-click="vm.revert()">{{:: vm.strings.get('prompt.REVERT') }}</a>
</div>
</div>
</div>
<div id="prompt-inventory"></div>
</div>

View File

@ -0,0 +1,92 @@
/*************************************************
* Copyright (c) 2017 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['ParseTypeChange', 'CreateSelect2', 'TemplatesStrings', function(ParseTypeChange, CreateSelect2, strings) {
const vm = this;
vm.strings = strings;
let scope;
let launch;
vm.init = (_scope_, _launch_) => {
scope = _scope_;
launch = _launch_;
scope.parseType = 'yaml';
// Can't pass otherPrompts.variables.value into ParseTypeChange
// due to the fact that Angular CodeMirror uses scope[string]
// notation.
scope.extraVariables = scope.promptData.prompts.variables.value;
scope.$watch('extraVariables', () => {
scope.promptData.prompts.variables.value = scope.extraVariables;
});
let codemirrorExtraVars = () => {
if(scope.promptData.launchConf.ask_variables_on_launch && !scope.promptData.prompts.variables.ignore) {
ParseTypeChange({
scope: scope,
variable: 'extraVariables',
field_id: 'job_launch_variables'
});
}
};
if(scope.promptData.launchConf.ask_job_type_on_launch) {
CreateSelect2({
element: '#job_launch_job_type',
multiple: false
});
}
if(scope.promptData.launchConf.ask_verbosity_on_launch) {
CreateSelect2({
element: '#job_launch_verbosity',
multiple: false
});
}
if(scope.promptData.launchConf.ask_tags_on_launch) {
CreateSelect2({
element: '#job_launch_job_tags',
multiple: true,
addNew: true
});
}
if(scope.promptData.launchConf.ask_skip_tags_on_launch) {
CreateSelect2({
element: '#job_launch_skip_tags',
multiple: true,
addNew: true
});
}
if(scope.isActiveStep) {
codemirrorExtraVars();
}
scope.$watch('isActiveStep', () => {
if(scope.isActiveStep) {
codemirrorExtraVars();
}
});
};
vm.toggleDiff = () => {
scope.promptData.prompts.diffMode.value = !scope.promptData.prompts.diffMode.value;
};
vm.updateParseType = (parseType) => {
scope.parseType = parseType;
// This function gets added to scope by the ParseTypeChange factory
scope.parseTypeChange('parseType', 'extraVariables');
};
}
];

View File

@ -0,0 +1,32 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import promptOtherPrompts from './prompt-other-prompts.controller';
export default [ 'templateUrl',
(templateUrl) => {
return {
scope: {
promptData: '=',
otherPromptsForm: '=',
isActiveStep: '='
},
templateUrl: templateUrl('templates/prompt/steps/other-prompts/prompt-other-prompts'),
controller: promptOtherPrompts,
controllerAs: 'vm',
require: ['^^prompt', 'promptOtherPrompts'],
restrict: 'E',
replace: true,
transclude: true,
link: (scope, el, attrs, controllers) => {
const launchController = controllers[0];
const promptOtherPromptsController = controllers[1];
promptOtherPromptsController.init(scope, launchController);
}
};
}];

View File

@ -0,0 +1,110 @@
<form name="otherPromptsForm" autocomplete="off" novalidate>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_job_type_on_launch">
<label for="job_type" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel" translate>{{:: vm.strings.get('prompt.JOB_TYPE') }}</span>
</label>
<div>
<select
id="job_launch_job_type"
ng-options="v.label for v in promptData.prompts.jobType.choices track by v.value"
ng-model="promptData.prompts.jobType.value"
class="form-control Form-dropDown"
name="job_type"
tabindex="-1"
aria-hidden="true"
required>
<option value="" class="" selected="selected">{{:: vm.strings.get('prompt.CHOOSE_JOB_TYPE') }}</option>
</select>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_limit_on_launch">
<label for="limit">
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.LIMIT') }}</span>
</label>
<div>
<input type="text" ng-model="promptData.prompts.limit.value" name="limit" class="form-control Form-textInput">
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_verbosity_on_launch">
<label for="verbosity" class="Form-inputLabelContainer">
<span class="Form-requiredAsterisk">*</span>
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.VERBOSITY') }}</span>
</label>
<div>
<select
id="job_launch_verbosity"
ng-options="v.label for v in promptData.prompts.verbosity.choices track by v.value"
ng-model="promptData.prompts.verbosity.value"
class="form-control Form-dropDown"
name="verbosity"
tabindex="-1"
aria-hidden="true"
required>
<option value="" class="" selected="selected">{{:: vm.strings.get('prompt.CHOOSE_VERBOSITY') }}</option>
</select>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_tags_on_launch">
<label for="tags">
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.JOB_TAGS') }}</span>
</label>
<div>
<select
id="job_launch_job_tags"
ng-options="v.label for v in promptData.prompts.tags.options track by v.value"
ng-model="promptData.prompts.tags.value"
class="form-control Form-dropDown"
name="job_tags"
tabindex="-1"
aria-hidden="true"
multiple>
</select>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_skip_tags_on_launch">
<label for="skip_tags">
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.SKIP_TAGS') }}</span>
</label>
<div>
<select
id="job_launch_skip_tags"
ng-options="v.label for v in promptData.prompts.skipTags.options track by v.value"
ng-model="promptData.prompts.skipTags.value"
class="form-control Form-dropDown"
name="skip_tags"
tabindex="-1"
aria-hidden="true"
multiple>
</select>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_diff_mode_on_launch">
<label for="diff_mode">
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.SHOW_CHANGES') }}</span>
</label>
<div>
<div class="ScheduleToggle" ng-class="{'is-on': promptData.prompts.diffMode.value}" aw-tool-tip="" data-placement="top" data-original-title="" title="" ng-click="vm.toggleDiff()">
<button ng-show="promptData.prompts.diffMode.value" class="ScheduleToggle-switch is-on">{{:: vm.strings.get('ON') }}</button>
<button ng-show="!promptData.prompts.diffMode.value" class="ScheduleToggle-switch ng-hide">{{:: vm.strings.get('OFF') }}</button>
</div>
</div>
</div>
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-if="promptData.launchConf.ask_variables_on_launch && !promptData.prompts.variables.ignore">
<label for="variables">
<span class="Form-inputLabel">{{:: vm.strings.get('prompt.EXTRA_VARIABLES') }}</span>
<a id="awp-variables" href="" aw-pop-over="<p>Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON.</p>JSON:<br /><blockquote>{<br />&quot;somevar&quot;: &quot;somevalue&quot;,<br />&quot;password&quot;: &quot;magic&quot;<br /> }</blockquote>YAML:<br /><blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>" data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
<i class="fa fa-question-circle"></i>
</a>
<div class="parse-selection" id="job_launch_variables_parse_type">
<input type="radio" ng-model="parseType" value="yaml" ng-change="vm.updateParseType(parseType)">
<span class="parse-label">{{:: vm.strings.get('YAML') }}</span>
<input type="radio" ng-model="parseType" value="json" ng-change="vm.updateParseType(parseType)">
<span class="parse-label">{{:: vm.strings.get('JSON') }}</span>
</div>
</label>
<div>
<textarea rows="6" ng-model="promptData.prompts.variables.value" name="variables" class="form-control Form-textArea Form-textAreaLabel" id="job_launch_variables"></textarea>
</div>
</div>
</form>

View File

@ -0,0 +1,85 @@
/*************************************************
* Copyright (c) 2017 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[ 'ParseTypeChange', 'ToJSON', 'TemplatesStrings', function(ParseTypeChange, ToJSON, strings) {
const vm = this;
vm.strings = strings;
let scope;
let launch;
let consolidateTags = (tagModel, tagId) => {
let tags = angular.copy(tagModel);
$(tagId).siblings(".select2").first().find(".select2-selection__choice").each((optionIndex, option) => {
tags.push({
value: option.title,
name: option.title,
label: option.title
});
});
return [...tags.reduce((map, tag) => map.has(tag.value) ? map : map.set(tag.value, tag), new Map()).values()];
};
vm.init = (_scope_, _launch_) => {
scope = _scope_;
launch = _launch_;
vm.showJobTags = true;
vm.showSkipTags = true;
scope.parseType = 'yaml';
scope.promptData.extraVars = ToJSON(scope.parseType, scope.promptData.prompts.variables.value, false);
if(scope.promptData.launchConf.ask_tags_on_launch) {
scope.promptData.prompts.tags.value = consolidateTags(scope.promptData.prompts.tags.value, "#job_launch_job_tags");
}
if(scope.promptData.launchConf.ask_skip_tags_on_launch) {
scope.promptData.prompts.skipTags.value = consolidateTags(scope.promptData.prompts.skipTags.value, "#job_launch_skip_tags");
}
if(scope.promptData.launchConf.survey_enabled){
scope.promptData.surveyQuestions.forEach(surveyQuestion => {
// grab all survey questions that have answers
if(surveyQuestion.required || (surveyQuestion.required === false && surveyQuestion.model.toString()!=="")) {
if(!scope.promptData.extraVars) {
scope.promptData.extraVars = {};
}
scope.promptData.extraVars[surveyQuestion.variable] = surveyQuestion.model;
}
if(surveyQuestion.required === false && _.isEmpty(surveyQuestion.model)) {
switch (surveyQuestion.type) {
// for optional text and text-areas, submit a blank string if min length is 0
// -- this is confusing, for an explanation see:
// http://docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html#optional-survey-questions
//
case "text":
case "textarea":
if (surveyQuestion.min === 0) {
scope.promptData.extraVars[surveyQuestion.variable] = "";
}
break;
}
}
});
}
scope.promptExtraVars = $.isEmptyObject(scope.promptData.extraVars) ? '---' : jsyaml.safeDump(scope.promptData.extraVars);
ParseTypeChange({
scope: scope,
variable: 'promptExtraVars',
field_id: 'job_launch_preview_variables',
readOnly: true
});
};
}
];

View File

@ -0,0 +1,30 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import promptPreview from './prompt-preview.controller';
export default [ 'templateUrl',
(templateUrl) => {
return {
scope: {
promptData: '='
},
templateUrl: templateUrl('templates/prompt/steps/preview/prompt-preview'),
controller: promptPreview,
controllerAs: 'vm',
require: ['^^prompt', 'promptPreview'],
restrict: 'E',
replace: true,
transclude: true,
link: (scope, el, attrs, controllers) => {
const launchController = controllers[0];
const promptPreviewController = controllers[1];
promptPreviewController.init(scope, launchController);
}
};
}];

View File

@ -0,0 +1,84 @@
<div>
<div class="Prompt-previewRow--flex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.JOB_TYPE') }}</div>
<div class="Prompt-previewRowValue">
<span ng-if="promptData.prompts.jobType.value.value === 'run'">{{:: vm.strings.get('prompt.PLAYBOOK_RUN') }}</span>
<span ng-if="promptData.prompts.jobType.value.value === 'check'">{{:: vm.strings.get('prompt.CHECK') }}</span>
</div>
</div>
<div class="Prompt-previewRow--flex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.CREDENTIAL') }}</div>
<div class="Prompt-previewRowValue" style="display: flex">
<div class="Prompt-previewTagContainer u-wordwrap" ng-repeat="credential in promptData.prompts.credentials.value">
<div class="Prompt-previewTag">
<span ng-switch="promptData.prompts.credentials.credentialTypes[credential.credential_type]">
<span class="fa fa-cloud" ng-switch-when="cloud"></span>
<span class="fa fa-info" ng-switch-when="insights"></span>
<span class="fa fa-sitemap" ng-switch-when="net"></span>
<span class="fa fa-code-fork" ng-switch-when="scm"></span>
<span class="fa fa-key" ng-switch-when="ssh"></span>
<span class="fa fa-archive" ng-switch-when="vault"></span>
</span>
<span ng-bind="credential.name"></span>
</div>
</div>
</div>
</div>
<div class="Prompt-previewRow--flex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.INVENTORY') }}</div>
<div class="Prompt-previewRowValue" ng-bind="promptData.prompts.inventory.value.name"></div>
</div>
<div class="Prompt-previewRow--flex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.LIMIT') }}</div>
<div class="Prompt-previewRowValue" ng-bind="promptData.prompts.limit.value"></div>
</div>
<div class="Prompt-previewRow--flex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.VERBOSITY') }}</div>
<div class="Prompt-previewRowValue" ng-bind="promptData.prompts.verbosity.value.label"></div>
</div>
<div class="Prompt-previewRow--noflex">
<div class="Prompt-previewRowTitle">
<span>{{:: vm.strings.get('prompt.JOB_TAGS') }}&nbsp;</span>
<span ng-click="vm.showJobTags = !vm.showJobTags">
<span class="fa fa-caret-down" ng-show="vm.showJobTags" ></span>
<span class="fa fa-caret-left" ng-show="!vm.showJobTags"></span>
</span>
</div>
<div ng-show="vm.showJobTags" style="display: flex">
<div class="Prompt-previewTagContainer u-wordwrap" ng-repeat="tag in promptData.prompts.tags.value">
<div class="LabelList-tag">
<span>{{tag.name}}</span>
</div>
</div>
</div>
</div>
<div class="Prompt-previewRow--noflex">
<div class="Prompt-previewRowTitle">
<span>{{:: vm.strings.get('prompt.SKIP_TAGS') }}&nbsp;</span>
<span ng-click="vm.showSkipTags = !vm.showSkipTags">
<span class="fa fa-caret-down" ng-show="vm.showSkipTags" ></span>
<span class="fa fa-caret-left" ng-show="!vm.showSkipTags"></span>
</span>
</div>
<div ng-show="vm.showSkipTags" style="display: flex">
<div class="Prompt-previewTagContainer u-wordwrap" ng-repeat="tag in promptData.prompts.skipTags.value">
<div class="LabelList-tag">
<span>{{tag.name}}</span>
</div>
</div>
</div>
</div>
<div class="Prompt-previewRow--flex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.SHOW_CHANGES') }}</div>
<div class="Prompt-previewRowValue">
<span ng-if="promptData.prompts.diffMode.value">{{:: vm.strings.get('ON') }}</span>
<span ng-if="!promptData.prompts.diffMode.value">{{:: vm.strings.get('OFF') }}</span>
</div>
</div>
<div class="Prompt-previewRow--noflex">
<div class="Prompt-previewRowTitle">{{:: vm.strings.get('prompt.EXTRA_VARIABLES') }}</div>
<div>
<textarea rows="6" ng-model="promptExtraVars" name="preview_variables" class="form-control Form-textArea Form-textAreaLabel" id="job_launch_preview_variables"></textarea>
</div>
</div>
</div>

View File

@ -0,0 +1,35 @@
/*************************************************
* Copyright (c) 2017 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[ 'TemplatesStrings', function(strings) {
const vm = this;
vm.strings = strings;
let scope;
let launch;
vm.init = (_scope_, _launch_) => {
scope = _scope_;
launch = _launch_;
};
// This function is used to hide/show the contents of a password
// within a form
vm.togglePassword = (id) => {
var buttonId = id + "_show_input_button",
inputId = id;
if ($(inputId).attr("type") === "password") {
$(buttonId).html(strings.get('HIDE'));
$(inputId).attr("type", "text");
} else {
$(buttonId).html(strings.get('SHOW'));
$(inputId).attr("type", "password");
}
};
}
];

View File

@ -0,0 +1,31 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import promptSurvey from './prompt-survey.controller';
export default [ 'templateUrl',
(templateUrl) => {
return {
scope: {
promptData: '=',
surveyForm: '='
},
templateUrl: templateUrl('templates/prompt/steps/survey/prompt-survey'),
controller: promptSurvey,
controllerAs: 'vm',
require: ['^^prompt', 'promptSurvey'],
restrict: 'E',
replace: true,
transclude: true,
link: (scope, el, attrs, controllers) => {
const launchController = controllers[0];
const promptSurveyController = controllers[1];
promptSurveyController.init(scope, launchController);
}
};
}];

View File

@ -0,0 +1,65 @@
<form name="surveyForm" autocomplete="off" novalidate>
<div ng-repeat="question in promptData.surveyQuestions track by $index" id="taker_{{$index}}" class="form-group Form-formGroup Form-formGroup--singleColumn">
<label ng-attr-for="{{question.variable}}" class="Form-inputLabelContainer">
<span ng-show="question.required===true" class="Form-requiredAsterisk">*</span>
<span class="label-text Form-inputLabel"> {{question.question_name}}</span>
</label>
<div class="survey_taker_description" ng-if="question.question_description">
<i ng-bind-html="question.question_description"></i>
</div>
<div ng-if="question.type === 'text'">
<input type="text" id="survey_question_{{$index}}" ng-model="question.model" name="survey_question_{{$index}}" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control Form-textInput" ng-required="question.required">
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength"><span translate>Please enter an answer between</span> {{question.minlength}} <span translate>to</span> {{question.maxlength}} <span translate>characters long.</span></div>
</div>
<div ng-if="question.type === 'textarea'">
<textarea id="survey_question_{{$index}}" name="survey_question_{{$index}}" ng-model="question.model" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control final Form-textArea" ng-required="question.required" rows="3"></textarea>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength"><span translate>Please enter an answer between</span> {{question.minlength}} <span translate>to</span> {{question.maxlength}} <span translate>characters long.</span></div>
</div>
<div ng-if="question.type === 'password'">
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-default show_input_button Prompt-passwordButton" id="survey_question_{{$index}}_show_input_button" aw-tool-tip="Toggle the display of plaintext." aw-tip-placement="top" ng-click="vm.togglePassword('#survey_question_' + $index)" data-original-title="" data-container="job-launch-modal" title="" translate>Show</button>
</span>
<input id="survey_question_{{$index}}" ng-if="!question.default" type="password" ng-model="question.model" name="survey_question_{{$index}}" ng-required="question.required" ng-minlength="question.minlength" ng-maxlength="question.maxlength" class="form-control Form-textInput" autocomplete="false">
<input id="survey_question_{{$index}}" ng-if="question.default" type="password" ng-model="question.model" name="survey_question_{{$index}}" ng-required="question.required" aw-password-min="question.minlength" aw-password-max="question.maxlength" class="form-control Form-textInput" autocomplete="false">
</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.awPasswordMin || forms.survey.survey_question_{{$index}}.$error.awPasswordMax || forms.survey.survey_question_{{$index}}.$error.minlength || forms.survey.survey_question_{{$index}}.$error.maxlength"><span translate>Please enter an answer between</span> {{question.minlength}} <span translate>to</span> {{question.maxlength}} <span translate>characters long.</span></div>
</div>
<div ng-if="question.type === 'integer'">
<input type="number" id="survey_question_{{$index}}" ng-model="question.model" class="form-control Form-textInput" name="survey_question_{{$index}}" ng-required="question.required" integer aw-min="question.minValue" aw-max="question.maxValue"/>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.number || forms.survey.survey_question_{{$index}}.$error.integer">{{:: vm.strings.get('prompt.VALID_INTEGER') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.awMin || forms.survey.survey_question_{{$index}}.$error.awMax"><span translate>Please enter an answer between</span> {{question.minValue}} <span>and</span> {{question.maxValue}}.</div>
</div>
<div ng-if="question.type === 'float'">
<input type="number" id="survey_question_{{$index}}" ng-model="question.model" class="form-control Form-textInput" name="survey_question_{{$index}}" ng-required="question.required" smart-float aw-min="question.minValue" aw-max="question.maxValue"/>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$dirty && forms.survey.survey_question_{{$index}}.$error.required">{{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.number || forms.survey.survey_question_{{$index}}.$error.float">{{:: vm.strings.get('prompt.VALID_DECIMAL') }}</div>
<div class="error survey_error" ng-show="forms.survey.survey_question_{{$index}}.$error.awMin || forms.survey.survey_question_{{$index}}.$error.awMax"><span translate>Please enter an answer between</span> {{question.minValue}} <span translate>and</span> {{question.maxValue}}.</div>
</div>
<div ng-if="question.type === 'multiplechoice'">
<div class="survey_taker_input">
<multiple-choice
multi-select="false"
question="question"
choices="question.choices"
ng-required="question.required"
ng-model="question.model">
</multiple-choice>
</div>
</div>
<div ng-if="question.type === 'multiselect'">
<multiple-choice
multi-select="true"
question="question"
choices="question.choices"
ng-required="question.required"
ng-model="question.model">
</multiple-choice>
</div>
</div>
</form>