refactor projects list, clean up dependencies and old list generators and factory methods

This commit is contained in:
Haokun-Chen
2018-08-02 12:32:16 -04:00
committed by John Mitchell
parent c95c2a4580
commit 92ac3054c6
18 changed files with 760 additions and 611 deletions
@@ -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;
+9
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;
@@ -0,0 +1,13 @@
<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 hide-dismiss="true">
{{:: vm.strings.get('list.PANEL_TITLE') }}
<div class="at-Panel-headingTitleBadge" ng-show="vm.count">
{{ vm.count }}
</div>
</at-panel-heading>
</div>
<div ui-view="projectsList"></div>
</at-panel>
@@ -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;
@@ -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 <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;
@@ -0,0 +1,91 @@
<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)}">
<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>
@@ -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);
},
],
}
};