mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
Merge branch 'devel' into workflow-visualizer-search
This commit is contained in:
commit
0bbf1d7014
@ -14,18 +14,18 @@ class Control(object):
|
||||
services = ('dispatcher', 'callback_receiver')
|
||||
result = None
|
||||
|
||||
def __init__(self, service):
|
||||
def __init__(self, service, host=None):
|
||||
if service not in self.services:
|
||||
raise RuntimeError('{} must be in {}'.format(service, self.services))
|
||||
self.service = service
|
||||
queuename = get_local_queuename()
|
||||
self.queue = Queue(queuename, Exchange(queuename), routing_key=queuename)
|
||||
self.queuename = host or get_local_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(
|
||||
exchange=self.queue.exchange,
|
||||
channel=conn,
|
||||
routing_key=get_local_queuename()
|
||||
routing_key=self.queuename
|
||||
)
|
||||
producer.publish(msg, expiration=5, **kwargs)
|
||||
|
||||
@ -35,14 +35,13 @@ class Control(object):
|
||||
def running(self, *args, **kwargs):
|
||||
return self.control_with_reply('running', *args, **kwargs)
|
||||
|
||||
def control_with_reply(self, command, host=None, timeout=5):
|
||||
host = host or settings.CLUSTER_HOST_ID
|
||||
logger.warn('checking {} {} for {}'.format(self.service, command, host))
|
||||
def control_with_reply(self, command, timeout=5):
|
||||
logger.warn('checking {} {} for {}'.format(self.service, command, self.queuename))
|
||||
reply_queue = Queue(name="amq.rabbitmq.reply-to")
|
||||
self.result = None
|
||||
with Connection(settings.BROKER_URL) as conn:
|
||||
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:
|
||||
conn.drain_events(timeout=timeout)
|
||||
except socket.timeout:
|
||||
@ -50,10 +49,9 @@ class Control(object):
|
||||
raise
|
||||
return self.result
|
||||
|
||||
def control(self, msg, host=None, **kwargs):
|
||||
host = host or settings.CLUSTER_HOST_ID
|
||||
def control(self, msg, **kwargs):
|
||||
with Connection(settings.BROKER_URL) as conn:
|
||||
self.publish(msg, conn, host)
|
||||
self.publish(msg, conn)
|
||||
|
||||
def process_message(self, body, message):
|
||||
self.result = body
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import traceback
|
||||
from uuid import uuid4
|
||||
@ -10,7 +11,7 @@ from multiprocessing import Queue as MPQueue
|
||||
from Queue import Full as QueueFull, Empty as QueueEmpty
|
||||
|
||||
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 jinja2 import Template
|
||||
import psutil
|
||||
@ -319,6 +320,8 @@ class AutoscalePool(WorkerPool):
|
||||
1. Discover worker processes that exited, and recover messages they
|
||||
were handling.
|
||||
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 = []
|
||||
for w in self.workers[::]:
|
||||
@ -354,6 +357,20 @@ class AutoscalePool(WorkerPool):
|
||||
idx = random.choice(range(len(self.workers)))
|
||||
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):
|
||||
if self.full:
|
||||
# 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()
|
||||
|
||||
def write(self, preferred_queue, body):
|
||||
# when the cluster heartbeat occurs, clean up internally
|
||||
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:
|
||||
self.cleanup()
|
||||
if self.should_grow:
|
||||
self.up()
|
||||
# we don't care about "preferred queue" round robin distribution, just
|
||||
# find the first non-busy worker and claim it
|
||||
workers = self.workers[:]
|
||||
random.shuffle(workers)
|
||||
for w in workers:
|
||||
if not w.busy:
|
||||
w.put(body)
|
||||
break
|
||||
else:
|
||||
return super(AutoscalePool, self).write(preferred_queue, body)
|
||||
try:
|
||||
# when the cluster heartbeat occurs, clean up internally
|
||||
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:
|
||||
self.cleanup()
|
||||
if self.should_grow:
|
||||
self.up()
|
||||
# we don't care about "preferred queue" round robin distribution, just
|
||||
# find the first non-busy worker and claim it
|
||||
workers = self.workers[:]
|
||||
random.shuffle(workers)
|
||||
for w in workers:
|
||||
if not w.busy:
|
||||
w.put(body)
|
||||
break
|
||||
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')
|
||||
|
||||
@ -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.
|
||||
'''
|
||||
@ -41,6 +41,6 @@ def reap(instance=None, status='failed'):
|
||||
Q(execution_node=me.hostname) |
|
||||
Q(controller_node=me.hostname)
|
||||
) & ~Q(polymorphic_ctype_id=workflow_ctype_id)
|
||||
)
|
||||
).exclude(celery_task_id__in=excluded_uuids)
|
||||
for j in jobs:
|
||||
reap_job(j, status)
|
||||
|
||||
@ -5,6 +5,7 @@ import sys
|
||||
import traceback
|
||||
|
||||
import six
|
||||
from django import db
|
||||
|
||||
from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown
|
||||
|
||||
@ -74,6 +75,10 @@ class TaskWorker(BaseWorker):
|
||||
'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
|
||||
try:
|
||||
result = self.run_callable(body)
|
||||
|
||||
@ -7,7 +7,7 @@ from multiprocessing import Process
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache as django_cache
|
||||
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 awx.main.dispatch import get_local_queuename, reaper
|
||||
@ -57,6 +57,10 @@ class Command(BaseCommand):
|
||||
return super(AWXScheduler, self).tick(*args, **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)
|
||||
result, queue = task.apply_async()
|
||||
|
||||
|
||||
@ -7,9 +7,11 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
import six
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -29,6 +31,7 @@ from polymorphic.models import PolymorphicModel
|
||||
|
||||
# AWX
|
||||
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.utils import (
|
||||
encrypt_dict, decrypt_field, _inventory_updates,
|
||||
@ -1248,6 +1251,31 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
# Done!
|
||||
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
|
||||
def can_cancel(self):
|
||||
return bool(self.status in CAN_CANCEL)
|
||||
@ -1270,6 +1298,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
if self.status in ('pending', 'waiting', 'new'):
|
||||
self.status = 'canceled'
|
||||
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:
|
||||
self.job_explanation = job_explanation
|
||||
cancel_fields.append('job_explanation')
|
||||
|
||||
@ -481,3 +481,9 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
|
||||
@property
|
||||
def preferred_instance_groups(self):
|
||||
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'
|
||||
|
||||
@ -348,6 +348,32 @@ class TestJobReaper(object):
|
||||
else:
|
||||
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):
|
||||
i = Instance(hostname='awx')
|
||||
i.save()
|
||||
|
||||
@ -9,6 +9,7 @@ import atFeaturesTemplates from '~features/templates';
|
||||
import atFeaturesUsers from '~features/users';
|
||||
import atFeaturesJobs from '~features/jobs';
|
||||
import atFeaturesPortalMode from '~features/portalMode';
|
||||
import atFeaturesProjects from '~features/projects';
|
||||
|
||||
const MODULE_NAME = 'at.features';
|
||||
|
||||
@ -24,6 +25,7 @@ angular.module(MODULE_NAME, [
|
||||
atFeaturesOutput,
|
||||
atFeaturesTemplates,
|
||||
atFeaturesPortalMode,
|
||||
atFeaturesProjects
|
||||
]);
|
||||
|
||||
export default MODULE_NAME;
|
||||
|
||||
19
awx/ui/client/features/projects/index.controller.js
Normal file
19
awx/ui/client/features/projects/index.controller.js
Normal 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;
|
||||
9
awx/ui/client/features/projects/index.js
Normal file
9
awx/ui/client/features/projects/index.js
Normal 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;
|
||||
12
awx/ui/client/features/projects/index.view.html
Normal file
12
awx/ui/client/features/projects/index.view.html
Normal 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>
|
||||
53
awx/ui/client/features/projects/projects.strings.js
Normal file
53
awx/ui/client/features/projects/projects.strings.js
Normal 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;
|
||||
442
awx/ui/client/features/projects/projectsList.controller.js
Normal file
442
awx/ui/client/features/projects/projectsList.controller.js
Normal 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;
|
||||
92
awx/ui/client/features/projects/projectsList.view.html
Normal file
92
awx/ui/client/features/projects/projectsList.view.html
Normal 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>
|
||||
90
awx/ui/client/features/projects/routes/projectsList.route.js
Normal file
90
awx/ui/client/features/projects/routes/projectsList.route.js
Normal 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);
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
@ -139,6 +139,9 @@
|
||||
|
||||
.at-RowItem-status {
|
||||
margin-right: @at-margin-right-list-row-item-status;
|
||||
& > a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.at-RowItem--isHeader {
|
||||
@ -254,6 +257,12 @@
|
||||
background-color: @at-color-list-row-action-hover-danger;
|
||||
}
|
||||
|
||||
.at-RowAction--disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.at-Row .at-Row-checkbox {
|
||||
align-self: start;
|
||||
margin: 2px 20px 0 0;
|
||||
|
||||
@ -15,6 +15,7 @@ function atRowItem () {
|
||||
headerTag: '@',
|
||||
status: '@',
|
||||
statusTip: '@',
|
||||
statusClick: '&?',
|
||||
labelValue: '@',
|
||||
labelLink: '@',
|
||||
labelState: '@',
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
<div class="at-RowItem" ng-class="{'at-RowItem--isHeader': headerValue, 'at-RowItem--inline': inline}"
|
||||
ng-show="status || headerValue || value || valueBindHtml || (smartStatus && smartStatus.summary_fields.recent_jobs.length) || (tagValues && tagValues.length)">
|
||||
<div class="at-RowItem-status" ng-if="status">
|
||||
<a ng-if="headerLink || headerState" ng-href="{{ headerLink }}" ui-sref="{{ headerState }}"
|
||||
<a ng-if="statusClick" ng-click="statusClick()"
|
||||
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
|
||||
data-placement="top">
|
||||
<i class="fa icon-job-{{ status }}"></i>
|
||||
</a>
|
||||
<a ng-if="(headerLink || headerState) && !statusClick" ng-href="{{ headerLink }}" ui-sref="{{ headerState }}"
|
||||
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
|
||||
data-placement="top">
|
||||
<i class="fa icon-job-{{ status }}"></i>
|
||||
</a>
|
||||
<div ng-if="!headerLink && !headerState"
|
||||
<div ng-if="!headerLink && !headerState && !statusClick"
|
||||
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
|
||||
data-placement="top">
|
||||
<i class="fa icon-job-{{ status }}"></i>
|
||||
|
||||
@ -19,6 +19,41 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
|
||||
orgBase = GetBasePath('organizations'),
|
||||
projBase = GetBasePath('projects');
|
||||
|
||||
|
||||
function updateStatus() {
|
||||
if ($scope.projects) {
|
||||
$scope.projects.forEach(function(project, i) {
|
||||
$scope.projects[i].statusIcon = GetProjectIcon(project.status);
|
||||
$scope.projects[i].statusTip = GetProjectToolTip(project.status);
|
||||
$scope.projects[i].scm_update_tooltip = i18n._("Get latest SCM revision");
|
||||
$scope.projects[i].scm_type_class = "";
|
||||
|
||||
if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') {
|
||||
$scope.projects[i].statusTip = i18n._('Canceled. Click for details');
|
||||
}
|
||||
|
||||
if (project.status === 'running' || project.status === 'updating') {
|
||||
$scope.projects[i].scm_update_tooltip = i18n._("SCM update currently running");
|
||||
$scope.projects[i].scm_type_class = "btn-disabled";
|
||||
}
|
||||
|
||||
if ($scope.project_scm_type_options) {
|
||||
$scope.project_scm_type_options.forEach(function(type) {
|
||||
if (type.value === project.scm_type) {
|
||||
$scope.projects[i].scm_type = type.label;
|
||||
if (type.label === 'Manual') {
|
||||
$scope.projects[i].scm_update_tooltip = i18n._('Manual projects do not require an SCM update');
|
||||
$scope.projects[i].scm_type_class = 'btn-disabled';
|
||||
$scope.projects[i].statusTip = 'Not configured for SCM';
|
||||
$scope.projects[i].statusIcon = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
@ -31,35 +66,7 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
|
||||
|
||||
$scope.$on('choicesReadyProjectList', function() {
|
||||
Wait('stop');
|
||||
if ($scope.projects) {
|
||||
$scope.projects.forEach(function(project, i) {
|
||||
$scope.projects[i].statusIcon = GetProjectIcon(project.status);
|
||||
$scope.projects[i].statusTip = GetProjectToolTip(project.status);
|
||||
$scope.projects[i].scm_update_tooltip = i18n._("Get latest SCM revision");
|
||||
$scope.projects[i].scm_type_class = "";
|
||||
|
||||
if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') {
|
||||
$scope.projects[i].statusTip = i18n._('Canceled. Click for details');
|
||||
}
|
||||
|
||||
if (project.status === 'running' || project.status === 'updating') {
|
||||
$scope.projects[i].scm_update_tooltip = i18n._("SCM update currently running");
|
||||
$scope.projects[i].scm_type_class = "btn-disabled";
|
||||
}
|
||||
|
||||
$scope.project_scm_type_options.forEach(function(type) {
|
||||
if (type.value === project.scm_type) {
|
||||
$scope.projects[i].scm_type = type.label;
|
||||
if (type.label === 'Manual') {
|
||||
$scope.projects[i].scm_update_tooltip = i18n._('Manual projects do not require an SCM update');
|
||||
$scope.projects[i].scm_type_class = 'btn-disabled';
|
||||
$scope.projects[i].statusTip = 'Not configured for SCM';
|
||||
$scope.projects[i].statusIcon = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@ -69,9 +76,9 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
|
||||
});
|
||||
|
||||
$scope.$watchCollection(`${$scope.list.name}`, function() {
|
||||
optionsRequestDataProcessing();
|
||||
}
|
||||
);
|
||||
optionsRequestDataProcessing();
|
||||
updateStatus();
|
||||
});
|
||||
|
||||
// iterate over the list and add fields like type label, after the
|
||||
// OPTIONS request returns, or the list is sorted/paginated/searched
|
||||
|
||||
@ -7,10 +7,10 @@
|
||||
export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath',
|
||||
'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n',
|
||||
'CredentialTypes', 'ConfigData',
|
||||
'CredentialTypes', 'ConfigData', 'resolvedModels',
|
||||
function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest,
|
||||
Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state,
|
||||
CreateSelect2, i18n, CredentialTypes, ConfigData) {
|
||||
CreateSelect2, i18n, CredentialTypes, ConfigData, resolvedModels) {
|
||||
|
||||
let form = ProjectsForm(),
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
@ -23,6 +23,9 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
$scope.canEditOrg = true;
|
||||
const virtualEnvs = ConfigData.custom_virtualenvs || [];
|
||||
$scope.custom_virtualenvs_options = virtualEnvs;
|
||||
|
||||
const [ProjectModel] = resolvedModels;
|
||||
$scope.canAdd = ProjectModel.options('actions.POST');
|
||||
|
||||
Rest.setUrl(GetBasePath('projects'));
|
||||
Rest.options()
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -4,12 +4,10 @@
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import ProjectsList from './list/projects-list.controller';
|
||||
import ProjectsAdd from './add/projects-add.controller';
|
||||
import ProjectsEdit from './edit/projects-edit.controller';
|
||||
import ProjectList from './projects.list';
|
||||
import ProjectsForm from './projects.form';
|
||||
import { N_ } from '../i18n';
|
||||
import ProjectList from './projects.list';
|
||||
import GetProjectPath from './factories/get-project-path.factory';
|
||||
import GetProjectIcon from './factories/get-project-icon.factory';
|
||||
import GetProjectToolTip from './factories/get-project-tool-tip.factory';
|
||||
@ -20,93 +18,60 @@ import {
|
||||
} from '../scheduler/schedules.route';
|
||||
|
||||
import ProjectsTemplatesRoute from '~features/templates/routes/projectsTemplatesList.route';
|
||||
import ProjectsStrings from './projects.strings';
|
||||
import projectsListRoute from '~features/projects/routes/projectsList.route.js';
|
||||
|
||||
export default
|
||||
angular.module('Projects', [])
|
||||
.controller('ProjectsList', ProjectsList)
|
||||
.controller('ProjectsAdd', ProjectsAdd)
|
||||
.controller('ProjectsEdit', ProjectsEdit)
|
||||
.factory('GetProjectPath', GetProjectPath)
|
||||
.factory('GetProjectIcon', GetProjectIcon)
|
||||
.factory('GetProjectToolTip', GetProjectToolTip)
|
||||
.factory('ProjectList', ProjectList)
|
||||
.factory('ProjectsForm', ProjectsForm)
|
||||
.service('ProjectsStrings', ProjectsStrings)
|
||||
.factory('ProjectList', ProjectList)
|
||||
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
|
||||
function($stateProvider, stateDefinitionsProvider,$stateExtenderProvider) {
|
||||
let stateDefinitions = stateDefinitionsProvider.$get();
|
||||
let stateExtender = $stateExtenderProvider.$get();
|
||||
var projectResolve = {
|
||||
CredentialTypes: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors',
|
||||
(Rest, $stateParams, GetBasePath, ProcessErrors) => {
|
||||
var path = GetBasePath('credential_types');
|
||||
Rest.setUrl(path);
|
||||
return Rest.get()
|
||||
.then(function(data) {
|
||||
return (data.data.results);
|
||||
}).catch(function(response) {
|
||||
ProcessErrors(null, response.data, response.status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get credential types. GET returned status: ' +
|
||||
response.status
|
||||
});
|
||||
});
|
||||
}
|
||||
],
|
||||
ConfigData: ['ConfigService', 'ProcessErrors', (ConfigService, ProcessErrors) => {
|
||||
return ConfigService.getConfig()
|
||||
.then(response => response)
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to get config. GET returned status: ' +
|
||||
'status: ' + status
|
||||
});
|
||||
});
|
||||
}]
|
||||
};
|
||||
|
||||
function generateStateTree() {
|
||||
let projectTree = stateDefinitions.generateTree({
|
||||
parent: 'projects', // top-most node in the generated tree (will replace this state definition)
|
||||
modes: ['add', 'edit'],
|
||||
generateSchedulerView: true,
|
||||
list: 'ProjectList',
|
||||
let projectAdd = stateDefinitions.generateTree({
|
||||
name: 'projects.add',
|
||||
url: '/add',
|
||||
modes: ['add'],
|
||||
form: 'ProjectsForm',
|
||||
controllers: {
|
||||
list: ProjectsList, // DI strings or objects
|
||||
add: ProjectsAdd,
|
||||
edit: ProjectsEdit
|
||||
add: 'ProjectsAdd',
|
||||
},
|
||||
});
|
||||
|
||||
let projectEdit = stateDefinitions.generateTree({
|
||||
name: 'projects.edit',
|
||||
url: '/:project_id',
|
||||
modes: ['edit'],
|
||||
form: 'ProjectsForm',
|
||||
controllers: {
|
||||
edit: 'ProjectsEdit',
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'project',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
activityStreamId: 'project_id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: N_('PROJECTS')
|
||||
},
|
||||
breadcrumbs: {
|
||||
breadcrumbs: {
|
||||
edit: '{{breadcrumb.project_name}}'
|
||||
},
|
||||
resolve: {
|
||||
add: projectResolve,
|
||||
edit: projectResolve
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
projectTree
|
||||
projectAdd,
|
||||
projectEdit,
|
||||
]).then((generated) => {
|
||||
return {
|
||||
states: _.reduce(generated, (result, definition) => {
|
||||
return result.concat(definition.states);
|
||||
}, [
|
||||
stateExtender.buildDefinition(projectsListRoute),
|
||||
stateExtender.buildDefinition(ProjectsTemplatesRoute),
|
||||
stateExtender.buildDefinition(projectsSchedulesListRoute),
|
||||
stateExtender.buildDefinition(projectsSchedulesAddRoute),
|
||||
|
||||
@ -5,124 +5,124 @@
|
||||
*************************************************/
|
||||
|
||||
export default ['i18n', function(i18n) {
|
||||
return {
|
||||
return {
|
||||
|
||||
name: 'projects',
|
||||
iterator: 'project',
|
||||
basePath: 'projects',
|
||||
selectTitle: i18n._('Add Project'),
|
||||
editTitle: i18n._('PROJECTS'),
|
||||
listTitle: i18n._('PROJECTS'),
|
||||
selectInstructions: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
|
||||
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
|
||||
index: false,
|
||||
hover: true,
|
||||
emptyListText: i18n._('No Projects Have Been Created'),
|
||||
name: 'projects',
|
||||
iterator: 'project',
|
||||
basePath: 'projects',
|
||||
selectTitle: i18n._('Add Project'),
|
||||
editTitle: i18n._('PROJECTS'),
|
||||
listTitle: i18n._('PROJECTS'),
|
||||
selectInstructions: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
|
||||
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
|
||||
index: false,
|
||||
hover: true,
|
||||
emptyListText: i18n._('No Projects Have Been Created'),
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
label: '',
|
||||
iconOnly: true,
|
||||
ngClick: 'showSCMStatus(project.id)',
|
||||
awToolTip: '{{ project.statusTip }}',
|
||||
dataTipWatch: 'project.statusTip',
|
||||
dataPlacement: 'right',
|
||||
icon: "icon-job-{{ project.statusIcon }}",
|
||||
columnClass: "List-staticColumn--smallStatus",
|
||||
nosort: true,
|
||||
excludeModal: true
|
||||
},
|
||||
name: {
|
||||
key: true,
|
||||
label: i18n._('Name'),
|
||||
columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent",
|
||||
modalColumnClass: 'col-md-8',
|
||||
awToolTip: '{{project.description | sanitize}}',
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
scm_type: {
|
||||
label: i18n._('Type'),
|
||||
ngBind: 'project.type_label',
|
||||
excludeModal: true,
|
||||
columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs'
|
||||
},
|
||||
scm_revision: {
|
||||
label: i18n._('Revision'),
|
||||
excludeModal: true,
|
||||
columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs',
|
||||
type: 'revision'
|
||||
},
|
||||
last_updated: {
|
||||
label: i18n._('Last Updated'),
|
||||
filter: "longDate",
|
||||
columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs",
|
||||
excludeModal: true
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
status: {
|
||||
label: '',
|
||||
iconOnly: true,
|
||||
ngClick: 'showSCMStatus(project.id)',
|
||||
awToolTip: '{{ project.statusTip }}',
|
||||
dataTipWatch: 'project.statusTip',
|
||||
dataPlacement: 'right',
|
||||
icon: "icon-job-{{ project.statusIcon }}",
|
||||
columnClass: "List-staticColumn--smallStatus",
|
||||
nosort: true,
|
||||
excludeModal: true
|
||||
},
|
||||
name: {
|
||||
key: true,
|
||||
label: i18n._('Name'),
|
||||
columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent",
|
||||
modalColumnClass: 'col-md-8',
|
||||
awToolTip: '{{project.description | sanitize}}',
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
scm_type: {
|
||||
label: i18n._('Type'),
|
||||
ngBind: 'project.type_label',
|
||||
excludeModal: true,
|
||||
columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs'
|
||||
},
|
||||
scm_revision: {
|
||||
label: i18n._('Revision'),
|
||||
excludeModal: true,
|
||||
columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs',
|
||||
type: 'revision'
|
||||
},
|
||||
last_updated: {
|
||||
label: i18n._('Last Updated'),
|
||||
filter: "longDate",
|
||||
columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs",
|
||||
excludeModal: true
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
refresh: {
|
||||
mode: 'all',
|
||||
awToolTip: i18n._("Refresh the page"),
|
||||
ngClick: "refresh()",
|
||||
ngShow: "socketStatus === 'error'",
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: i18n._('REFRESH')
|
||||
},
|
||||
add: {
|
||||
mode: 'all', // One of: edit, select, all
|
||||
ngClick: 'addProject()',
|
||||
awToolTip: i18n._('Create a new project'),
|
||||
actionClass: 'at-Button--add',
|
||||
actionId: 'button-add',
|
||||
ngShow: "canAdd"
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
refresh: {
|
||||
mode: 'all',
|
||||
awToolTip: i18n._("Refresh the page"),
|
||||
ngClick: "refresh()",
|
||||
ngShow: "socketStatus === 'error'",
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: i18n._('REFRESH')
|
||||
},
|
||||
add: {
|
||||
mode: 'all', // One of: edit, select, all
|
||||
ngClick: 'addProject()',
|
||||
awToolTip: i18n._('Create a new project'),
|
||||
actionClass: 'at-Button--add',
|
||||
actionId: 'button-add',
|
||||
ngShow: "canAdd"
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
fieldActions: {
|
||||
|
||||
columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5',
|
||||
edit: {
|
||||
ngClick: "editProject(project.id)",
|
||||
awToolTip: i18n._('Edit the project'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: "project.summary_fields.user_capabilities.edit"
|
||||
},
|
||||
scm_update: {
|
||||
ngClick: 'SCMUpdate(project.id, $event)',
|
||||
awToolTip: "{{ project.scm_update_tooltip }}",
|
||||
dataTipWatch: "project.scm_update_tooltip",
|
||||
ngClass: "project.scm_type_class",
|
||||
dataPlacement: 'top',
|
||||
ngShow: "project.summary_fields.user_capabilities.start"
|
||||
},
|
||||
copy: {
|
||||
label: i18n._('Copy'),
|
||||
ngClick: 'copyProject(project)',
|
||||
"class": 'btn-danger btn-xs',
|
||||
awToolTip: i18n._('Copy project'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'project.summary_fields.user_capabilities.copy'
|
||||
},
|
||||
view: {
|
||||
ngClick: "editProject(project.id)",
|
||||
awToolTip: i18n._('View the project'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: "!project.summary_fields.user_capabilities.edit",
|
||||
icon: 'fa-eye',
|
||||
},
|
||||
"delete": {
|
||||
ngClick: "deleteProject(project.id, project.name)",
|
||||
awToolTip: i18n._('Delete the project'),
|
||||
ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete",
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: "cancelUpdate(project)",
|
||||
awToolTip: i18n._('Cancel the SCM update'),
|
||||
ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start",
|
||||
dataPlacement: 'top',
|
||||
ngDisabled: "project.pending_cancellation || project.status == 'canceled'"
|
||||
}
|
||||
}
|
||||
};}];
|
||||
columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5',
|
||||
edit: {
|
||||
ngClick: "editProject(project.id)",
|
||||
awToolTip: i18n._('Edit the project'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: "project.summary_fields.user_capabilities.edit"
|
||||
},
|
||||
scm_update: {
|
||||
ngClick: 'SCMUpdate(project.id, $event)',
|
||||
awToolTip: "{{ project.scm_update_tooltip }}",
|
||||
dataTipWatch: "project.scm_update_tooltip",
|
||||
ngClass: "project.scm_type_class",
|
||||
dataPlacement: 'top',
|
||||
ngShow: "project.summary_fields.user_capabilities.start"
|
||||
},
|
||||
copy: {
|
||||
label: i18n._('Copy'),
|
||||
ngClick: 'copyProject(project)',
|
||||
"class": 'btn-danger btn-xs',
|
||||
awToolTip: i18n._('Copy project'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: 'project.summary_fields.user_capabilities.copy'
|
||||
},
|
||||
view: {
|
||||
ngClick: "editProject(project.id)",
|
||||
awToolTip: i18n._('View the project'),
|
||||
dataPlacement: 'top',
|
||||
ngShow: "!project.summary_fields.user_capabilities.edit",
|
||||
icon: 'fa-eye',
|
||||
},
|
||||
"delete": {
|
||||
ngClick: "deleteProject(project.id, project.name)",
|
||||
awToolTip: i18n._('Delete the project'),
|
||||
ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete",
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: "cancelUpdate(project)",
|
||||
awToolTip: i18n._('Cancel the SCM update'),
|
||||
ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start",
|
||||
dataPlacement: 'top',
|
||||
ngDisabled: "project.pending_cancellation || project.status == 'canceled'"
|
||||
}
|
||||
}
|
||||
};}];
|
||||
@ -1,7 +0,0 @@
|
||||
function ProjectsStrings (BaseString) {
|
||||
BaseString.call(this, 'projects');
|
||||
}
|
||||
|
||||
ProjectsStrings.$inject = ['BaseStringService'];
|
||||
|
||||
export default ProjectsStrings;
|
||||
@ -56,10 +56,10 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
list: {
|
||||
selector: '.Panel',
|
||||
selector: '.at-Panel',
|
||||
elements: {
|
||||
badge: 'span[class~="badge"]',
|
||||
title: 'div[class="List-titleText"]',
|
||||
badge: '.at-Panel-headingTitleBadge',
|
||||
title: '.at-Panel-headingTitle',
|
||||
add: '#button-add'
|
||||
},
|
||||
sections: {
|
||||
|
||||
@ -2,8 +2,8 @@ const search = {
|
||||
selector: 'smart-search',
|
||||
locateStrategy: 'css selector',
|
||||
elements: {
|
||||
clearAll: 'a[class*="clear"]',
|
||||
searchButton: 'i[class$="search"]',
|
||||
clearAll: 'a[class*="clearAll"]',
|
||||
searchButton: 'i[class*="fa-search"]',
|
||||
input: 'input',
|
||||
tags: '.SmartSearch-tagContainer'
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ module.exports = {
|
||||
projects.waitForElementNotVisible('div.spinny');
|
||||
|
||||
projects.section.list.expect.element('@badge').text.equal('1');
|
||||
projects.expect.element(`#projects_table tr[id="${data.project.id}"]`).visible;
|
||||
projects.expect.element(`#row-${data.project.id}`).visible;
|
||||
projects.expect.element('i[class*="copy"]').visible;
|
||||
projects.expect.element('i[class*="copy"]').enabled;
|
||||
|
||||
|
||||
@ -508,36 +508,36 @@ module.exports = {
|
||||
client.expect.element('#project_form').visible;
|
||||
},
|
||||
'check project list for unsanitized content': client => {
|
||||
const itemRow = `#projects_table tr[id="${data.project.id}"]`;
|
||||
const itemName = `${itemRow} td[class*="name-"] a`;
|
||||
const itemRow = `#row-${data.project.id}`;
|
||||
const itemName = `${itemRow} .at-RowItem-header`;
|
||||
|
||||
client.expect.element('div[class^="Panel"] smart-search').visible;
|
||||
client.expect.element('div[class^="Panel"] smart-search input').enabled;
|
||||
client.expect.element('.at-Panel smart-search').visible;
|
||||
client.expect.element('.at-Panel smart-search input').enabled;
|
||||
|
||||
client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`);
|
||||
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
|
||||
client.sendKeys('.at-Panel smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`);
|
||||
client.sendKeys('.at-Panel smart-search input', client.Keys.ENTER);
|
||||
|
||||
client.expect.element('div.spinny').visible;
|
||||
client.expect.element('div.spinny').not.visible;
|
||||
|
||||
client.expect.element('.List-titleBadge').text.equal('1');
|
||||
client.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
|
||||
client.expect.element(itemName).visible;
|
||||
|
||||
client.moveToElement(itemName, 0, 0, () => {
|
||||
client.expect.element(itemName).attribute('aria-describedby');
|
||||
|
||||
client.getAttribute(itemName, 'aria-describedby', ({ value }) => {
|
||||
const tooltip = `#${value}`;
|
||||
|
||||
client.expect.element(tooltip).present;
|
||||
client.expect.element(tooltip).visible;
|
||||
|
||||
client.expect.element('#xss').not.present;
|
||||
client.expect.element('[class=xss]').not.present;
|
||||
client.expect.element(tooltip).attribute('innerHTML')
|
||||
.contains('<div id="xss" class="xss">test</div>');
|
||||
});
|
||||
});
|
||||
// TODO: uncomment when tooltips are added
|
||||
// client.moveToElement(itemName, 0, 0, () => {
|
||||
// client.expect.element(itemName).attribute('aria-describedby');
|
||||
//
|
||||
// client.getAttribute(itemName, 'aria-describedby', ({ value }) => {
|
||||
// const tooltip = `#${value}`;
|
||||
//
|
||||
// client.expect.element(tooltip).present;
|
||||
// client.expect.element(tooltip).visible;
|
||||
//
|
||||
// client.expect.element('#xss').not.present;
|
||||
// client.expect.element('[class=xss]').not.present;
|
||||
// client.expect.element(tooltip).attribute('innerHTML')
|
||||
// .contains('<div id="xss" class="xss">test</div>');
|
||||
// });
|
||||
// });
|
||||
|
||||
client.click(`${itemRow} i[class*="trash"]`);
|
||||
|
||||
|
||||
@ -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:
|
||||
* 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.
|
||||
|
||||
* 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`.
|
||||
|
||||
@ -40,6 +40,7 @@ django-taggit==0.22.2
|
||||
django==1.11.16
|
||||
djangorestframework-yaml==1.0.3
|
||||
djangorestframework==3.7.7
|
||||
docutils==0.14 # via botocore
|
||||
enum34==1.1.6 # via cryptography
|
||||
functools32==3.2.3.post2 # via jsonschema
|
||||
futures==3.2.0 # via requests-futures
|
||||
|
||||
@ -1,36 +1,54 @@
|
||||
# GCE
|
||||
apache-libcloud==2.2.1
|
||||
# azure deps from https://github.com/ansible/ansible/blob/fe1153c0afa1ffd648147af97454e900560b3532/packaging/requirements/requirements-azure.txt
|
||||
azure-mgmt-compute>=2.0.0,<3 # Taken from Ansible core module requirements
|
||||
azure-mgmt-network>=1.3.0,<2
|
||||
azure-mgmt-storage>=1.5.0,<2
|
||||
azure-mgmt-resource>=1.1.0,<2
|
||||
azure-storage>=0.35.1,<0.36
|
||||
azure-cli-core>=2.0.12,<3
|
||||
msrest!=0.4.15
|
||||
msrestazure>=0.4.11,<0.5
|
||||
azure-mgmt-dns>=1.0.1,<2
|
||||
azure-mgmt-keyvault>=0.40.0,<0.41
|
||||
azure-mgmt-batch>=4.1.0,<5
|
||||
azure-mgmt-sql>=0.7.1,<0.8
|
||||
azure-mgmt-web>=0.32.0,<0.33
|
||||
azure-mgmt-containerservice>=2.0.0,<3.0.0
|
||||
azure-mgmt-containerregistry>=1.0.1
|
||||
azure-mgmt-rdbms>=0.2.0rc1,<0.3.0
|
||||
azure-mgmt-containerinstance>=0.3.1
|
||||
backports.ssl-match-hostname==3.5.0.1
|
||||
# Azure
|
||||
# azure deps from https://github.com/ansible/ansible/blob/stable-2.7/packaging/requirements/requirements-azure.txt
|
||||
packaging
|
||||
azure-cli-core==2.0.35
|
||||
azure-cli-nspkg==3.0.2
|
||||
azure-common==1.1.11
|
||||
azure-mgmt-batch==4.1.0
|
||||
azure-mgmt-compute==2.1.0
|
||||
azure-mgmt-containerinstance==0.4.0
|
||||
azure-mgmt-containerregistry==2.0.0
|
||||
azure-mgmt-containerservice==3.0.1
|
||||
azure-mgmt-dns==1.2.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-nspkg==2.0.0
|
||||
azure-mgmt-rdbms==1.2.0
|
||||
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
|
||||
boto3==1.6.2
|
||||
# netaddr filter
|
||||
netaddr
|
||||
# oVirt/RHV
|
||||
ovirt-engine-sdk-python==4.2.4 # minimum set inside Ansible facts module requirements
|
||||
# AWX usage
|
||||
pexpect==4.6.0 # same as AWX requirement
|
||||
python-memcached==1.59 # same as AWX requirement
|
||||
psphere==0.5.2
|
||||
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
|
||||
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
|
||||
|
||||
@ -4,60 +4,69 @@
|
||||
#
|
||||
# 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
|
||||
appdirs==1.4.3 # via openstacksdk, os-client-config
|
||||
applicationinsights==0.11.1 # via azure-cli-core
|
||||
argcomplete==1.9.4 # via azure-cli-core, knack
|
||||
asn1crypto==0.24.0 # via cryptography
|
||||
azure-cli-core==2.0.28
|
||||
azure-cli-nspkg==3.0.1 # via azure-cli-core
|
||||
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-cli-core==2.0.35
|
||||
azure-cli-nspkg==3.0.2
|
||||
azure-common==1.1.11
|
||||
azure-graphrbac==0.40.0
|
||||
azure-keyvault==1.0.0a1
|
||||
azure-mgmt-batch==4.1.0
|
||||
azure-mgmt-compute==2.1.0
|
||||
azure-mgmt-containerinstance==0.3.1
|
||||
azure-mgmt-containerregistry==1.0.1
|
||||
azure-mgmt-containerservice==2.0.0
|
||||
azure-mgmt-containerinstance==0.4.0
|
||||
azure-mgmt-containerregistry==2.0.0
|
||||
azure-mgmt-containerservice==3.0.1
|
||||
azure-mgmt-dns==1.2.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-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-rdbms==0.2.0rc1
|
||||
azure-mgmt-nspkg==2.0.0
|
||||
azure-mgmt-rdbms==1.2.0
|
||||
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 # via azure-cli-nspkg, azure-common, azure-mgmt-nspkg, azure-storage
|
||||
azure-nspkg==2.0.0
|
||||
azure-storage==0.35.1
|
||||
backports.ssl-match-hostname==3.5.0.1
|
||||
bcrypt==3.1.4 # via paramiko
|
||||
boto3==1.6.2
|
||||
boto==2.47.0
|
||||
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
|
||||
chardet==3.0.4 # via requests
|
||||
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
|
||||
deprecation==2.0 # via openstacksdk
|
||||
docutils==0.14 # via botocore
|
||||
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
|
||||
futures==3.2.0 # via openstacksdk, s3transfer
|
||||
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
|
||||
iso8601==0.1.12 # via keystoneauth1, openstacksdk
|
||||
isodate==0.6.0 # via msrest
|
||||
jmespath==0.9.3 # via azure-cli-core, boto3, botocore, knack, openstacksdk
|
||||
jsonpatch==1.21 # via openstacksdk
|
||||
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
|
||||
knack==0.3.1 # via azure-cli-core
|
||||
knack==0.3.3 # via azure-cli-core
|
||||
lxml==4.1.1 # via pyvmomi
|
||||
monotonic==1.4 # via humanfriendly
|
||||
msrest==0.4.26
|
||||
msrestazure==0.4.22
|
||||
msrest==0.4.29
|
||||
msrestazure==0.4.31
|
||||
munch==2.2.0 # via openstacksdk
|
||||
netaddr==0.7.19
|
||||
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-service-types==1.2.0 # via openstacksdk
|
||||
ovirt-engine-sdk-python==4.2.4
|
||||
packaging==17.1 # via deprecation
|
||||
packaging==17.1
|
||||
paramiko==2.4.0 # via azure-cli-core
|
||||
pexpect==4.6.0
|
||||
pbr==3.1.1 # via keystoneauth1, openstacksdk, os-service-types, shade, stevedore
|
||||
pexpect==4.6.0
|
||||
psphere==0.5.2
|
||||
psutil==5.4.3
|
||||
ptyprocess==0.5.2 # via pexpect
|
||||
@ -92,15 +101,16 @@ requests-credssp==0.1.0
|
||||
requests-kerberos==0.12.0 # via pywinrm
|
||||
requests-ntlm==1.1.0 # via pywinrm
|
||||
requests-oauthlib==0.8.0 # via msrest
|
||||
requests==2.15.1
|
||||
requests==2.20.0
|
||||
requestsexceptions==1.4.0 # via openstacksdk, os-client-config
|
||||
s3transfer==0.1.13 # via boto3
|
||||
secretstorage==2.3.1
|
||||
secretstorage==2.3.1 # via keyring
|
||||
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
|
||||
stevedore==1.28.0 # via keystoneauth1
|
||||
suds==0.4 # via psphere
|
||||
tabulate==0.7.7 # via azure-cli-core, knack
|
||||
urllib3==1.24 # via requests
|
||||
wheel==0.30.0 # via azure-cli-core
|
||||
xmltodict==0.11.0 # via pywinrm
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
git+https://github.com/ansible/docutils.git@master#egg=docutils
|
||||
@ -1,2 +1 @@
|
||||
certifi
|
||||
pycurl # requires system package version
|
||||
|
||||
@ -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/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/docutils.git@master#egg=docutils
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user