From 92ac3054c677b2f96d129383b69907ff0dd826a1 Mon Sep 17 00:00:00 2001 From: Haokun-Chen Date: Thu, 2 Aug 2018 12:32:16 -0400 Subject: [PATCH 1/8] refactor projects list, clean up dependencies and old list generators and factory methods --- awx/ui/client/features/index.js | 2 + .../features/projects/index.controller.js | 19 + awx/ui/client/features/projects/index.js | 9 + .../client/features/projects/index.view.html | 13 + .../features/projects/projects.strings.js | 53 +++ .../projects/projectsList.controller.js | 439 ++++++++++++++++++ .../features/projects/projectsList.view.html | 91 ++++ .../projects/routes/projectsList.route.js | 91 ++++ awx/ui/client/lib/components/list/_index.less | 9 + .../lib/components/list/row-item.directive.js | 1 + .../lib/components/list/row-item.partial.html | 9 +- .../projects/add/projects-add.controller.js | 7 +- .../factories/get-project-icon.factory.js | 30 -- .../factories/get-project-tool-tip.factory.js | 38 -- .../projects/list/projects-list.controller.js | 342 -------------- awx/ui/client/src/projects/main.js | 83 +--- awx/ui/client/src/projects/projects.list.js | 128 ----- .../client/src/projects/projects.strings.js | 7 - 18 files changed, 760 insertions(+), 611 deletions(-) create mode 100644 awx/ui/client/features/projects/index.controller.js create mode 100644 awx/ui/client/features/projects/index.js create mode 100644 awx/ui/client/features/projects/index.view.html create mode 100644 awx/ui/client/features/projects/projects.strings.js create mode 100644 awx/ui/client/features/projects/projectsList.controller.js create mode 100644 awx/ui/client/features/projects/projectsList.view.html create mode 100644 awx/ui/client/features/projects/routes/projectsList.route.js delete mode 100644 awx/ui/client/src/projects/factories/get-project-icon.factory.js delete mode 100644 awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js delete mode 100644 awx/ui/client/src/projects/list/projects-list.controller.js delete mode 100644 awx/ui/client/src/projects/projects.list.js delete mode 100644 awx/ui/client/src/projects/projects.strings.js diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index f206a206bc..b816d2c65d 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -9,6 +9,7 @@ import atFeaturesTemplates from '~features/templates'; import atFeaturesUsers from '~features/users'; import atFeaturesJobs from '~features/jobs'; import atFeaturesPortalMode from '~features/portalMode'; +import atFeaturesProjects from '~features/projects'; const MODULE_NAME = 'at.features'; @@ -24,6 +25,7 @@ angular.module(MODULE_NAME, [ atFeaturesOutput, atFeaturesTemplates, atFeaturesPortalMode, + atFeaturesProjects ]); export default MODULE_NAME; diff --git a/awx/ui/client/features/projects/index.controller.js b/awx/ui/client/features/projects/index.controller.js new file mode 100644 index 0000000000..a76b7ef329 --- /dev/null +++ b/awx/ui/client/features/projects/index.controller.js @@ -0,0 +1,19 @@ +function IndexProjectsController ($scope, strings, dataset) { + const vm = this; + vm.strings = strings; + vm.count = dataset.data.count; + + $scope.$on('updateCount', (e, count) => { + if (count) { + vm.count = count; + } + }); +} + +IndexProjectsController.$inject = [ + '$scope', + 'ProjectsStrings', + 'Dataset', +]; + +export default IndexProjectsController; diff --git a/awx/ui/client/features/projects/index.js b/awx/ui/client/features/projects/index.js new file mode 100644 index 0000000000..c9e51f7fbc --- /dev/null +++ b/awx/ui/client/features/projects/index.js @@ -0,0 +1,9 @@ +import ProjectsStrings from './projects.strings'; + +const MODULE_NAME = 'at.features.projects'; + +angular + .module(MODULE_NAME, []) + .service('ProjectsStrings', ProjectsStrings); + +export default MODULE_NAME; diff --git a/awx/ui/client/features/projects/index.view.html b/awx/ui/client/features/projects/index.view.html new file mode 100644 index 0000000000..4df353660a --- /dev/null +++ b/awx/ui/client/features/projects/index.view.html @@ -0,0 +1,13 @@ +
+
+ +
+ + {{:: vm.strings.get('list.PANEL_TITLE') }} +
+ {{ vm.count }} +
+
+
+
+
diff --git a/awx/ui/client/features/projects/projects.strings.js b/awx/ui/client/features/projects/projects.strings.js new file mode 100644 index 0000000000..c09f3104d9 --- /dev/null +++ b/awx/ui/client/features/projects/projects.strings.js @@ -0,0 +1,53 @@ +function ProjectsStrings (BaseString) { + BaseString.call(this, 'projects'); + + const { t } = this; + const ns = this.projects; + + ns.list = { + PANEL_TITLE: t.s('PROJECTS'), + ROW_ITEM_LABEL_REVISION: t.s('REVISION'), + ROW_ITEM_LABEL_ORGANIZATION: t.s('ORGANIZATION'), + ROW_ITEM_LABEL_MODIFIED: t.s('LAST MODIFIED'), + ROW_ITEM_LABEL_USED: t.s('LAST USED'), + }; + + ns.update = { + GET_LATEST: t.s('Get latest SCM revision'), + UPDATE_RUNNING: t.s('SCM update currently running'), + MANUAL_PROJECT_NO_UPDATE: t.s('Manual projects do not require an SCM update'), + CANCEL_UPDATE_REQUEST: t.s('Your request to cancel the update was submitted to the task manager.'), + NO_UPDATE_INFO: t.s('There is no SCM update information available for this project. An update has not yet been completed. If you have not already done so, start an update for this project.'), + NO_PROJ_SCM_CONFIG: t.s('The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, and then run an update.'), + NO_ACCESS_OR_COMPLETED_UPDATE: t.s('Either you do not have access or the SCM update process completed'), + NO_RUNNING_UPDATE: t.s('An SCM update does not appear to be running for project: '), + }; + + ns.alert = { + NO_UPDATE: t.s('No Updates Available'), + UPDATE_CANCEL: t.s('SCM Update Cancel'), + CANCEL_NOT_ALLOWED: t.s('Cancel Not Allowed'), + NO_SCM_CONFIG: t.s('No SCM Configuration'), + UPDATE_NOT_FOUND: t.s('Update Not Found'), + }; + + ns.status = { + NOT_CONFIG: t.s('Not configured for SCM'), + NEVER_UPDATE: t.s('No SCM updates have run for this project'), + UPDATE_QUEUED: t.s('Update queued. Click for details'), + UPDATE_RUNNING: t.s('Update running. Click for details'), + UPDATE_SUCCESS: t.s('Update succeeded. Click for details'), + UPDATE_FAILED: t.s('Update failed. Click for details'), + UPDATE_MISSING: t.s('Update missing. Click for details'), + UPDATE_CANCELED: t.s('Update canceled. Click for details'), + }; + + ns.error = { + HEADER: this.error.HEADER, + CALL: this.error.CALL, + }; +} + +ProjectsStrings.$inject = ['BaseStringService']; + +export default ProjectsStrings; diff --git a/awx/ui/client/features/projects/projectsList.controller.js b/awx/ui/client/features/projects/projectsList.controller.js new file mode 100644 index 0000000000..34b488d174 --- /dev/null +++ b/awx/ui/client/features/projects/projectsList.controller.js @@ -0,0 +1,439 @@ +/** *********************************************** + * Copyright (c) 2018 Ansible, Inc. + * + * All Rights Reserved + ************************************************ */ + +const mapChoices = choices => Object.assign(...choices.map(([k, v]) => ({ [k]: v.toUpperCase() }))); + +function projectsListController ( + $filter, $scope, $rootScope, $state, $log, Dataset, Alert, Rest, + ProcessErrors, resolvedModels, strings, Wait, ngToast, + Prompt, GetBasePath, qs, ProjectUpdate, +) { + const vm = this || {}; + const [ProjectModel] = resolvedModels; + $scope.canAdd = ProjectModel.options('actions.POST'); + + vm.strings = strings; + vm.scm_choices = ProjectModel.options('actions.GET.scm_type.choices'); + vm.projectTypes = mapChoices(vm.scm_choices); + + // smart-search + vm.list = { + iterator: 'project', + name: 'projects', + basePath: 'projects', + }; + vm.dataset = Dataset.data; + vm.projects = Dataset.data.results; + // build tooltips + _.forEach(vm.projects, buildTooltips); + $rootScope.flashMessage = null; + + // when a project is added/deleted, rebuild tooltips + $scope.$watchCollection('vm.projects', () => { + _.forEach(vm.projects, buildTooltips); + }); + // show active item in the list + $scope.$watch('$state.params', () => { + const projectId = _.get($state.params, 'project_id'); + if ((projectId)) { + vm.activeId = parseInt($state.params.project_id, 10); + } else { + vm.activeId = ''; + } + }, true); + + $scope.$on('ws-jobs', (e, data) => { + $log.debug(data); + if (vm.projects) { + // Assuming we have a list of projects available + const project = vm.projects.find((p) => p.id === data.project_id); + if (project) { + // And we found the affected project + $log.debug(`Received event for project: ${project.name}`); + $log.debug(`Status changed to: ${data.status}`); + if (data.status === 'successful' || data.status === 'failed' || data.status === 'canceled') { + reloadList(); + } else { + project.scm_update_tooltip = vm.strings.get('update.UPDATE_RUNNING'); + } + project.status = data.status; + buildTooltips(project); + } + } + }); + + if ($scope.removeGoTojobResults) { + $scope.removeGoTojobResults(); + } + + $scope.removeGoTojobResults = $scope.$on('GoTojobResults', (e, data) => { + if (data.summary_fields.current_update || data.summary_fields.last_update) { + Wait('start'); + // Grab the id from summary_fields + const updateJobid = (data.summary_fields.current_update) ? + data.summary_fields.current_update.id : data.summary_fields.last_update.id; + + $state.go('output', { id: updateJobid, type: 'project' }, { reload: true }); + } else { + Alert(vm.strings.get('alert.NO_UPDATE'), vm.strings.get('update.NO_UPDATE_INFO'), 'alert-info'); + } + }); + + if ($scope.removeCancelUpdate) { + $scope.removeCancelUpdate(); + } + + $scope.removeCancelUpdate = $scope.$on('Cancel_Update', (e, url) => { + // Cancel the project update process + Rest.setUrl(url); + Rest.post() + .then(() => { + Alert(vm.strings.get('alert.UPDATE_CANCEL'), vm.strings.get('update.CANCEL_UPDATE_REQUEST'), 'alert-info'); + }) + .catch(createErrorHandler(url, 'POST')); + }); + + if ($scope.removeCheckCancel) { + $scope.removeCheckCancel(); + } + + $scope.removeCheckCancel = $scope.$on('Check_Cancel', (e, projectData) => { + // Check that we 'can' cancel the update + const url = projectData.related.cancel; + Rest.setUrl(url); + Rest.get() + .then(({ data }) => { + if (data.can_cancel) { + $scope.$emit('Cancel_Update', url); + } else { + Alert(vm.strings.get('alert.CANCEL_NOT_ALLOWED'), vm.strings.get('update.NO_ACCESS_OR_COMPLETED_UPDATE'), 'alert-info', null, null, null, null, true); + } + }) + .catch(createErrorHandler(url, 'GET')); + }); + + vm.showSCMStatus = (id) => { + // Refresh the project list + const project = vm.projects.find((p) => p.id === id); + + if ((!project.scm_type) || project.scm_type === 'Manual') { + Alert(vm.strings.get('alert.NO_SCM_CONFIG'), vm.strings.get('update.NO_PROJ_SCM_CONFIG'), 'alert-info'); + } else { + // Refresh what we have in memory + // to insure we're accessing the most recent status record + Rest.setUrl(project.url); + Rest.get() + .then(({ data }) => { + $scope.$emit('GoTojobResults', data); + }) + .catch(createErrorHandler(project.url, 'GET')); + } + }; + + vm.getLastModified = project => { + const modified = _.get(project, 'modified'); + + if (!modified) { + return undefined; + } + + const html = $filter('longDate')(modified); + + // NEED api to add field project.summary_fields.modified_by + + // const { username, id } = _.get(project, 'summary_fields.modified_by', {}); + + // if (username && id) { + // html += ` by ${$filter('sanitize')(username)}`; + // } + + return html; + }; + + vm.getLastUsed = project => { + const modified = _.get(project, 'last_job_run'); + + if (!modified) { + return undefined; + } + + const html = $filter('longDate')(modified); + + // NEED api to add last_job user information such as launch_by + + // const { id } = _.get(project, 'summary_fields.last_job', {}); + // if (id) { + // html += ` by + // ${$filter('sanitize')('placehoder')}`; + // } + return html; + }; + + vm.copyProject = project => { + Wait('start'); + ProjectModel + .create('get', project.id) + .then(model => model.copy()) + .then((copiedProj) => { + ngToast.success({ + content: ` +
+
+ +
+
+ ${vm.strings.get('SUCCESSFUL_CREATION', copiedProj.name)} +
+
`, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); + }) + .catch(createErrorHandler('copy project', 'GET')) + .finally(() => Wait('stop')); + }; + + vm.deleteProject = (id, name) => { + const action = () => { + $('#prompt-modal').modal('hide'); + Wait('start'); + ProjectModel + .request('delete', id) + .then(() => { + let reloadListStateParams = null; + + if (vm.projects.length === 1 + && $state.params.project_search + && _.has($state, 'params.project_search.page') + && $state.params.project_search.page !== '1') { + reloadListStateParams = _.cloneDeep($state.params); + reloadListStateParams.project_search.page = + (parseInt(reloadListStateParams.project_search.page, 10) - 1).toString(); + } + + if (parseInt($state.params.project_id, 10) === id) { + $state.go('^', reloadListStateParams, { reload: true }); + } else { + $state.go('.', reloadListStateParams, { reload: true }); + } + }) + .catch(createErrorHandler(`${ProjectModel.path}${id}/`, 'DELETE')) + .finally(() => { + Wait('stop'); + }); + }; + + ProjectModel.getDependentResourceCounts(id) + .then((counts) => { + const invalidateRelatedLines = []; + let deleteModalBody = `
${vm.strings.get('deleteResource.CONFIRM', 'project')}
`; + + counts.forEach(countObj => { + if (countObj.count && countObj.count > 0) { + invalidateRelatedLines.push(`
${countObj.label}${countObj.count}
`); + } + }); + + if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { + deleteModalBody = `
${vm.strings.get('deleteResource.USED_BY', 'project')} ${vm.strings.get('deleteResource.CONFIRM', 'project')}
`; + invalidateRelatedLines.forEach(invalidateRelatedLine => { + deleteModalBody += invalidateRelatedLine; + }); + } + + Prompt({ + hdr: vm.strings.get('DELETE'), + resourceName: $filter('sanitize')(name), + body: deleteModalBody, + action, + actionText: vm.strings.get('DELETE'), + }); + }); + }; + + vm.cancelUpdate = (project) => { + project.pending_cancellation = true; + Rest.setUrl(GetBasePath('projects') + project.id); + Rest.get() + .then(({ data }) => { + if (data.related.current_update) { + cancelSCMUpdate(data); + } else { + Alert(vm.strings.get('update.UPDATE_NOT_FOUND'), vm.strings.get('update.NO_RUNNING_UPDATE') + project.name, 'alert-info', undefined, undefined, undefined, undefined, true); + } + }) + .catch(createErrorHandler('get project', 'GET')); + }; + + vm.SCMUpdate = (id, event) => { + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + vm.projects.forEach((project) => { + if (project.id === id) { + if (project.scm_type === 'Manual' || (!project.scm_type)) { + // Do not respond. Button appears greyed out as if it is disabled. + // Not disabled though, because we need mouse over event + // to work. So user can click, but we just won't do anything. + // Alert('Missing SCM Setup', 'Before running an SCM update, + // edit the project and provide the SCM access information.', 'alert-info'); + } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { + // Alert('Update in Progress', 'The SCM update process is running. + // Use the Refresh button to monitor the status.', 'alert-info'); + } else { + ProjectUpdate({ scope: $scope, project_id: project.id }); + } + } + }); + }; + + function buildTooltips (project) { + project.statusIcon = getStatusIcon(project); + project.statusTip = getStatusTooltip(project); + project.scm_update_tooltip = vm.strings.get('update.GET_LATEST'); + project.scm_update_disabled = false; + + if (project.status === 'pending' || project.status === 'waiting') { + project.scm_update_disabled = true; + } + + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + project.statusTip = vm.strings.get('status.UPDATE_CANCELED'); + project.scm_update_disabled = true; + } + + if (project.status === 'running' || project.status === 'updating') { + project.scm_update_tooltip = vm.strings.get('update.UPDATE_RUNNING'); + project.scm_update_disabled = true; + } + + if (project.scm_type === 'manual') { + project.statusIcon = 'none'; + project.statusTip = vm.strings.get('status.NOT_CONFIG'); + project.scm_update_tooltip = vm.strings.get('update.MANUAL_PROJECT_NO_UPDATE'); + project.scm_update_disabled = true; + } + } + + function cancelSCMUpdate (projectData) { + Rest.setUrl(projectData.related.current_update); + Rest.get() + .then(({ data }) => { + $scope.$emit('Check_Cancel', data); + }) + .catch(createErrorHandler(projectData.related.current_update, 'GET')); + } + + function reloadList () { + Wait('start'); + const path = GetBasePath(vm.list.basePath) || GetBasePath(vm.list.name); + qs.search(path, $state.params.project_search) + .then((searchResponse) => { + vm.dataset = searchResponse.data; + vm.projects = vm.dataset.results; + }) + .finally(() => Wait('stop')); + } + + 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 getStatusIcon (project) { + let icon = 'none'; + switch (project.status) { + case 'n/a': + case 'ok': + case 'never updated': + icon = 'none'; + break; + case 'pending': + case 'waiting': + case 'new': + icon = 'none'; + break; + case 'updating': + case 'running': + icon = 'running'; + break; + case 'successful': + icon = 'success'; + break; + case 'failed': + case 'missing': + case 'canceled': + icon = 'error'; + break; + default: + break; + } + return icon; + } + + function getStatusTooltip (project) { + let tooltip = ''; + switch (project.status) { + case 'n/a': + case 'ok': + case 'never updated': + tooltip = vm.strings.get('status.NEVER_UPDATE'); + break; + case 'pending': + case 'waiting': + case 'new': + tooltip = vm.strings.get('status.UPDATE_QUEUED'); + break; + case 'updating': + case 'running': + tooltip = vm.strings.get('status.UPDATE_RUNNING'); + break; + case 'successful': + tooltip = vm.strings.get('status.UPDATE_SUCCESS'); + break; + case 'failed': + tooltip = vm.strings.get('status.UPDATE_FAILED'); + break; + case 'missing': + tooltip = vm.strings.get('status.UPDATE_MISSING'); + break; + case 'canceled': + tooltip = vm.strings.get('status.UPDATE_CANCELED'); + break; + default: + break; + } + return tooltip; + } +} + +projectsListController.$inject = [ + '$filter', + '$scope', + '$rootScope', + '$state', + '$log', + 'Dataset', + 'Alert', + 'Rest', + 'ProcessErrors', + 'resolvedModels', + 'ProjectsStrings', + 'Wait', + 'ngToast', + 'Prompt', + 'GetBasePath', + 'QuerySet', + 'ProjectUpdate', +]; + +export default projectsListController; diff --git a/awx/ui/client/features/projects/projectsList.view.html b/awx/ui/client/features/projects/projectsList.view.html new file mode 100644 index 0000000000..91438ce64d --- /dev/null +++ b/awx/ui/client/features/projects/projectsList.view.html @@ -0,0 +1,91 @@ + +
+ + +
+ +
+
+ + +
+ + +
+
+ {{ :: vm.strings.get('list.ROW_ITEM_LABEL_REVISION') }} +
+ +
+ + + + + + +
+
+
+
+ +
+
+ + + + + + +
+
+
+ + +
\ No newline at end of file diff --git a/awx/ui/client/features/projects/routes/projectsList.route.js b/awx/ui/client/features/projects/routes/projectsList.route.js new file mode 100644 index 0000000000..2f83a7171f --- /dev/null +++ b/awx/ui/client/features/projects/routes/projectsList.route.js @@ -0,0 +1,91 @@ +import { N_ } from '../../../src/i18n'; +import projectsListController from '../projectsList.controller'; +import indexController from '../index.controller'; + +const indexTemplate = require('~features/projects/index.view.html'); +const projectsListTemplate = require('~features/projects/projectsList.view.html'); + +export default { + searchPrefix: 'project', + name: 'projects', + route: '/projects', + ncyBreadcrumb: { + label: N_('PROJECTS') + }, + data: { + activityStream: true, + activityStreamTarget: 'project', + socket: { + groups: { + jobs: ['status_changed'] + } + } + }, + params: { + project_search: { + dynamic: true, + } + }, + views: { + '@': { + templateUrl: indexTemplate, + controller: indexController, + controllerAs: 'vm' + }, + 'projectsList@projects': { + templateUrl: projectsListTemplate, + controller: projectsListController, + controllerAs: 'vm', + } + }, + resolve: { + CredentialTypes: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', + (Rest, $stateParams, GetBasePath, ProcessErrors) => { + const path = GetBasePath('credential_types'); + Rest.setUrl(path); + return Rest.get() + .then((data) => data.data.results) + .catch((response) => { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: `Failed to get credential types. GET returned status: ${response.status}`, + }); + }); + } + ], + ConfigData: ['ConfigService', 'ProcessErrors', + function (ConfigService, ProcessErrors) { + return ConfigService.getConfig() + .then(response => response) + .catch(({ data, status }) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: `Failed to get config. GET returned status: status: ${status}`, + }); + }); + }], + Dataset: [ + '$stateParams', + 'Wait', + 'GetBasePath', + 'QuerySet', + ($stateParams, Wait, GetBasePath, qs) => { + const searchParam = $stateParams.project_search; + const searchPath = GetBasePath('projects'); + + Wait('start'); + return qs.search(searchPath, searchParam) + .finally(() => Wait('stop')); + } + ], + resolvedModels: [ + 'ProjectModel', + (Project) => { + const models = [ + new Project(['options']), + ]; + return Promise.all(models); + }, + ], + } +}; diff --git a/awx/ui/client/lib/components/list/_index.less b/awx/ui/client/lib/components/list/_index.less index 4a804086a4..1f8d534034 100644 --- a/awx/ui/client/lib/components/list/_index.less +++ b/awx/ui/client/lib/components/list/_index.less @@ -139,6 +139,9 @@ .at-RowItem-status { margin-right: @at-margin-right-list-row-item-status; + & > a { + cursor: pointer; + } } .at-RowItem--isHeader { @@ -254,6 +257,12 @@ background-color: @at-color-list-row-action-hover-danger; } +.at-RowAction--disabled { + pointer-events: none; + opacity: 0.5; + cursor: not-allowed; +} + .at-Row .at-Row-checkbox { align-self: start; margin: 2px 20px 0 0; diff --git a/awx/ui/client/lib/components/list/row-item.directive.js b/awx/ui/client/lib/components/list/row-item.directive.js index 67e169835a..731aa837ec 100644 --- a/awx/ui/client/lib/components/list/row-item.directive.js +++ b/awx/ui/client/lib/components/list/row-item.directive.js @@ -15,6 +15,7 @@ function atRowItem () { headerTag: '@', status: '@', statusTip: '@', + statusClick: '&?', labelValue: '@', labelLink: '@', labelState: '@', diff --git a/awx/ui/client/lib/components/list/row-item.partial.html b/awx/ui/client/lib/components/list/row-item.partial.html index 23de062d4f..eebeab39a9 100644 --- a/awx/ui/client/lib/components/list/row-item.partial.html +++ b/awx/ui/client/lib/components/list/row-item.partial.html @@ -1,12 +1,17 @@
- + + + -
diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index aaa9fee909..4f2d9a0b5f 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -7,10 +7,10 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n', - 'CredentialTypes', 'ConfigData', + 'CredentialTypes', 'ConfigData', 'resolvedModels', function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest, Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state, - CreateSelect2, i18n, CredentialTypes, ConfigData) { + CreateSelect2, i18n, CredentialTypes, ConfigData, resolvedModels) { let form = ProjectsForm(), base = $location.path().replace(/^\//, '').split('/')[0], @@ -23,6 +23,9 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.canEditOrg = true; const virtualEnvs = ConfigData.custom_virtualenvs || []; $scope.custom_virtualenvs_options = virtualEnvs; + + const [ProjectModel] = resolvedModels; + $scope.canAdd = ProjectModel.options('actions.POST'); Rest.setUrl(GetBasePath('projects')); Rest.options() diff --git a/awx/ui/client/src/projects/factories/get-project-icon.factory.js b/awx/ui/client/src/projects/factories/get-project-icon.factory.js deleted file mode 100644 index 5234041e38..0000000000 --- a/awx/ui/client/src/projects/factories/get-project-icon.factory.js +++ /dev/null @@ -1,30 +0,0 @@ -export default - function GetProjectIcon() { - return function(status) { - var result = ''; - switch (status) { - case 'n/a': - case 'ok': - case 'never updated': - result = 'none'; - break; - case 'pending': - case 'waiting': - case 'new': - result = 'none'; - break; - case 'updating': - case 'running': - result = 'running'; - break; - case 'successful': - result = 'success'; - break; - case 'failed': - case 'missing': - case 'canceled': - result = 'error'; - } - return result; - }; - } diff --git a/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js deleted file mode 100644 index 834a6c9d99..0000000000 --- a/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js +++ /dev/null @@ -1,38 +0,0 @@ -export default - function GetProjectToolTip(i18n) { - return function(status) { - var result = ''; - switch (status) { - case 'n/a': - case 'ok': - case 'never updated': - result = i18n._('No SCM updates have run for this project'); - break; - case 'pending': - case 'waiting': - case 'new': - result = i18n._('Update queued. Click for details'); - break; - case 'updating': - case 'running': - result = i18n._('Update running. Click for details'); - break; - case 'successful': - result = i18n._('Update succeeded. Click for details'); - break; - case 'failed': - result = i18n._('Update failed. Click for details'); - break; - case 'missing': - result = i18n._('Update missing. Click for details'); - break; - case 'canceled': - result = i18n._('Update canceled. Click for details'); - break; - } - return result; - }; - } - -GetProjectToolTip.$inject = - [ 'i18n' ]; diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js deleted file mode 100644 index 11be31c54c..0000000000 --- a/awx/ui/client/src/projects/list/projects-list.controller.js +++ /dev/null @@ -1,342 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', - 'ProjectList', 'Prompt', 'ProcessErrors', 'GetBasePath', 'ProjectUpdate', - 'Wait', 'Empty', 'Find', 'GetProjectIcon', 'GetProjectToolTip', '$filter', - '$state', 'rbacUiControlService', 'Dataset', 'i18n', 'QuerySet', 'ProjectModel', - 'ProjectsStrings', 'ngToast', - function($scope, $rootScope, $log, Rest, Alert, ProjectList, - Prompt, ProcessErrors, GetBasePath, ProjectUpdate, Wait, Empty, Find, - GetProjectIcon, GetProjectToolTip, $filter, $state, rbacUiControlService, - Dataset, i18n, qs, Project, ProjectsStrings, ngToast) { - - let project = new Project(); - - var list = ProjectList; - - init(); - - function init() { - $scope.canAdd = false; - - rbacUiControlService.canAdd('projects') - .then(function(params) { - $scope.canAdd = params.canAdd; - }); - - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - _.forEach($scope[list.name], buildTooltips); - $rootScope.flashMessage = null; - } - - $scope.$on(`${list.iterator}_options`, function(event, data){ - $scope.options = data.data.actions.GET; - optionsRequestDataProcessing(); - }); - - $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); - } - ); - - // iterate over the list and add fields like type label, after the - // OPTIONS request returns, or the list is sorted/paginated/searched - function optionsRequestDataProcessing(){ - if ($scope[list.name] !== undefined) { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - - // Set the item type label - if (list.fields.scm_type && $scope.options && - $scope.options.hasOwnProperty('scm_type')) { - $scope.options.scm_type.choices.forEach(function(choice) { - if (choice[0] === item.scm_type) { - itm.type_label = choice[1]; - } - }); - } - - buildTooltips(itm); - - }); - } - } - - function buildTooltips(project) { - project.statusIcon = GetProjectIcon(project.status); - project.statusTip = GetProjectToolTip(project.status); - project.scm_update_tooltip = i18n._("Get latest SCM revision"); - project.scm_type_class = ""; - - if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { - project.statusTip = i18n._('Canceled. Click for details'); - project.scm_type_class = "btn-disabled"; - } - - if (project.status === 'running' || project.status === 'updating') { - project.scm_update_tooltip = i18n._("SCM update currently running"); - project.scm_type_class = "btn-disabled"; - } - if (project.scm_type === 'manual') { - project.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - project.scm_type_class = 'btn-disabled'; - project.statusTip = i18n._('Not configured for SCM'); - project.statusIcon = 'none'; - } - } - - $scope.reloadList = function(){ - let path = GetBasePath(list.basePath) || GetBasePath(list.name); - qs.search(path, $state.params[`${list.iterator}_search`]) - .then(function(searchResponse) { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - }); - }; - - $scope.$on(`ws-jobs`, function(e, data) { - var project; - $log.debug(data); - if ($scope.projects) { - // Assuming we have a list of projects available - project = Find({ list: $scope.projects, key: 'id', val: data.project_id }); - if (project) { - // And we found the affected project - $log.debug('Received event for project: ' + project.name); - $log.debug('Status changed to: ' + data.status); - if (data.status === 'successful' || data.status === 'failed' || data.status === 'canceled') { - $scope.reloadList(); - } else { - project.scm_update_tooltip = i18n._("SCM update currently running"); - project.scm_type_class = "btn-disabled"; - } - project.status = data.status; - project.statusIcon = GetProjectIcon(data.status); - project.statusTip = GetProjectToolTip(data.status); - } - } - }); - - $scope.addProject = function() { - $state.go('projects.add'); - }; - - $scope.editProject = function(id) { - $state.go('projects.edit', { project_id: id }); - }; - - if ($scope.removeGoTojobResults) { - $scope.removeGoTojobResults(); - } - $scope.removeGoTojobResults = $scope.$on('GoTojobResults', function(e, data) { - if (data.summary_fields.current_update || data.summary_fields.last_update) { - - Wait('start'); - - // Grab the id from summary_fields - var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; - - $state.go('output', { id: id, type: 'project'}, { reload: true }); - - } else { - Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' + - ' completed. If you have not already done so, start an update for this project.'), 'alert-info'); - } - }); - - $scope.copyProject = project => { - Wait('start'); - new Project('get', project.id) - .then(model => model.copy()) - .then((copiedProj) => { - ngToast.success({ - content: ` -
-
- -
-
- ${ProjectsStrings.get('SUCCESSFUL_CREATION', copiedProj.name)} -
-
`, - dismissButton: false, - dismissOnTimeout: true - }); - $state.go('.', null, { 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 }); - if (Empty(project.scm_type) || project.scm_type === 'Manual') { - Alert(i18n._('No SCM Configuration'), i18n._('The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, ' + - 'and then run an update.'), 'alert-info'); - } else { - // Refresh what we have in memory to insure we're accessing the most recent status record - Rest.setUrl(project.url); - Rest.get() - .then(({data}) => { - $scope.$emit('GoTojobResults', data); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n._('Project lookup failed. GET returned: ') + status }); - }); - } - }; - - $scope.deleteProject = function(id, name) { - var action = function() { - $('#prompt-modal').modal('hide'); - Wait('start'); - project.request('delete', id) - .then(() => { - - let reloadListStateParams = null; - - if($scope.projects.length === 1 && $state.params.project_search && _.has($state, 'params.project_search.page') && $state.params.project_search.page !== '1') { - reloadListStateParams = _.cloneDeep($state.params); - reloadListStateParams.project_search.page = (parseInt(reloadListStateParams.project_search.page)-1).toString(); - } - - if (parseInt($state.params.project_id) === id) { - $state.go("^", reloadListStateParams, { reload: true }); - } else { - $state.go('.', reloadListStateParams, {reload: true}); - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), `${project.path}${id}/`) + status }); - }) - .finally(function() { - Wait('stop'); - }); - }; - - project.getDependentResourceCounts(id) - .then((counts) => { - const invalidateRelatedLines = []; - let deleteModalBody = `
${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}
`; - - counts.forEach(countObj => { - if(countObj.count && countObj.count > 0) { - invalidateRelatedLines.push(`
${countObj.label}${countObj.count}
`); - } - }); - - if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { - deleteModalBody = `
${ProjectsStrings.get('deleteResource.USED_BY', 'project')} ${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}
`; - invalidateRelatedLines.forEach(invalidateRelatedLine => { - deleteModalBody += invalidateRelatedLine; - }); - } - - Prompt({ - hdr: i18n._('Delete'), - resourceName: $filter('sanitize')(name), - body: deleteModalBody, - action: action, - actionText: i18n._('DELETE') - }); - }); - }; - - if ($scope.removeCancelUpdate) { - $scope.removeCancelUpdate(); - } - $scope.removeCancelUpdate = $scope.$on('Cancel_Update', function(e, url) { - // Cancel the project update process - Rest.setUrl(url); - Rest.post() - .then(() => { - Alert(i18n._('SCM Update Cancel'), i18n._('Your request to cancel the update was submitted to the task manager.'), 'alert-info'); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST status: '), url) + status }); - }); - }); - - if ($scope.removeCheckCancel) { - $scope.removeCheckCancel(); - } - $scope.removeCheckCancel = $scope.$on('Check_Cancel', function(e, data) { - // Check that we 'can' cancel the update - var url = data.related.cancel; - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - if (data.can_cancel) { - $scope.$emit('Cancel_Update', url); - } else { - Alert(i18n._('Cancel Not Allowed'), '
' + i18n.sprintf(i18n._('Either you do not have access or the SCM update process completed. ' + - 'Click the %sRefresh%s button to view the latest status.'), '', '') + '
', 'alert-info', null, null, null, null, true); - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), url) + status }); - }); - }); - - $scope.cancelUpdate = function(project) { - project.pending_cancellation = true; - Rest.setUrl(GetBasePath("projects") + project.id); - Rest.get() - .then(({data}) => { - if (data.related.current_update) { - Rest.setUrl(data.related.current_update); - Rest.get() - .then(({data}) => { - $scope.$emit('Check_Cancel', data); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), data.related.current_update) + status }); - }); - } else { - Alert(i18n._('Update Not Found'), '
' + i18n.sprintf(i18n._('An SCM update does not appear to be running for project: %s. Click the %sRefresh%s ' + - 'button to view the latest status.'), $filter('sanitize')(name), '', '') + '
', 'alert-info',undefined,undefined,undefined,undefined,true); - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n._('Call to get project failed. GET status: ') + status }); - }); - }; - - $scope.SCMUpdate = function(project_id, event) { - try { - $(event.target).tooltip('hide'); - } catch (e) { - // ignore - } - $scope.projects.forEach(function(project) { - if (project.id === project_id) { - if (project.scm_type === "Manual" || Empty(project.scm_type)) { - // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event - // to work. So user can click, but we just won't do anything. - //Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info'); - } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { - // Alert('Update in Progress', 'The SCM update process is running. Use the Refresh button to monitor the status.', 'alert-info'); - } else { - ProjectUpdate({ scope: $scope, project_id: project.id }); - } - } - }); - }; - } -]; diff --git a/awx/ui/client/src/projects/main.js b/awx/ui/client/src/projects/main.js index 144fb99b91..6fd8437f38 100644 --- a/awx/ui/client/src/projects/main.js +++ b/awx/ui/client/src/projects/main.js @@ -4,15 +4,10 @@ * All Rights Reserved *************************************************/ -import ProjectsList from './list/projects-list.controller'; import ProjectsAdd from './add/projects-add.controller'; import ProjectsEdit from './edit/projects-edit.controller'; -import ProjectList from './projects.list'; import ProjectsForm from './projects.form'; -import { N_ } from '../i18n'; import GetProjectPath from './factories/get-project-path.factory'; -import GetProjectIcon from './factories/get-project-icon.factory'; -import GetProjectToolTip from './factories/get-project-tool-tip.factory'; import { projectsSchedulesListRoute, projectsSchedulesAddRoute, @@ -20,93 +15,57 @@ import { } from '../scheduler/schedules.route'; import ProjectsTemplatesRoute from '~features/templates/routes/projectsTemplatesList.route'; -import ProjectsStrings from './projects.strings'; +import projectsListRoute from '~features/projects/routes/projectsList.route.js'; export default angular.module('Projects', []) - .controller('ProjectsList', ProjectsList) .controller('ProjectsAdd', ProjectsAdd) .controller('ProjectsEdit', ProjectsEdit) .factory('GetProjectPath', GetProjectPath) - .factory('GetProjectIcon', GetProjectIcon) - .factory('GetProjectToolTip', GetProjectToolTip) - .factory('ProjectList', ProjectList) .factory('ProjectsForm', ProjectsForm) - .service('ProjectsStrings', ProjectsStrings) .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', function($stateProvider, stateDefinitionsProvider,$stateExtenderProvider) { let stateDefinitions = stateDefinitionsProvider.$get(); let stateExtender = $stateExtenderProvider.$get(); - var projectResolve = { - CredentialTypes: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', - (Rest, $stateParams, GetBasePath, ProcessErrors) => { - var path = GetBasePath('credential_types'); - Rest.setUrl(path); - return Rest.get() - .then(function(data) { - return (data.data.results); - }).catch(function(response) { - ProcessErrors(null, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get credential types. GET returned status: ' + - response.status - }); - }); - } - ], - ConfigData: ['ConfigService', 'ProcessErrors', (ConfigService, ProcessErrors) => { - return ConfigService.getConfig() - .then(response => response) - .catch(({data, status}) => { - ProcessErrors(null, data, status, null, { - hdr: 'Error!', - msg: 'Failed to get config. GET returned status: ' + - 'status: ' + status - }); - }); - }] - }; function generateStateTree() { - let projectTree = stateDefinitions.generateTree({ - parent: 'projects', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - generateSchedulerView: true, - list: 'ProjectList', + let projectAdd = stateDefinitions.generateTree({ + name: 'projects.add', + url: '/add', + modes: ['add'], form: 'ProjectsForm', controllers: { - list: ProjectsList, // DI strings or objects - add: ProjectsAdd, - edit: ProjectsEdit + add: 'ProjectsAdd', + }, + }); + + let projectEdit = stateDefinitions.generateTree({ + name: 'projects.edit', + url: '/:project_id', + modes: ['edit'], + form: 'ProjectsForm', + controllers: { + edit: 'ProjectsEdit', }, data: { activityStream: true, activityStreamTarget: 'project', - socket: { - "groups": { - "jobs": ["status_changed"] - } - } + activityStreamId: 'project_id' }, - ncyBreadcrumb: { - label: N_('PROJECTS') - }, - breadcrumbs: { + breadcrumbs: { edit: '{{breadcrumb.project_name}}' }, - resolve: { - add: projectResolve, - edit: projectResolve - } }); return Promise.all([ - projectTree + projectAdd, + projectEdit, ]).then((generated) => { return { states: _.reduce(generated, (result, definition) => { return result.concat(definition.states); }, [ + stateExtender.buildDefinition(projectsListRoute), stateExtender.buildDefinition(ProjectsTemplatesRoute), stateExtender.buildDefinition(projectsSchedulesListRoute), stateExtender.buildDefinition(projectsSchedulesAddRoute), diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js deleted file mode 100644 index 64b0ddb4ab..0000000000 --- a/awx/ui/client/src/projects/projects.list.js +++ /dev/null @@ -1,128 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default ['i18n', function(i18n) { - return { - - name: 'projects', - iterator: 'project', - basePath: 'projects', - selectTitle: i18n._('Add Project'), - editTitle: i18n._('PROJECTS'), - listTitle: i18n._('PROJECTS'), - selectInstructions: '

Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' + - 'Select button, located bottom right.

Create a new project by clicking the button.

', - index: false, - hover: true, - emptyListText: i18n._('No Projects Have Been Created'), - - fields: { - status: { - label: '', - iconOnly: true, - ngClick: 'showSCMStatus(project.id)', - awToolTip: '{{ project.statusTip }}', - dataTipWatch: 'project.statusTip', - dataPlacement: 'right', - icon: "icon-job-{{ project.statusIcon }}", - columnClass: "List-staticColumn--smallStatus", - nosort: true, - excludeModal: true - }, - name: { - key: true, - label: i18n._('Name'), - columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent", - modalColumnClass: 'col-md-8', - awToolTip: '{{project.description | sanitize}}', - dataPlacement: 'top' - }, - scm_type: { - label: i18n._('Type'), - ngBind: 'project.type_label', - excludeModal: true, - columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs' - }, - scm_revision: { - label: i18n._('Revision'), - excludeModal: true, - columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs', - type: 'revision' - }, - last_updated: { - label: i18n._('Last Updated'), - filter: "longDate", - columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs", - excludeModal: true - } - }, - - actions: { - refresh: { - mode: 'all', - awToolTip: i18n._("Refresh the page"), - ngClick: "refresh()", - ngShow: "socketStatus === 'error'", - actionClass: 'btn List-buttonDefault', - buttonContent: i18n._('REFRESH') - }, - add: { - mode: 'all', // One of: edit, select, all - ngClick: 'addProject()', - awToolTip: i18n._('Create a new project'), - actionClass: 'at-Button--add', - actionId: 'button-add', - ngShow: "canAdd" - } - }, - - fieldActions: { - - columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5', - edit: { - ngClick: "editProject(project.id)", - awToolTip: i18n._('Edit the project'), - dataPlacement: 'top', - ngShow: "project.summary_fields.user_capabilities.edit" - }, - scm_update: { - ngClick: 'SCMUpdate(project.id, $event)', - awToolTip: "{{ project.scm_update_tooltip }}", - dataTipWatch: "project.scm_update_tooltip", - ngClass: "project.scm_type_class", - dataPlacement: 'top', - ngShow: "project.summary_fields.user_capabilities.start" - }, - copy: { - label: i18n._('Copy'), - ngClick: 'copyProject(project)', - "class": 'btn-danger btn-xs', - awToolTip: i18n._('Copy project'), - dataPlacement: 'top', - ngShow: 'project.summary_fields.user_capabilities.copy' - }, - view: { - ngClick: "editProject(project.id)", - awToolTip: i18n._('View the project'), - dataPlacement: 'top', - ngShow: "!project.summary_fields.user_capabilities.edit", - icon: 'fa-eye', - }, - "delete": { - ngClick: "deleteProject(project.id, project.name)", - awToolTip: i18n._('Delete the project'), - ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete", - dataPlacement: 'top' - }, - cancel: { - ngClick: "cancelUpdate(project)", - awToolTip: i18n._('Cancel the SCM update'), - ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start", - dataPlacement: 'top', - ngDisabled: "project.pending_cancellation || project.status == 'canceled'" - } - } - };}]; diff --git a/awx/ui/client/src/projects/projects.strings.js b/awx/ui/client/src/projects/projects.strings.js deleted file mode 100644 index 37e4141a7b..0000000000 --- a/awx/ui/client/src/projects/projects.strings.js +++ /dev/null @@ -1,7 +0,0 @@ -function ProjectsStrings (BaseString) { - BaseString.call(this, 'projects'); -} - -ProjectsStrings.$inject = ['BaseStringService']; - -export default ProjectsStrings; From 110671532d8088eb73786a571dcb15cd87f9ac1b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 20 Sep 2018 11:57:55 -0400 Subject: [PATCH 2/8] fix lint error with projects list route --- .../projects/routes/projectsList.route.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/features/projects/routes/projectsList.route.js b/awx/ui/client/features/projects/routes/projectsList.route.js index 2f83a7171f..d89b80c3f1 100644 --- a/awx/ui/client/features/projects/routes/projectsList.route.js +++ b/awx/ui/client/features/projects/routes/projectsList.route.js @@ -54,16 +54,15 @@ export default { } ], ConfigData: ['ConfigService', 'ProcessErrors', - function (ConfigService, ProcessErrors) { - return ConfigService.getConfig() - .then(response => response) - .catch(({ data, status }) => { - ProcessErrors(null, data, status, null, { - hdr: 'Error!', - msg: `Failed to get config. GET returned status: status: ${status}`, - }); + (ConfigService, ProcessErrors) => ConfigService + .getConfig() + .then(response => response) + .catch(({ data, status }) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: `Failed to get config. GET returned status: status: ${status}`, }); - }], + })], Dataset: [ '$stateParams', 'Wait', From 8057438c6716c5987705c3dc1d5716b0cf51ca59 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 10 Oct 2018 12:11:41 -0400 Subject: [PATCH 3/8] add back in old-style project list json and relevant factories --- .../organizations-projects.controller.js | 71 +++++----- .../factories/get-project-icon.factory.js | 30 ++++ .../factories/get-project-tool-tip.factory.js | 38 ++++++ awx/ui/client/src/projects/main.js | 6 + awx/ui/client/src/projects/projects.list.js | 128 ++++++++++++++++++ 5 files changed, 241 insertions(+), 32 deletions(-) create mode 100644 awx/ui/client/src/projects/factories/get-project-icon.factory.js create mode 100644 awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js create mode 100644 awx/ui/client/src/projects/projects.list.js diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js index 0a3ddcf57d..5e444a0eed 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -19,6 +19,41 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', orgBase = GetBasePath('organizations'), projBase = GetBasePath('projects'); + + function updateStatus() { + if ($scope.projects) { + $scope.projects.forEach(function(project, i) { + $scope.projects[i].statusIcon = GetProjectIcon(project.status); + $scope.projects[i].statusTip = GetProjectToolTip(project.status); + $scope.projects[i].scm_update_tooltip = i18n._("Get latest SCM revision"); + $scope.projects[i].scm_type_class = ""; + + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + $scope.projects[i].statusTip = i18n._('Canceled. Click for details'); + } + + if (project.status === 'running' || project.status === 'updating') { + $scope.projects[i].scm_update_tooltip = i18n._("SCM update currently running"); + $scope.projects[i].scm_type_class = "btn-disabled"; + } + + if ($scope.project_scm_type_options) { + $scope.project_scm_type_options.forEach(function(type) { + if (type.value === project.scm_type) { + $scope.projects[i].scm_type = type.label; + if (type.label === 'Manual') { + $scope.projects[i].scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); + $scope.projects[i].scm_type_class = 'btn-disabled'; + $scope.projects[i].statusTip = 'Not configured for SCM'; + $scope.projects[i].statusIcon = 'none'; + } + } + }); + } + }); + } + } + init(); function init() { @@ -31,35 +66,7 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', $scope.$on('choicesReadyProjectList', function() { Wait('stop'); - if ($scope.projects) { - $scope.projects.forEach(function(project, i) { - $scope.projects[i].statusIcon = GetProjectIcon(project.status); - $scope.projects[i].statusTip = GetProjectToolTip(project.status); - $scope.projects[i].scm_update_tooltip = i18n._("Get latest SCM revision"); - $scope.projects[i].scm_type_class = ""; - - if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { - $scope.projects[i].statusTip = i18n._('Canceled. Click for details'); - } - - if (project.status === 'running' || project.status === 'updating') { - $scope.projects[i].scm_update_tooltip = i18n._("SCM update currently running"); - $scope.projects[i].scm_type_class = "btn-disabled"; - } - - $scope.project_scm_type_options.forEach(function(type) { - if (type.value === project.scm_type) { - $scope.projects[i].scm_type = type.label; - if (type.label === 'Manual') { - $scope.projects[i].scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - $scope.projects[i].scm_type_class = 'btn-disabled'; - $scope.projects[i].statusTip = 'Not configured for SCM'; - $scope.projects[i].statusIcon = 'none'; - } - } - }); - }); - } + updateStatus(); }); } @@ -69,9 +76,9 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', }); $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); - } - ); + optionsRequestDataProcessing(); + updateStatus(); + }); // iterate over the list and add fields like type label, after the // OPTIONS request returns, or the list is sorted/paginated/searched diff --git a/awx/ui/client/src/projects/factories/get-project-icon.factory.js b/awx/ui/client/src/projects/factories/get-project-icon.factory.js new file mode 100644 index 0000000000..5234041e38 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-icon.factory.js @@ -0,0 +1,30 @@ +export default + function GetProjectIcon() { + return function(status) { + var result = ''; + switch (status) { + case 'n/a': + case 'ok': + case 'never updated': + result = 'none'; + break; + case 'pending': + case 'waiting': + case 'new': + result = 'none'; + break; + case 'updating': + case 'running': + result = 'running'; + break; + case 'successful': + result = 'success'; + break; + case 'failed': + case 'missing': + case 'canceled': + result = 'error'; + } + return result; + }; + } diff --git a/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js new file mode 100644 index 0000000000..834a6c9d99 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js @@ -0,0 +1,38 @@ +export default + function GetProjectToolTip(i18n) { + return function(status) { + var result = ''; + switch (status) { + case 'n/a': + case 'ok': + case 'never updated': + result = i18n._('No SCM updates have run for this project'); + break; + case 'pending': + case 'waiting': + case 'new': + result = i18n._('Update queued. Click for details'); + break; + case 'updating': + case 'running': + result = i18n._('Update running. Click for details'); + break; + case 'successful': + result = i18n._('Update succeeded. Click for details'); + break; + case 'failed': + result = i18n._('Update failed. Click for details'); + break; + case 'missing': + result = i18n._('Update missing. Click for details'); + break; + case 'canceled': + result = i18n._('Update canceled. Click for details'); + break; + } + return result; + }; + } + +GetProjectToolTip.$inject = + [ 'i18n' ]; diff --git a/awx/ui/client/src/projects/main.js b/awx/ui/client/src/projects/main.js index 6fd8437f38..ebb3aa7bb9 100644 --- a/awx/ui/client/src/projects/main.js +++ b/awx/ui/client/src/projects/main.js @@ -7,7 +7,10 @@ import ProjectsAdd from './add/projects-add.controller'; import ProjectsEdit from './edit/projects-edit.controller'; import ProjectsForm from './projects.form'; +import ProjectList from './projects.list'; import GetProjectPath from './factories/get-project-path.factory'; +import GetProjectIcon from './factories/get-project-icon.factory'; +import GetProjectToolTip from './factories/get-project-tool-tip.factory'; import { projectsSchedulesListRoute, projectsSchedulesAddRoute, @@ -22,7 +25,10 @@ angular.module('Projects', []) .controller('ProjectsAdd', ProjectsAdd) .controller('ProjectsEdit', ProjectsEdit) .factory('GetProjectPath', GetProjectPath) + .factory('GetProjectIcon', GetProjectIcon) + .factory('GetProjectToolTip', GetProjectToolTip) .factory('ProjectsForm', ProjectsForm) + .factory('ProjectList', ProjectList) .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', function($stateProvider, stateDefinitionsProvider,$stateExtenderProvider) { let stateDefinitions = stateDefinitionsProvider.$get(); diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js new file mode 100644 index 0000000000..436ee5c3b6 --- /dev/null +++ b/awx/ui/client/src/projects/projects.list.js @@ -0,0 +1,128 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['i18n', function(i18n) { + return { + + name: 'projects', + iterator: 'project', + basePath: 'projects', + selectTitle: i18n._('Add Project'), + editTitle: i18n._('PROJECTS'), + listTitle: i18n._('PROJECTS'), + selectInstructions: '

Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' + + 'Select button, located bottom right.

Create a new project by clicking the button.

', + index: false, + hover: true, + emptyListText: i18n._('No Projects Have Been Created'), + + fields: { + status: { + label: '', + iconOnly: true, + ngClick: 'showSCMStatus(project.id)', + awToolTip: '{{ project.statusTip }}', + dataTipWatch: 'project.statusTip', + dataPlacement: 'right', + icon: "icon-job-{{ project.statusIcon }}", + columnClass: "List-staticColumn--smallStatus", + nosort: true, + excludeModal: true + }, + name: { + key: true, + label: i18n._('Name'), + columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent", + modalColumnClass: 'col-md-8', + awToolTip: '{{project.description | sanitize}}', + dataPlacement: 'top' + }, + scm_type: { + label: i18n._('Type'), + ngBind: 'project.type_label', + excludeModal: true, + columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs' + }, + scm_revision: { + label: i18n._('Revision'), + excludeModal: true, + columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs', + type: 'revision' + }, + last_updated: { + label: i18n._('Last Updated'), + filter: "longDate", + columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs", + excludeModal: true + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: i18n._("Refresh the page"), + ngClick: "refresh()", + ngShow: "socketStatus === 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: i18n._('REFRESH') + }, + add: { + mode: 'all', // One of: edit, select, all + ngClick: 'addProject()', + awToolTip: i18n._('Create a new project'), + actionClass: 'at-Button--add', + actionId: 'button-add', + ngShow: "canAdd" + } + }, + + fieldActions: { + + columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5', + edit: { + ngClick: "editProject(project.id)", + awToolTip: i18n._('Edit the project'), + dataPlacement: 'top', + ngShow: "project.summary_fields.user_capabilities.edit" + }, + scm_update: { + ngClick: 'SCMUpdate(project.id, $event)', + awToolTip: "{{ project.scm_update_tooltip }}", + dataTipWatch: "project.scm_update_tooltip", + ngClass: "project.scm_type_class", + dataPlacement: 'top', + ngShow: "project.summary_fields.user_capabilities.start" + }, + copy: { + label: i18n._('Copy'), + ngClick: 'copyProject(project)', + "class": 'btn-danger btn-xs', + awToolTip: i18n._('Copy project'), + dataPlacement: 'top', + ngShow: 'project.summary_fields.user_capabilities.copy' + }, + view: { + ngClick: "editProject(project.id)", + awToolTip: i18n._('View the project'), + dataPlacement: 'top', + ngShow: "!project.summary_fields.user_capabilities.edit", + icon: 'fa-eye', + }, + "delete": { + ngClick: "deleteProject(project.id, project.name)", + awToolTip: i18n._('Delete the project'), + ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete", + dataPlacement: 'top' + }, + cancel: { + ngClick: "cancelUpdate(project)", + awToolTip: i18n._('Cancel the SCM update'), + ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start", + dataPlacement: 'top', + ngDisabled: "project.pending_cancellation || project.status == 'canceled'" + } + } + };}]; \ No newline at end of file From d61cd519d73f55bf6ef8d11076c512890a454a3f Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 17 Oct 2018 11:13:36 -0400 Subject: [PATCH 4/8] fix panel title and badge for new projects list --- awx/ui/client/features/projects/index.view.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/features/projects/index.view.html b/awx/ui/client/features/projects/index.view.html index 4df353660a..13e18dfb28 100644 --- a/awx/ui/client/features/projects/index.view.html +++ b/awx/ui/client/features/projects/index.view.html @@ -2,11 +2,10 @@
- - {{:: vm.strings.get('list.PANEL_TITLE') }} -
- {{ vm.count }} -
+
From 89344c2eeef66f8190a0d1817eada67bb4359a60 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 17 Oct 2018 14:30:13 -0400 Subject: [PATCH 5/8] update project list selectors --- awx/ui/test/e2e/objects/projects.js | 2 +- awx/ui/test/e2e/tests/test-xss.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/test/e2e/objects/projects.js b/awx/ui/test/e2e/objects/projects.js index 85965c4019..6c66896d8c 100644 --- a/awx/ui/test/e2e/objects/projects.js +++ b/awx/ui/test/e2e/objects/projects.js @@ -56,7 +56,7 @@ module.exports = { } }, list: { - selector: '.Panel', + selector: '.at-Panel', elements: { badge: 'span[class~="badge"]', title: 'div[class="List-titleText"]', diff --git a/awx/ui/test/e2e/tests/test-xss.js b/awx/ui/test/e2e/tests/test-xss.js index 6383926709..5e2e2b0fbe 100644 --- a/awx/ui/test/e2e/tests/test-xss.js +++ b/awx/ui/test/e2e/tests/test-xss.js @@ -511,11 +511,11 @@ module.exports = { const itemRow = `#projects_table tr[id="${data.project.id}"]`; const itemName = `${itemRow} td[class*="name-"] a`; - client.expect.element('div[class^="Panel"] smart-search').visible; - client.expect.element('div[class^="Panel"] smart-search input').enabled; + client.expect.element('div[class^="at-Panel"] smart-search').visible; + client.expect.element('div[class^="at-Panel"] smart-search input').enabled; - 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.sendKeys('div[class^="at-Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); + client.sendKeys('div[class^="at-Panel"] smart-search input', client.Keys.ENTER); client.expect.element('div.spinny').visible; client.expect.element('div.spinny').not.visible; From 73dc58e810947bc9a9efa1007ebb859d2548bbff Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 17 Oct 2018 14:53:40 -0400 Subject: [PATCH 6/8] update project badge selector --- awx/ui/test/e2e/objects/projects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/test/e2e/objects/projects.js b/awx/ui/test/e2e/objects/projects.js index 6c66896d8c..345bf1664a 100644 --- a/awx/ui/test/e2e/objects/projects.js +++ b/awx/ui/test/e2e/objects/projects.js @@ -58,8 +58,8 @@ module.exports = { list: { selector: '.at-Panel', elements: { - badge: 'span[class~="badge"]', - title: 'div[class="List-titleText"]', + badge: '.at-Panel-headingTitleBadge', + title: '.at-Panel-headingTitle', add: '#button-add' }, sections: { From 5e0ecc7f43435bd3453ed65102bea99ea702290c Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 18 Oct 2018 09:45:41 -0400 Subject: [PATCH 7/8] fix projects list search selectors --- awx/ui/test/e2e/objects/sections/search.js | 4 ++-- awx/ui/test/e2e/tests/test-xss.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/awx/ui/test/e2e/objects/sections/search.js b/awx/ui/test/e2e/objects/sections/search.js index 9feed81554..64fd557da5 100644 --- a/awx/ui/test/e2e/objects/sections/search.js +++ b/awx/ui/test/e2e/objects/sections/search.js @@ -2,8 +2,8 @@ const search = { selector: 'smart-search', locateStrategy: 'css selector', elements: { - clearAll: 'a[class*="clear"]', - searchButton: 'i[class$="search"]', + clearAll: 'a[class*="clearAll"]', + searchButton: 'i[class*="fa-search"]', input: 'input', tags: '.SmartSearch-tagContainer' } diff --git a/awx/ui/test/e2e/tests/test-xss.js b/awx/ui/test/e2e/tests/test-xss.js index 5e2e2b0fbe..719fbd3ae2 100644 --- a/awx/ui/test/e2e/tests/test-xss.js +++ b/awx/ui/test/e2e/tests/test-xss.js @@ -511,11 +511,11 @@ module.exports = { const itemRow = `#projects_table tr[id="${data.project.id}"]`; const itemName = `${itemRow} td[class*="name-"] a`; - client.expect.element('div[class^="at-Panel"] smart-search').visible; - client.expect.element('div[class^="at-Panel"] smart-search input').enabled; + client.expect.element('div[class*="at-Panel"] smart-search').visible; + client.expect.element('div[class*="at-Panel"] smart-search input').enabled; - client.sendKeys('div[class^="at-Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); - client.sendKeys('div[class^="at-Panel"] smart-search input', client.Keys.ENTER); + client.sendKeys('div[class*="at-Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); + client.sendKeys('div[class*="at-Panel"] smart-search input', client.Keys.ENTER); client.expect.element('div.spinny').visible; client.expect.element('div.spinny').not.visible; From bb921af146e13f2fb2dd6b1eaac5b7653e935440 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 18 Oct 2018 15:37:29 -0400 Subject: [PATCH 8/8] fix badge updating and xss e2e test for projects list updates --- .../features/projects/index.controller.js | 2 +- .../projects/projectsList.controller.js | 3 ++ .../features/projects/projectsList.view.html | 3 +- .../e2e/tests/test-projects-list-actions.js | 2 +- awx/ui/test/e2e/tests/test-xss.js | 46 +++++++++---------- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/awx/ui/client/features/projects/index.controller.js b/awx/ui/client/features/projects/index.controller.js index a76b7ef329..4722753e97 100644 --- a/awx/ui/client/features/projects/index.controller.js +++ b/awx/ui/client/features/projects/index.controller.js @@ -4,7 +4,7 @@ function IndexProjectsController ($scope, strings, dataset) { vm.count = dataset.data.count; $scope.$on('updateCount', (e, count) => { - if (count) { + if (typeof count === 'number') { vm.count = count; } }); diff --git a/awx/ui/client/features/projects/projectsList.controller.js b/awx/ui/client/features/projects/projectsList.controller.js index 34b488d174..fcfecba65f 100644 --- a/awx/ui/client/features/projects/projectsList.controller.js +++ b/awx/ui/client/features/projects/projectsList.controller.js @@ -27,6 +27,9 @@ function projectsListController ( }; vm.dataset = Dataset.data; vm.projects = Dataset.data.results; + $scope.$watch('vm.dataset.count', () => { + $scope.$emit('updateCount', vm.dataset.count, 'projects'); + }); // build tooltips _.forEach(vm.projects, buildTooltips); $rootScope.flashMessage = null; diff --git a/awx/ui/client/features/projects/projectsList.view.html b/awx/ui/client/features/projects/projectsList.view.html index 91438ce64d..78cb78ddf2 100644 --- a/awx/ui/client/features/projects/projectsList.view.html +++ b/awx/ui/client/features/projects/projectsList.view.html @@ -21,7 +21,8 @@
+ ng-class="{'at-Row--active': (project.id === vm.activeId)}" + id="row-{{ project.id }}">
{ - const itemRow = `#projects_table tr[id="${data.project.id}"]`; - const itemName = `${itemRow} td[class*="name-"] a`; + const itemRow = `#row-${data.project.id}`; + const itemName = `${itemRow} .at-RowItem-header`; - client.expect.element('div[class*="at-Panel"] smart-search').visible; - client.expect.element('div[class*="at-Panel"] smart-search input').enabled; + client.expect.element('.at-Panel smart-search').visible; + client.expect.element('.at-Panel smart-search input').enabled; - client.sendKeys('div[class*="at-Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); - client.sendKeys('div[class*="at-Panel"] smart-search input', client.Keys.ENTER); + client.sendKeys('.at-Panel smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); + client.sendKeys('.at-Panel smart-search input', client.Keys.ENTER); - client.expect.element('div.spinny').visible; client.expect.element('div.spinny').not.visible; - client.expect.element('.List-titleBadge').text.equal('1'); + client.expect.element('.at-Panel-headingTitleBadge').text.equal('1'); client.expect.element(itemName).visible; - client.moveToElement(itemName, 0, 0, () => { - client.expect.element(itemName).attribute('aria-describedby'); - - client.getAttribute(itemName, 'aria-describedby', ({ value }) => { - const tooltip = `#${value}`; - - client.expect.element(tooltip).present; - client.expect.element(tooltip).visible; - - client.expect.element('#xss').not.present; - client.expect.element('[class=xss]').not.present; - client.expect.element(tooltip).attribute('innerHTML') - .contains('<div id="xss" class="xss">test</div>'); - }); - }); + // TODO: uncomment when tooltips are added + // client.moveToElement(itemName, 0, 0, () => { + // client.expect.element(itemName).attribute('aria-describedby'); + // + // client.getAttribute(itemName, 'aria-describedby', ({ value }) => { + // const tooltip = `#${value}`; + // + // client.expect.element(tooltip).present; + // client.expect.element(tooltip).visible; + // + // client.expect.element('#xss').not.present; + // client.expect.element('[class=xss]').not.present; + // client.expect.element(tooltip).attribute('innerHTML') + // .contains('<div id="xss" class="xss">test</div>'); + // }); + // }); client.click(`${itemRow} i[class*="trash"]`);