Merge pull request #1201 from jakemcdermott/item_copy_ui

api-backed copy ui
This commit is contained in:
Jake McDermott 2018-02-13 17:42:00 -05:00 committed by GitHub
commit c1b6595a0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1361 additions and 584 deletions

View File

@ -1,200 +1,406 @@
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(),
workflowJobTemplate = new WorkflowJobTemplate();
/** ***********************************************
* Copyright (c) 2018 Ansible, Inc.
*
* All Rights Reserved
************************************************ */
const JOB_TEMPLATE_ALIASES = ['job_template', 'Job Template'];
const WORKFLOW_TEMPLATE_ALIASES = ['workflow_job_template', 'Workflow Job Template'];
const isJobTemplate = ({ type }) => JOB_TEMPLATE_ALIASES.indexOf(type) > -1;
const isWorkflowTemplate = ({ type }) => WORKFLOW_TEMPLATE_ALIASES.indexOf(type) > -1;
const mapChoices = choices => Object.assign(...choices.map(([k, v]) => ({[k]: v})));
function ListTemplatesController(
$filter,
$scope,
$state,
Alert,
Dataset,
InitiatePlaybookRun,
ProcessErrors,
Prompt,
PromptService,
resolvedModels,
strings,
Wait,
) {
const vm = this || {};
const [jobTemplate, workflowTemplate] = resolvedModels;
const choices = workflowTemplate.options('actions.GET.type.choices')
.concat(jobTemplate.options('actions.GET.type.choices'));
vm.strings = strings;
// TODO: add the permission based functionality to the base model
$scope.canAdd = false;
rbacUiControlService.canAdd("job_templates")
.then(function(params) {
$scope.canAddJobTemplate = params.canAdd;
});
rbacUiControlService.canAdd("workflow_job_templates")
.then(function(params) {
$scope.canAddWorkflowJobTemplate = params.canAdd;
});
$scope.$watchGroup(["canAddJobTemplate", "canAddWorkflowJobTemplate"], function() {
if ($scope.canAddJobTemplate || $scope.canAddWorkflowJobTemplate) {
$scope.canAdd = true;
} else {
$scope.canAdd = false;
}
});
$scope.list = {
iterator: 'template',
name: 'templates'
};
$scope.collection = {
basePath: 'unified_job_templates',
iterator: 'template'
};
$scope[`${$scope.list.iterator}_dataset`] = Dataset.data;
$scope[$scope.list.name] = $scope[`${$scope.list.iterator}_dataset`].results;
$scope.$on('updateDataset', function(e, dataset) {
$scope[`${$scope.list.iterator}_dataset`] = dataset;
$scope[$scope.list.name] = dataset.results;
});
// get modified date and user who modified it
vm.getModified = function(template) {
let val = "";
if (template.modified) {
val += $filter('longDate')(template.modified);
}
if (_.has(template, 'summary_fields.modified_by.username')) {
val += ` by <a href="/#/users/${template.summary_fields.modified_by.id}">${template.summary_fields.modified_by.username}</a>`;
}
if (val === "") {
val = undefined;
}
return val;
};
// get last ran date and user who ran it
vm.getRan = function(template) {
let val = "";
if (template.last_job_run) {
val += $filter('longDate')(template.last_job_run);
}
// TODO: when API gives back a user who last ran the job in summary fields, uncomment and
// update this code
// if (template && template.summary_fields && template.summary_fields.modified_by &&
// template.summary_fields.modified_by.username) {
// val += ` by <a href="/#/users/${template.summary_fields.modified_by.id}">${template.summary_fields.modified_by.username}</a>`;
// }
if (val === "") {
val = undefined;
}
return val;
};
// get pretified template type names from options
vm.templateTypes = unifiedJobTemplate.options('actions.GET.type.choices')
.reduce((acc, i) => {
acc[i[0]] = i[1];
return acc;
}, {});
// get if you should show the active indicator for the row or not
// TODO: edit indicator doesn't update when you enter edit route after initial load right now
vm.templateTypes = mapChoices(choices);
vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id);
vm.submitJob = function(template) {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
let jobTemplate = new JobTemplate();
$scope.canAddJobTemplate = jobTemplate.options('actions.POST')
$scope.canAddWorkflowJobTemplate = workflowTemplate.options('actions.POST')
$scope.canAdd = ($scope.canAddJobTemplate || $scope.canAddWorkflowJobTemplate);
$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 {
// smart-search
const name = 'templates';
const iterator = 'template';
const key = 'template_dataset';
if(responses[1].data.survey_enabled) {
$scope.list = { iterator, name };
$scope.collection = { iterator, basePath: 'unified_job_templates' };
$scope[key] = Dataset.data;
$scope[name] = Dataset.data.results;
$scope.$on('updateDataset', (e, dataset) => {
$scope[key] = dataset;
$scope[name] = dataset.results;
});
// 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 {
// 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.');
}
vm.runTemplate = template => {
if (!template) {
Alert(strings.get('error.LAUNCH'), strings.get('alert.MISSING_PARAMETER'));
return;
}
else {
Alert('Error: Unable to launch template', 'Template parameter is missing');
if (isJobTemplate(template)) {
runJobTemplate(template);
} else if (isWorkflowTemplate(template)) {
runWorkflowTemplate(template);
} else {
Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_LAUNCH'));
}
};
$scope.launchJob = () => {
vm.scheduleTemplate = template => {
if (!template) {
Alert(strings.get('error.SCHEDULE'), strings.get('alert.MISSING_PARAMETER'));
return;
}
let jobLaunchData = {
if (isJobTemplate(template)) {
$state.go('jobTemplateSchedules', { id: template.id });
} else if (isWorkflowTemplate(template)) {
$state.go('workflowJobTemplateSchedules', { id: template.id });
} else {
Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_SCHEDULE'));
}
};
vm.deleteTemplate = template => {
if (!template) {
Alert(strings.get('error.DELETE'), strings.get('alert.MISSING_PARAMETER'));
return;
}
if (isWorkflowTemplate(template)) {
displayWorkflowTemplateDeletePrompt(template);
} else if (isJobTemplate(template)) {
jobTemplate.getDependentResourceCounts(template.id)
.then(counts => displayJobTemplateDeletePrompt(template, counts));
} else {
Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_DELETE'));
}
};
vm.copyTemplate = template => {
if (!template) {
Alert(strings.get('error.COPY'), strings.get('alert.MISSING_PARAMETER'));
return;
}
if (isJobTemplate(template)) {
copyJobTemplate(template);
} else if (isWorkflowTemplate(template)) {
copyWorkflowTemplate(template);
} else {
Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_COPY'));
}
};
vm.getModified = template => {
const modified = _.get(template, 'modified');
if (!modified) {
return undefined;
}
let html = $filter('longDate')(modified);
const { username, id } = _.get(template, 'summary_fields.modified_by', {});
if (username && id) {
html += ` by <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
}
return html;
};
vm.getLastRan = template => {
const lastJobRun = _.get(template, 'last_job_run');
if (!lastJobRun) {
return undefined;
}
let html = $filter('longDate')(modified);
// TODO: uncomment and update when last job run user is returned by api
// const { username, id } = _.get(template, 'summary_fields.last_job_run_by', {});
// if (username && id) {
// html += ` by <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
//}
return html;
};
function createErrorHandler(path, action) {
return ({ data, status }) => {
const hdr = strings.get('error.HEADER');
const msg = strings.get('error.CALL', { path, action, status });
ProcessErrors($scope, data, status, null, { hdr, msg });
};
}
function copyJobTemplate(template) {
Wait('start');
jobTemplate
.create('get', template.id)
.then(model => model.copy())
.then(({ id }) => {
const params = { job_template_id: id };
$state.go('templates.editJobTemplate', params, { reload: true });
})
.catch(createErrorHandler('copy job template', 'POST'))
.finally(() => Wait('stop'));
};
function copyWorkflowTemplate(template) {
Wait('start');
workflowTemplate
.create('get', template.id)
.then(model => model.extend('get', 'copy'))
.then(model => {
const action = () => {
Wait('start');
model.copy()
.then(({ id }) => {
const params = { workflow_job_template_id: id };
$state.go('templates.editWorkflowJobTemplate', params, { reload: true });
})
.catch(createErrorHandler('copy workflow', 'POST'))
.finally(() => Wait('stop'));
};
if (model.get('related.copy.can_copy_without_user_input')) {
action();
} else if (model.get('related.copy.can_copy')) {
Prompt({
action,
actionText: strings.get('COPY'),
body: buildWorkflowCopyPromptHTML(model.get('related.copy')),
class: 'Modal-primaryButton',
hdr: strings.get('actions.COPY_WORKFLOW'),
});
} else {
Alert(strings.get('error.COPY'), strings.get('alert.NO_PERMISSION'));
}
})
.catch(createErrorHandler('copy workflow', 'GET'))
.finally(() => Wait('stop'));
}
function handleSuccessfulDelete(template) {
const { page } = _.get($state.params, 'template_search');
let reloadListStateParams = null;
if ($scope.templates.length === 1 && !_.isEmpty(page) && page !== '1') {
reloadListStateParams = _.cloneDeep($state.params);
const pageNumber = (parseInt(reloadListStateParams.template_search.page, 0) - 1);
reloadListStateParams.template_search.page = pageNumber.toString();
}
if (parseInt($state.params.job_template_id, 0) === template.id) {
$state.go('templates', reloadListStateParams, { reload: true });
} else if (parseInt($state.params.workflow_job_template_id, 0) === template.id) {
$state.go('templates', reloadListStateParams, { reload: true });
} else {
$state.go('.', reloadListStateParams, { reload: true });
}
}
function displayJobTemplateDeletePrompt(template, counts) {
Prompt({
action() {
$('#prompt-modal').modal('hide');
Wait('start');
jobTemplate
.request('delete', template.id)
.then(() => handleSuccessfulDelete(template))
.catch(createErrorHandler('delete template', 'DELETE'))
.finally(() => Wait('stop'));
},
hdr: strings.get('DELETE'),
resourceName: $filter('sanitize')(template.name),
body: buildJobTemplateDeletePromptHTML(counts),
});
}
function displayWorkflowTemplateDeletePrompt(template) {
Prompt({
action() {
$('#prompt-modal').modal('hide');
Wait('start');
workflowTemplate
.request('delete', template.id)
.then(() => handleSuccessfulDelete(template))
.catch(createErrorHandler('delete template', 'DELETE'))
.finally(() => Wait('stop'))
},
hdr: strings.get('DELETE'),
resourceName: $filter('sanitize')(template.name),
body: strings.get('deleteResource.CONFIRM', 'workflow template'),
});
}
function buildJobTemplateDeletePromptHTML(counts) {
const buildCount = count => `<span class="badge List-titleBadge">${count}</span>`;
const buildLabel = label => `<span class="Prompt-warningResourceTitle">
${$filter('sanitize')(label)}</span>`;
const buildCountLabel = ({ count, label }) => `<div>
${buildLabel(label)}${buildCount(count)}</div>`;
const displayedCounts = counts.filter(({ count }) => count > 0);
const html = `
${displayedCounts.length ? strings.get('deleteResource.USED_BY', 'job template') : ''}
${strings.get('deleteResource.CONFIRM', 'job template')}
${displayedCounts.map(buildCountLabel).join('')}
`;
return html;
}
function buildWorkflowCopyPromptHTML(data) {
const pull = (data, param) => _.get(data, param, []).map($filter('sanitize'));
const credentials = pull(data, 'credentials_unable_to_copy');
const inventories = pull(data, 'inventories_unable_to_copy');
const templates = pull(data, 'templates_unable_to_copy');
const html = `
<div class="Prompt-bodyQuery">
${strings.get('warnings.WORKFLOW_RESTRICTED_COPY')}
</div>
<div class="Prompt-bodyTarget">
${templates.length ? `<div>Unified Job Templates<ul>` : ''}
${templates.map(item => `<li>${item}</li>`).join('')}
${templates.length ? `</ul></div>` : ''}
</div>
<div class="Prompt-bodyTarget">
${credentials.length ? `<div>Credentials<ul>` : ''}
${credentials.map(item => `<li>${item}</li>`).join('')}
${credentials.length ? `</ul></div>` : ''}
</div>
<div class="Prompt-bodyTarget">
${inventories.length ? `<div>Inventories<ul>` : ''}
${inventories.map(item => `<li>${item}</li>`).join('')}
${inventories.length ? `</ul></div>` : ''}
</div>
`;
return html;
}
function runJobTemplate(template) {
const selectedJobTemplate = jobTemplate.create();
const preLaunchPromises = [
selectedJobTemplate.getLaunch(template.id),
selectedJobTemplate.optionsLaunch(template.id),
];
Promise.all(preLaunchPromises)
.then(([launchData, launchOptions]) => {
if (selectedJobTemplate.canLaunchWithoutPrompt()) {
return selectedJobTemplate
.postLaunch({ id: template.id })
.then(({ data }) => {
$state.go('jobResult', { id: data.job }, { reload: true })
});
}
const promptData = {
launchConf: launchData.data,
launchOptions: launchOptions.data,
template: template.id,
prompts: PromptService.processPromptValues({
launchConf: launchData.data,
launchOptions: launchOptions.data
}),
triggerModalOpen: true,
};
if (launchData.data.survey_enabled) {
selectedJobTemplate.getSurveyQuestions(template.id)
.then(({ data }) => {
const processed = PromptService.processSurveyQuestions({ surveyQuestions: data.spec });
promptData.surveyQuestions = processed.surveyQuestions;
$scope.promptData = promptData;
});
} else {
$scope.promptData = promptData;
}
});
}
function runWorkflowTemplate(template) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' });
}
$scope.launchJob = () => {
const jobLaunchData = {
extra_vars: $scope.promptData.extraVars
};
let jobTemplate = new JobTemplate();
if($scope.promptData.launchConf.ask_tags_on_launch){
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){
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')){
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')) {
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')) {
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)){
if ($scope.promptData.launchConf.ask_inventory_on_launch && !_.isEmpty($scope.promptData.prompts.inventory.value.id)){
jobLaunchData.inventory_id = $scope.promptData.prompts.inventory.value.id;
}
if($scope.promptData.launchConf.ask_credential_on_launch){
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')) {
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) {
if ($scope.promptData.prompts.credentials.passwords) {
_.forOwn($scope.promptData.prompts.credentials.passwords, (val, key) => {
if(!jobLaunchData.credential_passwords) {
if (!jobLaunchData.credential_passwords) {
jobLaunchData.credential_passwords = {};
}
if(key === "ssh_key_unlock") {
if (key === "ssh_key_unlock") {
jobLaunchData.credential_passwords.ssh_key_unlock = val.value;
} else if(key !== "vault") {
} else if (key !== "vault") {
jobLaunchData.credential_passwords[`${key}_password`] = val.value;
} else {
_.each(val, (vaultCred) => {
@ -209,265 +415,30 @@ function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strin
delete jobLaunchData.extra_vars;
}
jobTemplate.postLaunch({
jobTemplate.create().postLaunch({
id: $scope.promptData.template,
launchData: jobLaunchData
}).then((launchRes) => {
})
.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 if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
$state.go('workflowJobTemplateSchedules', {id: template.id});
}
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');
}
};
vm.scheduleJob = (template) => {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
$state.go('jobTemplateSchedules', {id: template.id});
}
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
$state.go('workflowJobTemplateSchedules', {id: template.id});
}
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');
}
};
vm.copyTemplate = function(template) {
if(template) {
if(template.type && template.type === 'job_template') {
Wait('start');
TemplateCopyService.get(template.id)
.then(function(response){
TemplateCopyService.set(response.data.results)
.then(function(results){
Wait('stop');
if(results.type && results.type === 'job_template') {
$state.go('templates.editJobTemplate', {job_template_id: results.id}, {reload: true});
}
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {
hdr: 'Error!',
msg: 'Call failed. Return status: ' + status
});
});
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {hdr: 'Error!',
msg: 'Call failed. Return status: '+ status});
});
}
else if(template.type && template.type === 'workflow_job_template') {
TemplateCopyService.getWorkflowCopy(template.id)
.then(function(result) {
if(result.data.can_copy) {
if(result.data.can_copy_without_user_input) {
// Go ahead and copy the workflow - the user has full priveleges on all the resources
TemplateCopyService.copyWorkflow(template.id)
.then(function(result) {
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: result.data.id}, {reload: true});
})
.catch(function (response) {
Wait('stop');
ProcessErrors($scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Call to copy workflow job template failed. Return status: ' + response.status + '.'});
});
}
else {
let bodyHtml = `
<div class="Prompt-bodyQuery">
You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.
</div>
<div class="Prompt-bodyTarget">`;
// List the unified job templates user can not access
if (result.data.templates_unable_to_copy.length > 0) {
bodyHtml += '<div>Unified Job Templates that can not be copied<ul>';
_.forOwn(result.data.templates_unable_to_copy, function(ujt) {
if(ujt) {
bodyHtml += '<li>' + ujt + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted inventories user can not access
if (result.data.inventories_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted inventories that can not be copied<ul>';
_.forOwn(result.data.inventories_unable_to_copy, function(inv) {
if(inv) {
bodyHtml += '<li>' + inv + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted credentials user can not access
if (result.data.credentials_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted credentials that can not be copied<ul>';
_.forOwn(result.data.credentials_unable_to_copy, function(cred) {
if(cred) {
bodyHtml += '<li>' + cred + '</li>';
}
});
bodyHtml += '</ul></div>';
}
bodyHtml += '</div>';
Prompt({
hdr: 'Copy Workflow',
body: bodyHtml,
action: function() {
$('#prompt-modal').modal('hide');
Wait('start');
TemplateCopyService.copyWorkflow(template.id)
.then(function(result) {
Wait('stop');
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: result.data.id}, {reload: true});
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to copy template failed. POST returned status: ' + status });
});
},
actionText: 'COPY',
class: 'Modal-primaryButton'
});
}
}
else {
Alert('Error: Unable to copy workflow job template', 'You do not have permission to perform this action.');
}
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to copy template failed. GET returned status: ' + status });
});
}
else {
// Something went wrong - Let the user know that we're unable to copy 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 copying.');
}
}
else {
Alert('Error: Unable to copy job', 'Template parameter is missing');
}
};
vm.deleteTemplate = function(template) {
var action = function() {
$('#prompt-modal').modal('hide');
Wait('start');
let deleteComplete = () => {
let reloadListStateParams = null;
if($scope.templates.length === 1 && $state.params.template_search && !_.isEmpty($state.params.template_search.page) && $state.params.template_search.page !== '1') {
reloadListStateParams = _.cloneDeep($state.params);
reloadListStateParams.template_search.page = (parseInt(reloadListStateParams.template_search.page)-1).toString();
}
if (parseInt($state.params.template_id) === template.id) {
$state.go("^", reloadListStateParams, { reload: true });
} else {
$state.go('.', reloadListStateParams, {reload: true});
}
};
if(template.type === "job_template") {
jobTemplate.request('delete', template.id)
.then(() => {
deleteComplete();
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {
hdr: strings.get('error.HEADER'),
msg: strings.get('error.CALL', {path: "" + unifiedJobTemplate.path + template.id, status})
});
})
.finally(function() {
Wait('stop');
});
} else if(template.type === "workflow_job_template") {
workflowJobTemplate.request('delete', template.id)
.then(() => {
deleteComplete();
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {
hdr: strings.get('error.HEADER'),
msg: strings.get('error.CALL', {path: "" + unifiedJobTemplate.path + template.id, status})
});
})
.finally(function() {
Wait('stop');
});
}
};
let deleteModalBody = `<div class="Prompt-bodyQuery">${strings.get('deleteResource.CONFIRM', 'template')}</div>`;
Prompt({
hdr: strings.get('deleteResource.HEADER'),
resourceName: $filter('sanitize')(template.name),
body: deleteModalBody,
action: action,
actionText: 'DELETE'
});
})
.catch(createErrorHandler('launch job template', 'POST'))
};
}
ListTemplatesController.$inject = [
'resolvedModels',
'JobTemplateModel',
'WorkflowJobTemplateModel',
'TemplatesStrings',
'$state',
'$scope',
'rbacUiControlService',
'Dataset',
'$filter',
'$scope',
'$state',
'Alert',
'Dataset',
'InitiatePlaybookRun',
'Prompt',
'Wait',
'ProcessErrors',
'TemplateCopyService',
'$q',
'Empty',
'i18n',
'PromptService'
'Prompt',
'PromptService',
'resolvedModels',
'TemplatesStrings',
'Wait',
];
export default ListTemplatesController;

View File

@ -2,14 +2,6 @@ import ListController from './list-templates.controller';
const listTemplate = require('~features/templates/list.view.html');
import { N_ } from '../../src/i18n';
function TemplatesResolve (UnifiedJobTemplate) {
return new UnifiedJobTemplate(['get', 'options']);
}
TemplatesResolve.$inject = [
'UnifiedJobTemplateModel'
];
export default {
name: 'templates',
route: '/templates',
@ -32,10 +24,10 @@ export default {
},
params: {
template_search: {
dynamic: true,
value: {
type: 'workflow_job_template,job_template'
},
dynamic: true
type: 'workflow_job_template,job_template',
},
}
},
searchPrefix: 'template',
@ -43,16 +35,34 @@ export default {
'@': {
controller: ListController,
templateUrl: listTemplate,
controllerAs: 'vm'
controllerAs: 'vm',
}
},
resolve: {
resolvedModels: TemplatesResolve,
Dataset: ['TemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
return qs.search(path, $stateParams[`${list.iterator}_search`]);
resolvedModels: [
'JobTemplateModel',
'WorkflowJobTemplateModel',
(JobTemplate, WorkflowJobTemplate) => {
const models = [
new JobTemplate(['options']),
new WorkflowJobTemplate(['options']),
];
return Promise.all(models);
},
],
Dataset: [
'$stateParams',
'Wait',
'GetBasePath',
'QuerySet',
($stateParams, Wait, GetBasePath, qs) => {
const searchParam = $stateParams.template_search;
const searchPath = GetBasePath('unified_job_templates');
Wait('start');
return qs.search(searchPath, searchParam)
.finally(() => Wait('stop'))
}
]
],
}
};

View File

@ -89,16 +89,16 @@
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_RAN') }}"
value="{{ vm.getRan(template) }}">
value="{{ vm.getLastRan(template) }}">
</at-row-item>
<labels-list class="LabelList" show-delete="false" is-row-item="true">
</labels-list>
</div>
<div class="at-Row-actions">
<at-row-action icon="icon-launch" ng-click="vm.submitJob(template)"
<at-row-action icon="icon-launch" ng-click="vm.runTemplate(template)"
ng-show="template.summary_fields.user_capabilities.start">
</at-row-action>
<at-row-action icon="fa-calendar" ng-click="vm.scheduleJob(template)"
<at-row-action icon="fa-calendar" ng-click="vm.scheduleTemplate(template)"
ng-show="template.summary_fields.user_capabilities.schedule">
</at-row-action>
<at-row-action icon="fa-copy" ng-click="vm.copyTemplate(template)"

View File

@ -55,8 +55,37 @@ function TemplatesStrings (BaseString) {
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')
NO_CREDS_MATCHING_TYPE: t.s('No Credentials Matching This Type Have Been Created'),
};
ns.alert = {
MISSING_PARAMETER: t.s('Template parameter is missing.'),
NO_PERMISSION: t.s('You do not have permission to perform this action.'),
UNKNOWN_COPY: t.s('Unable to determine this template\'s type while copying.'),
UNKNOWN_DELETE: t.s('Unable to determine this template\'s type while deleting.'),
UNKNOWN_EDIT: t.s('Unable to determine this template\'s type while editing.'),
UNKNOWN_LAUNCH: t.s('Unable to determine this template\'s type while launching.'),
UNKNOWN_SCHEDULE: t.s('Unable to determine this template\'s type while scheduling.'),
};
ns.actions = {
COPY_WORKFLOW: t.s('Copy Workflow')
};
ns.error = {
HEADER: this.error.HEADER,
CALL: this.error.CALL,
EDIT: t.s('Unable to edit template.'),
DELETE: t.s('Unable to delete template.'),
LAUNCH: t.s('Unable to launch template.'),
UNKNOWN: t.s('Unable to determine template type.'),
SCHEDULE: t.s('Unable to schedule job.'),
COPY: t.s('Unable to copy template.'),
};
ns.warnings = {
WORKFLOW_RESTRICTED_COPY: t.s('You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.')
}
}
TemplatesStrings.$inject = ['BaseStringService'];

View File

@ -418,6 +418,25 @@ function getDependentResourceCounts (id) {
return Promise.all(promises);
}
function copy () {
if (!this.has('POST', 'related.copy')) {
return Promise.reject(new Error('No related property, copy, exists'));
}
const date = new Date();
const name = `${this.get('name')}@${date.toLocaleTimeString()}`;
const url = `${this.path}${this.get('id')}/copy/`;
const req = {
url,
method: 'POST',
data: { name }
};
return $http(req).then(res => res.data);
}
/**
* `create` is called on instantiation of every model. Models can be
* instantiated empty or with `GET` and/or `OPTIONS` requests that yield data.
@ -517,6 +536,7 @@ function BaseModel (resource, settings) {
this.set = set;
this.unset = unset;
this.extend = extend;
this.copy = copy;
this.getDependentResourceCounts = getDependentResourceCounts;
this.http = {

View File

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

View File

@ -4,22 +4,22 @@ import Base from '~models/Base';
import Config from '~models/Config';
import Credential from '~models/Credential';
import CredentialType from '~models/CredentialType';
import Me from '~models/Me';
import Organization from '~models/Organization';
import Project from '~models/Project';
import JobTemplate from '~models/JobTemplate';
import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode';
import Instance from '~models/Instance';
import InstanceGroup from '~models/InstanceGroup';
import InventorySource from '~models/InventorySource';
import Inventory from '~models/Inventory';
import InventoryScript from '~models/InventoryScript';
import InventorySource from '~models/InventorySource';
import Job from '~models/Job';
import JobTemplate from '~models/JobTemplate';
import Me from '~models/Me';
import ModelsStrings from '~models/models.strings';
import NotificationTemplate from '~models/NotificationTemplate';
import Organization from '~models/Organization';
import Project from '~models/Project';
import UnifiedJobTemplate from '~models/UnifiedJobTemplate';
import WorkflowJob from '~models/WorkflowJob';
import WorkflowJobTemplate from '~models/WorkflowJobTemplate';
import ModelsStrings from '~models/models.strings';
import UnifiedJobTemplate from '~models/UnifiedJobTemplate';
import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode';
const MODULE_NAME = 'at.lib.models';
@ -31,21 +31,21 @@ angular
.service('ConfigModel', Config)
.service('CredentialModel', Credential)
.service('CredentialTypeModel', CredentialType)
.service('MeModel', Me)
.service('OrganizationModel', Organization)
.service('ProjectModel', Project)
.service('JobTemplateModel', JobTemplate)
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode)
.service('InstanceModel', Instance)
.service('InstanceGroupModel', InstanceGroup)
.service('InventorySourceModel', InventorySource)
.service('InstanceModel', Instance)
.service('InventoryModel', Inventory)
.service('InventoryScriptModel', InventoryScript)
.service('WorkflowJobTemplateModel', WorkflowJobTemplate)
.service('ModelsStrings', ModelsStrings)
.service('UnifiedJobTemplateModel', UnifiedJobTemplate)
.service('InventorySourceModel', InventorySource)
.service('JobModel', Job)
.service('JobTemplateModel', JobTemplate)
.service('MeModel', Me)
.service('ModelsStrings', ModelsStrings)
.service('NotificationTemplate', NotificationTemplate)
.service('OrganizationModel', Organization)
.service('ProjectModel', Project)
.service('UnifiedJobTemplateModel', UnifiedJobTemplate)
.service('WorkflowJobModel', WorkflowJob)
.service('WorkflowJobTemplateModel', WorkflowJobTemplate);
.service('WorkflowJobTemplateModel', WorkflowJobTemplate)
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode);
export default MODULE_NAME;

View File

@ -67,15 +67,18 @@ function BaseStringService (namespace) {
this.OFF = t.s('OFF');
this.YAML = t.s('YAML');
this.JSON = t.s('JSON');
this.DELETE = t.s('DELETE');
this.COPY = t.s('COPY');
this.deleteResource = {
HEADER: t.s('Delete'),
USED_BY: resourceType => t.s('The {{ resourceType }} is currently being used by other resources.', { resourceType }),
CONFIRM: resourceType => t.s('Are you sure you want to delete this {{ resourceType }}?', { resourceType })
};
this.error = {
HEADER: t.s('Error!'),
CALL: ({ path, status }) => t.s('Call to {{ path }} failed. DELETE returned status: {{ status }}.', { path, status })
CALL: ({ path, action, status }) => t.s('Call to {{ path }} failed. {{ action }} returned status: {{ status }}.', { path, action, status }),
};
this.ALERT = ({ header, body }) => t.s('{{ header }} {{ body }}', { header, body });

View File

@ -69,7 +69,14 @@ export default ['i18n', function(i18n) {
dataPlacement: 'top',
ngShow: 'credential.summary_fields.user_capabilities.edit'
},
copy: {
label: i18n._('Copy'),
ngClick: 'copyCredential(credential)',
"class": 'btn-danger btn-xs',
awToolTip: i18n._('Copy credential'),
dataPlacement: 'top',
ngShow: 'credential.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editCredential(credential.id)",
label: i18n._('View'),

View File

@ -89,6 +89,21 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
}
}
$scope.copyCredential = credential => {
Wait('start');
new Credential('get', credential.id)
.then(model => model.copy())
.then(({ id }) => {
const params = { credential_id: id };
$state.go('credentials.edit', params, { reload: true });
})
.catch(({ data, status }) => {
const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` };
ProcessErrors($scope, data, status, null, params);
})
.finally(() => Wait('stop'));
};
$scope.addCredential = function() {
$state.go('credentials.add');
};

View File

@ -100,6 +100,14 @@ export default ['i18n', function(i18n) {
dataPlacement: 'top',
ngShow: '!inventory.pending_deletion && inventory.summary_fields.user_capabilities.edit'
},
copy: {
label: i18n._('Copy'),
ngClick: 'copyInventory(inventory)',
"class": 'btn-danger btn-xs',
awToolTip: i18n._('Copy inventory'),
dataPlacement: 'top',
ngShow: 'inventory.summary_fields.user_capabilities.edit'
},
view: {
label: i18n._('View'),
ngClick: 'editInventory(inventory)',

View File

@ -73,6 +73,18 @@ function InventoriesList($scope,
inventory.linkToDetails = (inventory.kind && inventory.kind === 'smart') ? `inventories.editSmartInventory({smartinventory_id:${inventory.id}})` : `inventories.edit({inventory_id:${inventory.id}})`;
}
$scope.copyInventory = inventory => {
Wait('start');
new Inventory('get', inventory.id)
.then(model => model.copy())
.then(copy => $scope.editInventory(copy))
.catch(({ data, status }) => {
const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` };
ProcessErrors($scope, data, status, null, params);
})
.finally(() => Wait('stop'));
};
$scope.editInventory = function (inventory) {
if(inventory.kind && inventory.kind === 'smart') {
$state.go('inventories.editSmartInventory', {smartinventory_id: inventory.id});

View File

@ -57,6 +57,14 @@ export default ['i18n', function(i18n){
dataPlacement: 'top',
ngShow: 'inventory_script.summary_fields.user_capabilities.edit'
},
copy: {
label: i18n._('Copy'),
ngClick: 'copyCustomInv(inventory_script)',
"class": 'btn-danger btn-xs',
awToolTip: i18n._('Copy inventory scruot'),
dataPlacement: 'top',
ngShow: 'inventory_script.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editCustomInv(inventory_script.id)",
label: i18n._('View'),

View File

@ -47,6 +47,21 @@ export default ['$rootScope', '$scope', 'Wait', 'InventoryScriptsList',
});
};
$scope.copyCustomInv = inventoryScript => {
Wait('start');
new InventoryScript('get', inventoryScript.id)
.then(model => model.copy())
.then(({ id }) => {
const params = { inventory_script_id: id };
$state.go('inventoryScripts.edit', params, { reload: true });
})
.catch(({ data, status }) => {
const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` };
ProcessErrors($scope, data, status, null, params);
})
.finally(() => Wait('stop'));
};
$scope.deleteCustomInv = function(id, name) {
var action = function() {

View File

@ -7,12 +7,12 @@
export default ['$scope', 'Wait', 'NotificationTemplatesList',
'GetBasePath', 'Rest', 'ProcessErrors', 'Prompt', '$state',
'ngToast', '$filter', 'Dataset', 'rbacUiControlService',
'i18n',
'i18n', 'NotificationTemplate',
function(
$scope, Wait, NotificationTemplatesList,
GetBasePath, Rest, ProcessErrors, Prompt, $state,
ngToast, $filter, Dataset, rbacUiControlService,
i18n) {
i18n, NotificationTemplate) {
var defaultUrl = GetBasePath('notification_templates'),
list = NotificationTemplatesList;
@ -31,7 +31,7 @@
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
}
$scope.$on(`notification_template_options`, function(event, data){
$scope.options = data.data.actions.GET;
@ -88,6 +88,24 @@
notification_template.template_status_html = html;
}
$scope.copyNotification = notificationTemplate => {
Wait('start');
new NotificationTemplate('get', notificationTemplate.id)
.then(model => model.copy())
.then(({ id }) => {
const params = {
notification_template_id: id,
notification_template: this.notification_templates
};
$state.go('notifications.edit', params, { reload: true });
})
.catch(({ data, status }) => {
const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` };
ProcessErrors($scope, data, status, null, params);
})
.finally(() => Wait('stop'));
};
$scope.testNotification = function() {
var name = $filter('sanitize')(this.notification_template.name),
pending_retries = 10;
@ -152,6 +170,7 @@
}
};
$scope.addNotification = function() {
$state.go('notifications.add');
};

View File

@ -77,6 +77,14 @@ export default ['i18n', function(i18n){
dataPlacement: 'top',
ngShow: 'notification_template.summary_fields.user_capabilities.edit'
},
copy: {
label: i18n._('Copy'),
ngClick: 'copyNotification(notification_template)',
"class": 'btn-danger btn-xs',
awToolTip: i18n._('Copy notification'),
dataPlacement: 'top',
ngShow: 'notification_template.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editNotification(notification_template.id)",
label: i18n._('View'),

View File

@ -154,6 +154,21 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert',
}
});
$scope.copyProject = project => {
Wait('start');
new Project('get', project.id)
.then(model => model.copy())
.then(({ id }) => {
const params = { project_id: id };
$state.go('projects.edit', params, { reload: true });
})
.catch(({ data, status }) => {
const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` };
ProcessErrors($scope, data, status, null, params);
})
.finally(() => Wait('stop'));
};
$scope.showSCMStatus = function(id) {
// Refresh the project list
var project = Find({ list: $scope.projects, key: 'id', val: id });

View File

@ -100,6 +100,14 @@ export default ['i18n', function(i18n) {
dataPlacement: 'top',
ngShow: "project.summary_fields.user_capabilities.schedule"
},
copy: {
label: i18n._('Copy'),
ngClick: 'copyProject(project)',
"class": 'btn-danger btn-xs',
awToolTip: i18n._('Copy project'),
dataPlacement: 'top',
ngShow: 'project.summary_fields.user_capabilities.edit'
},
edit: {
ngClick: "editProject(project.id)",
awToolTip: i18n._('Edit the project'),

View File

@ -1,74 +0,0 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$rootScope', 'Rest', 'ProcessErrors', 'GetBasePath', 'moment',
function($rootScope, Rest, ProcessErrors, GetBasePath, moment){
return {
get: function(id){
var defaultUrl = GetBasePath('job_templates') + '?id=' + id;
Rest.setUrl(defaultUrl);
return Rest.get()
.then(response => response)
.catch((error) => {
ProcessErrors($rootScope, error.response, error.status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
getSurvey: function(endpoint){
Rest.setUrl(endpoint);
return Rest.get();
},
copySurvey: function(source, target){
return this.getSurvey(source.related.survey_spec).then( (response) => {
Rest.setUrl(target.related.survey_spec);
return Rest.post(response.data);
});
},
set: function(results){
var defaultUrl = GetBasePath('job_templates');
var self = this;
Rest.setUrl(defaultUrl);
var name = this.buildName(results[0].name);
results[0].name = name + ' @ ' + moment().format('h:mm:ss a'); // 2:49:11 pm
return Rest.post(results[0])
.then((response) => {
// also copy any associated survey_spec
if (results[0].summary_fields.survey){
return self.copySurvey(results[0], response.data).then( () => response.data);
}
else {
return response.data;
}
})
.catch(({res, status}) => {
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
buildName: function(name){
var result = name.split('@')[0];
return result;
},
getWorkflowCopy: function(id) {
let url = GetBasePath('workflow_job_templates');
url = url + id + '/copy';
Rest.setUrl(url);
return Rest.get();
},
copyWorkflow: function(id) {
let url = GetBasePath('workflow_job_templates');
url = url + id + '/copy';
Rest.setUrl(url);
return Rest.post();
}
};
}
];

View File

@ -15,7 +15,6 @@ import workflowChart from './workflows/workflow-chart/main';
import workflowMaker from './workflows/workflow-maker/main';
import workflowControls from './workflows/workflow-controls/main';
import workflowService from './workflows/workflow.service';
import templateCopyService from './copy-template/template-copy.service';
import WorkflowForm from './workflows.form';
import CompletedJobsList from './completed-jobs.list';
import InventorySourcesList from './inventory-sources.list';
@ -29,7 +28,6 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
])
.service('TemplatesService', templatesService)
.service('WorkflowService', workflowService)
.service('TemplateCopyService', templateCopyService)
.factory('WorkflowForm', WorkflowForm)
.factory('CompletedJobsList', CompletedJobsList)
// TODO: currently being kept arround for rbac selection, templates within projects and orgs, etc.

View File

@ -77,6 +77,4 @@ module.exports = {
post,
patch,
put,
all: axios.all,
spread: axios.spread
};

View File

@ -3,18 +3,15 @@ import uuid from 'uuid';
import { AWX_E2E_PASSWORD } from './settings';
import {
all,
get,
post,
spread
} from './api';
const session = `e2e-${uuid().substr(0, 8)}`;
const store = {};
const getOrCreate = (endpoint, data) => {
const identifier = Object.keys(data).find(key => ['name', 'username'].includes(key));
const getOrCreate = (endpoint, data, unique = ['name', 'username', 'id']) => {
const identifier = Object.keys(data).find(key => unique.includes(key));
if (identifier === undefined) {
throw new Error('A unique key value must be provided.');
@ -22,12 +19,10 @@ const getOrCreate = (endpoint, data) => {
const identity = data[identifier];
if (store[endpoint] && store[endpoint][identity]) {
return store[endpoint][identity].then(created => created.data);
}
store[endpoint] = store[endpoint] || {};
if (!store[endpoint]) {
store[endpoint] = {};
if (store[endpoint][identity]) {
return store[endpoint][identity].then(created => created.data);
}
const query = { params: { [identifier]: identity } };
@ -61,13 +56,8 @@ const getInventory = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate('/inventories/', {
name: `${namespace}-inventory`,
description: namespace,
organization: organization.id,
}).then(inventory => getOrCreate('/hosts/', {
name: `${namespace}-host`,
description: namespace,
inventory: inventory.id,
variables: JSON.stringify({ ansible_connection: 'local' }),
}).then(() => inventory)));
organization: organization.id
}));
const getHost = (namespace = session) => getInventory(namespace)
.then(inventory => getOrCreate('/hosts/', {
@ -75,7 +65,7 @@ const getHost = (namespace = session) => getInventory(namespace)
description: namespace,
inventory: inventory.id,
variables: JSON.stringify({ ansible_connection: 'local' }),
}).then((host) => host));
}));
const getInventoryScript = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate('/inventory_scripts/', {
@ -91,14 +81,14 @@ const getInventorySource = (namespace = session) => {
getInventoryScript(namespace)
];
return all(promises)
.then(spread((inventory, inventoryScript) => getOrCreate('/inventory_sources/', {
return Promise.all(promises)
.then(([inventory, inventoryScript]) => getOrCreate('/inventory_sources/', {
name: `${namespace}-inventory-source-custom`,
description: namespace,
source: 'custom',
inventory: inventory.id,
source_script: inventoryScript.id
})));
}));
};
const getAdminAWSCredential = (namespace = session) => {
@ -109,8 +99,8 @@ const getAdminAWSCredential = (namespace = session) => {
})
];
return all(promises)
.then(spread((me, credentialType) => {
return Promise.all(promises)
.then(([me, credentialType]) => {
const [admin] = me.data.results;
return getOrCreate('/credentials/', {
@ -124,7 +114,7 @@ const getAdminAWSCredential = (namespace = session) => {
security_token: 'AAAAAAAAAAAAAAAA'
}
});
}));
});
};
const getAdminMachineCredential = (namespace = session) => {
@ -133,17 +123,16 @@ const getAdminMachineCredential = (namespace = session) => {
getOrCreate('/credential_types/', { name: 'Machine' })
];
return all(promises)
.then(spread((me, credentialType) => {
return Promise.all(promises)
.then(([me, credentialType]) => {
const [admin] = me.data.results;
return getOrCreate('/credentials/', {
name: `${namespace}-credential-machine-admin`,
description: namespace,
credential_type: credentialType.id,
user: admin.id
});
}));
});
};
const getTeam = (namespace = session) => getOrganization(namespace)
@ -235,15 +224,57 @@ const getJobTemplate = (namespace = session) => {
getUpdatedProject(namespace)
];
return all(promises)
.then(spread((inventory, credential, project) => getOrCreate('/job_templates/', {
return Promise.all(promises)
.then(([inventory, credential, project]) => getOrCreate('/job_templates/', {
name: `${namespace}-job-template`,
description: namespace,
inventory: inventory.id,
credential: credential.id,
project: project.id,
playbook: 'hello_world.yml',
})));
}));
};
const getWorkflowTemplate = (namespace = session) => {
const endpoint = '/workflow_job_templates/';
const workflowTemplatePromise = getOrganization(namespace)
.then(organization => getOrCreate(endpoint, {
name: `${namespace}-workflow-template`,
organization: organization.id,
variables: '---',
extra_vars: '',
}));
const resources = [
workflowTemplatePromise,
getInventorySource(namespace),
getUpdatedProject(namespace),
getJobTemplate(namespace),
];
const workflowNodePromise = Promise.all(resources)
.then(([workflowTemplate, source, project, jobTemplate]) => {
const workflowNodes = workflowTemplate.related.workflow_nodes;
const unique = 'unified_job_template';
const nodes = [
getOrCreate(workflowNodes, { [unique]: project.id }, [unique]),
getOrCreate(workflowNodes, { [unique]: jobTemplate.id }, [unique]),
getOrCreate(workflowNodes, { [unique]: source.id }, [unique]),
];
const createSuccessNodes = ([projectNode, jobNode, sourceNode]) => Promise.all([
getOrCreate(projectNode.related.success_nodes, { id: jobNode.id }),
getOrCreate(jobNode.related.success_nodes, { id: sourceNode.id }),
]);
return Promise.all(nodes)
.then(createSuccessNodes);
});
return Promise.all([workflowTemplatePromise, workflowNodePromise])
.then(([workflowTemplate, nodes]) => workflowTemplate);
};
const getAuditor = (namespace = session) => getOrganization(namespace)
@ -287,10 +318,10 @@ const getJobTemplateAdmin = (namespace = session) => {
}));
const assignRolePromise = Promise.all([userPromise, rolePromise])
.then(spread((user, role) => post(`/api/v2/roles/${role.id}/users/`, { id: user.id })));
.then(([user, role]) => post(`/api/v2/roles/${role.id}/users/`, { id: user.id }));
return Promise.all([userPromise, assignRolePromise])
.then(spread(user => user));
.then(([user, assignment]) => user);
};
const getProjectAdmin = (namespace = session) => {
@ -310,10 +341,10 @@ const getProjectAdmin = (namespace = session) => {
}));
const assignRolePromise = Promise.all([userPromise, rolePromise])
.then(spread((user, role) => post(`/api/v2/roles/${role.id}/users/`, { id: user.id })));
.then(([user, role]) => post(`/api/v2/roles/${role.id}/users/`, { id: user.id }));
return Promise.all([userPromise, assignRolePromise])
.then(spread(user => user));
.then(([user, assignment]) => user);
};
const getInventorySourceSchedule = (namespace = session) => getInventorySource(namespace)
@ -334,21 +365,23 @@ module.exports = {
getAdminAWSCredential,
getAdminMachineCredential,
getAuditor,
getHost,
getInventory,
getInventoryScript,
getInventorySource,
getInventorySourceSchedule,
getJob,
getJobTemplate,
getJobTemplateAdmin,
getJobTemplateSchedule,
getNotificationTemplate,
getOrCreate,
getOrganization,
getOrCreate,
getProject,
getProjectAdmin,
getSmartInventory,
getTeam,
getUpdatedProject,
getUser,
getJob,
getHost,
getWorkflowTemplate,
};

View File

@ -1,5 +1,3 @@
import { all } from '../api';
import {
getAdminAWSCredential,
getAdminMachineCredential,
@ -49,7 +47,7 @@ module.exports = {
getUpdatedProject().then(obj => { data.project = obj; })
];
all(promises)
Promise.all(promises)
.then(() => {
client.useCss();

View File

@ -0,0 +1,50 @@
import { getAdminMachineCredential } from '../fixtures';
const data = {};
module.exports = {
before: (client, done) => {
getAdminMachineCredential('test-actions')
.then(obj => { data.credential = obj; })
.then(done);
},
'copy credential': client => {
const credentials = client.page.credentials();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
credentials.navigate();
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
credentials.section.list.expect.element('smart-search').visible;
credentials.section.list.expect.element('smart-search input').enabled;
credentials.section.list
.sendKeys('smart-search input', `id:>${data.credential.id - 1} id:<${data.credential.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
credentials.expect.element(`#credentials_table tr[id="${data.credential.id}"]`).visible;
credentials.expect.element('i[class*="copy"]').visible;
credentials.expect.element('i[class*="copy"]').enabled;
credentials.click('i[class*="copy"]');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
credentials.expect.element('div[ui-view="edit"] form').visible;
credentials.section.edit.expect.element('@title').visible;
credentials.section.edit.expect.element('@title').text.contain(data.credential.name);
credentials.section.edit.expect.element('@title').text.not.equal(data.credential.name);
credentials.section.edit.section.details.expect.element('@save').visible;
credentials.section.edit.section.details.expect.element('@save').enabled;
client.end();
}
};

View File

@ -0,0 +1,50 @@
import { getInventory } from '../fixtures';
const data = {};
module.exports = {
before: (client, done) => {
getInventory('test-actions')
.then(obj => { data.inventory = obj; })
.then(done);
},
'copy inventory': client => {
const inventories = client.page.inventories();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
inventories.navigate();
inventories.waitForElementVisible('div.spinny');
inventories.waitForElementNotVisible('div.spinny');
inventories.section.list.expect.element('smart-search').visible;
inventories.section.list.section.search.expect.element('@input').enabled;
inventories.section.list.section.search
.sendKeys('@input', `id:>${data.inventory.id - 1} id:<${data.inventory.id + 1}`)
.sendKeys('@input', client.Keys.ENTER);
inventories.waitForElementVisible('div.spinny');
inventories.waitForElementNotVisible('div.spinny');
inventories.expect.element(`#inventories_table tr[id="${data.inventory.id}"]`).visible;
inventories.expect.element('i[class*="copy"]').visible;
inventories.expect.element('i[class*="copy"]').enabled;
inventories.click('i[class*="copy"]');
inventories.waitForElementVisible('div.spinny');
inventories.waitForElementNotVisible('div.spinny');
inventories.expect.element('#inventory_form').visible;
inventories.section.editStandardInventory.expect.element('@title').visible;
inventories.section.editStandardInventory.expect.element('@title').text.contain(data.inventory.name);
inventories.section.editStandardInventory.expect.element('@title').text.not.equal(data.inventory.name);
inventories.expect.element('@save').visible;
inventories.expect.element('@save').enabled;
client.end();
}
};

View File

@ -0,0 +1,50 @@
import { getInventoryScript } from '../fixtures';
const data = {};
module.exports = {
before: (client, done) => {
getInventoryScript('test-actions')
.then(obj => { data.inventoryScript = obj; })
.then(done);
},
'copy inventory script': client => {
const inventoryScripts = client.page.inventoryScripts();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
inventoryScripts.navigate();
inventoryScripts.waitForElementVisible('div.spinny');
inventoryScripts.waitForElementNotVisible('div.spinny');
inventoryScripts.section.list.expect.element('smart-search').visible;
inventoryScripts.section.list.expect.element('smart-search input').enabled;
inventoryScripts.section.list
.sendKeys('smart-search input', `id:>${data.inventoryScript.id - 1} id:<${data.inventoryScript.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
inventoryScripts.waitForElementVisible('div.spinny');
inventoryScripts.waitForElementNotVisible('div.spinny');
inventoryScripts.expect.element(`#inventory_scripts_table tr[id="${data.inventoryScript.id}"]`).visible;
inventoryScripts.expect.element('i[class*="copy"]').visible;
inventoryScripts.expect.element('i[class*="copy"]').enabled;
inventoryScripts.click('i[class*="copy"]');
inventoryScripts.waitForElementVisible('div.spinny');
inventoryScripts.waitForElementNotVisible('div.spinny');
inventoryScripts.expect.element('#inventory_script_form').visible;
inventoryScripts.section.edit.expect.element('@title').visible;
inventoryScripts.section.edit.expect.element('@title').text.contain(data.inventoryScript.name);
inventoryScripts.section.edit.expect.element('@title').text.not.equal(data.inventoryScript.name);
inventoryScripts.expect.element('@save').visible;
inventoryScripts.expect.element('@save').enabled;
client.end();
}
};

View File

@ -0,0 +1,50 @@
import { getNotificationTemplate } from '../fixtures';
const data = {};
module.exports = {
before: (client, done) => {
getNotificationTemplate('test-actions')
.then(obj => { data.notification = obj; })
.then(done);
},
'copy notification template': client => {
const notifications = client.page.notificationTemplates();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
notifications.navigate();
notifications.waitForElementVisible('div.spinny');
notifications.waitForElementNotVisible('div.spinny');
notifications.section.list.expect.element('smart-search').visible;
notifications.section.list.expect.element('smart-search input').enabled;
notifications.section.list
.sendKeys('smart-search input', `id:>${data.notification.id - 1} id:<${data.notification.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
notifications.waitForElementVisible('div.spinny');
notifications.waitForElementNotVisible('div.spinny');
notifications.expect.element(`#notification_templates_table tr[id="${data.notification.id}"]`).visible;
notifications.expect.element('i[class*="copy"]').visible;
notifications.expect.element('i[class*="copy"]').enabled;
notifications.click('i[class*="copy"]');
notifications.waitForElementVisible('div.spinny');
notifications.waitForElementNotVisible('div.spinny');
notifications.expect.element('#notification_template_form').visible;
notifications.section.edit.expect.element('@title').visible;
notifications.section.edit.expect.element('@title').text.contain(data.notification.name);
notifications.section.edit.expect.element('@title').text.not.equal(data.notification.name);
notifications.expect.element('@save').visible;
notifications.expect.element('@save').enabled;
client.end();
}
};

View File

@ -0,0 +1,51 @@
import { getProject } from '../fixtures';
const data = {};
module.exports = {
before: (client, done) => {
getProject('test-actions')
.then(obj => { data.project = obj; })
.then(done);
},
'copy project': client => {
const projects = client.page.projects();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
projects.navigate();
projects.waitForElementVisible('div.spinny');
projects.waitForElementNotVisible('div.spinny');
projects.section.list.expect.element('smart-search').visible;
projects.section.list.section.search.expect.element('@input').enabled;
projects.section.list.section.search
.sendKeys('@input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`)
.sendKeys('@input', client.Keys.ENTER);
projects.waitForElementVisible('div.spinny');
projects.waitForElementNotVisible('div.spinny');
projects.section.list.expect.element('@badge').text.equal('1');
projects.expect.element(`#projects_table tr[id="${data.project.id}"]`).visible;
projects.expect.element('i[class*="copy"]').visible;
projects.expect.element('i[class*="copy"]').enabled;
projects.click('i[class*="copy"]');
projects.waitForElementVisible('div.spinny');
projects.waitForElementNotVisible('div.spinny');
projects.expect.element('#project_form').visible;
projects.section.edit.expect.element('@title').visible;
projects.section.edit.expect.element('@title').text.contain(data.project.name);
projects.section.edit.expect.element('@title').text.not.equal(data.project.name);
projects.expect.element('@save').visible;
projects.expect.element('@save').enabled;
client.end();
}
};

View File

@ -0,0 +1,214 @@
import { post } from '../api';
import {
getInventoryScript,
getInventorySource,
getJobTemplate,
getOrCreate,
getOrganization,
getProject,
getUser,
getWorkflowTemplate,
} from '../fixtures';
let data;
const promptHeader = '#prompt-header';
const promptWarning = '#prompt-body';
const promptResource = 'span[class="Prompt-warningResourceTitle"]';
const promptResourceCount = 'span[class="badge List-titleBadge"]';
const promptCancelButton = '#prompt_cancel_btn';
const promptActionButton = '#prompt_action_btn';
const promptCloseButton = '#prompt-header + div i[class*="times-circle"]';
module.exports = {
before: (client, done) => {
const resources = [
getUser('test-warnings'),
getOrganization('test-warnings'),
getWorkflowTemplate('test-warnings'),
getProject('test-warnings'),
getJobTemplate('test-warnings'),
getInventoryScript('external-org'),
getInventorySource('external-org'),
];
Promise.all(resources)
.then(([user, org, workflow, project, template, script, source]) => {
const unique = 'unified_job_template';
const nodes = workflow.related.workflow_nodes;
const nodePromise = getOrCreate(nodes, { [unique]: source.id }, [unique]);
const permissions = [
[org, 'admin_role'],
[workflow, 'admin_role'],
[project, 'admin_role'],
[template, 'admin_role'],
[script, 'read_role'],
];
const assignments = permissions
.map(([resource, name]) => resource.summary_fields.object_roles[name])
.map(role => `/api/v2/roles/${role.id}/users/`)
.map(url => post(url, { id: user.id }))
.concat([nodePromise]);
Promise.all(assignments)
.then(() => { data = { user, project, source, template, workflow }; })
.then(done);
});
},
'verify job template delete warning': client => {
const templates = client.page.templates();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
templates.navigate();
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('smart-search').visible;
templates.expect.element('smart-search input').enabled;
templates
.sendKeys('smart-search input', `id:>${data.template.id - 1} id:<${data.template.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
templates.expect.element(`#row-${data.template.id}`).visible;
templates.expect.element('i[class*="trash"]').visible;
templates.expect.element('i[class*="trash"]').enabled;
templates.click('i[class*="trash"]');
templates.expect.element(promptHeader).visible;
templates.expect.element(promptWarning).visible;
templates.expect.element(promptResource).visible;
templates.expect.element(promptResourceCount).visible;
templates.expect.element(promptCancelButton).visible;
templates.expect.element(promptActionButton).visible;
templates.expect.element(promptCloseButton).visible;
templates.expect.element(promptCancelButton).enabled;
templates.expect.element(promptActionButton).enabled;
templates.expect.element(promptCloseButton).enabled;
templates.expect.element(promptHeader).text.contain('DELETE');
templates.expect.element(promptHeader).text.contain(`${data.template.name.toUpperCase()}`);
templates.expect.element(promptWarning).text.contain('job template');
templates.expect.element(promptResource).text.contain('Workflow Job Template Nodes');
templates.expect.element(promptResourceCount).text.contain('1');
templates.click(promptCancelButton);
templates.expect.element(promptHeader).not.visible;
client.end();
},
'verify workflow template delete warning': client => {
const templates = client.page.templates();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
templates.navigate();
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('smart-search').visible;
templates.expect.element('smart-search input').enabled;
templates
.sendKeys('smart-search input', `id:>${data.workflow.id - 1} id:<${data.workflow.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
templates.expect.element(`#row-${data.workflow.id}`).visible;
templates.expect.element('i[class*="trash"]').visible;
templates.expect.element('i[class*="trash"]').enabled;
templates.click('i[class*="trash"]');
templates.expect.element(promptHeader).visible;
templates.expect.element(promptWarning).visible;
templates.expect.element(promptCancelButton).visible;
templates.expect.element(promptActionButton).visible;
templates.expect.element(promptCloseButton).visible;
templates.expect.element(promptCancelButton).enabled;
templates.expect.element(promptActionButton).enabled;
templates.expect.element(promptCloseButton).enabled;
templates.expect.element(promptHeader).text.contain('DELETE');
templates.expect.element(promptHeader).text.contain(`${data.workflow.name.toUpperCase()}`);
templates.expect.element(promptWarning).text.contain('workflow template');
templates.click(promptCancelButton);
templates.expect.element(promptHeader).not.visible;
client.end();
},
'verify workflow restricted copy warning': client => {
const templates = client.page.templates();
client.useCss();
client.resizeWindow(1200, 800);
client.login(data.user.username);
client.waitForAngular();
templates.navigate();
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('smart-search').visible;
templates.expect.element('smart-search input').enabled;
templates
.sendKeys('smart-search input', `id:>${data.workflow.id - 1} id:<${data.workflow.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
templates.expect.element(`#row-${data.workflow.id}`).visible;
templates.expect.element('i[class*="copy"]').visible;
templates.expect.element('i[class*="copy"]').enabled;
templates.click('i[class*="copy"]');
templates.expect.element(promptHeader).visible;
templates.expect.element(promptWarning).visible;
templates.expect.element(promptCancelButton).visible;
templates.expect.element(promptActionButton).visible;
templates.expect.element(promptCloseButton).visible;
templates.expect.element(promptCancelButton).enabled;
templates.expect.element(promptActionButton).enabled;
templates.expect.element(promptCloseButton).enabled;
templates.expect.element(promptHeader).text.contain('COPY WORKFLOW');
templates.expect.element(promptWarning).text.contain('Unified Job Templates');
templates.expect.element(promptWarning).text.contain(`${data.source.name}`);
templates.click(promptCancelButton);
templates.expect.element(promptHeader).not.visible;
client.end();
},
};

View File

@ -0,0 +1,161 @@
import {
getInventorySource,
getJobTemplate,
getProject,
getWorkflowTemplate
} from '../fixtures';
let data;
module.exports = {
before: (client, done) => {
const resources = [
getInventorySource('test-actions'),
getJobTemplate('test-actions'),
getProject('test-actions'),
getWorkflowTemplate('test-actions'),
];
Promise.all(resources)
.then(([source, template, project, workflow]) => {
data = { source, template, project, workflow };
done();
});
},
'copy job template': client => {
const templates = client.page.templates();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
templates.navigate();
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('smart-search').visible;
templates.expect.element('smart-search input').enabled;
templates
.sendKeys('smart-search input', `id:>${data.template.id - 1} id:<${data.template.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
templates.expect.element(`#row-${data.template.id}`).visible;
templates.expect.element('i[class*="copy"]').visible;
templates.expect.element('i[class*="copy"]').enabled;
templates.click('i[class*="copy"]');
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('#job_template_form').visible;
templates.section.addJobTemplate.expect.element('@title').visible;
templates.section.addJobTemplate.expect.element('@title').text.contain(data.template.name);
templates.section.addJobTemplate.expect.element('@title').text.not.equal(data.template.name);
templates.expect.element('@save').visible;
templates.expect.element('@save').enabled;
client.end();
},
'copy workflow template': client => {
const templates = client.page.templates();
client.useCss();
client.resizeWindow(1200, 800);
client.login();
client.waitForAngular();
templates.navigate();
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('smart-search').visible;
templates.expect.element('smart-search input').enabled;
templates
.sendKeys('smart-search input', `id:>${data.workflow.id - 1} id:<${data.workflow.id + 1}`)
.sendKeys('smart-search input', client.Keys.ENTER);
templates.waitForElementVisible('div.spinny');
templates.waitForElementNotVisible('div.spinny');
templates.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
templates.expect.element(`#row-${data.workflow.id}`).visible;
templates.expect.element('i[class*="copy"]').visible;
templates.expect.element('i[class*="copy"]').enabled;
templates
.click('i[class*="copy"]')
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny')
.waitForAngular();
templates.expect.element('#workflow_job_template_form').visible;
templates.section.editWorkflowJobTemplate.expect.element('@title').visible;
templates.section.editWorkflowJobTemplate.expect.element('@title').text.contain(data.workflow.name);
templates.section.editWorkflowJobTemplate.expect.element('@title').text.not.equal(data.workflow.name);
templates.expect.element('@save').visible;
templates.expect.element('@save').enabled;
client
.useXpath()
.pause(1000)
.waitForElementVisible('//*[text()=" Workflow Editor"]')
.click('//*[text()=" Workflow Editor"]')
.useCss()
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny')
.waitForAngular();
client.expect.element('#workflow-modal-dialog').visible;
client.expect.element('#workflow-modal-dialog span[class^="badge"]').visible;
client.expect.element('#workflow-modal-dialog span[class^="badge"]').text.equal('3');
client.expect.element('div[class="WorkflowMaker-title"]').visible;
client.expect.element('div[class="WorkflowMaker-title"]').text.contain(data.workflow.name);
client.expect.element('div[class="WorkflowMaker-title"]').text.not.equal(data.workflow.name);
client.expect.element('#workflow-modal-dialog i[class*="fa-cog"]').visible;
client.expect.element('#workflow-modal-dialog i[class*="fa-cog"]').enabled;
client.click('#workflow-modal-dialog i[class*="fa-cog"]');
client.waitForElementVisible('workflow-controls');
client.waitForElementVisible('div[class*="-zoomPercentage"]');
client.click('i[class*="fa-home"]').expect.element('div[class*="-zoomPercentage"]').text.equal('100%');
client.click('i[class*="fa-minus"]').expect.element('div[class*="-zoomPercentage"]').text.equal('90%');
client.click('i[class*="fa-minus"]').expect.element('div[class*="-zoomPercentage"]').text.equal('80%');
client.click('i[class*="fa-minus"]').expect.element('div[class*="-zoomPercentage"]').text.equal('70%');
client.click('i[class*="fa-minus"]').expect.element('div[class*="-zoomPercentage"]').text.equal('60%');
client.expect.element('#node-1').visible;
client.expect.element('#node-2').visible;
client.expect.element('#node-3').visible;
client.expect.element('#node-4').visible;
client.expect.element('#node-1 text').text.not.equal('').after(5000);
client.expect.element('#node-2 text').text.not.equal('').after(5000);
client.expect.element('#node-3 text').text.not.equal('').after(5000);
client.expect.element('#node-4 text').text.not.equal('').after(5000);
const checkNodeText = (selector, text) => client.getText(selector, ({ value }) => {
client.assert.equal(text.indexOf(value.replace('...', '')) >= 0, true);
});
checkNodeText('#node-1 text', 'START');
checkNodeText('#node-2 text', data.project.name);
checkNodeText('#node-3 text', data.template.name);
checkNodeText('#node-4 text', data.source.name);
templates.expect.element('@save').visible;
templates.expect.element('@save').enabled;
client.end();
}
};

View File

@ -113,7 +113,7 @@ module.exports = {
client.expect.element('.at-Panel smart-search').visible;
client.expect.element('.at-Panel smart-search input').enabled;
client.sendKeys('.at-Panel smart-search input', `id:${data.jobTemplate.id}`);
client.sendKeys('.at-Panel smart-search input', `id:>${data.jobTemplate.id - 1} id:<${data.jobTemplate.id + 1}`);
client.sendKeys('.at-Panel smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').not.visible;
@ -176,7 +176,7 @@ module.exports = {
client.expect.element('div[ui-view="related"]').visible;
client.expect.element('div[ui-view="related"] smart-search input').enabled;
client.sendKeys('div[ui-view="related"] smart-search input', `id:${adminRole.id}`);
client.sendKeys('div[ui-view="related"] smart-search input', `id:>${adminRole.id - 1} id:<${adminRole.id + 1}`);
client.sendKeys('div[ui-view="related"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').not.visible;
@ -231,7 +231,7 @@ module.exports = {
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.notification.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.notification.id - 1} id:<${data.notification.id + 1}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -282,7 +282,7 @@ module.exports = {
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.organization.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.organization.id - 1} id:<${data.organization.id + 1}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -333,7 +333,7 @@ module.exports = {
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.inventory.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.inventory.id - 1} id:<${data.inventory.id + 1}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -393,7 +393,7 @@ module.exports = {
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.inventoryScript.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.inventoryScript.id - 1} id:<${data.inventoryScript.id + 1}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -455,7 +455,7 @@ module.exports = {
client.expect.element('div[ui-view="related"]').visible;
client.expect.element('div[ui-view="related"] smart-search input').enabled;
client.sendKeys('div[ui-view="related"] smart-search input', `id:${data.user.id}`);
client.sendKeys('div[ui-view="related"] smart-search input', `id:>${data.user.id - 1} id:<${data.user.id + 1}`);
client.sendKeys('div[ui-view="related"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').not.visible;
@ -514,7 +514,7 @@ module.exports = {
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.project.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -566,7 +566,7 @@ module.exports = {
client.expect.element('div[ui-view="list"] smart-search').visible;
client.expect.element('div[ui-view="list"] smart-search input').enabled;
client.sendKeys('div[ui-view="list"] smart-search input', `id:${data.credential.id}`);
client.sendKeys('div[ui-view="list"] smart-search input', `id:>${data.credential.id - 1} id:<${data.credential.id + 1}`);
client.sendKeys('div[ui-view="list"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -618,7 +618,7 @@ module.exports = {
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.team.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.team.id - 1} id:<${data.team.id + 1}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
@ -684,7 +684,6 @@ module.exports = {
.contains('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
});
});
client.end();
},
'check host recent jobs popup for unsanitized content': client => {
const itemRow = `#hosts_table tr[id="${data.host.id}"]`;

View File

@ -1,2 +1,2 @@
import './components';
import './models';

View File

@ -0,0 +1,25 @@
describe('Models | BaseModel', () => {
let baseModel;
beforeEach(() => {
angular.mock.module('at.lib.services');
angular.mock.module('at.lib.models');
});
beforeEach(angular.mock.inject(($injector) => {
baseModel = new ($injector.get('BaseModel'))('test');
}));
describe('parseRequestConfig', () => {
it('always returns the expected configuration', () => {
const { parseRequestConfig } = baseModel;
const data = { name: 'foo' };
expect(parseRequestConfig('get')).toEqual({ method: 'get', resource: undefined });
expect(parseRequestConfig('get', 1)).toEqual({ method: 'get', resource: 1 });
expect(parseRequestConfig('post', { data })).toEqual({ method: 'post', data });
expect(parseRequestConfig(['get', 'post'], [1, 2], { data }))
.toEqual({ resource: [1, 2], method: ['get', 'post'] });
});
});
});

View File

@ -0,0 +1,5 @@
// Import angular and angular-mocks to the global scope
import 'angular-mocks';
// Import tests
import './base.unit';