Merge pull request #2356 from ansible/updateProjectList

Update project list

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot] 2018-10-18 21:43:13 +00:00 committed by GitHub
commit ce8117ef19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 948 additions and 588 deletions

View File

@ -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;

View File

@ -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 (typeof count === 'number') {
vm.count = count;
}
});
}
IndexProjectsController.$inject = [
'$scope',
'ProjectsStrings',
'Dataset',
];
export default IndexProjectsController;

View File

@ -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;

View File

@ -0,0 +1,12 @@
<div ui-view="scheduler"></div>
<div ui-view="form"></div>
<at-panel ng-cloak id="htmlTemplate">
<div ng-if="$state.includes('projects')">
<at-panel-heading
title="{{:: vm.strings.get('list.PANEL_TITLE') }}"
hide-dismiss="true"
badge="{{ vm.count }}">
</at-panel-heading>
</div>
<div ui-view="projectsList"></div>
</at-panel>

View File

@ -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;

View File

@ -0,0 +1,442 @@
/** ***********************************************
* 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;
$scope.$watch('vm.dataset.count', () => {
$scope.$emit('updateCount', vm.dataset.count, 'projects');
});
// 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 <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
// }
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 <a href="/#/jobs/project/${id}">
// ${$filter('sanitize')('placehoder')}</a>`;
// }
return html;
};
vm.copyProject = project => {
Wait('start');
ProjectModel
.create('get', project.id)
.then(model => model.copy())
.then((copiedProj) => {
ngToast.success({
content: `
<div class="Toast-wrapper">
<div class="Toast-icon">
<i class="fa fa-check-circle Toast-successIcon"></i>
</div>
<div>
${vm.strings.get('SUCCESSFUL_CREATION', copiedProj.name)}
</div>
</div>`,
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 = `<div class="Prompt-bodyQuery">${vm.strings.get('deleteResource.CONFIRM', 'project')}</div>`;
counts.forEach(countObj => {
if (countObj.count && countObj.count > 0) {
invalidateRelatedLines.push(`<div><span class="Prompt-warningResourceTitle">${countObj.label}</span><span class="badge List-titleBadge">${countObj.count}</span></div>`);
}
});
if (invalidateRelatedLines && invalidateRelatedLines.length > 0) {
deleteModalBody = `<div class="Prompt-bodyQuery">${vm.strings.get('deleteResource.USED_BY', 'project')} ${vm.strings.get('deleteResource.CONFIRM', 'project')}</div>`;
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;

View File

@ -0,0 +1,92 @@
<at-panel-body>
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="projects"
base-path="projects"
iterator="project"
list="vm.list"
collection="vm.projects"
dataset="vm.dataset"
search-tags="searchTags">
</smart-search>
<div class="at-List-toolbarAction" ng-show="canAdd">
<button
type="button"
class="at-Button--add"
id="button-add"
ui-sref="projects.add">
</button>
</div>
</div>
<at-list results="vm.projects">
<at-row ng-repeat="project in vm.projects"
ng-class="{'at-Row--active': (project.id === vm.activeId)}"
id="row-{{ project.id }}">
<div class="at-Row-items">
<at-row-item
status="{{ project.statusIcon }}"
status-tip="{{ project.statusTip }}"
status-click="vm.showSCMStatus(project.id)"
header-value="{{ project.name }}"
header-link="/#/projects/{{ project.id }}"
header-tag="{{ vm.projectTypes[project.scm_type] }}">
</at-row-item>
<div class="at-RowItem" ng-if="project.scm_revision">
<div class="at-RowItem-label">
{{ :: vm.strings.get('list.ROW_ITEM_LABEL_REVISION') }}
</div>
<at-truncate string="{{ project.scm_revision }}" maxLength="7"></at-truncate>
</div>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_ORGANIZATION')}}"
value="{{ project.summary_fields.organization.name }}"
value-link="/#/organizations/{{ project.organization }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_MODIFIED') }}"
value-bind-html="{{ vm.getLastModified(project) }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_USED') }}"
value-bind-html="{{ vm.getLastUsed(project) }}">
</at-row-item>
</div>
<div class="at-Row-actions">
<div aw-tool-tip="{{ project.scm_update_tooltip }}"
data-tip-watch="project.scm_update_tooltip"
data-placement="top">
<div class="at-RowAction"
ng-class="{'at-RowAction--disabled': project.scm_update_disabled }"
ng-click="vm.SCMUpdate(project.id, $event)"
ng-show="project.summary_fields.user_capabilities.start">
<i class="fa fa-refresh"></i>
</div>
</div>
<at-row-action icon="fa-copy" ng-click="vm.copyProject(project)"
ng-show="project.summary_fields.user_capabilities.copy">
</at-row-action>
<at-row-action icon="fa-trash" ng-click="vm.deleteProject(project.id, project.name)"
ng-show="(project.status !== 'updating'
&& project.status !== 'running'
&& project.status !== 'pending'
&& project.status !== 'waiting')
&& project.summary_fields.user_capabilities.delete">
</at-row-action>
<at-row-action icon="fa-minus-circle" ng-click="vm.cancelUpdate(project)"
ng-show="(project.status == 'updating'
|| project.status == 'running'
|| project.status == 'pending'
|| project.status == 'waiting')
&& project.summary_fields.user_capabilities.start">
</at-row-action>
</div>
</at-row>
</at-list>
<paginate
collection="vm.projects"
dataset="vm.dataset"
iterator="project"
base-path="projects">
</paginate>
</at-panel-body>

View File

@ -0,0 +1,90 @@
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',
(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',
'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);
},
],
}
};

View File

@ -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;

View File

@ -15,6 +15,7 @@ function atRowItem () {
headerTag: '@',
status: '@',
statusTip: '@',
statusClick: '&?',
labelValue: '@',
labelLink: '@',
labelState: '@',

View File

@ -1,12 +1,17 @@
<div class="at-RowItem" ng-class="{'at-RowItem--isHeader': headerValue, 'at-RowItem--inline': inline}"
ng-show="status || headerValue || value || valueBindHtml || (smartStatus && smartStatus.summary_fields.recent_jobs.length) || (tagValues && tagValues.length)">
<div class="at-RowItem-status" ng-if="status">
<a ng-if="headerLink || headerState" ng-href="{{ headerLink }}" ui-sref="{{ headerState }}"
<a ng-if="statusClick" ng-click="statusClick()"
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>
</a>
<a ng-if="(headerLink || headerState) && !statusClick" ng-href="{{ headerLink }}" ui-sref="{{ headerState }}"
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>
</a>
<div ng-if="!headerLink && !headerState"
<div ng-if="!headerLink && !headerState && !statusClick"
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>

View File

@ -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

View File

@ -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()

View File

@ -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: `
<div class="Toast-wrapper">
<div class="Toast-icon">
<i class="fa fa-check-circle Toast-successIcon"></i>
</div>
<div>
${ProjectsStrings.get('SUCCESSFUL_CREATION', copiedProj.name)}
</div>
</div>`,
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 = `<div class="Prompt-bodyQuery">${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}</div>`;
counts.forEach(countObj => {
if(countObj.count && countObj.count > 0) {
invalidateRelatedLines.push(`<div><span class="Prompt-warningResourceTitle">${countObj.label}</span><span class="badge List-titleBadge">${countObj.count}</span></div>`);
}
});
if (invalidateRelatedLines && invalidateRelatedLines.length > 0) {
deleteModalBody = `<div class="Prompt-bodyQuery">${ProjectsStrings.get('deleteResource.USED_BY', 'project')} ${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}</div>`;
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'), '<div>' + 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.'), '<em>', '</em>') + '</div>', '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'), '<div>' + 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), '<em>', '</em>') + '</div>', '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 });
}
}
});
};
}
];

View File

@ -4,12 +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 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';
@ -20,93 +18,60 @@ 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)
.factory('ProjectList', ProjectList)
.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),

View File

@ -5,124 +5,124 @@
*************************************************/
export default ['i18n', function(i18n) {
return {
return {
name: 'projects',
iterator: 'project',
basePath: 'projects',
selectTitle: i18n._('Add Project'),
editTitle: i18n._('PROJECTS'),
listTitle: i18n._('PROJECTS'),
selectInstructions: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
index: false,
hover: true,
emptyListText: i18n._('No Projects Have Been Created'),
name: 'projects',
iterator: 'project',
basePath: 'projects',
selectTitle: i18n._('Add Project'),
editTitle: i18n._('PROJECTS'),
listTitle: i18n._('PROJECTS'),
selectInstructions: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
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
}
},
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"
}
},
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: {
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'"
}
}
};}];
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'"
}
}
};}];

View File

@ -1,7 +0,0 @@
function ProjectsStrings (BaseString) {
BaseString.call(this, 'projects');
}
ProjectsStrings.$inject = ['BaseStringService'];
export default ProjectsStrings;

View File

@ -56,10 +56,10 @@ module.exports = {
}
},
list: {
selector: '.Panel',
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: {

View File

@ -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'
}

View File

@ -31,7 +31,7 @@ module.exports = {
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(`#row-${data.project.id}`).visible;
projects.expect.element('i[class*="copy"]').visible;
projects.expect.element('i[class*="copy"]').enabled;

View File

@ -508,36 +508,36 @@ module.exports = {
client.expect.element('#project_form').visible;
},
'check project list for unsanitized content': client => {
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^="Panel"] smart-search').visible;
client.expect.element('div[class^="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^="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('.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('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
});
});
// 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('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
// });
// });
client.click(`${itemRow} i[class*="trash"]`);