From 894f0cf2c525220050d5c171ddcb585b22be7c1a Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sat, 2 Dec 2017 22:02:52 -0500 Subject: [PATCH 01/22] update current workflow copy implementation to be compatible with recent api changes --- .../src/templates/copy-template/template-copy.service.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/templates/copy-template/template-copy.service.js b/awx/ui/client/src/templates/copy-template/template-copy.service.js index d4ac1e8adf..e55e0d2da3 100644 --- a/awx/ui/client/src/templates/copy-template/template-copy.service.js +++ b/awx/ui/client/src/templates/copy-template/template-copy.service.js @@ -61,13 +61,16 @@ Rest.setUrl(url); return Rest.get(); }, - copyWorkflow: function(id) { + getWorkflowCopyName: function(baseName) { + return `${baseName}@${moment().format('h:mm:ss a')}`; + }, + copyWorkflow: function(id, name) { let url = GetBasePath('workflow_job_templates'); url = url + id + '/copy'; Rest.setUrl(url); - return Rest.post(); + return Rest.post({ name }); } }; } From 73fa8521d076f58c2642ff0079a1cb131590dd48 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sat, 2 Dec 2017 22:06:01 -0500 Subject: [PATCH 02/22] add e2e test for job and workflow template copy --- awx/ui/test/e2e/fixtures.js | 62 +++++-- .../e2e/tests/test-templates-list-actions.js | 161 ++++++++++++++++++ 2 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 awx/ui/test/e2e/tests/test-templates-list-actions.js diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js index caaabb5ff3..403028f252 100644 --- a/awx/ui/test/e2e/fixtures.js +++ b/awx/ui/test/e2e/fixtures.js @@ -10,11 +10,10 @@ import { } 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 +21,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 } }; @@ -246,6 +243,47 @@ const getJobTemplate = (namespace = session) => { }))); }; +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 = all(resources) + .then(spread((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) => all([ + getOrCreate(projectNode.related.success_nodes, { id: jobNode.id }), + getOrCreate(jobNode.related.success_nodes, { id: sourceNode.id }), + ]); + + return all(nodes).then(spread(createSuccessNodes)); + })); + + return all([workflowTemplatePromise, workflowNodePromise]) + .then(spread((workflowTemplate, nodes) => workflowTemplate)); +}; + const getAuditor = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate('/users/', { username: `auditor-${uuid().substr(0, 8)}`, @@ -334,21 +372,23 @@ module.exports = { getAdminAWSCredential, getAdminMachineCredential, getAuditor, + getHost, getInventory, getInventoryScript, getInventorySource, getInventorySourceSchedule, + getJob, getJobTemplate, getJobTemplateAdmin, getJobTemplateSchedule, getNotificationTemplate, - getOrCreate, getOrganization, + getProject, getProjectAdmin, getSmartInventory, getTeam, getUpdatedProject, getUser, - getJob, - getHost, + getUser, + getWorkflowTemplate, }; diff --git a/awx/ui/test/e2e/tests/test-templates-list-actions.js b/awx/ui/test/e2e/tests/test-templates-list-actions.js new file mode 100644 index 0000000000..000338126e --- /dev/null +++ b/awx/ui/test/e2e/tests/test-templates-list-actions.js @@ -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(); + } +}; From cfba11f8d799b3fd5e2ce1954cc233b3b0a75db4 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 3 Dec 2017 23:26:58 -0500 Subject: [PATCH 03/22] slight cleanup of templates list controller lint / fix all of the indentation issues smaller functions use a variable for any string a user sees --- .../templates/list-templates.controller.js | 723 ++++++++++-------- 1 file changed, 406 insertions(+), 317 deletions(-) diff --git a/awx/ui/client/features/templates/list-templates.controller.js b/awx/ui/client/features/templates/list-templates.controller.js index 1f6e156e4a..9e5d2b18ef 100644 --- a/awx/ui/client/features/templates/list-templates.controller.js +++ b/awx/ui/client/features/templates/list-templates.controller.js @@ -1,91 +1,403 @@ -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) 2017 Ansible, Inc. + * + * All Rights Reserved + ************************************************ */ - vm.strings = strings; +const ALERT_MISSING = 'Template parameter is missing'; +const ALERT_NO_PERMISSION = 'You do not have permission to perform this action.'; +const ALERT_UNKNOWN = 'We were unable to determine this template\'s type'; +const ALERT_UNKNOWN_COPY = `${ALERT_UNKNOWN} while copying.`; +const ALERT_UNKNOWN_DELETE = `${ALERT_UNKNOWN} while deleting.`; +const ALERT_UNKNOWN_EDIT = `${ALERT_UNKNOWN} while routing to edit.`; +const ALERT_UNKNOWN_LAUNCH = `${ALERT_UNKNOWN} while launching.`; +const ALERT_UNKNOWN_SCHEDULE = `${ALERT_UNKNOWN} while routing to schedule.`; +const ERROR_EDIT = 'Error: Unable to edit template'; +const ERROR_DELETE = 'Error: Unable to delete template'; +const ERROR_LAUNCH = 'Error: Unable to launch template'; +const ERROR_UNKNOWN = 'Error: Unable to determine template type'; +const ERROR_JOB_SCHEDULE = 'Error: Unable to schedule job'; +const ERROR_TEMPLATE_COPY = 'Error: Unable to copy job template'; +const ERROR_WORKFLOW_COPY = 'Error: Unable to copy workflow job template'; - // 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) { +const JOB_TEMPLATE_ALIASES = ['job_template', 'Job Template']; +const WORKFLOW_TEMPLATE_ALIASES = ['workflow_job_template', 'Workflow Job Template']; + +const isJobTemplate = obj => _.includes(JOB_TEMPLATE_ALIASES, _.get(obj, 'type')); +const isWorkflowTemplate = obj => _.includes(WORKFLOW_TEMPLATE_ALIASES, _.get(obj, 'type')); + +function TemplatesListController ( + $scope, + $rootScope, + Alert, + TemplateList, + Prompt, + ProcessErrors, + GetBasePath, + InitiatePlaybookRun, + Wait, + $state, + $filter, + Dataset, + rbacUiControlService, + TemplatesService, + qs, + i18n, + JobTemplate, + WorkflowJobTemplate, + TemplatesStrings, + $q, + Empty, + i18n, + PromptService, +) { + const jobTemplate = new JobTemplate(); + const list = TemplateList; + + init(); + + function init () { + $scope.canAdd = false; + + rbacUiControlService.canAdd('job_templates').then(params => { + $scope.canAddJobTemplate = params.canAdd; + }); + + rbacUiControlService.canAdd('workflow_job_templates').then(params => { $scope.canAddWorkflowJobTemplate = params.canAdd; }); - $scope.$watchGroup(["canAddJobTemplate", "canAddWorkflowJobTemplate"], function() { - if ($scope.canAddJobTemplate || $scope.canAddWorkflowJobTemplate) { - $scope.canAdd = true; + + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + $scope.options = {}; + + $rootScope.flashMessage = null; + } + + $scope.$on(`${list.iterator}_options`, (event, data) => { + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection('templates', () => { + optionsRequestDataProcessing(); + }); + + $scope.$on('ws-jobs', () => { + const path = GetBasePath(list.basePath) || GetBasePath(list.name); + qs.search(path, $state.params[`${list.iterator}_search`]) + .then(searchResponse => { + $scope[`${list.iterator}_dataset`] = searchResponse.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + }); + }); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing () { + $scope[list.name].forEach((item, idx) => { + const itm = $scope[list.name][idx]; + // Set the item type label + if (list.fields.type && _.has($scope.options, 'type.choices')) { + $scope.options.type.choices.forEach(choice => { + if (choice[0] === item.type) { + [itm.type_label] = choice; + } + }); + } + }); + } + + $scope.editJobTemplate = template => { + if (!template) { + Alert(ERROR_EDIT, ALERT_MISSING); + return; + } + + if (isJobTemplate(template)) { + $state.transitionTo('templates.editJobTemplate', { job_template_id: template.id }); + } else if (isWorkflowTemplate(template)) { + $state.transitionTo('templates.editWorkflowJobTemplate', { workflow_job_template_id: template.id }); } else { - $scope.canAdd = false; + Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_EDIT); } - }); - - $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 ${template.summary_fields.modified_by.username}`; - } - 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); + $scope.submitJob = template => { + if (!template) { + Alert(ERROR_LAUNCH, ALERT_MISSING); + return; } - // 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 ${template.summary_fields.modified_by.username}`; - // } - - if (val === "") { - val = undefined; + if (isJobTemplate(template)) { + submitJobTemplate(template) + } else if (isWorkflowTemplate(template)) { + InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' }); + } else { + Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_LAUNCH); } - 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; - }, {}); + $scope.scheduleJob = template => { + if (!template) { + Alert(ERROR_JOB_SCHEDULE, ALERT_MISSING); + return; + } - // 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.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id); + if (isJobTemplate(template)) { + $state.go('jobTemplateSchedules', { id: template.id }); + } else if (isWorkflowTemplate(template)) { + $state.go('workflowJobTemplateSchedules', { id: template.id }); + } else { + Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_SCHEDULE); + } + }; - vm.submitJob = function(template) { + $scope.deleteJobTemplate = template => { + if (!template) { + Alert(ERROR_DELETE, ALERT_MISSING); + return; + } + + if (isWorkflowTemplate(template)) { + const body = TemplatesStrings.get('deleteResource.CONFIRM', 'workflow job template'); + $scope.displayTemplateDeletePrompt(template, body); + } else if (isJobTemplate(template)) { + jobTemplate.getDependentResourceCounts(template.id) + .then(counts => { + const body = buildTemplateDeletePromptHTML(counts); + $scope.displayTemplateDeletePrompt(template, body); + }); + } else { + Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_DELETE); + } + }; + + $scope.copyTemplate = template => { + if (!template) { + Alert(ERROR_TEMPLATE_COPY, ALERT_MISSING); + return; + } + + if (isJobTemplate(template)) { + $scope.copyJobTemplate(template); + } else if (isWorkflowTemplate(template)) { + $scope.copyWorkflowJobTemplate(template); + } else { + Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_COPY); + } + }; + + $scope.copyJobTemplate = template => { + Wait('start'); + new JobTemplate('get', template.id) + .then(model => model.copy()) + .then(({ id }) => { + const params = { job_template_id: id }; + $state.go('templates.editJobTemplate', 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.copyWorkflowJobTemplate = template => { + Wait('start'); + new WorkflowJobTemplate('get', template.id) + .then(model => model.extend('GET', 'copy')) + .then(model => { + const action = () => { + model.copy() + .then(({ id }) => { + const params = { workflow_job_template_id: id }; + $state.go('templates.editWorkflowJobTemplate', params, { reload: true }); + }) + .catch(({ data, status }) => { + const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; + ProcessErrors($scope, data, status, null, params); + }); + }; + if (model.get('related.copy.can_copy_without_user_input')) { + action(); + } else if (model.get('related.copy.can_copy')) { + Prompt({ + hdr: 'Copy Workflow', + action, + actionText: 'COPY', + class: 'Modal-primaryButton', + body: buildWorkflowCopyPromptHTML(model.get('related.copy')) + }); + } else { + Alert(ERROR_WORKFLOW_COPY, ALERT_NO_PERMISSION); + } + }) + .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.displayTemplateDeletePrompt = (template, body) => { + const action = () => { + function handleSuccessfulDelete (isWorkflow) { + let reloadListStateParams = null; + let stateParamID; + + if (isWorkflow) { + stateParamID = $state.params.workflow_job_template_id; + } else { + stateParamID = $state.params.job_template_id; + } + + const templateSearch = _.get($state.params, 'template_search'); + const { page } = templateSearch; + + if ($scope.templates.length === 1 && !_.isEmpty(page) && page !== '1') { + reloadListStateParams = _.cloneDeep($state.params); + + const pageNum = (parseInt(reloadListStateParams.template_search.page, 0) - 1); + reloadListStateParams.template_search.page = pageNum.toString(); + } + + if (parseInt(stateParamID, 0) === template.id) { + $state.go('templates', reloadListStateParams, { reload: true }); + } else { + $state.go('.', reloadListStateParams, { reload: true }); + } + + Wait('stop'); + } // end handler + + let deleteServiceMethod; + let failMsg; + + if (isWorkflowTemplate(template)) { + deleteServiceMethod = TemplatesService.deleteWorkflowJobTemplate; + failMsg = 'Call to delete workflow job template failed. DELETE returned status: '; + } else if (isJobTemplate(template)) { + deleteServiceMethod = TemplatesService.deleteJobTemplate; + failMsg = 'Call to delete job template failed. DELETE returned status: '; + } else { + Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_DELETE); + return; + } + + $('#prompt-modal').modal('hide'); + Wait('start'); + + deleteServiceMethod(template.id) + .then(() => handleSuccessfulDelete(isWorkflowTemplate(template))) + .catch(res => { + ProcessErrors($scope, res.data, res.status, null, { + hdr: 'Error!', + msg: `${failMsg} ${res.status}.` + }); + }); + }; // end action + + Prompt({ + action, + actionText: 'DELETE', + body, + hdr: i18n._('Delete'), + resourceName: $filter('sanitize')(template.name) + }); + }; + + function buildTemplateDeletePromptHTML (dependentResourceCounts) { + const invalidateRelatedLines = []; + + let bodyHTML = ` +
+ ${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')} +
`; + + dependentResourceCounts.forEach(countObj => { + if (countObj.count && countObj.count > 0) { + invalidateRelatedLines.push(`
+ + ${countObj.label} + + + ${countObj.count} + +
`); + } + }); + + if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { + bodyHTML = ` +
+ ${TemplatesStrings.get('deleteResource.USED_BY', 'job template')} + ${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')} +
`; + invalidateRelatedLines.forEach(invalidateRelatedLine => { + bodyHTML += invalidateRelatedLine; + }); + } + + return bodyHTML; + } + + function buildWorkflowCopyPromptHTML (data) { + const { + credentials_unable_to_copy, + inventories_unable_to_copy, + job_templates_unable_to_copy + } = data; + + let itemsHTML = ''; + + if (job_templates_unable_to_copy.length > 0) { + itemsHTML += '
Unified Job Templates that cannot be copied
    '; + _.forOwn(job_templates_unable_to_copy, ujt => { + if (ujt) { + itemsHTML += `
  • '${ujt}
  • `; + } + }); + itemsHTML += '
'; + } + + if (inventories_unable_to_copy.length > 0) { + itemsHTML += '
Node prompted inventories that cannot be copied
    '; + _.forOwn(inventories_unable_to_copy, inv => { + if (inv) { + itemsHTML += `
  • '${inv}
  • `; + } + }); + itemsHTML += '
'; + } + + if (credentials_unable_to_copy.length > 0) { + itemsHTML += '
Node prompted credentials that cannot be copied
    '; + _.forOwn(credentials_unable_to_copy, cred => { + if (cred) { + itemsHTML += `
  • '${cred}
  • `; + } + }); + itemsHTML += '
'; + } + + const bodyHTML = ` +
+ 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. +
+
+ ${itemsHTML} +
+ `; + + return bodyHTML; + } + + function submitJobTemplate(template) { if(template) { if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) { let jobTemplate = new JobTemplate(); @@ -149,7 +461,7 @@ function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strin else { Alert('Error: Unable to launch template', 'Template parameter is missing'); } - }; + } $scope.launchJob = () => { @@ -219,255 +531,32 @@ function ListTemplatesController (model, JobTemplate, WorkflowJobTemplate, strin 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 = ` -
- 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. -
-
`; - - // List the unified job templates user can not access - if (result.data.templates_unable_to_copy.length > 0) { - bodyHtml += '
Unified Job Templates that can not be copied
    '; - _.forOwn(result.data.templates_unable_to_copy, function(ujt) { - if(ujt) { - bodyHtml += '
  • ' + ujt + '
  • '; - } - }); - bodyHtml += '
'; - } - // List the prompted inventories user can not access - if (result.data.inventories_unable_to_copy.length > 0) { - bodyHtml += '
Node prompted inventories that can not be copied
    '; - _.forOwn(result.data.inventories_unable_to_copy, function(inv) { - if(inv) { - bodyHtml += '
  • ' + inv + '
  • '; - } - }); - bodyHtml += '
'; - } - // List the prompted credentials user can not access - if (result.data.credentials_unable_to_copy.length > 0) { - bodyHtml += '
Node prompted credentials that can not be copied
    '; - _.forOwn(result.data.credentials_unable_to_copy, function(cred) { - if(cred) { - bodyHtml += '
  • ' + cred + '
  • '; - } - }); - bodyHtml += '
'; - } - - bodyHtml += '
'; - - - 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 = `
${strings.get('deleteResource.CONFIRM', 'template')}
`; - - Prompt({ - hdr: strings.get('deleteResource.HEADER'), - resourceName: $filter('sanitize')(template.name), - body: deleteModalBody, - action: action, - actionText: 'DELETE' - }); - }; } -ListTemplatesController.$inject = [ - 'resolvedModels', +TemplatesListController.$inject = [ + '$scope', + '$rootScope', + 'Alert', + 'TemplateList', + 'Prompt', + 'ProcessErrors', + 'GetBasePath', + 'InitiatePlaybookRun', + 'Wait', + '$state', + '$filter', + 'Dataset', + 'rbacUiControlService', + 'TemplatesService', + 'QuerySet', + 'i18n', 'JobTemplateModel', 'WorkflowJobTemplateModel', 'TemplatesStrings', - '$state', - '$scope', - 'rbacUiControlService', - 'Dataset', - '$filter', - 'Alert', - 'InitiatePlaybookRun', - 'Prompt', - 'Wait', - 'ProcessErrors', - 'TemplateCopyService', - '$q', + '$q,' 'Empty', 'i18n', - 'PromptService' + 'PromptService', ]; -export default ListTemplatesController; +export default TemplatesListController; \ No newline at end of file From 78b975b2a963866160fa46b68c661010798067ea Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 00:10:17 -0500 Subject: [PATCH 04/22] add copy to base model --- awx/ui/client/lib/models/Base.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 7fafb05c75..01392eafb9 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -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 = { From c40feb52b7ad0af61e2f6e38035d18acca55727c Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 7 Dec 2017 13:45:23 -0500 Subject: [PATCH 05/22] add base model unit test --- awx/ui/test/unit/index.js | 2 +- awx/ui/test/unit/models/base.unit.js | 25 +++++++++++++++++++++++++ awx/ui/test/unit/models/index.js | 5 +++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 awx/ui/test/unit/models/base.unit.js create mode 100644 awx/ui/test/unit/models/index.js diff --git a/awx/ui/test/unit/index.js b/awx/ui/test/unit/index.js index 6fd7b1b02e..7c8967ef87 100644 --- a/awx/ui/test/unit/index.js +++ b/awx/ui/test/unit/index.js @@ -1,2 +1,2 @@ import './components'; - +import './models'; diff --git a/awx/ui/test/unit/models/base.unit.js b/awx/ui/test/unit/models/base.unit.js new file mode 100644 index 0000000000..f721c12a0a --- /dev/null +++ b/awx/ui/test/unit/models/base.unit.js @@ -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'] }); + }); + }); +}); diff --git a/awx/ui/test/unit/models/index.js b/awx/ui/test/unit/models/index.js new file mode 100644 index 0000000000..a1a9b3452a --- /dev/null +++ b/awx/ui/test/unit/models/index.js @@ -0,0 +1,5 @@ +// Import angular and angular-mocks to the global scope +import 'angular-mocks'; + +// Import tests +import './base.unit'; From 4c988fbc02076c3870c981d817cd7975ec1b9d1a Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 00:10:56 -0500 Subject: [PATCH 06/22] add WorkflowJobTemplate model --- awx/ui/client/lib/models/index.js | 38 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 6b45bcad52..4185bb0d71 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -4,22 +4,21 @@ 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 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 +30,20 @@ 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('OrganizationModel', Organization) + .service('ProjectModel', Project) + .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) - .service('WorkflowJobTemplateModel', WorkflowJobTemplate); + .service('WorkflowJobTemplateModel', WorkflowJobTemplate) + .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode); export default MODULE_NAME; From 8679651d4c48cfba14e96917ebf19efa2b50b12d Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 12 Feb 2018 02:44:56 -0500 Subject: [PATCH 07/22] add e2e test for template copy and delete warnings --- awx/ui/test/e2e/fixtures.js | 15 +- .../test-templates-copy-delete-warnings.js | 214 ++++++++++++++++++ 2 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 awx/ui/test/e2e/tests/test-templates-copy-delete-warnings.js diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js index 403028f252..cb3bf48c4a 100644 --- a/awx/ui/test/e2e/fixtures.js +++ b/awx/ui/test/e2e/fixtures.js @@ -58,13 +58,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/', { @@ -72,7 +67,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/', { @@ -248,7 +243,7 @@ const getWorkflowTemplate = (namespace = session) => { const workflowTemplatePromise = getOrganization(namespace) .then(organization => getOrCreate(endpoint, { - name: `${namespace}-workflow-template` + name: `${namespace}-workflow-template`, organization: organization.id, variables: '---', extra_vars: '', @@ -383,12 +378,12 @@ module.exports = { getJobTemplateSchedule, getNotificationTemplate, getOrganization, + getOrCreate, getProject, getProjectAdmin, getSmartInventory, getTeam, getUpdatedProject, getUser, - getUser, getWorkflowTemplate, }; diff --git a/awx/ui/test/e2e/tests/test-templates-copy-delete-warnings.js b/awx/ui/test/e2e/tests/test-templates-copy-delete-warnings.js new file mode 100644 index 0000000000..4feb1eb346 --- /dev/null +++ b/awx/ui/test/e2e/tests/test-templates-copy-delete-warnings.js @@ -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(); + }, +}; From e5fd483d06a9614864434ecf22481900f4d5396d Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 00:11:59 -0500 Subject: [PATCH 08/22] implement model-based copy for job templates and workflow templates --- .../templates/list-templates.controller.js | 676 ++++++++---------- .../client/features/templates/list.route.js | 46 +- .../client/features/templates/list.view.html | 6 +- .../features/templates/templates.strings.js | 31 +- .../lib/services/base-string.service.js | 5 +- .../copy-template/template-copy.service.js | 77 -- awx/ui/client/src/templates/main.js | 2 - 7 files changed, 344 insertions(+), 499 deletions(-) delete mode 100644 awx/ui/client/src/templates/copy-template/template-copy.service.js diff --git a/awx/ui/client/features/templates/list-templates.controller.js b/awx/ui/client/features/templates/list-templates.controller.js index 9e5d2b18ef..927e807656 100644 --- a/awx/ui/client/features/templates/list-templates.controller.js +++ b/awx/ui/client/features/templates/list-templates.controller.js @@ -1,148 +1,75 @@ /** *********************************************** - * Copyright (c) 2017 Ansible, Inc. + * Copyright (c) 2018 Ansible, Inc. * * All Rights Reserved ************************************************ */ - -const ALERT_MISSING = 'Template parameter is missing'; -const ALERT_NO_PERMISSION = 'You do not have permission to perform this action.'; -const ALERT_UNKNOWN = 'We were unable to determine this template\'s type'; -const ALERT_UNKNOWN_COPY = `${ALERT_UNKNOWN} while copying.`; -const ALERT_UNKNOWN_DELETE = `${ALERT_UNKNOWN} while deleting.`; -const ALERT_UNKNOWN_EDIT = `${ALERT_UNKNOWN} while routing to edit.`; -const ALERT_UNKNOWN_LAUNCH = `${ALERT_UNKNOWN} while launching.`; -const ALERT_UNKNOWN_SCHEDULE = `${ALERT_UNKNOWN} while routing to schedule.`; -const ERROR_EDIT = 'Error: Unable to edit template'; -const ERROR_DELETE = 'Error: Unable to delete template'; -const ERROR_LAUNCH = 'Error: Unable to launch template'; -const ERROR_UNKNOWN = 'Error: Unable to determine template type'; -const ERROR_JOB_SCHEDULE = 'Error: Unable to schedule job'; -const ERROR_TEMPLATE_COPY = 'Error: Unable to copy job template'; -const ERROR_WORKFLOW_COPY = 'Error: Unable to copy workflow job template'; - const JOB_TEMPLATE_ALIASES = ['job_template', 'Job Template']; const WORKFLOW_TEMPLATE_ALIASES = ['workflow_job_template', 'Workflow Job Template']; -const isJobTemplate = obj => _.includes(JOB_TEMPLATE_ALIASES, _.get(obj, 'type')); -const isWorkflowTemplate = obj => _.includes(WORKFLOW_TEMPLATE_ALIASES, _.get(obj, 'type')); +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 TemplatesListController ( - $scope, - $rootScope, - Alert, - TemplateList, - Prompt, - ProcessErrors, - GetBasePath, - InitiatePlaybookRun, - Wait, - $state, +function ListTemplatesController( $filter, + $scope, + $state, + Alert, Dataset, - rbacUiControlService, - TemplatesService, - qs, - i18n, - JobTemplate, - WorkflowJobTemplate, - TemplatesStrings, - $q, - Empty, - i18n, + InitiatePlaybookRun, + ProcessErrors, + Prompt, PromptService, + resolvedModels, + strings, + Wait, ) { - const jobTemplate = new JobTemplate(); - const list = TemplateList; + const vm = this || {}; + const [jobTemplate, workflowTemplate] = resolvedModels; - init(); + const choices = workflowTemplate.options('actions.GET.type.choices') + .concat(jobTemplate.options('actions.GET.type.choices')); - function init () { - $scope.canAdd = false; + vm.strings = strings; + vm.templateTypes = mapChoices(choices); + vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id); - rbacUiControlService.canAdd('job_templates').then(params => { - $scope.canAddJobTemplate = params.canAdd; - }); + $scope.canAddJobTemplate = jobTemplate.options('actions.POST') + $scope.canAddWorkflowJobTemplate = workflowTemplate.options('actions.POST') + $scope.canAdd = ($scope.canAddJobTemplate || $scope.canAddWorkflowJobTemplate); - rbacUiControlService.canAdd('workflow_job_templates').then(params => { - $scope.canAddWorkflowJobTemplate = params.canAdd; - }); + // smart-search + const name = 'templates'; + const iterator = 'template'; + const key = 'template_dataset'; - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - $scope.options = {}; - - $rootScope.flashMessage = null; - } - - $scope.$on(`${list.iterator}_options`, (event, data) => { - $scope.options = data.data.actions.GET; - optionsRequestDataProcessing(); + $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; }); - $scope.$watchCollection('templates', () => { - optionsRequestDataProcessing(); - }); - - $scope.$on('ws-jobs', () => { - const path = GetBasePath(list.basePath) || GetBasePath(list.name); - qs.search(path, $state.params[`${list.iterator}_search`]) - .then(searchResponse => { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - }); - }); - - // iterate over the list and add fields like type label, after the - // OPTIONS request returns, or the list is sorted/paginated/searched - function optionsRequestDataProcessing () { - $scope[list.name].forEach((item, idx) => { - const itm = $scope[list.name][idx]; - // Set the item type label - if (list.fields.type && _.has($scope.options, 'type.choices')) { - $scope.options.type.choices.forEach(choice => { - if (choice[0] === item.type) { - [itm.type_label] = choice; - } - }); - } - }); - } - - $scope.editJobTemplate = template => { + vm.runTemplate = template => { if (!template) { - Alert(ERROR_EDIT, ALERT_MISSING); + Alert(strings.get('error.LAUNCH'), strings.get('alert.MISSING_PARAMETER')); return; } if (isJobTemplate(template)) { - $state.transitionTo('templates.editJobTemplate', { job_template_id: template.id }); + runJobTemplate(template); } else if (isWorkflowTemplate(template)) { - $state.transitionTo('templates.editWorkflowJobTemplate', { workflow_job_template_id: template.id }); + runWorkflowTemplate(template); } else { - Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_EDIT); + Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_LAUNCH')); } }; - $scope.submitJob = template => { + vm.scheduleTemplate = template => { if (!template) { - Alert(ERROR_LAUNCH, ALERT_MISSING); - return; - } - - if (isJobTemplate(template)) { - submitJobTemplate(template) - } else if (isWorkflowTemplate(template)) { - InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' }); - } else { - Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_LAUNCH); - } - }; - - $scope.scheduleJob = template => { - if (!template) { - Alert(ERROR_JOB_SCHEDULE, ALERT_MISSING); + Alert(strings.get('error.SCHEDULE'), strings.get('alert.MISSING_PARAMETER')); return; } @@ -151,362 +78,329 @@ function TemplatesListController ( } else if (isWorkflowTemplate(template)) { $state.go('workflowJobTemplateSchedules', { id: template.id }); } else { - Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_SCHEDULE); + Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_SCHEDULE')); } }; - $scope.deleteJobTemplate = template => { + vm.deleteTemplate = template => { if (!template) { - Alert(ERROR_DELETE, ALERT_MISSING); + Alert(strings.get('error.DELETE'), strings.get('alert.MISSING_PARAMETER')); return; } if (isWorkflowTemplate(template)) { - const body = TemplatesStrings.get('deleteResource.CONFIRM', 'workflow job template'); - $scope.displayTemplateDeletePrompt(template, body); + displayWorkflowTemplateDeletePrompt(template); } else if (isJobTemplate(template)) { - jobTemplate.getDependentResourceCounts(template.id) - .then(counts => { - const body = buildTemplateDeletePromptHTML(counts); - $scope.displayTemplateDeletePrompt(template, body); - }); + jobTemplate.getDependentResourceCounts(template.id) + .then(counts => displayJobTemplateDeletePrompt(template, counts)); } else { - Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_DELETE); + Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_DELETE')); } }; - $scope.copyTemplate = template => { + vm.copyTemplate = template => { if (!template) { - Alert(ERROR_TEMPLATE_COPY, ALERT_MISSING); + Alert(strings.get('error.COPY'), strings.get('alert.MISSING_PARAMETER')); return; } if (isJobTemplate(template)) { - $scope.copyJobTemplate(template); + copyJobTemplate(template); } else if (isWorkflowTemplate(template)) { - $scope.copyWorkflowJobTemplate(template); + copyWorkflowTemplate(template); } else { - Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_COPY); + Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_COPY')); } }; - $scope.copyJobTemplate = template => { + 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 ${$filter('sanitize')(username)}`; + } + + 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 ${$filter('sanitize')(username)}`; + //} + + 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'); - new JobTemplate('get', template.id) + 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(({ data, status }) => { - const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; - ProcessErrors($scope, data, status, null, params); - }) + .catch(createErrorHandler('copy job template', 'POST')) .finally(() => Wait('stop')); }; - $scope.copyWorkflowJobTemplate = template => { + function copyWorkflowTemplate(template) { Wait('start'); - new WorkflowJobTemplate('get', template.id) - .then(model => model.extend('GET', 'copy')) + 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(({ data, status }) => { - const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; - ProcessErrors($scope, data, status, null, params); - }); + .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({ - hdr: 'Copy Workflow', action, - actionText: 'COPY', + actionText: strings.get('COPY'), + body: buildWorkflowCopyPromptHTML(model.get('related.copy')), class: 'Modal-primaryButton', - body: buildWorkflowCopyPromptHTML(model.get('related.copy')) + hdr: strings.get('actions.COPY_WORKFLOW'), }); } else { - Alert(ERROR_WORKFLOW_COPY, ALERT_NO_PERMISSION); + Alert(strings.get('error.COPY'), strings.get('alert.NO_PERMISSION')); } }) - .catch(({ data, status }) => { - const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; - ProcessErrors($scope, data, status, null, params); - }) + .catch(createErrorHandler('copy workflow', 'GET')) .finally(() => Wait('stop')); - }; - - $scope.displayTemplateDeletePrompt = (template, body) => { - const action = () => { - function handleSuccessfulDelete (isWorkflow) { - let reloadListStateParams = null; - let stateParamID; - - if (isWorkflow) { - stateParamID = $state.params.workflow_job_template_id; - } else { - stateParamID = $state.params.job_template_id; - } - - const templateSearch = _.get($state.params, 'template_search'); - const { page } = templateSearch; - - if ($scope.templates.length === 1 && !_.isEmpty(page) && page !== '1') { - reloadListStateParams = _.cloneDeep($state.params); - - const pageNum = (parseInt(reloadListStateParams.template_search.page, 0) - 1); - reloadListStateParams.template_search.page = pageNum.toString(); - } - - if (parseInt(stateParamID, 0) === template.id) { - $state.go('templates', reloadListStateParams, { reload: true }); - } else { - $state.go('.', reloadListStateParams, { reload: true }); - } - - Wait('stop'); - } // end handler - - let deleteServiceMethod; - let failMsg; - - if (isWorkflowTemplate(template)) { - deleteServiceMethod = TemplatesService.deleteWorkflowJobTemplate; - failMsg = 'Call to delete workflow job template failed. DELETE returned status: '; - } else if (isJobTemplate(template)) { - deleteServiceMethod = TemplatesService.deleteJobTemplate; - failMsg = 'Call to delete job template failed. DELETE returned status: '; - } else { - Alert(ERROR_UNKNOWN, ALERT_UNKNOWN_DELETE); - return; - } - - $('#prompt-modal').modal('hide'); - Wait('start'); - - deleteServiceMethod(template.id) - .then(() => handleSuccessfulDelete(isWorkflowTemplate(template))) - .catch(res => { - ProcessErrors($scope, res.data, res.status, null, { - hdr: 'Error!', - msg: `${failMsg} ${res.status}.` - }); - }); - }; // end action - - Prompt({ - action, - actionText: 'DELETE', - body, - hdr: i18n._('Delete'), - resourceName: $filter('sanitize')(template.name) - }); - }; - - function buildTemplateDeletePromptHTML (dependentResourceCounts) { - const invalidateRelatedLines = []; - - let bodyHTML = ` -
- ${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')} -
`; - - dependentResourceCounts.forEach(countObj => { - if (countObj.count && countObj.count > 0) { - invalidateRelatedLines.push(`
- - ${countObj.label} - - - ${countObj.count} - -
`); - } - }); - - if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { - bodyHTML = ` -
- ${TemplatesStrings.get('deleteResource.USED_BY', 'job template')} - ${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')} -
`; - invalidateRelatedLines.forEach(invalidateRelatedLine => { - bodyHTML += invalidateRelatedLine; - }); - } - - return bodyHTML; } - function buildWorkflowCopyPromptHTML (data) { - const { - credentials_unable_to_copy, - inventories_unable_to_copy, - job_templates_unable_to_copy - } = data; + function handleSuccessfulDelete(template) { + const { page } = _.get($state.params, 'template_search'); + let reloadListStateParams = null; - let itemsHTML = ''; - - if (job_templates_unable_to_copy.length > 0) { - itemsHTML += '
Unified Job Templates that cannot be copied
    '; - _.forOwn(job_templates_unable_to_copy, ujt => { - if (ujt) { - itemsHTML += `
  • '${ujt}
  • `; - } - }); - itemsHTML += '
'; + 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 (inventories_unable_to_copy.length > 0) { - itemsHTML += '
Node prompted inventories that cannot be copied
    '; - _.forOwn(inventories_unable_to_copy, inv => { - if (inv) { - itemsHTML += `
  • '${inv}
  • `; - } - }); - itemsHTML += '
'; + 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 }); } + } - if (credentials_unable_to_copy.length > 0) { - itemsHTML += '
Node prompted credentials that cannot be copied
    '; - _.forOwn(credentials_unable_to_copy, cred => { - if (cred) { - itemsHTML += `
  • '${cred}
  • `; - } - }); - itemsHTML += '
'; - } + 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), + }); + } - const bodyHTML = ` + 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 => `${count}`; + const buildLabel = label => ` + ${$filter('sanitize')(label)}`; + const buildCountLabel = ({ count, label }) => `
+ ${buildLabel(label)}${buildCount(count)}
`; + + 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 = `
- 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. + ${strings.get('warnings.WORKFLOW_RESTRICTED_COPY')}
- ${itemsHTML} + ${templates.length ? `
Unified Job Templates
    ` : ''} + ${templates.map(item => `
  • ${item}
  • `).join('')} + ${templates.length ? `
` : ''} +
+
+ ${credentials.length ? `
Credentials
    ` : ''} + ${credentials.map(item => `
  • ${item}
  • `).join('')} + ${credentials.length ? `
` : ''} +
+
+ ${inventories.length ? `
Inventories
    ` : ''} + ${inventories.map(item => `
  • ${item}
  • `).join('')} + ${inventories.length ? `
` : ''}
`; - return bodyHTML; + return html; } - function submitJobTemplate(template) { - if(template) { - if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) { - let jobTemplate = new JobTemplate(); + function runJobTemplate(template) { + const selectedJobTemplate = jobTemplate.create(); + const preLaunchPromises = [ + selectedJobTemplate.getLaunch(template.id), + selectedJobTemplate.optionsLaunch(template.id), + ]; - $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 { + 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 }) + }); + } - if(responses[1].data.survey_enabled) { + const promptData = { + launchConf: launchData.data, + launchOptions: launchOptions.data, + template: template.id, + prompts: PromptService.processPromptValues({ + launchConf: launchData.data, + launchOptions: launchOptions.data + }), + triggerModalOpen: true, + }; - // go out and get the survey questions - jobTemplate.getSurveyQuestions(template.id) - .then((surveyQuestionRes) => { + 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; + } + }); + } - 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.'); - } - } - else { - Alert('Error: Unable to launch template', 'Template parameter is missing'); - } + function runWorkflowTemplate(template) { + InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' }); } $scope.launchJob = () => { - - let jobLaunchData = { + 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) => { @@ -521,42 +415,30 @@ function TemplatesListController ( 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) }); - }); + }) + .catch(createErrorHandler('launch job template', 'POST')) }; } -TemplatesListController.$inject = [ - '$scope', - '$rootScope', - 'Alert', - 'TemplateList', - 'Prompt', - 'ProcessErrors', - 'GetBasePath', - 'InitiatePlaybookRun', - 'Wait', - '$state', +ListTemplatesController.$inject = [ '$filter', + '$scope', + '$state', + 'Alert', 'Dataset', - 'rbacUiControlService', - 'TemplatesService', - 'QuerySet', - 'i18n', - 'JobTemplateModel', - 'WorkflowJobTemplateModel', - 'TemplatesStrings', - '$q,' - 'Empty', - 'i18n', + 'InitiatePlaybookRun', + 'ProcessErrors', + 'Prompt', 'PromptService', + 'resolvedModels', + 'TemplatesStrings', + 'Wait', ]; -export default TemplatesListController; \ No newline at end of file +export default ListTemplatesController; diff --git a/awx/ui/client/features/templates/list.route.js b/awx/ui/client/features/templates/list.route.js index 384164b421..2750a0e8a7 100644 --- a/awx/ui/client/features/templates/list.route.js +++ b/awx/ui/client/features/templates/list.route.js @@ -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')) } - ] + ], } }; diff --git a/awx/ui/client/features/templates/list.view.html b/awx/ui/client/features/templates/list.view.html index d22d64cfb4..7759287085 100644 --- a/awx/ui/client/features/templates/list.view.html +++ b/awx/ui/client/features/templates/list.view.html @@ -89,16 +89,16 @@ + value="{{ vm.getLastRan(template) }}">
- - 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 }); diff --git a/awx/ui/client/src/templates/copy-template/template-copy.service.js b/awx/ui/client/src/templates/copy-template/template-copy.service.js deleted file mode 100644 index e55e0d2da3..0000000000 --- a/awx/ui/client/src/templates/copy-template/template-copy.service.js +++ /dev/null @@ -1,77 +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(); - }, - getWorkflowCopyName: function(baseName) { - return `${baseName}@${moment().format('h:mm:ss a')}`; - }, - copyWorkflow: function(id, name) { - let url = GetBasePath('workflow_job_templates'); - - url = url + id + '/copy'; - - Rest.setUrl(url); - return Rest.post({ name }); - } - }; - } - ]; diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index abc320451a..5d1eee77d8 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -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. From 779385ddb6be259e4fb640a790a46da889ec34a9 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 15:52:44 -0500 Subject: [PATCH 09/22] implement model based copy for projects --- .../src/projects/list/projects-list.controller.js | 15 +++++++++++++++ awx/ui/client/src/projects/projects.list.js | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js index f129fe5f7a..2fc7a60794 100644 --- a/awx/ui/client/src/projects/list/projects-list.controller.js +++ b/awx/ui/client/src/projects/list/projects-list.controller.js @@ -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 }); diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js index 7c445408b8..5bec3aefad 100644 --- a/awx/ui/client/src/projects/projects.list.js +++ b/awx/ui/client/src/projects/projects.list.js @@ -100,6 +100,15 @@ 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', + // requires future api rbac changes + //ngShow: 'project.summary_fields.user_capabilities.copy' + }, edit: { ngClick: "editProject(project.id)", awToolTip: i18n._('Edit the project'), From a02eda1beaf60ad08db2af7b7b988e72815528e7 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 15:53:22 -0500 Subject: [PATCH 10/22] add e2e test for project copy --- .../e2e/tests/test-projects-list-actions.js | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-projects-list-actions.js diff --git a/awx/ui/test/e2e/tests/test-projects-list-actions.js b/awx/ui/test/e2e/tests/test-projects-list-actions.js new file mode 100644 index 0000000000..e8bd6c0bc3 --- /dev/null +++ b/awx/ui/test/e2e/tests/test-projects-list-actions.js @@ -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(); + } +}; From e9ce9621f2f7b5c65d465829104a0de921dd6c09 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 16:19:59 -0500 Subject: [PATCH 11/22] implement model-based copy for inventories --- .../inventories-hosts/inventories/inventory.list.js | 9 +++++++++ .../inventories/list/inventory-list.controller.js | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js index f675fd57ff..a76589e299 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js @@ -100,6 +100,15 @@ 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', + // requires future api rbac changes + //ngShow: 'project.summary_fields.user_capabilities.copy' + }, view: { label: i18n._('View'), ngClick: 'editInventory(inventory)', diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js index 0846a21561..7b22a22dba 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js @@ -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}); From e0f3e4feb709cfaffb4a148e15be78f7c8c17bf7 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 16:20:27 -0500 Subject: [PATCH 12/22] add e2e test for inventory copy --- .../tests/test-inventories-list-actions.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-inventories-list-actions.js diff --git a/awx/ui/test/e2e/tests/test-inventories-list-actions.js b/awx/ui/test/e2e/tests/test-inventories-list-actions.js new file mode 100644 index 0000000000..280608b197 --- /dev/null +++ b/awx/ui/test/e2e/tests/test-inventories-list-actions.js @@ -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(); + } +}; From 29f1d695ae6b3aa523b3c8ed351c3c5552b78995 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:08:19 -0500 Subject: [PATCH 13/22] add NotificationTemplate model --- .../client/lib/models/NotificationTemplate.js | 21 +++++++++++++++++++ awx/ui/client/lib/models/index.js | 2 ++ .../list.controller.js | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 awx/ui/client/lib/models/NotificationTemplate.js diff --git a/awx/ui/client/lib/models/NotificationTemplate.js b/awx/ui/client/lib/models/NotificationTemplate.js new file mode 100644 index 0000000000..6418a7184d --- /dev/null +++ b/awx/ui/client/lib/models/NotificationTemplate.js @@ -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; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 4185bb0d71..950d0d2c73 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -13,6 +13,7 @@ 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'; @@ -39,6 +40,7 @@ angular .service('JobTemplateModel', JobTemplate) .service('MeModel', Me) .service('ModelsStrings', ModelsStrings) + .service('NotificationTemplate', NotificationTemplate) .service('OrganizationModel', Organization) .service('ProjectModel', Project) .service('UnifiedJobTemplateModel', UnifiedJobTemplate) diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index 6c3eb4b5bf..3e64db1412 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -7,7 +7,7 @@ export default ['$scope', 'Wait', 'NotificationTemplatesList', 'GetBasePath', 'Rest', 'ProcessErrors', 'Prompt', '$state', 'ngToast', '$filter', 'Dataset', 'rbacUiControlService', - 'i18n', + 'i18n', 'NotificationTemplateModel', function( $scope, Wait, NotificationTemplatesList, GetBasePath, Rest, ProcessErrors, Prompt, $state, From 09d3e6cd98c6766664352ef9d4df5cf95cdb6d0e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:09:24 -0500 Subject: [PATCH 14/22] implement model-based copy for notification templates --- .../list.controller.js | 25 ++++++++++++++++--- .../notificationTemplates.list.js | 9 +++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js index 3e64db1412..8e588d9671 100644 --- a/awx/ui/client/src/notifications/notification-templates-list/list.controller.js +++ b/awx/ui/client/src/notifications/notification-templates-list/list.controller.js @@ -7,12 +7,12 @@ export default ['$scope', 'Wait', 'NotificationTemplatesList', 'GetBasePath', 'Rest', 'ProcessErrors', 'Prompt', '$state', 'ngToast', '$filter', 'Dataset', 'rbacUiControlService', - 'i18n', 'NotificationTemplateModel', + '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'); }; diff --git a/awx/ui/client/src/notifications/notificationTemplates.list.js b/awx/ui/client/src/notifications/notificationTemplates.list.js index fc0b3dd484..e50e7547ca 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.list.js +++ b/awx/ui/client/src/notifications/notificationTemplates.list.js @@ -77,6 +77,15 @@ 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', + // requires future api rbac changes + //ngShow: 'notification_template.summary_fields.user_capabilities.copy' + }, view: { ngClick: "editNotification(notification_template.id)", label: i18n._('View'), From 21a32f90ce2896f66e852275bf0440607c4b74da Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:09:46 -0500 Subject: [PATCH 15/22] add e2e test for notification template copy --- .../tests/test-notifications-list-actions.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-notifications-list-actions.js diff --git a/awx/ui/test/e2e/tests/test-notifications-list-actions.js b/awx/ui/test/e2e/tests/test-notifications-list-actions.js new file mode 100644 index 0000000000..fc32d1553b --- /dev/null +++ b/awx/ui/test/e2e/tests/test-notifications-list-actions.js @@ -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(); + } +}; From 50d95ddc3f042ef5d6209c0c3121581dafa52fc4 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:38:41 -0500 Subject: [PATCH 16/22] implement model-based credential copy --- awx/ui/client/src/credentials/credentials.list.js | 10 +++++++++- .../list/credentials-list.controller.js | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/credentials/credentials.list.js b/awx/ui/client/src/credentials/credentials.list.js index 36f87541a7..edd30261a6 100644 --- a/awx/ui/client/src/credentials/credentials.list.js +++ b/awx/ui/client/src/credentials/credentials.list.js @@ -69,7 +69,15 @@ 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', + // requires future api rbac changes + //ngShow: 'credential.summary_fields.user_capabilities.copy' + }, view: { ngClick: "editCredential(credential.id)", label: i18n._('View'), diff --git a/awx/ui/client/src/credentials/list/credentials-list.controller.js b/awx/ui/client/src/credentials/list/credentials-list.controller.js index 6222f37000..2e8c6e87e8 100644 --- a/awx/ui/client/src/credentials/list/credentials-list.controller.js +++ b/awx/ui/client/src/credentials/list/credentials-list.controller.js @@ -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'); }; From d56f1a0120d6227a37d20685a60fd3ca0545c3eb Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:39:17 -0500 Subject: [PATCH 17/22] add e2e test for credential copy --- .../tests/test-credentials-list-actions.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-credentials-list-actions.js diff --git a/awx/ui/test/e2e/tests/test-credentials-list-actions.js b/awx/ui/test/e2e/tests/test-credentials-list-actions.js new file mode 100644 index 0000000000..11f075dc79 --- /dev/null +++ b/awx/ui/test/e2e/tests/test-credentials-list-actions.js @@ -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(); + } +}; From a680d188c0a2d39bf7b862416a290d7346eeffec Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:51:47 -0500 Subject: [PATCH 18/22] implement model based copy for inventory scripts --- .../inventory-scripts/inventory-scripts.list.js | 9 +++++++++ .../src/inventory-scripts/list/list.controller.js | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js b/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js index d81977d49d..9cfb3ba3c0 100644 --- a/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js +++ b/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js @@ -57,6 +57,15 @@ 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', + // requires future api rbac changes + //ngShow: 'inventory_script.summary_fields.user_capabilities.copy' + }, view: { ngClick: "editCustomInv(inventory_script.id)", label: i18n._('View'), diff --git a/awx/ui/client/src/inventory-scripts/list/list.controller.js b/awx/ui/client/src/inventory-scripts/list/list.controller.js index a9d1140fb0..b4307b54c8 100644 --- a/awx/ui/client/src/inventory-scripts/list/list.controller.js +++ b/awx/ui/client/src/inventory-scripts/list/list.controller.js @@ -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() { From 16153daa147bfdcaec84406f6761080eb115f251 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 5 Dec 2017 17:52:16 -0500 Subject: [PATCH 19/22] add e2e test for inventory script copy --- .../test-inventory-scripts-list-actions.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-inventory-scripts-list-actions.js diff --git a/awx/ui/test/e2e/tests/test-inventory-scripts-list-actions.js b/awx/ui/test/e2e/tests/test-inventory-scripts-list-actions.js new file mode 100644 index 0000000000..51c52798ea --- /dev/null +++ b/awx/ui/test/e2e/tests/test-inventory-scripts-list-actions.js @@ -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(); + } +}; From 0ff94c63f2fd2be66c93045a59500349a8b8e228 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 12 Feb 2018 04:15:53 -0500 Subject: [PATCH 20/22] use edit capability for showing copy on most views --- awx/ui/client/src/credentials/credentials.list.js | 3 +-- .../client/src/inventories-hosts/inventories/inventory.list.js | 3 +-- awx/ui/client/src/inventory-scripts/inventory-scripts.list.js | 3 +-- awx/ui/client/src/notifications/notificationTemplates.list.js | 3 +-- awx/ui/client/src/projects/projects.list.js | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/awx/ui/client/src/credentials/credentials.list.js b/awx/ui/client/src/credentials/credentials.list.js index edd30261a6..f381927a89 100644 --- a/awx/ui/client/src/credentials/credentials.list.js +++ b/awx/ui/client/src/credentials/credentials.list.js @@ -75,8 +75,7 @@ export default ['i18n', function(i18n) { "class": 'btn-danger btn-xs', awToolTip: i18n._('Copy credential'), dataPlacement: 'top', - // requires future api rbac changes - //ngShow: 'credential.summary_fields.user_capabilities.copy' + ngShow: 'credential.summary_fields.user_capabilities.edit' }, view: { ngClick: "editCredential(credential.id)", diff --git a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js index a76589e299..f90b953ab6 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js @@ -106,8 +106,7 @@ export default ['i18n', function(i18n) { "class": 'btn-danger btn-xs', awToolTip: i18n._('Copy inventory'), dataPlacement: 'top', - // requires future api rbac changes - //ngShow: 'project.summary_fields.user_capabilities.copy' + ngShow: 'inventory.summary_fields.user_capabilities.edit' }, view: { label: i18n._('View'), diff --git a/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js b/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js index 9cfb3ba3c0..07d8492a77 100644 --- a/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js +++ b/awx/ui/client/src/inventory-scripts/inventory-scripts.list.js @@ -63,8 +63,7 @@ export default ['i18n', function(i18n){ "class": 'btn-danger btn-xs', awToolTip: i18n._('Copy inventory scruot'), dataPlacement: 'top', - // requires future api rbac changes - //ngShow: 'inventory_script.summary_fields.user_capabilities.copy' + ngShow: 'inventory_script.summary_fields.user_capabilities.edit' }, view: { ngClick: "editCustomInv(inventory_script.id)", diff --git a/awx/ui/client/src/notifications/notificationTemplates.list.js b/awx/ui/client/src/notifications/notificationTemplates.list.js index e50e7547ca..e8bf90ffa6 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.list.js +++ b/awx/ui/client/src/notifications/notificationTemplates.list.js @@ -83,8 +83,7 @@ export default ['i18n', function(i18n){ "class": 'btn-danger btn-xs', awToolTip: i18n._('Copy notification'), dataPlacement: 'top', - // requires future api rbac changes - //ngShow: 'notification_template.summary_fields.user_capabilities.copy' + ngShow: 'notification_template.summary_fields.user_capabilities.edit' }, view: { ngClick: "editNotification(notification_template.id)", diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js index 5bec3aefad..b05d923a8b 100644 --- a/awx/ui/client/src/projects/projects.list.js +++ b/awx/ui/client/src/projects/projects.list.js @@ -106,8 +106,7 @@ export default ['i18n', function(i18n) { "class": 'btn-danger btn-xs', awToolTip: i18n._('Copy project'), dataPlacement: 'top', - // requires future api rbac changes - //ngShow: 'project.summary_fields.user_capabilities.copy' + ngShow: 'project.summary_fields.user_capabilities.edit' }, edit: { ngClick: "editProject(project.id)", From bf0683f7fe96101dac07004631f4bb54d4ac6db3 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 13 Feb 2018 16:40:20 -0500 Subject: [PATCH 21/22] replace usage of all and spread --- awx/ui/test/e2e/api.js | 2 - awx/ui/test/e2e/fixtures.js | 50 +++++++++---------- .../e2e/tests/test-auditor-read-only-forms.js | 4 +- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/awx/ui/test/e2e/api.js b/awx/ui/test/e2e/api.js index ba03b1fd21..47559c8961 100644 --- a/awx/ui/test/e2e/api.js +++ b/awx/ui/test/e2e/api.js @@ -77,6 +77,4 @@ module.exports = { post, patch, put, - all: axios.all, - spread: axios.spread }; diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js index cb3bf48c4a..ecb45db9dc 100644 --- a/awx/ui/test/e2e/fixtures.js +++ b/awx/ui/test/e2e/fixtures.js @@ -3,10 +3,8 @@ import uuid from 'uuid'; import { AWX_E2E_PASSWORD } from './settings'; import { - all, get, post, - spread } from './api'; const session = `e2e-${uuid().substr(0, 8)}`; @@ -83,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) => { @@ -101,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/', { @@ -116,7 +114,7 @@ const getAdminAWSCredential = (namespace = session) => { security_token: 'AAAAAAAAAAAAAAAA' } }); - })); + }); }; const getAdminMachineCredential = (namespace = session) => { @@ -125,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) @@ -227,15 +224,15 @@ 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) => { @@ -256,8 +253,8 @@ const getWorkflowTemplate = (namespace = session) => { getJobTemplate(namespace), ]; - const workflowNodePromise = all(resources) - .then(spread((workflowTemplate, source, project, jobTemplate) => { + const workflowNodePromise = Promise.all(resources) + .then(([workflowTemplate, source, project, jobTemplate]) => { const workflowNodes = workflowTemplate.related.workflow_nodes; const unique = 'unified_job_template'; @@ -267,16 +264,17 @@ const getWorkflowTemplate = (namespace = session) => { getOrCreate(workflowNodes, { [unique]: source.id }, [unique]), ]; - const createSuccessNodes = (projectNode, jobNode, sourceNode) => all([ + const createSuccessNodes = ([projectNode, jobNode, sourceNode]) => Promise.all([ getOrCreate(projectNode.related.success_nodes, { id: jobNode.id }), getOrCreate(jobNode.related.success_nodes, { id: sourceNode.id }), ]); - return all(nodes).then(spread(createSuccessNodes)); - })); + return Promise.all(nodes) + .then(createSuccessNodes); + }); - return all([workflowTemplatePromise, workflowNodePromise]) - .then(spread((workflowTemplate, nodes) => workflowTemplate)); + return Promise.all([workflowTemplatePromise, workflowNodePromise]) + .then(([workflowTemplate, nodes]) => workflowTemplate); }; const getAuditor = (namespace = session) => getOrganization(namespace) @@ -320,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) => { @@ -343,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) diff --git a/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js b/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js index ecce0836c5..9a9fc07225 100644 --- a/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js +++ b/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js @@ -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(); From d4e46a35cef4cdb0041099912477e49f36d1ac8a Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 13 Feb 2018 16:52:52 -0500 Subject: [PATCH 22/22] get exact match on ids --- awx/ui/test/e2e/tests/test-xss.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/awx/ui/test/e2e/tests/test-xss.js b/awx/ui/test/e2e/tests/test-xss.js index 16d84cd8ba..5d15bdfc99 100644 --- a/awx/ui/test/e2e/tests/test-xss.js +++ b/awx/ui/test/e2e/tests/test-xss.js @@ -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('<div id="xss" class="xss">test</div>'); }); }); - client.end(); }, 'check host recent jobs popup for unsanitized content': client => { const itemRow = `#hosts_table tr[id="${data.host.id}"]`;