Merge branch 'devel' into workflow-visualizer-search

This commit is contained in:
Daniel Sami
2018-10-19 10:26:12 -04:00
committed by GitHub
36 changed files with 1149 additions and 671 deletions

View File

@@ -14,18 +14,18 @@ class Control(object):
services = ('dispatcher', 'callback_receiver') services = ('dispatcher', 'callback_receiver')
result = None result = None
def __init__(self, service): def __init__(self, service, host=None):
if service not in self.services: if service not in self.services:
raise RuntimeError('{} must be in {}'.format(service, self.services)) raise RuntimeError('{} must be in {}'.format(service, self.services))
self.service = service self.service = service
queuename = get_local_queuename() self.queuename = host or get_local_queuename()
self.queue = Queue(queuename, Exchange(queuename), routing_key=queuename) self.queue = Queue(self.queuename, Exchange(self.queuename), routing_key=self.queuename)
def publish(self, msg, conn, host, **kwargs): def publish(self, msg, conn, **kwargs):
producer = Producer( producer = Producer(
exchange=self.queue.exchange, exchange=self.queue.exchange,
channel=conn, channel=conn,
routing_key=get_local_queuename() routing_key=self.queuename
) )
producer.publish(msg, expiration=5, **kwargs) producer.publish(msg, expiration=5, **kwargs)
@@ -35,14 +35,13 @@ class Control(object):
def running(self, *args, **kwargs): def running(self, *args, **kwargs):
return self.control_with_reply('running', *args, **kwargs) return self.control_with_reply('running', *args, **kwargs)
def control_with_reply(self, command, host=None, timeout=5): def control_with_reply(self, command, timeout=5):
host = host or settings.CLUSTER_HOST_ID logger.warn('checking {} {} for {}'.format(self.service, command, self.queuename))
logger.warn('checking {} {} for {}'.format(self.service, command, host))
reply_queue = Queue(name="amq.rabbitmq.reply-to") reply_queue = Queue(name="amq.rabbitmq.reply-to")
self.result = None self.result = None
with Connection(settings.BROKER_URL) as conn: with Connection(settings.BROKER_URL) as conn:
with Consumer(conn, reply_queue, callbacks=[self.process_message], no_ack=True): with Consumer(conn, reply_queue, callbacks=[self.process_message], no_ack=True):
self.publish({'control': command}, conn, host, reply_to='amq.rabbitmq.reply-to') self.publish({'control': command}, conn, reply_to='amq.rabbitmq.reply-to')
try: try:
conn.drain_events(timeout=timeout) conn.drain_events(timeout=timeout)
except socket.timeout: except socket.timeout:
@@ -50,10 +49,9 @@ class Control(object):
raise raise
return self.result return self.result
def control(self, msg, host=None, **kwargs): def control(self, msg, **kwargs):
host = host or settings.CLUSTER_HOST_ID
with Connection(settings.BROKER_URL) as conn: with Connection(settings.BROKER_URL) as conn:
self.publish(msg, conn, host) self.publish(msg, conn)
def process_message(self, body, message): def process_message(self, body, message):
self.result = body self.result = body

View File

@@ -1,5 +1,6 @@
import logging import logging
import os import os
import sys
import random import random
import traceback import traceback
from uuid import uuid4 from uuid import uuid4
@@ -10,7 +11,7 @@ from multiprocessing import Queue as MPQueue
from Queue import Full as QueueFull, Empty as QueueEmpty from Queue import Full as QueueFull, Empty as QueueEmpty
from django.conf import settings from django.conf import settings
from django.db import connection as django_connection from django.db import connection as django_connection, connections
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
from jinja2 import Template from jinja2 import Template
import psutil import psutil
@@ -319,6 +320,8 @@ class AutoscalePool(WorkerPool):
1. Discover worker processes that exited, and recover messages they 1. Discover worker processes that exited, and recover messages they
were handling. were handling.
2. Clean up unnecessary, idle workers. 2. Clean up unnecessary, idle workers.
3. Check to see if the database says this node is running any tasks
that aren't actually running. If so, reap them.
""" """
orphaned = [] orphaned = []
for w in self.workers[::]: for w in self.workers[::]:
@@ -354,6 +357,20 @@ class AutoscalePool(WorkerPool):
idx = random.choice(range(len(self.workers))) idx = random.choice(range(len(self.workers)))
self.write(idx, m) self.write(idx, m)
# if the database says a job is running on this node, but it's *not*,
# then reap it
running_uuids = []
for worker in self.workers:
worker.calculate_managed_tasks()
running_uuids.extend(worker.managed_tasks.keys())
try:
reaper.reap(excluded_uuids=running_uuids)
except Exception:
# we _probably_ failed here due to DB connectivity issues, so
# don't use our logger (it accesses the database for configuration)
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
def up(self): def up(self):
if self.full: if self.full:
# if we can't spawn more workers, just toss this message into a # if we can't spawn more workers, just toss this message into a
@@ -364,18 +381,25 @@ class AutoscalePool(WorkerPool):
return super(AutoscalePool, self).up() return super(AutoscalePool, self).up()
def write(self, preferred_queue, body): def write(self, preferred_queue, body):
# when the cluster heartbeat occurs, clean up internally try:
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']: # when the cluster heartbeat occurs, clean up internally
self.cleanup() if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:
if self.should_grow: self.cleanup()
self.up() if self.should_grow:
# we don't care about "preferred queue" round robin distribution, just self.up()
# find the first non-busy worker and claim it # we don't care about "preferred queue" round robin distribution, just
workers = self.workers[:] # find the first non-busy worker and claim it
random.shuffle(workers) workers = self.workers[:]
for w in workers: random.shuffle(workers)
if not w.busy: for w in workers:
w.put(body) if not w.busy:
break w.put(body)
else: break
return super(AutoscalePool, self).write(preferred_queue, body) else:
return super(AutoscalePool, self).write(preferred_queue, body)
except Exception:
for conn in connections.all():
# If the database connection has a hiccup, re-establish a new
# connection
conn.close_if_unusable_or_obsolete()
logger.exception('failed to write inbound message')

View File

@@ -26,7 +26,7 @@ def reap_job(j, status):
) )
def reap(instance=None, status='failed'): def reap(instance=None, status='failed', excluded_uuids=[]):
''' '''
Reap all jobs in waiting|running for this instance. Reap all jobs in waiting|running for this instance.
''' '''
@@ -41,6 +41,6 @@ def reap(instance=None, status='failed'):
Q(execution_node=me.hostname) | Q(execution_node=me.hostname) |
Q(controller_node=me.hostname) Q(controller_node=me.hostname)
) & ~Q(polymorphic_ctype_id=workflow_ctype_id) ) & ~Q(polymorphic_ctype_id=workflow_ctype_id)
) ).exclude(celery_task_id__in=excluded_uuids)
for j in jobs: for j in jobs:
reap_job(j, status) reap_job(j, status)

View File

@@ -5,6 +5,7 @@ import sys
import traceback import traceback
import six import six
from django import db
from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown
@@ -74,6 +75,10 @@ class TaskWorker(BaseWorker):
'task': u'awx.main.tasks.RunProjectUpdate' 'task': u'awx.main.tasks.RunProjectUpdate'
} }
''' '''
for conn in db.connections.all():
# If the database connection has a hiccup during at task, close it
# so we can establish a new connection
conn.close_if_unusable_or_obsolete()
result = None result = None
try: try:
result = self.run_callable(body) result = self.run_callable(body)

View File

@@ -7,7 +7,7 @@ from multiprocessing import Process
from django.conf import settings from django.conf import settings
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import connection as django_connection from django.db import connection as django_connection, connections
from kombu import Connection, Exchange, Queue from kombu import Connection, Exchange, Queue
from awx.main.dispatch import get_local_queuename, reaper from awx.main.dispatch import get_local_queuename, reaper
@@ -57,6 +57,10 @@ class Command(BaseCommand):
return super(AWXScheduler, self).tick(*args, **kwargs) return super(AWXScheduler, self).tick(*args, **kwargs)
def apply_async(self, entry, producer=None, advance=True, **kwargs): def apply_async(self, entry, producer=None, advance=True, **kwargs):
for conn in connections.all():
# If the database connection has a hiccup, re-establish a new
# connection
conn.close_if_unusable_or_obsolete()
task = TaskWorker.resolve_callable(entry.task) task = TaskWorker.resolve_callable(entry.task)
result, queue = task.apply_async() result, queue = task.apply_async()

View File

@@ -7,9 +7,11 @@ import json
import logging import logging
import os import os
import re import re
import socket
import subprocess import subprocess
import tempfile import tempfile
from collections import OrderedDict from collections import OrderedDict
import six
# Django # Django
from django.conf import settings from django.conf import settings
@@ -29,6 +31,7 @@ from polymorphic.models import PolymorphicModel
# AWX # AWX
from awx.main.models.base import * # noqa from awx.main.models.base import * # noqa
from awx.main.dispatch.control import Control as ControlDispatcher
from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin
from awx.main.utils import ( from awx.main.utils import (
encrypt_dict, decrypt_field, _inventory_updates, encrypt_dict, decrypt_field, _inventory_updates,
@@ -1248,6 +1251,31 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
# Done! # Done!
return True return True
@property
def actually_running(self):
# returns True if the job is running in the appropriate dispatcher process
running = False
if all([
self.status == 'running',
self.celery_task_id,
self.execution_node
]):
# If the job is marked as running, but the dispatcher
# doesn't know about it (or the dispatcher doesn't reply),
# then cancel the job
timeout = 5
try:
running = self.celery_task_id in ControlDispatcher(
'dispatcher', self.execution_node
).running(timeout=timeout)
except socket.timeout:
logger.error(six.text_type(
'could not reach dispatcher on {} within {}s'
).format(self.execution_node, timeout))
running = False
return running
@property @property
def can_cancel(self): def can_cancel(self):
return bool(self.status in CAN_CANCEL) return bool(self.status in CAN_CANCEL)
@@ -1270,6 +1298,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
if self.status in ('pending', 'waiting', 'new'): if self.status in ('pending', 'waiting', 'new'):
self.status = 'canceled' self.status = 'canceled'
cancel_fields.append('status') cancel_fields.append('status')
if self.status == 'running' and not self.actually_running:
self.status = 'canceled'
cancel_fields.append('status')
if job_explanation is not None: if job_explanation is not None:
self.job_explanation = job_explanation self.job_explanation = job_explanation
cancel_fields.append('job_explanation') cancel_fields.append('job_explanation')

View File

@@ -481,3 +481,9 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
@property @property
def preferred_instance_groups(self): def preferred_instance_groups(self):
return [] return []
@property
def actually_running(self):
# WorkflowJobs don't _actually_ run anything in the dispatcher, so
# there's no point in asking the dispatcher if it knows about this task
return self.status == 'running'

View File

@@ -348,6 +348,32 @@ class TestJobReaper(object):
else: else:
assert job.status == status assert job.status == status
@pytest.mark.parametrize('excluded_uuids, fail', [
(['abc123'], False),
([], True),
])
def test_do_not_reap_excluded_uuids(self, excluded_uuids, fail):
i = Instance(hostname='awx')
i.save()
j = Job(
status='running',
execution_node='awx',
controller_node='',
start_args='SENSITIVE',
celery_task_id='abc123',
)
j.save()
# if the UUID is excluded, don't reap it
reaper.reap(i, excluded_uuids=excluded_uuids)
job = Job.objects.first()
if fail:
assert job.status == 'failed'
assert 'marked as failed' in job.job_explanation
assert job.start_args == ''
else:
assert job.status == 'running'
def test_workflow_does_not_reap(self): def test_workflow_does_not_reap(self):
i = Instance(hostname='awx') i = Instance(hostname='awx')
i.save() i.save()

View File

@@ -9,6 +9,7 @@ import atFeaturesTemplates from '~features/templates';
import atFeaturesUsers from '~features/users'; import atFeaturesUsers from '~features/users';
import atFeaturesJobs from '~features/jobs'; import atFeaturesJobs from '~features/jobs';
import atFeaturesPortalMode from '~features/portalMode'; import atFeaturesPortalMode from '~features/portalMode';
import atFeaturesProjects from '~features/projects';
const MODULE_NAME = 'at.features'; const MODULE_NAME = 'at.features';
@@ -24,6 +25,7 @@ angular.module(MODULE_NAME, [
atFeaturesOutput, atFeaturesOutput,
atFeaturesTemplates, atFeaturesTemplates,
atFeaturesPortalMode, atFeaturesPortalMode,
atFeaturesProjects
]); ]);
export default MODULE_NAME; 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 { .at-RowItem-status {
margin-right: @at-margin-right-list-row-item-status; margin-right: @at-margin-right-list-row-item-status;
& > a {
cursor: pointer;
}
} }
.at-RowItem--isHeader { .at-RowItem--isHeader {
@@ -254,6 +257,12 @@
background-color: @at-color-list-row-action-hover-danger; 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 { .at-Row .at-Row-checkbox {
align-self: start; align-self: start;
margin: 2px 20px 0 0; margin: 2px 20px 0 0;

View File

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

View File

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

View File

@@ -19,6 +19,41 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
orgBase = GetBasePath('organizations'), orgBase = GetBasePath('organizations'),
projBase = GetBasePath('projects'); 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(); init();
function init() { function init() {
@@ -31,35 +66,7 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
$scope.$on('choicesReadyProjectList', function() { $scope.$on('choicesReadyProjectList', function() {
Wait('stop'); Wait('stop');
if ($scope.projects) { updateStatus();
$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';
}
}
});
});
}
}); });
} }
@@ -69,9 +76,9 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
}); });
$scope.$watchCollection(`${$scope.list.name}`, function() { $scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing(); optionsRequestDataProcessing();
} updateStatus();
); });
// iterate over the list and add fields like type label, after the // iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched // OPTIONS request returns, or the list is sorted/paginated/searched

View File

@@ -7,10 +7,10 @@
export default ['$scope', '$location', '$stateParams', 'GenerateForm', export default ['$scope', '$location', '$stateParams', 'GenerateForm',
'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath',
'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n', 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n',
'CredentialTypes', 'ConfigData', 'CredentialTypes', 'ConfigData', 'resolvedModels',
function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest, function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest,
Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state, Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state,
CreateSelect2, i18n, CredentialTypes, ConfigData) { CreateSelect2, i18n, CredentialTypes, ConfigData, resolvedModels) {
let form = ProjectsForm(), let form = ProjectsForm(),
base = $location.path().replace(/^\//, '').split('/')[0], base = $location.path().replace(/^\//, '').split('/')[0],
@@ -24,6 +24,9 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
const virtualEnvs = ConfigData.custom_virtualenvs || []; const virtualEnvs = ConfigData.custom_virtualenvs || [];
$scope.custom_virtualenvs_options = virtualEnvs; $scope.custom_virtualenvs_options = virtualEnvs;
const [ProjectModel] = resolvedModels;
$scope.canAdd = ProjectModel.options('actions.POST');
Rest.setUrl(GetBasePath('projects')); Rest.setUrl(GetBasePath('projects'));
Rest.options() Rest.options()
.then(({data}) => { .then(({data}) => {

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 * All Rights Reserved
*************************************************/ *************************************************/
import ProjectsList from './list/projects-list.controller';
import ProjectsAdd from './add/projects-add.controller'; import ProjectsAdd from './add/projects-add.controller';
import ProjectsEdit from './edit/projects-edit.controller'; import ProjectsEdit from './edit/projects-edit.controller';
import ProjectList from './projects.list';
import ProjectsForm from './projects.form'; import ProjectsForm from './projects.form';
import { N_ } from '../i18n'; import ProjectList from './projects.list';
import GetProjectPath from './factories/get-project-path.factory'; import GetProjectPath from './factories/get-project-path.factory';
import GetProjectIcon from './factories/get-project-icon.factory'; import GetProjectIcon from './factories/get-project-icon.factory';
import GetProjectToolTip from './factories/get-project-tool-tip.factory'; import GetProjectToolTip from './factories/get-project-tool-tip.factory';
@@ -20,93 +18,60 @@ import {
} from '../scheduler/schedules.route'; } from '../scheduler/schedules.route';
import ProjectsTemplatesRoute from '~features/templates/routes/projectsTemplatesList.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 export default
angular.module('Projects', []) angular.module('Projects', [])
.controller('ProjectsList', ProjectsList)
.controller('ProjectsAdd', ProjectsAdd) .controller('ProjectsAdd', ProjectsAdd)
.controller('ProjectsEdit', ProjectsEdit) .controller('ProjectsEdit', ProjectsEdit)
.factory('GetProjectPath', GetProjectPath) .factory('GetProjectPath', GetProjectPath)
.factory('GetProjectIcon', GetProjectIcon) .factory('GetProjectIcon', GetProjectIcon)
.factory('GetProjectToolTip', GetProjectToolTip) .factory('GetProjectToolTip', GetProjectToolTip)
.factory('ProjectList', ProjectList)
.factory('ProjectsForm', ProjectsForm) .factory('ProjectsForm', ProjectsForm)
.service('ProjectsStrings', ProjectsStrings) .factory('ProjectList', ProjectList)
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
function($stateProvider, stateDefinitionsProvider,$stateExtenderProvider) { function($stateProvider, stateDefinitionsProvider,$stateExtenderProvider) {
let stateDefinitions = stateDefinitionsProvider.$get(); let stateDefinitions = stateDefinitionsProvider.$get();
let stateExtender = $stateExtenderProvider.$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() { function generateStateTree() {
let projectTree = stateDefinitions.generateTree({ let projectAdd = stateDefinitions.generateTree({
parent: 'projects', // top-most node in the generated tree (will replace this state definition) name: 'projects.add',
modes: ['add', 'edit'], url: '/add',
generateSchedulerView: true, modes: ['add'],
list: 'ProjectList',
form: 'ProjectsForm', form: 'ProjectsForm',
controllers: { controllers: {
list: ProjectsList, // DI strings or objects add: 'ProjectsAdd',
add: ProjectsAdd, },
edit: ProjectsEdit });
let projectEdit = stateDefinitions.generateTree({
name: 'projects.edit',
url: '/:project_id',
modes: ['edit'],
form: 'ProjectsForm',
controllers: {
edit: 'ProjectsEdit',
}, },
data: { data: {
activityStream: true, activityStream: true,
activityStreamTarget: 'project', activityStreamTarget: 'project',
socket: { activityStreamId: 'project_id'
"groups": {
"jobs": ["status_changed"]
}
}
},
ncyBreadcrumb: {
label: N_('PROJECTS')
}, },
breadcrumbs: { breadcrumbs: {
edit: '{{breadcrumb.project_name}}' edit: '{{breadcrumb.project_name}}'
}, },
resolve: {
add: projectResolve,
edit: projectResolve
}
}); });
return Promise.all([ return Promise.all([
projectTree projectAdd,
projectEdit,
]).then((generated) => { ]).then((generated) => {
return { return {
states: _.reduce(generated, (result, definition) => { states: _.reduce(generated, (result, definition) => {
return result.concat(definition.states); return result.concat(definition.states);
}, [ }, [
stateExtender.buildDefinition(projectsListRoute),
stateExtender.buildDefinition(ProjectsTemplatesRoute), stateExtender.buildDefinition(ProjectsTemplatesRoute),
stateExtender.buildDefinition(projectsSchedulesListRoute), stateExtender.buildDefinition(projectsSchedulesListRoute),
stateExtender.buildDefinition(projectsSchedulesAddRoute), stateExtender.buildDefinition(projectsSchedulesAddRoute),

View File

@@ -5,124 +5,124 @@
*************************************************/ *************************************************/
export default ['i18n', function(i18n) { export default ['i18n', function(i18n) {
return { return {
name: 'projects', name: 'projects',
iterator: 'project', iterator: 'project',
basePath: 'projects', basePath: 'projects',
selectTitle: i18n._('Add Project'), selectTitle: i18n._('Add Project'),
editTitle: i18n._('PROJECTS'), editTitle: i18n._('PROJECTS'),
listTitle: i18n._('PROJECTS'), listTitle: i18n._('PROJECTS'),
selectInstructions: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' + 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>', '<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, index: false,
hover: true, hover: true,
emptyListText: i18n._('No Projects Have Been Created'), emptyListText: i18n._('No Projects Have Been Created'),
fields: { fields: {
status: { status: {
label: '', label: '',
iconOnly: true, iconOnly: true,
ngClick: 'showSCMStatus(project.id)', ngClick: 'showSCMStatus(project.id)',
awToolTip: '{{ project.statusTip }}', awToolTip: '{{ project.statusTip }}',
dataTipWatch: 'project.statusTip', dataTipWatch: 'project.statusTip',
dataPlacement: 'right', dataPlacement: 'right',
icon: "icon-job-{{ project.statusIcon }}", icon: "icon-job-{{ project.statusIcon }}",
columnClass: "List-staticColumn--smallStatus", columnClass: "List-staticColumn--smallStatus",
nosort: true, nosort: true,
excludeModal: true excludeModal: true
}, },
name: { name: {
key: true, key: true,
label: i18n._('Name'), label: i18n._('Name'),
columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent", columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent",
modalColumnClass: 'col-md-8', modalColumnClass: 'col-md-8',
awToolTip: '{{project.description | sanitize}}', awToolTip: '{{project.description | sanitize}}',
dataPlacement: 'top' dataPlacement: 'top'
}, },
scm_type: { scm_type: {
label: i18n._('Type'), label: i18n._('Type'),
ngBind: 'project.type_label', ngBind: 'project.type_label',
excludeModal: true, excludeModal: true,
columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs' columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs'
}, },
scm_revision: { scm_revision: {
label: i18n._('Revision'), label: i18n._('Revision'),
excludeModal: true, excludeModal: true,
columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs', columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs',
type: 'revision' type: 'revision'
}, },
last_updated: { last_updated: {
label: i18n._('Last Updated'), label: i18n._('Last Updated'),
filter: "longDate", filter: "longDate",
columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs", columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs",
excludeModal: true excludeModal: true
} }
}, },
actions: { actions: {
refresh: { refresh: {
mode: 'all', mode: 'all',
awToolTip: i18n._("Refresh the page"), awToolTip: i18n._("Refresh the page"),
ngClick: "refresh()", ngClick: "refresh()",
ngShow: "socketStatus === 'error'", ngShow: "socketStatus === 'error'",
actionClass: 'btn List-buttonDefault', actionClass: 'btn List-buttonDefault',
buttonContent: i18n._('REFRESH') buttonContent: i18n._('REFRESH')
}, },
add: { add: {
mode: 'all', // One of: edit, select, all mode: 'all', // One of: edit, select, all
ngClick: 'addProject()', ngClick: 'addProject()',
awToolTip: i18n._('Create a new project'), awToolTip: i18n._('Create a new project'),
actionClass: 'at-Button--add', actionClass: 'at-Button--add',
actionId: 'button-add', actionId: 'button-add',
ngShow: "canAdd" ngShow: "canAdd"
} }
}, },
fieldActions: { fieldActions: {
columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5', columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5',
edit: { edit: {
ngClick: "editProject(project.id)", ngClick: "editProject(project.id)",
awToolTip: i18n._('Edit the project'), awToolTip: i18n._('Edit the project'),
dataPlacement: 'top', dataPlacement: 'top',
ngShow: "project.summary_fields.user_capabilities.edit" ngShow: "project.summary_fields.user_capabilities.edit"
}, },
scm_update: { scm_update: {
ngClick: 'SCMUpdate(project.id, $event)', ngClick: 'SCMUpdate(project.id, $event)',
awToolTip: "{{ project.scm_update_tooltip }}", awToolTip: "{{ project.scm_update_tooltip }}",
dataTipWatch: "project.scm_update_tooltip", dataTipWatch: "project.scm_update_tooltip",
ngClass: "project.scm_type_class", ngClass: "project.scm_type_class",
dataPlacement: 'top', dataPlacement: 'top',
ngShow: "project.summary_fields.user_capabilities.start" ngShow: "project.summary_fields.user_capabilities.start"
}, },
copy: { copy: {
label: i18n._('Copy'), label: i18n._('Copy'),
ngClick: 'copyProject(project)', ngClick: 'copyProject(project)',
"class": 'btn-danger btn-xs', "class": 'btn-danger btn-xs',
awToolTip: i18n._('Copy project'), awToolTip: i18n._('Copy project'),
dataPlacement: 'top', dataPlacement: 'top',
ngShow: 'project.summary_fields.user_capabilities.copy' ngShow: 'project.summary_fields.user_capabilities.copy'
}, },
view: { view: {
ngClick: "editProject(project.id)", ngClick: "editProject(project.id)",
awToolTip: i18n._('View the project'), awToolTip: i18n._('View the project'),
dataPlacement: 'top', dataPlacement: 'top',
ngShow: "!project.summary_fields.user_capabilities.edit", ngShow: "!project.summary_fields.user_capabilities.edit",
icon: 'fa-eye', icon: 'fa-eye',
}, },
"delete": { "delete": {
ngClick: "deleteProject(project.id, project.name)", ngClick: "deleteProject(project.id, project.name)",
awToolTip: i18n._('Delete the project'), awToolTip: i18n._('Delete the project'),
ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete", ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete",
dataPlacement: 'top' dataPlacement: 'top'
}, },
cancel: { cancel: {
ngClick: "cancelUpdate(project)", ngClick: "cancelUpdate(project)",
awToolTip: i18n._('Cancel the SCM update'), 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", ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start",
dataPlacement: 'top', dataPlacement: 'top',
ngDisabled: "project.pending_cancellation || project.status == 'canceled'" 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: { list: {
selector: '.Panel', selector: '.at-Panel',
elements: { elements: {
badge: 'span[class~="badge"]', badge: '.at-Panel-headingTitleBadge',
title: 'div[class="List-titleText"]', title: '.at-Panel-headingTitle',
add: '#button-add' add: '#button-add'
}, },
sections: { sections: {

View File

@@ -2,8 +2,8 @@ const search = {
selector: 'smart-search', selector: 'smart-search',
locateStrategy: 'css selector', locateStrategy: 'css selector',
elements: { elements: {
clearAll: 'a[class*="clear"]', clearAll: 'a[class*="clearAll"]',
searchButton: 'i[class$="search"]', searchButton: 'i[class*="fa-search"]',
input: 'input', input: 'input',
tags: '.SmartSearch-tagContainer' tags: '.SmartSearch-tagContainer'
} }

View File

@@ -31,7 +31,7 @@ module.exports = {
projects.waitForElementNotVisible('div.spinny'); projects.waitForElementNotVisible('div.spinny');
projects.section.list.expect.element('@badge').text.equal('1'); 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"]').visible;
projects.expect.element('i[class*="copy"]').enabled; projects.expect.element('i[class*="copy"]').enabled;

View File

@@ -508,36 +508,36 @@ module.exports = {
client.expect.element('#project_form').visible; client.expect.element('#project_form').visible;
}, },
'check project list for unsanitized content': client => { 'check project list for unsanitized content': client => {
const itemRow = `#projects_table tr[id="${data.project.id}"]`; const itemRow = `#row-${data.project.id}`;
const itemName = `${itemRow} td[class*="name-"] a`; const itemName = `${itemRow} .at-RowItem-header`;
client.expect.element('div[class^="Panel"] smart-search').visible; client.expect.element('.at-Panel smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled; 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('.at-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', client.Keys.ENTER);
client.expect.element('div.spinny').visible;
client.expect.element('div.spinny').not.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.expect.element(itemName).visible;
client.moveToElement(itemName, 0, 0, () => { // TODO: uncomment when tooltips are added
client.expect.element(itemName).attribute('aria-describedby'); // client.moveToElement(itemName, 0, 0, () => {
// client.expect.element(itemName).attribute('aria-describedby');
client.getAttribute(itemName, 'aria-describedby', ({ value }) => { //
const tooltip = `#${value}`; // client.getAttribute(itemName, 'aria-describedby', ({ value }) => {
// const tooltip = `#${value}`;
client.expect.element(tooltip).present; //
client.expect.element(tooltip).visible; // 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('#xss').not.present;
client.expect.element(tooltip).attribute('innerHTML') // client.expect.element('[class=xss]').not.present;
.contains('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;'); // client.expect.element(tooltip).attribute('innerHTML')
}); // .contains('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
}); // });
// });
client.click(`${itemRow} i[class*="trash"]`); client.click(`${itemRow} i[class*="trash"]`);

View File

@@ -21,5 +21,3 @@ pip-compile requirements/requirements_ansible.in > requirements/requirements_ans
* As of `pip-tools` `1.8.1` `pip-compile` does not resolve packages specified using a git url. Thus, dependencies for things like `dm.xmlsec.binding` do not get resolved and output to `requirements.txt`. This means that: * As of `pip-tools` `1.8.1` `pip-compile` does not resolve packages specified using a git url. Thus, dependencies for things like `dm.xmlsec.binding` do not get resolved and output to `requirements.txt`. This means that:
* can't use `pip install --no-deps` because other deps WILL be sucked in * can't use `pip install --no-deps` because other deps WILL be sucked in
* all dependencies are NOT captured in our `.txt` files. This means you can't rely on the `.txt` when gathering licenses. * all dependencies are NOT captured in our `.txt` files. This means you can't rely on the `.txt` when gathering licenses.
* Package `docutils`, as an upstream of `boto3`, is commented out in both `requirements.txt` and `requirements_ansible.txt` because the official package has a bug that causes RPM build failure. [Here](https://sourceforge.net/p/docutils/bugs/321/) is the bug report. Please do not uncomment it before the bug fix lands. For now we are using [a monkey-patch version of `docutils`](https://github.com/ansible/docutils.git) that comes with the bug fix. It's included in `requirements_git.txt` and `requirements_ansible_git.txt`.

View File

@@ -40,6 +40,7 @@ django-taggit==0.22.2
django==1.11.16 django==1.11.16
djangorestframework-yaml==1.0.3 djangorestframework-yaml==1.0.3
djangorestframework==3.7.7 djangorestframework==3.7.7
docutils==0.14 # via botocore
enum34==1.1.6 # via cryptography enum34==1.1.6 # via cryptography
functools32==3.2.3.post2 # via jsonschema functools32==3.2.3.post2 # via jsonschema
futures==3.2.0 # via requests-futures futures==3.2.0 # via requests-futures

View File

@@ -1,36 +1,54 @@
# GCE
apache-libcloud==2.2.1 apache-libcloud==2.2.1
# azure deps from https://github.com/ansible/ansible/blob/fe1153c0afa1ffd648147af97454e900560b3532/packaging/requirements/requirements-azure.txt # Azure
azure-mgmt-compute>=2.0.0,<3 # Taken from Ansible core module requirements # azure deps from https://github.com/ansible/ansible/blob/stable-2.7/packaging/requirements/requirements-azure.txt
azure-mgmt-network>=1.3.0,<2 packaging
azure-mgmt-storage>=1.5.0,<2 azure-cli-core==2.0.35
azure-mgmt-resource>=1.1.0,<2 azure-cli-nspkg==3.0.2
azure-storage>=0.35.1,<0.36 azure-common==1.1.11
azure-cli-core>=2.0.12,<3 azure-mgmt-batch==4.1.0
msrest!=0.4.15 azure-mgmt-compute==2.1.0
msrestazure>=0.4.11,<0.5 azure-mgmt-containerinstance==0.4.0
azure-mgmt-dns>=1.0.1,<2 azure-mgmt-containerregistry==2.0.0
azure-mgmt-keyvault>=0.40.0,<0.41 azure-mgmt-containerservice==3.0.1
azure-mgmt-batch>=4.1.0,<5 azure-mgmt-dns==1.2.0
azure-mgmt-sql>=0.7.1,<0.8 azure-mgmt-keyvault==0.40.0
azure-mgmt-web>=0.32.0,<0.33 azure-mgmt-marketplaceordering==0.1.0
azure-mgmt-containerservice>=2.0.0,<3.0.0 azure-mgmt-monitor==0.5.2
azure-mgmt-containerregistry>=1.0.1 azure-mgmt-network==1.7.1
azure-mgmt-rdbms>=0.2.0rc1,<0.3.0 azure-mgmt-nspkg==2.0.0
azure-mgmt-containerinstance>=0.3.1 azure-mgmt-rdbms==1.2.0
backports.ssl-match-hostname==3.5.0.1 azure-mgmt-resource==1.2.2
azure-mgmt-sql==0.7.1
azure-mgmt-storage==1.5.0
azure-mgmt-trafficmanager==0.50.0
azure-mgmt-web==0.32.0
azure-nspkg==2.0.0
azure-storage==0.35.1
msrest==0.4.29
msrestazure==0.4.31
azure-keyvault==1.0.0a1
azure-graphrbac==0.40.0
# AWS
boto==2.47.0 # last which does not break ec2 scripts boto==2.47.0 # last which does not break ec2 scripts
boto3==1.6.2 boto3==1.6.2
# netaddr filter
netaddr netaddr
# oVirt/RHV
ovirt-engine-sdk-python==4.2.4 # minimum set inside Ansible facts module requirements ovirt-engine-sdk-python==4.2.4 # minimum set inside Ansible facts module requirements
# AWX usage
pexpect==4.6.0 # same as AWX requirement pexpect==4.6.0 # same as AWX requirement
python-memcached==1.59 # same as AWX requirement python-memcached==1.59 # same as AWX requirement
psphere==0.5.2
psutil==5.4.3 # same as AWX requirement psutil==5.4.3 # same as AWX requirement
pyvmomi==6.5
pywinrm[kerberos]==0.3.0
requests<2.16 # Older versions rely on certify
requests-credssp==0.1.0 # For windows authentication awx/issues/1144
secretstorage==2.3.1
shade==1.27.0
setuptools==36.0.1 setuptools==36.0.1
pip==9.0.1 pip==9.0.1
# VMware
psphere==0.5.2
pyvmomi==6.5
# WinRM
backports.ssl-match-hostname==3.5.0.1
pywinrm[kerberos]==0.3.0
requests
requests-credssp==0.1.0 # For windows authentication awx/issues/1144
# OpenStack
shade==1.27.0

View File

@@ -4,60 +4,69 @@
# #
# pip-compile --output-file requirements/requirements_ansible.txt requirements/requirements_ansible.in # pip-compile --output-file requirements/requirements_ansible.txt requirements/requirements_ansible.in
# #
adal==0.5.0 # via azure-cli-core, msrestazure adal==0.5.0 # via msrestazure
apache-libcloud==2.2.1 apache-libcloud==2.2.1
appdirs==1.4.3 # via openstacksdk, os-client-config appdirs==1.4.3 # via openstacksdk, os-client-config
applicationinsights==0.11.1 # via azure-cli-core applicationinsights==0.11.1 # via azure-cli-core
argcomplete==1.9.4 # via azure-cli-core, knack argcomplete==1.9.4 # via azure-cli-core, knack
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
azure-cli-core==2.0.28 azure-cli-core==2.0.35
azure-cli-nspkg==3.0.1 # via azure-cli-core azure-cli-nspkg==3.0.2
azure-common==1.1.8 # via azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-keyvault, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-web, azure-storage azure-common==1.1.11
azure-graphrbac==0.40.0
azure-keyvault==1.0.0a1
azure-mgmt-batch==4.1.0 azure-mgmt-batch==4.1.0
azure-mgmt-compute==2.1.0 azure-mgmt-compute==2.1.0
azure-mgmt-containerinstance==0.3.1 azure-mgmt-containerinstance==0.4.0
azure-mgmt-containerregistry==1.0.1 azure-mgmt-containerregistry==2.0.0
azure-mgmt-containerservice==2.0.0 azure-mgmt-containerservice==3.0.1
azure-mgmt-dns==1.2.0 azure-mgmt-dns==1.2.0
azure-mgmt-keyvault==0.40.0 azure-mgmt-keyvault==0.40.0
azure-mgmt-marketplaceordering==0.1.0
azure-mgmt-monitor==0.5.2
azure-mgmt-network==1.7.1 azure-mgmt-network==1.7.1
azure-mgmt-nspkg==2.0.0 # via azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-keyvault, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-web azure-mgmt-nspkg==2.0.0
azure-mgmt-rdbms==0.2.0rc1 azure-mgmt-rdbms==1.2.0
azure-mgmt-resource==1.2.2 azure-mgmt-resource==1.2.2
azure-mgmt-sql==0.7.1 azure-mgmt-sql==0.7.1
azure-mgmt-storage==1.5.0 azure-mgmt-storage==1.5.0
azure-mgmt-trafficmanager==0.50.0
azure-mgmt-web==0.32.0 azure-mgmt-web==0.32.0
azure-nspkg==2.0.0 # via azure-cli-nspkg, azure-common, azure-mgmt-nspkg, azure-storage azure-nspkg==2.0.0
azure-storage==0.35.1 azure-storage==0.35.1
backports.ssl-match-hostname==3.5.0.1 backports.ssl-match-hostname==3.5.0.1
bcrypt==3.1.4 # via paramiko bcrypt==3.1.4 # via paramiko
boto3==1.6.2 boto3==1.6.2
boto==2.47.0 boto==2.47.0
botocore==1.9.3 # via boto3, s3transfer botocore==1.9.3 # via boto3, s3transfer
certifi==2018.1.18 # via msrest certifi==2018.1.18 # via msrest, requests
cffi==1.11.5 # via bcrypt, cryptography, pynacl cffi==1.11.5 # via bcrypt, cryptography, pynacl
chardet==3.0.4 # via requests
colorama==0.3.9 # via azure-cli-core, knack colorama==0.3.9 # via azure-cli-core, knack
cryptography==2.1.4 # via adal, azure-storage, paramiko, pyopenssl, requests-kerberos, requests-ntlm, secretstorage configparser==3.5.0 # via entrypoints
cryptography==2.1.4 # via adal, azure-keyvault, azure-storage, paramiko, pyopenssl, requests-kerberos, requests-ntlm, secretstorage
decorator==4.2.1 # via openstacksdk decorator==4.2.1 # via openstacksdk
deprecation==2.0 # via openstacksdk deprecation==2.0 # via openstacksdk
docutils==0.14 # via botocore
dogpile.cache==0.6.5 # via openstacksdk dogpile.cache==0.6.5 # via openstacksdk
entrypoints==0.2.3 # via keyring
enum34==1.1.6 # via cryptography, knack, msrest, ovirt-engine-sdk-python enum34==1.1.6 # via cryptography, knack, msrest, ovirt-engine-sdk-python
futures==3.2.0 # via openstacksdk, s3transfer futures==3.2.0 # via openstacksdk, s3transfer
humanfriendly==4.8 # via azure-cli-core humanfriendly==4.8 # via azure-cli-core
idna==2.6 # via cryptography idna==2.6 # via cryptography, requests
ipaddress==1.0.19 # via cryptography, openstacksdk ipaddress==1.0.19 # via cryptography, openstacksdk
iso8601==0.1.12 # via keystoneauth1, openstacksdk iso8601==0.1.12 # via keystoneauth1, openstacksdk
isodate==0.6.0 # via msrest isodate==0.6.0 # via msrest
jmespath==0.9.3 # via azure-cli-core, boto3, botocore, knack, openstacksdk jmespath==0.9.3 # via azure-cli-core, boto3, botocore, knack, openstacksdk
jsonpatch==1.21 # via openstacksdk jsonpatch==1.21 # via openstacksdk
jsonpointer==2.0 # via jsonpatch jsonpointer==2.0 # via jsonpatch
keyring==11.0.0 # via msrestazure keyring==15.1.0 # via msrestazure
keystoneauth1==3.4.0 # via openstacksdk, os-client-config keystoneauth1==3.4.0 # via openstacksdk, os-client-config
knack==0.3.1 # via azure-cli-core knack==0.3.3 # via azure-cli-core
lxml==4.1.1 # via pyvmomi lxml==4.1.1 # via pyvmomi
monotonic==1.4 # via humanfriendly monotonic==1.4 # via humanfriendly
msrest==0.4.26 msrest==0.4.29
msrestazure==0.4.22 msrestazure==0.4.31
munch==2.2.0 # via openstacksdk munch==2.2.0 # via openstacksdk
netaddr==0.7.19 netaddr==0.7.19
netifaces==0.10.6 # via openstacksdk netifaces==0.10.6 # via openstacksdk
@@ -67,10 +76,10 @@ openstacksdk==0.12.0 # via shade
os-client-config==1.29.0 # via shade os-client-config==1.29.0 # via shade
os-service-types==1.2.0 # via openstacksdk os-service-types==1.2.0 # via openstacksdk
ovirt-engine-sdk-python==4.2.4 ovirt-engine-sdk-python==4.2.4
packaging==17.1 # via deprecation packaging==17.1
paramiko==2.4.0 # via azure-cli-core paramiko==2.4.0 # via azure-cli-core
pexpect==4.6.0
pbr==3.1.1 # via keystoneauth1, openstacksdk, os-service-types, shade, stevedore pbr==3.1.1 # via keystoneauth1, openstacksdk, os-service-types, shade, stevedore
pexpect==4.6.0
psphere==0.5.2 psphere==0.5.2
psutil==5.4.3 psutil==5.4.3
ptyprocess==0.5.2 # via pexpect ptyprocess==0.5.2 # via pexpect
@@ -92,15 +101,16 @@ requests-credssp==0.1.0
requests-kerberos==0.12.0 # via pywinrm requests-kerberos==0.12.0 # via pywinrm
requests-ntlm==1.1.0 # via pywinrm requests-ntlm==1.1.0 # via pywinrm
requests-oauthlib==0.8.0 # via msrest requests-oauthlib==0.8.0 # via msrest
requests==2.15.1 requests==2.20.0
requestsexceptions==1.4.0 # via openstacksdk, os-client-config requestsexceptions==1.4.0 # via openstacksdk, os-client-config
s3transfer==0.1.13 # via boto3 s3transfer==0.1.13 # via boto3
secretstorage==2.3.1 secretstorage==2.3.1 # via keyring
shade==1.27.0 shade==1.27.0
six==1.11.0 # via azure-cli-core, bcrypt, cryptography, isodate, keystoneauth1, knack, munch, ntlm-auth, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, python-memcached, pyvmomi, pywinrm, stevedore six==1.11.0 # via azure-cli-core, bcrypt, cryptography, isodate, keystoneauth1, knack, munch, ntlm-auth, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, python-memcached, pyvmomi, pywinrm, stevedore
stevedore==1.28.0 # via keystoneauth1 stevedore==1.28.0 # via keystoneauth1
suds==0.4 # via psphere suds==0.4 # via psphere
tabulate==0.7.7 # via azure-cli-core, knack tabulate==0.7.7 # via azure-cli-core, knack
urllib3==1.24 # via requests
wheel==0.30.0 # via azure-cli-core wheel==0.30.0 # via azure-cli-core
xmltodict==0.11.0 # via pywinrm xmltodict==0.11.0 # via pywinrm

View File

@@ -1 +0,0 @@
git+https://github.com/ansible/docutils.git@master#egg=docutils

View File

@@ -1,2 +1 @@
certifi
pycurl # requires system package version pycurl # requires system package version

View File

@@ -2,4 +2,3 @@ git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv
git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic
git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding
git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield
git+https://github.com/ansible/docutils.git@master#egg=docutils