From dd5fcbbfbd4d116752a873860983b7e9c436e23b Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Thu, 12 Jun 2014 16:51:48 -0500 Subject: [PATCH 01/32] Vagrant Local Development This commit adds playbooks and files necessary to do local development from within Vagrant. These playbooks start with a fresh Ubuntu 12.04 machine and: - Install Ansible - Install the Tower nightly - Install all components to exactly mirror production, except Apache - Install uwsgi and nginx for local development (since Apache lacks a working auto-reload) This isn't entirely perfect -- in particular, developing on task code is probably challenging until I figure out how to get celery not to read from the install, but it is a very easy way to get 90% of the way there very, very quickly. --- .gitignore | 3 +-- Vagrantfile | 41 +++++++++++++++++++++++++++++++++++++ ansible.cfg | 15 ++++++++++++++ awx/settings/development.py | 3 +++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Vagrantfile create mode 100644 ansible.cfg diff --git a/.gitignore b/.gitignore index bc8c451f61..d6fbb456b7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,5 @@ pep8.txt nohup.out reports package.json -Vagrantfile -ansible.cfg +tools/vagrant/local.yml *~ diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000000..bc42d085c4 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,41 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.require_version '>= 1.5.1' + +Vagrant.configure('2') do |config| + config.vm.define 'tower-precise', primary: true do |precise| + precise.vm.box = "precise-server-cloudimg-amd64" + precise.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/precise/current/precise-server-cloudimg-amd64-vagrant-disk1.box" + + precise.vm.hostname = 'tower-precise' + + precise.vm.network :private_network, ip: '33.33.33.13' + precise.vm.network :forwarded_port, guest: 80, host: 8013 + precise.vm.network :forwarded_port, guest: 8080, host: 8080 + precise.vm.network :forwarded_port, guest: 15672, host: 15013 + precise.vm.network :forwarded_port, guest: 24013, host: 24013 + + precise.vm.synced_folder '.', '/var/ansible/tower/', + :mount_options => [ 'gid=5853', 'dmode=2775' ] + + precise.vm.provider :virtualbox do |vb| + vb.customize ["modifyvm", :id, "--memory", "512"] + end + end + + config.ssh.forward_agent = true + + config.vm.provision 'ansible' do |ansible| + ansible.extra_vars = { + 'development' => true, + 'target_hosts' => 'vagrant', + 'target_user' => 'vagrant', + 'vagrant' => true, + 'vagrant_host_user' => ENV['USER'], + } + ansible.inventory_path = 'tools/vagrant/inventory' + ansible.playbook = 'tools/vagrant/playbook.yml' + ansible.verbose = 'v' + end +end diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000000..32ffcc5216 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,15 @@ +[defaults] + +# Connections +host_key_checking = False +record_host_keys = False +pipelining = True + + +# Command-line specific +module_name = shell + + +# Paths +roles_path = setup/roles:tools/vagrant/roles + diff --git a/awx/settings/development.py b/awx/settings/development.py index 43fe6c1d74..ba6cb1362f 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -47,6 +47,9 @@ if 'django_jenkins' in INSTALLED_APPS: JSHINT_CHECKED_FILES = [os.path.join(BASE_DIR, 'ui/static/js'), os.path.join(BASE_DIR, 'ui/static/lib/ansible'),] +# If there is an `/etc/awx/settings.py`, include it. +include(optional('/etc/awx/settings.py'), scope=locals()) + # If any local_*.py files are present in awx/settings/, use them to override # default settings for development. If not present, we can still run using # only the defaults. From a28d90a1ed81308213762284cd85bdfddf3a840f Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Tue, 17 Jun 2014 12:31:17 -0500 Subject: [PATCH 02/32] Add more memoryz. --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index bc42d085c4..eecb9d743a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -20,7 +20,7 @@ Vagrant.configure('2') do |config| :mount_options => [ 'gid=5853', 'dmode=2775' ] precise.vm.provider :virtualbox do |vb| - vb.customize ["modifyvm", :id, "--memory", "512"] + vb.customize ["modifyvm", :id, "--memory", '1024'] end end From e8337cffd4ad3decedbec10ded8ddd70702d080a Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 17 Jun 2014 11:53:33 -0400 Subject: [PATCH 03/32] Job stdout - Safari support Safari's DOMParser object does not support parsing HTML. When the app initializes we now detect the browser type and define $rootScope.browser accordingly. If the browser is Safari, then we do some ugly looping and regex'ing to separate out the html and style sheet from the stdout API response. --- awx/ui/static/js/app.js | 29 ++++++++++++- awx/ui/static/js/controllers/JobStdout.js | 53 +++++++++++++++++++---- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index a3ef0a3242..d8c1177230 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -426,7 +426,32 @@ angular.module('Tower', [ $rootScope.crumbCache = []; $rootScope.sessionTimer = Timer.init(); + function detectBrowser() { + var ua = window.navigator.userAgent, + browser; + if (ua.search("MSIE") >= 0) { + browser = "MSIE"; + } + else if (navigator.userAgent.search("Chrome") >= 0) { + browser = "CHROME"; + } + else if (navigator.userAgent.search("Firefox") >= 0) { + browser = "FF"; + } + else if (navigator.userAgent.search("Safari") >= 0 && navigator.userAgent.search("Chrome") < 0) { + browser = "SAFARI"; + } + else if (navigator.userAgent.search("Opera") >= 0) { + browser = "OPERA"; + } + return browser; + } + + $rootScope.browser = detectBrowser(); + $rootScope.$on("$routeChangeStart", function (event, next) { + var base; + // Before navigating away from current tab, make sure the primary view is visible if ($('#stream-container').is(':visible')) { HideStream(); @@ -456,7 +481,7 @@ angular.module('Tower', [ } // Make the correct tab active - var base = $location.path().replace(/^\//, '').split('/')[0]; + base = $location.path().replace(/^\//, '').split('/')[0]; if (base === '') { base = 'home'; } else { @@ -464,6 +489,7 @@ angular.module('Tower', [ base = (base === 'job_events' || base === 'job_host_summaries') ? 'jobs' : base; } $('.nav-tabs a[href="#' + base + '"]').tab('show'); + }); if (!Authorization.getToken()) { @@ -515,7 +541,6 @@ angular.module('Tower', [ setTimeout(function() { $rootScope.$apply(function() { sock.checkStatus(); - //$rootScope.$emit('SocketErrorEncountered'); $log.debug('socket status: ' + $rootScope.socketStatus); }); },2000); diff --git a/awx/ui/static/js/controllers/JobStdout.js b/awx/ui/static/js/controllers/JobStdout.js index 47bc044004..9dec929fed 100644 --- a/awx/ui/static/js/controllers/JobStdout.js +++ b/awx/ui/static/js/controllers/JobStdout.js @@ -7,7 +7,7 @@ 'use strict'; -function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, Socket) { +function JobStdoutController ($rootScope, $scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, Socket) { ClearScope(); @@ -36,14 +36,50 @@ function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBas Rest.setUrl(stdout_url + '?format=html'); Rest.get() .success(function(data) { + var lines, styles=[], html=[], found=false, doc, style, pre, parser; api_complete = true; Wait('stop'); - var doc, style, pre, parser = new DOMParser(); - doc = parser.parseFromString(data, "text/html"); - pre = doc.getElementsByTagName('pre'); - style = doc.getElementsByTagName('style'); - $('#style-sheet-container').empty().html(style[0]); - $('#pre-container-content').empty().html($(pre[0]).html()); + if ($rootScope.browser === "SAFARI") { + // Safari's DOMParser will not parse HTML, so we have to do our best to extract the + // parts we want. + + lines = data.split("\n"); + // Get the style sheet + lines.forEach(function(line) { + if (//.test(line)) { + found = false; + } + }); + found = false; + // Get all the bits between
 and  
+ lines.forEach(function(line) { + if (/
/.test(line)) {
+                            found = true;
+                        }
+                        else if (/<\/pre>/.test(line)) {
+                            found = false;
+                        }
+                        else if (found) {
+                            html.push(line);
+                        }
+                    });
+                    $('#style-sheet-container').empty().html(styles.join("\n"));
+                    $('#pre-container-content').empty().html(html.join("\n"));
+                }
+                else {
+                    parser = new DOMParser();
+                    doc = parser.parseFromString(data, "text/html");
+                    pre = doc.getElementsByTagName('pre');
+                    style = doc.getElementsByTagName('style');
+                    $('#style-sheet-container').empty().html(style[0]);
+                    $('#pre-container-content').empty().html($(pre[0]).html());
+                }
                 setTimeout(function() { $('#pre-container').mCustomScrollbar("scrollTo", 'bottom'); }, 1000);
             })
             .error(function(data, status) {
@@ -83,4 +119,5 @@ function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBas
         });
 }
 
-JobStdoutController.$inject = [ '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'Socket' ];
\ No newline at end of file
+JobStdoutController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'Socket' ];
+

From ba6049c704e62916af62ec1b0a5da51b8ad245b6 Mon Sep 17 00:00:00 2001
From: Chris Houseknecht 
Date: Tue, 17 Jun 2014 13:51:24 -0400
Subject: [PATCH 04/32] Job detail page refactor Removed need to sort
 hostResults and hosts arrays. Drawing graph on start of a new task, rather
 than start of new play. Now listening for job status events. If a status
 event indicates the job is completed, stop processing event and reload the
 job from the API.

---
 awx/ui/static/js/app.js                   |  1 +
 awx/ui/static/js/controllers/JobDetail.js | 36 ++++++++++++----
 awx/ui/static/js/helpers/JobDetail.js     | 51 ++++++++++-------------
 3 files changed, 50 insertions(+), 38 deletions(-)

diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js
index d8c1177230..b33a240b17 100644
--- a/awx/ui/static/js/app.js
+++ b/awx/ui/static/js/app.js
@@ -545,6 +545,7 @@ angular.module('Tower', [
                     });
                 },2000);
                 sock.on("status_changed", function(data) {
+                    $log.debug('Job ' + data.unified_job_id + ' status changed to ' + data.status);
                     $rootScope.$emit('JobStatusChange', data);
                 });
             }
diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js
index 837ade928e..f1873dee21 100644
--- a/awx/ui/static/js/controllers/JobDetail.js
+++ b/awx/ui/static/js/controllers/JobDetail.js
@@ -81,6 +81,23 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
         }
     });
 
+    if ($rootScope.removeJobStatusChange) {
+        $rootScope.removeJobStatusChange();
+    }
+    $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, data) {
+        // if we receive a status change event for the current job indicating the job
+        // is finished, stop event queue processing and reload
+        if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) {
+            if (data.status === 'failed' || data.status === 'canceled' ||
+                    data.status === 'error' || data.status === 'successful') {
+                $log.debug('Job completed!');
+                api_complete = false;
+                scope.haltEventQueue = true;
+                queue = [];
+                scope.$emit('LoadJob');
+            }
+        }
+    });
 
     if (scope.removeAPIComplete) {
         scope.removeAPIComplete();
@@ -114,11 +131,6 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
         api_complete = true;
         Wait('stop');
 
-        ProcessEventQueue({
-            scope: scope,
-            eventQueue: queue
-        });
-
         // Draw the graph
         if (JobIsFinished(scope)) {
             url = scope.job.related.job_events + '?event=playbook_on_stats';
@@ -139,10 +151,16 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                         msg: 'Call to ' + url + '. GET returned: ' + status });
                 });
         }
-        else {
+        else if (scope.host_summary.total > 0) {
             // Draw the graph based on summary values in memory
             DrawGraph({ scope: scope, resize: true });
         }
+
+        ProcessEventQueue({
+            scope: scope,
+            eventQueue: queue
+        });
+
     });
 
     if (scope.removeInitialDataLoaded) {
@@ -212,10 +230,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                         elapsed: elapsed,
                         playActiveClass: ''
                     };
-                    scope.host_summary.ok += data.ok_count;
-                    scope.host_summary.changed += data.changed_count;
+                    scope.host_summary.ok += (data.ok_count) ? data.ok_count : 0;
+                    scope.host_summary.changed += (data.changed_count) ? data.changed_count : 0;
                     scope.host_summary.unreachable += (data.unreachable_count) ? data.unreachable_count : 0;
-                    scope.host_summary.failed += data.failed_count;
+                    scope.host_summary.failed += (data.failed_count) ? data.failed_count : 0;
                     scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed +
                         scope.host_summary.unreachable + scope.host_summary.failed;
                 });
diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js
index c5cd38cc3d..75e7625d86 100644
--- a/awx/ui/static/js/helpers/JobDetail.js
+++ b/awx/ui/static/js/helpers/JobDetail.js
@@ -123,6 +123,9 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                     changed: event.changed,
                     modified: event.modified
                 });
+                if (scope.host_summary.total > 0) {
+                    DrawGraph({ scope: scope, resize: true });
+                }
                 break;
 
             case 'playbook_on_task_start':
@@ -161,6 +164,9 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                     changed: event.changed,
                     modified: event.modified
                 });
+                if (scope.host_summary.total > 0) {
+                    DrawGraph({ scope: scope, resize: true });
+                }
                 break;
 
             case 'runner_on_ok':
@@ -251,7 +257,6 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                 });
                 scope.job_status.status = (event.failed) ? 'failed' : 'successful';
                 scope.job_status.status_class = "";
-                scope.host_summary = {};
                 LoadHostSummary({ scope: scope, data: event.event_data });
                 DrawGraph({ scope: scope, resize: true });
                 break;
@@ -432,24 +437,22 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             task_id = params.task_id,
             modified = params.modified,
             created = params.created,
-            msg = params.message,
-            host;
+            msg = params.message;
+
+        scope.host_summary.ok += (status === 'successful') ? 1 : 0;
+        scope.host_summary.changed += (status === 'changed') ? 1 : 0;
+        scope.host_summary.unreachable += (status === 'unreachable') ? 1 : 0;
+        scope.host_summary.failed += (status === 'failed') ? 1 : 0;
+        scope.host_summary.total  = scope.host_summary.ok + scope.host_summary.changed + scope.host_summary.unreachable +
+            scope.host_summary.failed;
 
         if (scope.hostsMap[host_id]) {
-            host = scope.hosts[scope.hostsMap[host_id]];
-            host.ok += (status === 'successful') ? 1 : 0;
-            host.changed += (status === 'changed') ? 1 : 0;
-            host.unreachable += (status === 'unreachable') ? 1 : 0;
-            host.failed += (status === 'failed') ? 1 : 0;
+            scope.hosts[scope.hostsMap[host_id]].ok += (status === 'successful') ? 1 : 0;
+            scope.hosts[scope.hostsMap[host_id]].changed += (status === 'changed') ? 1 : 0;
+            scope.hosts[scope.hostsMap[host_id]].unreachable += (status === 'unreachable') ? 1 : 0;
+            scope.hosts[scope.hostsMap[host_id]].failed += (status === 'failed') ? 1 : 0;
         }
-        else {
-            // Totals for the summary graph
-            scope.host_summary.total += 1;
-            scope.host_summary.ok += (status === 'successful') ? 1 : 0;
-            scope.host_summary.changed += (status === 'changed') ? 1 : 0;
-            scope.host_summary.unreachable += (status === 'unreachable') ? 1 : 0;
-            scope.host_summary.failed += (status === 'failed') ? 1 : 0;
-
+        else if (scope.hosts.length < scope.hostSummaryTableRows) {
             scope.hosts.push({
                 id: host_id,
                 name: name,
@@ -467,11 +470,6 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                 // a must be equal to b
                 return 0;
             });
-
-            // prune the hosts array and rebuild the map
-            if (scope.hosts.length > scope.hostSummaryTableRows) {
-                scope.hosts.pop();
-            }
             scope.hostsMap = {};
             scope.hosts.forEach(function(host, idx){
                 scope.hostsMap[host.id] = idx;
@@ -513,7 +511,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             msg = params.message,
             play_id, first;
 
-        if (!scope.hostResultsMap[host_id]) {
+        if (!scope.hostResultsMap[host_id] && scope.hostResults.length < scope.hostTableRows) {
             scope.hostResults.push({
                 id: event_id,
                 status: status,
@@ -532,10 +530,6 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                 }
                 return 0;
             });
-            // Keep the list pruned to a limited # of hosts
-            if (scope.hostResults.length > scope.hostTableRows) {
-                scope.hostResults.splice(0,1);
-            }
             // Refresh the map
             scope.hostResultsMap = {};
             scope.hostResults.forEach(function(result, idx) {
@@ -543,10 +537,9 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             });
         }
 
-        // update the task
+        // update the task status bar
         if (scope.tasks[task_id]) {
             play_id = scope.tasks[task_id].play_id;
-
             first = FindFirstTaskofPlay({
                 scope: scope,
                 play_id: play_id
@@ -904,7 +897,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
         scope.host_summary.unreachable = Object.keys(data.dark).length;
         scope.host_summary.failed = Object.keys(data.failures).length;
         scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed +
-        scope.host_summary.unreachable + scope.host_summary.failed;
+            scope.host_summary.unreachable + scope.host_summary.failed;
     };
 }])
 

From 93a34b001f3e748a980457c2e5b361027421c4ce Mon Sep 17 00:00:00 2001
From: Michael DeHaan 
Date: Tue, 17 Jun 2014 13:52:27 -0500
Subject: [PATCH 05/32] Start of guidelines around API development.

---
 API_STANDARDS.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)
 create mode 100644 API_STANDARDS.md

diff --git a/API_STANDARDS.md b/API_STANDARDS.md
new file mode 100644
index 0000000000..469fef585d
--- /dev/null
+++ b/API_STANDARDS.md
@@ -0,0 +1,85 @@
+Coding Standards and Practices
+==============================
+
+This is not meant to be a style document so much as a practices document for ensuring performance and convention in the Ansible Tower API.
+
+Paginate Everything
+===================
+
+Anything that returns a collection must be paginated.
+
+Assume large data sets
+======================
+
+Don't test exclusively with small data.  Assume 1000-10000 hosts in all operations, with years of event data.
+
+Some of our users have 30,000 machines they manage.
+
+API performance
+===============
+
+In general, the expected response time for any API call is something like 1/4 of a second or less.  Signs of slow API
+performance should be regularly checked, particularly for missing indexes.
+
+Missing Indexes
+===============
+
+Any filters the UI uses should be indexed.
+
+Migrations
+==========
+
+Always think about any existing data when adding any new fields.  It's ok to wait in upgrade time to get the database to be 
+consistent.
+
+Limit Queries
+=============
+
+The number of queries made should be constant time and must not vary with the size of the result set.
+
+Consider RBAC
+=============
+
+The returns of all collections must be filtered by who has access to view them, without exception
+
+Discoverability
+===============
+
+All API endpoints must be able to be traversed from "/", and have comments, where possible, explaining their purpose
+
+Friendly Comments
+=================
+
+All API comments are exposed by the API browser and must be fit for customers.   Avoid jokes in API comments and error
+messages, as well as FIXME comments in places that the API will display.
+
+UI Sanity
+=========
+
+Where possible the API should provide API endpoints that feed raw data into the UI, the UI should not have to do lots of
+data transformations, as it is going to be less responsive and able to do these things.
+
+When requiring a collection of times of size N, the UI must not make any extra API queries for each item in the result set
+
+Effective Usage of Query Sets
+=============================
+
+The system must return Django result sets rather than building JSON in memory in nearly all cases.  Use things like
+exclude and joins, and let the database do the work.
+
+Serializers
+===========
+
+No database queries may be made in serializers because these are executed once per item, rather than paginated.
+
+REST verbs
+==========
+
+REST verbs should be RESTy.  Don't use GETs to do things that should be a PUT or POST.
+
+Unit tests
+==========
+
+Every URL/route must have unit test coverage.  Consider both positive and negative tests.
+
+

From ea2bb5e201c0417442d0e0d96cf99a37cce1aaee Mon Sep 17 00:00:00 2001
From: Luke Sneeringer 
Date: Tue, 17 Jun 2014 15:22:13 -0500
Subject: [PATCH 06/32] Make /api/v1/jobs/{id}/job_tasks/ scale.

This commit fixes an issue where the `job_tasks` endpoint
scaled poorly as the number of job tasks increased.

AC-1341
---
 awx/api/views.py | 103 ++++++++++++++++++++++++++++++++++-------------
 1 file changed, 76 insertions(+), 27 deletions(-)

diff --git a/awx/api/views.py b/awx/api/views.py
index 70f2d157ec..e27c877952 100644
--- a/awx/api/views.py
+++ b/awx/api/views.py
@@ -1528,40 +1528,89 @@ class JobJobTasksList(BaseJobEventsList):
     def get(self, request, *args, **kwargs):
         tasks = []
         job = get_object_or_404(self.parent_model, pk=self.kwargs['pk'])
-        parent_task = get_object_or_404(job.job_events, pk=int(request.QUERY_PARAMS.get('event_id', -1)))
-        for task_start_event in parent_task.children.filter(Q(event='playbook_on_task_start') | Q(event='playbook_on_setup')):
-            task_data = dict(id=task_start_event.id, name="Gathering Facts" if task_start_event.event == 'playbook_on_setup' else task_start_event.task,
-                             created=task_start_event.created, modified=task_start_event.modified,
-                             failed=False, changed=False, host_count=0, reported_hosts=0, successful_count=0, failed_count=0,
-                             changed_count=0, skipped_count=0)
-            for child_event in task_start_event.children.all():
-                if child_event.event == 'runner_on_failed':
+        parent_task = get_object_or_404(job.job_events,
+            pk=int(request.QUERY_PARAMS.get('event_id', -1)),
+        )
+
+        # Some events correspond to a playbook or task starting up,
+        # and these are what we're interested in here.
+        STARTING_EVENTS = ('playbook_on_task_start', 'playbook_on_setup')
+
+        # We need to pull information about each start event.
+        #
+        # This is super tricky, because this table has a one-to-many
+        # relationship with itself (parent-child), and we're getting
+        # information for an arbitrary number of children. This means we
+        # need stats on grandchildren, sorted by child. 
+        raw_data = (JobEvent.objects.filter(parent__parent=parent_task,
+                                            parent__event__in=STARTING_EVENTS)
+                                    .values('parent__id', 'event', 'changed')
+                                    .annotate(num=Count('event'))
+                                    .order_by('parent__id'))
+
+        # The data above will come back in a list, but we are going to
+        # want to access it based on the parent id, so map it into a
+        # dictionary.
+        data = {}
+        for line in raw_data:
+            parent_id = line.pop('parent__id')
+            data.setdefault(parent_id, [])
+            data[parent_id].append(line)
+
+        # Iterate over the start events and compile information about each one.
+        qs = parent_task.children.filter(event__in=STARTING_EVENTS)
+        for task_start_event in qs:
+            # Create initial task data.
+            task_data = {
+                'changed': False,
+                'changed_count': 0,
+                'created': task_start_event.created,
+                'failed': False,
+                'failed_count': 0,
+                'host_count': 0,
+                'id': task_start_event.id,
+                'modified': task_start_event.modified,
+                'name': 'Gathering Facts' if
+                            task_start_event.event == 'playbook_on_setup' else
+                            task_start_event.task,
+                'reported_hosts': 0,
+                'skipped_count': 0,
+                'successful_count': 0,
+            }
+
+            # Iterate over the data compiled for this child event, and
+            # make appropriate changes to the task data.
+            for child_data in data.get(task_start_event.id, []):
+                if child_data['event'] == 'runner_on_failed':
                     task_data['failed'] = True
-                    task_data['host_count'] += 1
-                    task_data['reported_hosts'] += 1
-                    task_data['failed_count'] += 1
-                elif child_event.event == 'runner_on_ok':
-                    task_data['host_count'] += 1
-                    task_data['reported_hosts'] += 1
-                    if child_event.changed:
-                        task_data['changed_count'] += 1
+                    task_data['host_count'] += child_data['num']
+                    task_data['reported_hosts'] += child_data['num']
+                    task_data['failed_count'] += child_data['num']
+                elif child_data['event'] == 'runner_on_ok':
+                    task_data['host_count'] += child_data['num']
+                    task_data['reported_hosts'] += child_data['num']
+                    if child_data['changed']:
+                        task_data['changed_count'] += child_data['num']
                         task_data['changed'] = True
                     else:
-                        task_data['successful_count'] += 1
-                elif child_event.event == 'runner_on_skipped':
-                    task_data['host_count'] += 1
-                    task_data['reported_hosts'] += 1
-                    task_data['skipped_count'] += 1
-                elif child_event.event == 'runner_on_error':
-                    task_data['host_count'] += 1
-                    task_data['reported_hosts'] += 1
+                        task_data['successful_count'] += child_data['num']
+                elif child_data['event'] == 'runner_on_skipped':
+                    task_data['host_count'] += child_data['num']
+                    task_data['reported_hosts'] += child_data['num']
+                    task_data['skipped_count'] += child_data['num']
+                elif child_data['event'] == 'runner_on_error':
+                    task_data['host_count'] += child_data['num']
+                    task_data['reported_hosts'] += child_data['num']
                     task_data['failed'] = True
-                    task_data['failed_count'] += 1
-                elif child_event.event == 'runner_on_no_hosts':
-                    task_data['host_count'] += 1
+                    task_data['failed_count'] += child_data['num']
+                elif child_data['event'] == 'runner_on_no_hosts':
+                    task_data['host_count'] += child_data['num']
             tasks.append(task_data)
+
+        # Okay, we're done; return response data.
         return Response(tasks)
 
+
 class UnifiedJobTemplateList(ListAPIView):
 
     model = UnifiedJobTemplate

From 8c393c6efd28a632ed54f30c95bd787d8751ca80 Mon Sep 17 00:00:00 2001
From: Chris Houseknecht 
Date: Tue, 17 Jun 2014 18:21:18 -0400
Subject: [PATCH 07/32] Job detail page refactor Added custom scrollbar to task
 list and made it do the endless scroll thing. Switched tasks and plays from
 objects back to arrays, in support of endless scroll.  Still need to apply
 scrollbar to play list and enable endless scroll. Modified calls to job_tasks
 endpoint to use the 'paginated' structure. Will need to do the same to plays.
 None of this is tested yet.

---
 awx/ui/static/js/controllers/JobDetail.js | 233 ++++++++++++--
 awx/ui/static/js/helpers/JobDetail.js     | 370 ++++++++++------------
 awx/ui/static/partials/job_detail.html    |  11 +-
 3 files changed, 384 insertions(+), 230 deletions(-)

diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js
index f1873dee21..c7ea8c4edf 100644
--- a/awx/ui/static/js/controllers/JobDetail.js
+++ b/awx/ui/static/js/controllers/JobDetail.js
@@ -9,7 +9,7 @@
 
 function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest,
     ProcessErrors, ProcessEventQueue, SelectPlay, SelectTask, Socket, GetElapsed, SelectHost, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList,
-    JobIsFinished) {
+    JobIsFinished, SetTaskStyles) {
 
     ClearScope();
 
@@ -21,14 +21,21 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
         lastEventId = 0,
         queue = [];
 
-    scope.plays = {};
+    scope.plays = [];
+    scope.playsMap = {};
     scope.hosts = [];
     scope.hostsMap = {};
-    scope.tasks = {};
+    scope.tasks = [];
+    scope.tasksMap = {};
     scope.hostResults = [];
     scope.hostResultsMap = {};
     api_complete = false;
 
+    scope.hostTableRows = 150;
+    scope.hostSummaryTableRows = 150;
+    scope.tasksMaxRows = 150;
+    scope.playsMaxRows = 150;
+
     scope.search_all_tasks = [];
     scope.search_all_plays = [];
     scope.job_status = {};
@@ -36,8 +43,6 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
     scope.auto_scroll = false;
     scope.searchTaskHostsEnabled = true;
     scope.searchSummaryHostsEnabled = true;
-    scope.hostTableRows = 150;
-    scope.hostSummaryTableRows = 150;
     scope.searchAllHostsEnabled = true;
     scope.haltEventQueue = false;
 
@@ -202,8 +207,8 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                 data.forEach(function(event, idx) {
                     var status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful',
                         start = event.started,
-                        end,
-                        elapsed;
+                        end, elapsed, play;
+
                     if (idx < data.length - 1) {
                         // end date = starting date of the next event
                         end = data[idx + 1].started;
@@ -221,15 +226,25 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                     else {
                         elapsed = '00:00:00';
                     }
-                    scope.plays[event.id] = {
-                        id: event.id,
-                        name: event.play,
-                        created: start,
-                        finished: end,
-                        status: status,
-                        elapsed: elapsed,
-                        playActiveClass: ''
-                    };
+                    if (scope.playsMap[event.id]) {
+                        play = scope.plays[scope.playsMapp[event.id]];
+                        play.finished = end;
+                        play.elapsed = elapsed;
+                        play.status = status;
+                        play.playActiveClass = '';
+                    }
+                    else {
+                        scope.plays.push({
+                            id: event.id,
+                            name: event.play,
+                            created: start,
+                            finished: end,
+                            status: status,
+                            elapsed: elapsed,
+                            playActiveClass: ''
+                        });
+                        scope.playsMap[event.id] = scope.plays.length - 1;
+                    }
                     scope.host_summary.ok += (data.ok_count) ? data.ok_count : 0;
                     scope.host_summary.changed += (data.changed_count) ? data.changed_count : 0;
                     scope.host_summary.unreachable += (data.unreachable_count) ? data.unreachable_count : 0;
@@ -487,9 +502,23 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
         return true;
     };
 
-    /*
+
+    function rebuildHostResultsMap() {
+        scope.hostResultsMap = {};
+        scope.hostResults.forEach(function(result, idx) {
+            scope.hostResultsMap[result.id] = idx;
+        });
+    }
+
+    function rebuildTasksMap() {
+        scope.tasksMap = {};
+        scope.tasks.forEach(function(task, idx) {
+            scope.tasksMap[task.id] = idx;
+        });
+    }
+
     scope.HostDetailOnTotalScroll = _.debounce(function() {
-        // Called when user scrolls down (or forward in time). Using _.debounce
+        // Called when user scrolls down (or forward in time)
         var url, mcs = arguments[0];
         scope.$apply(function() {
             if (!scope.auto_scroll && scope.activeTask && scope.hostResults.length) {
@@ -513,7 +542,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                                 msg: ( (row.event_data && row.event_data.res) ? row.event_data.res.msg : '' )
                             });
                             if (scope.hostResults.length > scope.hostTableRows) {
-                                scope.hostResults.splice(0,1);
+                                scope.hostResults.shift();
                             }
                         });
                         if (data.next) {
@@ -521,6 +550,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                             setTimeout(function() { $('#hosts-table-detail .mCSB_dragger').css({ top: (mcs.draggerTop - 15) + 'px'}); }, 700);
                         }
                         scope.auto_scroll = false;
+                        rebuildHostResultsMap();
                         Wait('stop');
                     })
                     .error(function(data, status) {
@@ -566,6 +596,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                             // there are more rows. move dragger down, letting user know.
                             setTimeout(function() { $('#hosts-table-detail .mCSB_dragger').css({ top: (mcs.draggerTop + 15) + 'px' }); }, 700);
                         }
+                        rebuildHostResultsMap();
                         Wait('stop');
                         scope.auto_scroll = false;
                     })
@@ -580,6 +611,162 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
         });
     }, 300);
 
+    scope.TasksOnTotalScroll = _.debounce(function() {
+        // Called when user scrolls down (or forward in time)
+        var url, mcs = arguments[0];
+        scope.$apply(function() {
+            if (!scope.auto_scroll && scope.activePlay && scope.tasks.length) {
+                scope.auto_scroll = true;
+                url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay;
+                url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : '';
+                url += (scope.searchAllStatus === 'failed') ? '&failed=true' : '';
+                url += 'id__gt=' + scope.tasks[scope.tasks.length - 1].id + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id';
+                Wait('start');
+                Rest.setUrl(url);
+                Rest.get()
+                    .success(function(data) {
+                        data.results.forEach(function(event, idx) {
+                            var end, elapsed;
+                            if (idx < data.length - 1) {
+                                // end date = starting date of the next event
+                                end = data[idx + 1].created;
+                            }
+                            else {
+                                // no next event (task), get the end time of the play
+                                end = scope.plays[scope.activePlay].finished;
+                            }
+                            if (end) {
+                                elapsed = GetElapsed({
+                                    start: event.created,
+                                    end: end
+                                });
+                            }
+                            else {
+                                elapsed = '00:00:00';
+                            }
+                            scope.tasks.push({
+                                id: event.id,
+                                play_id: scope.activePlay,
+                                name: event.name,
+                                status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ),
+                                created: event.created,
+                                modified: event.modified,
+                                finished: end,
+                                elapsed: elapsed,
+                                hostCount: event.host_count,          // hostCount,
+                                reportedHosts: event.reported_hosts,
+                                successfulCount: event.successful_count,
+                                failedCount: event.failed_count,
+                                changedCount: event.changed_count,
+                                skippedCount: event.skipped_count,
+                                taskActiveClass: ''
+                            });
+                            scope.tasksMap[event.id] = scope.tasks.length - 1;
+                            SetTaskStyles({
+                                scope: scope,
+                                task_id: event.id
+                            });
+                            if (scope.tasks.length > scope.tasksMaxRows) {
+                                scope.tasks.shift();
+                            }
+                        });
+                        if (data.next) {
+                            // there are more rows. move dragger up, letting user know.
+                            setTimeout(function() { $('#tasks-table-detail .mCSB_dragger').css({ top: (mcs.draggerTop - 15) + 'px'}); }, 700);
+                        }
+                        scope.auto_scroll = false;
+                        rebuildTasksMap();
+                        Wait('stop');
+                    })
+                    .error(function(data, status) {
+                        ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+                            msg: 'Call to ' + url + '. GET returned: ' + status });
+                    });
+            }
+            else {
+                scope.auto_scroll = false;
+            }
+        });
+    }, 300);
+
+    scope.TasksOnTotalScrollBack = _.debounce(function() {
+        // Called when user scrolls up (or back in time)
+        var url, mcs = arguments[0];
+        scope.$apply(function() {
+            if (!scope.auto_scroll && scope.activePlay && scope.tasks.length) {
+                scope.auto_scroll = true;
+                url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay;
+                url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : '';
+                url += (scope.searchAllStatus === 'failed') ? '&failed=true' : '';
+                url += 'id__lt=' + scope.tasks[scope.tasks[0]].name + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id';
+                Wait('start');
+                Rest.setUrl(url);
+                Rest.get()
+                    .success(function(data) {
+                        data.results.forEach(function(event, idx) {
+                            var end, elapsed;
+                            if (idx < data.length - 1) {
+                                // end date = starting date of the next event
+                                end = data[idx + 1].created;
+                            }
+                            else {
+                                // no next event (task), get the end time of the play
+                                end = scope.plays[scope.activePlay].finished;
+                            }
+                            if (end) {
+                                elapsed = GetElapsed({
+                                    start: event.created,
+                                    end: end
+                                });
+                            }
+                            else {
+                                elapsed = '00:00:00';
+                            }
+                            scope.tasks.unshift({
+                                id: event.id,
+                                play_id: scope.activePlay,
+                                name: event.name,
+                                status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ),
+                                created: event.created,
+                                modified: event.modified,
+                                finished: end,
+                                elapsed: elapsed,
+                                hostCount: event.host_count,          // hostCount,
+                                reportedHosts: event.reported_hosts,
+                                successfulCount: event.successful_count,
+                                failedCount: event.failed_count,
+                                changedCount: event.changed_count,
+                                skippedCount: event.skipped_count,
+                                taskActiveClass: ''
+                            });
+                            scope.tasksMap[event.id] = scope.tasks.length - 1;
+                            SetTaskStyles({
+                                scope: scope,
+                                task_id: event.id
+                            });
+                            if (scope.tasks.length > scope.tasksMaxRows) {
+                                scope.tasks.pop();
+                            }
+                        });
+                        if (data.next) {
+                            // there are more rows. move dragger up, letting user know.
+                            setTimeout(function() { $('#tasks-table-detail .mCSB_dragger').css({ top: (mcs.draggerTop + 15) + 'px'}); }, 700);
+                        }
+                        scope.auto_scroll = false;
+                        rebuildTasksMap();
+                        Wait('stop');
+                    })
+                    .error(function(data, status) {
+                        ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+                            msg: 'Call to ' + url + '. GET returned: ' + status });
+                    });
+            }
+            else {
+                scope.auto_scroll = false;
+            }
+        });
+    }, 300);
+
     scope.HostSummaryOnTotalScroll = function(mcs) {
         var url;
         if (!scope.auto_scroll && scope.hosts) {
@@ -603,7 +790,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
                                     failed: row.failures
                                 });
                                 if (scope.hosts.length > scope.hostSummaryTableRows) {
-                                    scope.hosts.splice(0,1);
+                                    scope.hosts.shift();
                                 }
                             });
                             if (data.next) {
@@ -667,7 +854,6 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
             scope.auto_scroll = false;
         }
     };
-    */
 
     scope.searchAllByHost = function() {
         var keys, nxtPlay;
@@ -723,9 +909,6 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
         ReloadHostSummaryList({
             scope: scope
         });
-        //setTimeout(function() {
-        //    SelectPlay({ scope: scope, id: scope.activePlay });
-        //}, 2000);
     };
 
     scope.viewEvent = function(event_id) {
@@ -736,5 +919,5 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
 
 JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath',
     'Wait', 'Rest', 'ProcessErrors', 'ProcessEventQueue', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'SelectHost', 'FilterAllByHostName', 'DrawGraph',
-    'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished'
+    'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles'
 ];
diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js
index 75e7625d86..6ef45433c7 100644
--- a/awx/ui/static/js/helpers/JobDetail.js
+++ b/awx/ui/static/js/helpers/JobDetail.js
@@ -61,14 +61,13 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices'])
 }])
 
 .factory('DigestEvent', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', 'SelectPlay', 'SelectTask',
-    'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished',
+    'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished', 'AddNewTask',
 function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed,
-    UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished) {
+    UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished, AddNewTask) {
     return function(params) {
 
         var scope = params.scope,
-            event = params.event,
-            hostCount;
+            event = params.event;
 
         switch (event.event) {
             case 'playbook_on_start':
@@ -79,94 +78,29 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                 break;
 
             case 'playbook_on_play_start':
-                scope.plays[event.id] = {
+                scope.plays.push({
                     id: event.id,
                     name: event.play,
                     created: event.created,
                     status: (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful',
-                    elapsed: '00:00:00'
-                };
-                scope.tasks = {};
+                    elapsed: '00:00:00',
+                    hostCount: 0,
+                    fistTask: null
+                });
+                scope.playsMap[event.id] = scope.plays.length -1;
+                scope.tasks = [];
+                scope.tasksMap = {};
                 scope.hostResults = [];
                 scope.hostResultsMap = {};
                 scope.activePlay = event.id;
                 break;
 
             case 'playbook_on_setup':
-                if (scope.activePlay === event.parent) {
-                    scope.tasks[event.id] = {
-                        id: event.id,
-                        play_id: event.parent,
-                        name: event.event_display,
-                        status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ),
-                        created: event.created,
-                        modified: event.modified,
-                        hostCount: 0,
-                        reportedHosts: 0,
-                        successfulCount: 0,
-                        failedCount: 0,
-                        changedCount: 0,
-                        skippedCount: 0,
-                        successfulStyle: { display: 'none'},
-                        failedStyle: { display: 'none' },
-                        changedStyle: { display: 'none' },
-                        skippedStyle: { display: 'none' }
-                    };
-                    scope.hostResults = [];
-                    scope.hostResultsMap = {};
-                    scope.activeTask = event.id;
-                }
-                UpdatePlayStatus({
-                    scope: scope,
-                    play_id: event.parent,
-                    failed: event.failed,
-                    changed: event.changed,
-                    modified: event.modified
-                });
-                if (scope.host_summary.total > 0) {
-                    DrawGraph({ scope: scope, resize: true });
-                }
+                AddNewTask({ scope: scope, event: event });
                 break;
 
             case 'playbook_on_task_start':
-                if (scope.activePlay === event.parent) {
-                    hostCount = GetHostCount({ scope: scope });
-                    scope.tasks[event.id] = {
-                        id: event.id,
-                        name: event.task,
-                        play_id: event.parent,
-                        status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ),
-                        role: event.role,
-                        created: event.created,
-                        modified: event.modified,
-                        hostCount: hostCount,
-                        reportedHosts: 0,
-                        successfulCount: 0,
-                        failedCount: 0,
-                        changedCount: 0,
-                        skippedCount: 0,
-                        successfulStyle: { display: 'none'},
-                        failedStyle: { display: 'none' },
-                        changedStyle: { display: 'none' },
-                        skippedStyle: { display: 'none' }
-                    };
-                    scope.hostResults = [];
-                    scope.hostResultsMap = {};
-                    scope.activeTask = event.id;
-                }
-                if (event.role) {
-                    scope.hasRoles = true;
-                }
-                UpdatePlayStatus({
-                    scope: scope,
-                    play_id: event.parent,
-                    failed: event.failed,
-                    changed: event.changed,
-                    modified: event.modified
-                });
-                if (scope.host_summary.total > 0) {
-                    DrawGraph({ scope: scope, resize: true });
-                }
+                AddNewTask({ scope: scope, event: event });
                 break;
 
             case 'runner_on_ok':
@@ -271,28 +205,6 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
     };
 }])
 
-//Get the # of expected hosts for a task by looking at the number
-//on the very first task for a play
-.factory('GetHostCount', [ 'FindFirstTaskofPlay', function(FindFirstTaskofPlay) {
-    return function(params) {
-        var scope = params.scope,
-            task_id = FindFirstTaskofPlay({ scope: scope });
-        if (task_id) {
-            return scope.tasks[task_id].hostCount;
-        }
-        return 0;
-    };
-}])
-
-.factory('FindFirstTaskofPlay', function() {
-    return function(params) {
-        var scope = params.scope,
-            taskIds;
-        taskIds = Object.keys(scope.tasks);
-        return (taskIds.length > 0) ? scope.tasks[taskIds[0]].id : null;
-    };
-})
-
 .factory('GetElapsed', [ function() {
     return function(params) {
         var start = params.start,
@@ -319,6 +231,60 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
     };
 }])
 
+.factory('AddNewTask', ['DrawGraph', 'UpdatePlayStatus', function(DrawGraph, UpdatePlayStatus) {
+    return function(params) {
+        var scope = params.scope,
+            event = params.event;
+
+        scope.tasks.push({
+            id: event.id,
+            play_id: event.parent,
+            name: event.event_display,
+            status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ),
+            created: event.created,
+            modified: event.modified,
+            hostCount: scope.plays[scope.playsMap[scope.activePlay]].hostCount,
+            reportedHosts: 0,
+            successfulCount: 0,
+            failedCount: 0,
+            changedCount: 0,
+            skippedCount: 0,
+            successfulStyle: { display: 'none'},
+            failedStyle: { display: 'none' },
+            changedStyle: { display: 'none' },
+            skippedStyle: { display: 'none' }
+        });
+
+        if (scope.tasks.length > scope.tasksMaxRows) {
+            scope.tasks.shift();
+        }
+
+        if (!scope.playsMap[scope.activePlay].firstTask) {
+            scope.playsMap[scope.activePlay].firstTask = event.id;
+        }
+
+        scope.hostResults = [];
+        scope.hostResultsMap = {};
+        scope.activeTask = event.id;
+
+        // Not sure if this still works
+        scope.hasRoles = (event.role) ? true : false;
+
+        // Record the first task id
+        UpdatePlayStatus({
+            scope: scope,
+            play_id: event.parent,
+            failed: event.failed,
+            changed: event.changed,
+            modified: event.modified
+        });
+
+        if (scope.host_summary.total > 0) {
+            DrawGraph({ scope: scope, resize: true });
+        }
+    };
+}])
+
 .factory('UpdateJobStatus', ['GetElapsed', 'Empty', function(GetElapsed, Empty) {
     return function(params) {
         var scope = params.scope,
@@ -355,27 +321,28 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             modified = params.modified,
             no_hosts = params.no_hosts,
             status_text = params.status_text,
-            play = scope.plays[id];
+            play;
 
-        if (scope.plays[id]) {
+        if (scope.playsMap[id]) {
+            play = scope.plays[scope.playsMap[id]];
             if (failed) {
-                scope.plays[id].status = 'failed';
+                play.status = 'failed';
             }
             else if (play.status !== 'changed' && play.status !== 'failed') {
                 // once the status becomes 'changed' or 'failed' don't modify it
                 if (no_hosts) {
-                    scope.plays[id].status = 'no-matching-hosts';
+                    play.status = 'no-matching-hosts';
                 }
                 else {
-                    scope.plays[id].status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful';
+                    play.status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful';
                 }
             }
-            scope.plays[id].finished = modified;
-            scope.plays[id].elapsed = GetElapsed({
+            play.finished = modified;
+            play.elapsed = GetElapsed({
                 start: play.created,
                 end: modified
             });
-            scope.plays[id].status_text = (status_text) ? status_text : scope.plays[id].status;
+            play.status_text = (status_text) ? status_text : play.status;
         }
 
         UpdateJobStatus({
@@ -394,9 +361,10 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             id = params.task_id,
             modified = params.modified,
             no_hosts = params.no_hosts,
-            task = scope.tasks[id];
+            task;
 
-        if (scope.tasks[id]) {
+        if (scope.tasksMap[id]) {
+            task = scope.tasks[scope.tasksMap[id]];
             if (no_hosts){
                 task.status = 'no-matching-hosts';
             }
@@ -509,7 +477,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             created = params.created,
             name = params.name,
             msg = params.message,
-            play_id, first;
+            task;
 
         if (!scope.hostResultsMap[host_id] && scope.hostResults.length < scope.hostTableRows) {
             scope.hostResults.push({
@@ -538,20 +506,17 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
         }
 
         // update the task status bar
-        if (scope.tasks[task_id]) {
-            play_id = scope.tasks[task_id].play_id;
-            first = FindFirstTaskofPlay({
-                scope: scope,
-                play_id: play_id
-            });
-            if (task_id === first) {
-                scope.tasks[task_id].hostCount += 1;
+        if (scope.tasksMap[task_id]) {
+            task = scope.tasks[scope.tasksMap[task_id]];
+            if (task_id === scope.plays[scope.playsMap[scope.activePlays]].firstPlay) {
+                scope.plays[scope.playsMap[scope.activePlay]].hostCount++;
+                task.hostCount++;
             }
-            scope.tasks[task_id].reportedHosts += 1;
-            scope.tasks[task_id].failedCount += (status === 'failed' || status === 'unreachable') ? 1 : 0;
-            scope.tasks[task_id].changedCount += (status === 'changed') ? 1 : 0;
-            scope.tasks[task_id].successfulCount += (status === 'successful') ? 1 : 0;
-            scope.tasks[task_id].skippedCount += (status === 'skipped') ? 1 : 0;
+            task.reportedHosts += 1;
+            task.failedCount += (status === 'failed' || status === 'unreachable') ? 1 : 0;
+            task.changedCount += (status === 'changed') ? 1 : 0;
+            task.successfulCount += (status === 'successful') ? 1 : 0;
+            task.skippedCount += (status === 'skipped') ? 1 : 0;
             SetTaskStyles({
                 scope: scope,
                 task_id: task_id
@@ -564,31 +529,33 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
     return function(params) {
         var task_id = params.task_id,
             scope = params.scope,
-            diff;
-        scope.tasks[task_id].failedPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].failedCount / scope.tasks[task_id].hostCount))) : 0;
-        scope.tasks[task_id].changedPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].changedCount / scope.tasks[task_id].hostCount))) : 0;
-        scope.tasks[task_id].skippedPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].skippedCount / scope.tasks[task_id].hostCount))) : 0;
-        scope.tasks[task_id].successfulPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].successfulCount / scope.tasks[task_id].hostCount))) : 0;
+            diff, task;
 
-        diff = (scope.tasks[task_id].failedPct + scope.tasks[task_id].changedPct + scope.tasks[task_id].skippedPct + scope.tasks[task_id].successfulPct) - 100;
+        task = scope.tasks[scope.tasksMap[task_id]];
+        task.failedPct = (task.hostCount > 0) ? Math.ceil((100 * (task.failedCount / task.hostCount))) : 0;
+        task.changedPct = (task.hostCount > 0) ? Math.ceil((100 * (task.changedCount / task.hostCount))) : 0;
+        task.skippedPct = (task.hostCount > 0) ? Math.ceil((100 * (task.skippedCount / task.hostCount))) : 0;
+        task.successfulPct = (task.hostCount > 0) ? Math.ceil((100 * (task.successfulCount / task.hostCount))) : 0;
+
+        diff = (task.failedPct + task.changedPct + task.skippedPct + task.successfulPct) - 100;
         if (diff > 0) {
-            if (scope.tasks[task_id].failedPct > diff) {
-                scope.tasks[task_id].failedPct  = scope.tasks[task_id].failedPct - diff;
+            if (task.failedPct > diff) {
+                task.failedPct  = task.failedPct - diff;
             }
-            else if (scope.tasks[task_id].changedPct > diff) {
-                scope.tasks[task_id].changedPct = scope.tasks[task_id].changedPct - diff;
+            else if (task.changedPct > diff) {
+                task.changedPct = task.changedPct - diff;
             }
-            else if (scope.tasks[task_id].skippedPct > diff) {
-                scope.tasks[task_id].skippedPct = scope.tasks[task_id].skippedPct - diff;
+            else if (task.skippedPct > diff) {
+                task.skippedPct = task.skippedPct - diff;
             }
-            else if (scope.tasks[task_id].successfulPct > diff) {
-                scope.tasks[task_id].successfulPct = scope.tasks[task_id].successfulPct - diff;
+            else if (task.successfulPct > diff) {
+                task.successfulPct = task.successfulPct - diff;
             }
         }
-        scope.tasks[task_id].successfulStyle = (scope.tasks[task_id].successfulPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].successfulPct + "%" } : { 'display': 'none' };
-        scope.tasks[task_id].changedStyle = (scope.tasks[task_id].changedPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].changedPct + "%" } : { 'display': 'none' };
-        scope.tasks[task_id].skippedStyle = (scope.tasks[task_id].skippedPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].skippedPct + "%" } : { 'display': 'none' };
-        scope.tasks[task_id].failedStyle = (scope.tasks[task_id].failedPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].failedPct + "%" } : { 'display': 'none' };
+        task.successfulStyle = (task.successfulPct > 0) ? { 'display': 'inline-block', 'width': task.successfulPct + "%" } : { 'display': 'none' };
+        task.changedStyle = (task.changedPct > 0) ? { 'display': 'inline-block', 'width': task.changedPct + "%" } : { 'display': 'none' };
+        task.skippedStyle = (task.skippedPct > 0) ? { 'display': 'inline-block', 'width': task.skippedPct + "%" } : { 'display': 'none' };
+        task.failedStyle = (task.failedPct > 0) ? { 'display': 'inline-block', 'width': task.failedPct + "%" } : { 'display': 'none' };
     };
 }])
 
@@ -608,11 +575,11 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             clear = (scope.activePlay === id) ? false : true;  //are we moving to a new play?
         }
 
-        if (scope.plays[scope.activePlay]) {
-            scope.plays[scope.activePlay].playActiveClass = '';
+        if (scope.playsMap[scope.activePlay]) {
+            scope.plays[scope.playsMap[scope.activePlay]].playActiveClass = '';
         }
         if (id) {
-            scope.plays[id].playActiveClass = 'active';
+            scope.plays[scope.playsMap[id]].playActiveClass = 'active';
         }
         scope.activePlay = id;
 
@@ -634,48 +601,59 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
         var scope = params.scope,
             callback = params.callback,
             clear = params.clear,
-            url, tIds, lastId;
+            url;
 
         if (clear) {
-            scope.tasks = {};
+            scope.tasks = [];
+            scope.tasksMap = {};
         }
 
         if (scope.activePlay) {
             url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay;
-            // job_tasks seems to ignore all query predicates other than event_id
-            //+ '&';
-            //url += (scope.search_all_plays.length > 0) ? 'event_id__in=' + scope.search_all_plays.join() + '&' : '';
-            //url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : '';
-            //url += 'order_by=id';
+            url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : '';
+            url += (scope.searchAllStatus === 'failed') ? '&failed=true' : '';
+            url += '&page_size=' + scope.tasksMaxRows;
 
             Rest.setUrl(url);
             Rest.get()
                 .success(function(data) {
-                    data.forEach(function(event, idx) {
-                        var end, elapsed;
-                        if ((!scope.searchAllStatus) || (scope.searchAllStatus === 'failed' && event.failed) &&
-                            ((!scope.search_all_hosts_name) || (scope.search_all_hosts_name && scope.search_all_tasks.indexOf(event.id)))) {
+                    data.results.forEach(function(event, idx) {
+                        var end, elapsed, task;
 
-                            if (idx < data.length - 1) {
-                                // end date = starting date of the next event
-                                end = data[idx + 1].created;
-                            }
-                            else {
-                                // no next event (task), get the end time of the play
-                                end = scope.plays[scope.activePlay].finished;
-                            }
+                        if (idx < data.length - 1) {
+                            // end date = starting date of the next event
+                            end = data[idx + 1].created;
+                        }
+                        else {
+                            // no next event (task), get the end time of the play
+                            end = scope.plays[scope.activePlay].finished;
+                        }
 
-                            if (end) {
-                                elapsed = GetElapsed({
-                                    start: event.created,
-                                    end: end
-                                });
-                            }
-                            else {
-                                elapsed = '00:00:00';
-                            }
+                        if (end) {
+                            elapsed = GetElapsed({
+                                start: event.created,
+                                end: end
+                            });
+                        }
+                        else {
+                            elapsed = '00:00:00';
+                        }
 
-                            scope.tasks[event.id] = {
+                        if (scope.tasksMap[event.id]) {
+                            task = scope.tasks[scope.tasksMap[event.id]];
+                            task.status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful';
+                            task.finished = end;
+                            task.elapsed = elapsed;
+                            task.hostCount = (event.host_count) ? event.host_count : 0;
+                            task.reportedHosts = (event.reported_hosts) ? event.reported_hosts : 0;
+                            task.successfulCount = (event.successful_count) ? event.successful_count : 0;
+                            task.failedCount = (event.failed_count) ? event.failed_count : 0;
+                            task.changedCount = (event.changed_count) ? event.changed_count : 0;
+                            task.skippedCount = (event.skipped_count) ? event.skipped_count : 0;
+                            task.taskActiveClass = '';
+                        }
+                        else {
+                            scope.tasks.push({
                                 id: event.id,
                                 play_id: scope.activePlay,
                                 name: event.name,
@@ -684,28 +662,26 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                                 modified: event.modified,
                                 finished: end,
                                 elapsed: elapsed,
-                                hostCount: event.host_count,          // hostCount,
-                                reportedHosts: event.reported_hosts,
-                                successfulCount: event.successful_count,
-                                failedCount: event.failed_count,
-                                changedCount: event.changed_count,
-                                skippedCount: event.skipped_count,
+                                hostCount: (event.host_count) ? event.host_count : 0,
+                                reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0,
+                                successfulCount: (event.successful_count) ? event.successful_count : 0,
+                                failedCount: (event.failed_count) ? event.failed_count : 0,
+                                changedCount: (event.changed_count) ? event.changed_count : 0,
+                                skippedCount: (event.skipped_count) ? event.skipped_count : 0,
                                 taskActiveClass: ''
-                            };
-
-                            SetTaskStyles({
-                                scope: scope,
-                                task_id: event.id
                             });
+                            scope.tasksMap[event.id] = scope.tasks.length - 1;
                         }
+                        SetTaskStyles({
+                            scope: scope,
+                            task_id: event.id
+                        });
                     });
 
                     // set the active task
-                    tIds = Object.keys(scope.tasks);
-                    lastId = (tIds.length > 0) ? tIds[tIds.length - 1] : null;
                     SelectTask({
                         scope: scope,
-                        id: lastId,
+                        id: (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].id : null,
                         callback: callback
                     });
                 })
@@ -715,13 +691,8 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
                 });
         }
         else {
-            // set the active task
-            //tIds = Object.keys(scope.tasks);
-            //lastId = (tIds.length > 0) ? tIds[tIds.length - 1] : null;
-            //console.log('selecting task: ' + lastId);
-            //console.log('tasks: ');
-            //console.log(scope.tasks);
-            scope.tasks = {};
+            scope.tasks = [];
+            scope.tasksMap = {};
             SelectTask({
                 scope: scope,
                 id: null,
@@ -746,12 +717,11 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se
             clear = (scope.activeTask === id) ? false : true;
         }
 
-        if (scope.activeTask && scope.tasks[scope.activeTask]) {
-            scope.tasks[scope.activeTask].taskActiveClass = '';
+        if (scope.activeTask && scope.tasksMap[scope.activeTask]) {
+            scope.tasks[scope.tasksMap[scope.activeTask]].taskActiveClass = '';
         }
         if (id) {
-            scope.tasks[id].taskActiveClass = 'active';
-            scope.activeTaskName = scope.tasks[id].name;
+            scope.tasks[scope.tasksMap[id]].taskActiveClass = 'active';
         }
         scope.activeTask = id;
 
diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html
index bd16937a7a..2e21a9730b 100644
--- a/awx/ui/static/partials/job_detail.html
+++ b/awx/ui/static/partials/job_detail.html
@@ -67,7 +67,7 @@
                                 
                                 
-
No plays matched
+
No matching plays
@@ -83,7 +83,8 @@ -
+
@@ -102,7 +103,7 @@
-
No tasks matched
+
No matching tasks
@@ -131,7 +132,7 @@
-
No hosts matched
+
No matching hosts
@@ -205,7 +206,7 @@
-
No hosts matched
+
No matching hosts
From ebd5146eeabc54d60aef38e486436773fd0cf2cf Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 18 Jun 2014 10:03:14 -0400 Subject: [PATCH 08/32] Job detail page refactor Working through testing of task and play pagination changes. Fixed initial errors. More to do once API changes show up. --- awx/ui/static/js/controllers/JobDetail.js | 4 +--- awx/ui/static/js/helpers/JobDetail.js | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index c7ea8c4edf..1547003a31 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -186,11 +186,9 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } scope.removePlaysReady = scope.$on('PlaysReady', function() { // Select the most recent play, which will trigger tasks and hosts to load - var ids = Object.keys(scope.plays), - lastPlay = (ids.length > 0) ? ids[ids.length - 1] : null; SelectPlay({ scope: scope, - id: lastPlay, + id: ((scope.plays.length > 0) ? scope.plays[scope.plays.length - 1].id : null ), callback: 'InitialDataLoaded' }); }); diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 6ef45433c7..5691cef2f7 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -61,8 +61,8 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices']) }]) .factory('DigestEvent', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', 'SelectPlay', 'SelectTask', - 'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished', 'AddNewTask', -function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed, + 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished', 'AddNewTask', +function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, SelectTask, GetElapsed, UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished, AddNewTask) { return function(params) { @@ -467,7 +467,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Se }]) // Add a new host result -.factory('AddHostResult', ['FindFirstTaskofPlay', 'SetTaskStyles', function(FindFirstTaskofPlay, SetTaskStyles) { +.factory('AddHostResult', ['SetTaskStyles', function(SetTaskStyles) { return function(params) { var scope = params.scope, task_id = params.task_id, From 6cf80d3a9b554ce3f09b31672073a14a9563a182 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 18 Jun 2014 13:42:25 -0400 Subject: [PATCH 09/32] 2.0 styling Started applying new styles starting with removal of tabs. Brought in new breadcrumb format and blended with inventory edit group breadcrumbs. Rather than attempting to override bootstrap styles after the fact, copied boostrap less file and started modifying those to create ansible-boostrap.css, as described in the boostrap documentation. --- .gitignore | 1 + Gruntfile.js | 6 +- awx/ui/static/ansible-bootstrap/alerts.less | 67 ++ awx/ui/static/ansible-bootstrap/badges.less | 55 ++ .../static/ansible-bootstrap/bootstrap.less | 49 + .../static/ansible-bootstrap/breadcrumbs.less | 26 + .../ansible-bootstrap/button-groups.less | 226 +++++ awx/ui/static/ansible-bootstrap/buttons.less | 159 +++ awx/ui/static/ansible-bootstrap/carousel.less | 232 +++++ awx/ui/static/ansible-bootstrap/close.less | 33 + awx/ui/static/ansible-bootstrap/code.less | 63 ++ .../component-animations.less | 29 + .../static/ansible-bootstrap/dropdowns.less | 213 ++++ awx/ui/static/ansible-bootstrap/forms.less | 438 +++++++++ .../static/ansible-bootstrap/glyphicons.less | 233 +++++ awx/ui/static/ansible-bootstrap/grid.less | 84 ++ .../ansible-bootstrap/input-groups.less | 162 +++ .../static/ansible-bootstrap/jumbotron.less | 44 + awx/ui/static/ansible-bootstrap/labels.less | 64 ++ .../static/ansible-bootstrap/list-group.less | 110 +++ awx/ui/static/ansible-bootstrap/media.less | 56 ++ awx/ui/static/ansible-bootstrap/mixins.less | 929 ++++++++++++++++++ awx/ui/static/ansible-bootstrap/modals.less | 139 +++ awx/ui/static/ansible-bootstrap/navbar.less | 616 ++++++++++++ awx/ui/static/ansible-bootstrap/navs.less | 242 +++++ .../static/ansible-bootstrap/normalize.less | 423 ++++++++ awx/ui/static/ansible-bootstrap/pager.less | 55 ++ .../static/ansible-bootstrap/pagination.less | 88 ++ awx/ui/static/ansible-bootstrap/panels.less | 241 +++++ awx/ui/static/ansible-bootstrap/popovers.less | 133 +++ awx/ui/static/ansible-bootstrap/print.less | 101 ++ .../ansible-bootstrap/progress-bars.less | 80 ++ .../responsive-utilities.less | 92 ++ .../static/ansible-bootstrap/scaffolding.less | 134 +++ awx/ui/static/ansible-bootstrap/tables.less | 233 +++++ awx/ui/static/ansible-bootstrap/theme.less | 247 +++++ .../static/ansible-bootstrap/thumbnails.less | 36 + awx/ui/static/ansible-bootstrap/tooltip.less | 95 ++ awx/ui/static/ansible-bootstrap/type.less | 293 ++++++ .../static/ansible-bootstrap/utilities.less | 56 ++ .../static/ansible-bootstrap/variables.less | 829 ++++++++++++++++ awx/ui/static/ansible-bootstrap/wells.less | 29 + awx/ui/static/img/tower_console_bug_black.png | Bin 0 -> 851 bytes awx/ui/static/js/app.js | 43 +- awx/ui/static/less/ansible-ui.less | 61 +- awx/ui/static/less/breadcrumbs.less | 73 ++ awx/ui/static/less/inventory-edit.less | 64 -- awx/ui/static/less/main-layout.less | 47 + awx/ui/static/lib/ansible/form-generator.js | 8 +- .../static/lib/ansible/generator-helpers.js | 8 +- awx/ui/static/partials/inventory-edit.html | 7 +- awx/ui/templates/ui/index.html | 106 +- 52 files changed, 7637 insertions(+), 221 deletions(-) create mode 100644 awx/ui/static/ansible-bootstrap/alerts.less create mode 100644 awx/ui/static/ansible-bootstrap/badges.less create mode 100644 awx/ui/static/ansible-bootstrap/bootstrap.less create mode 100644 awx/ui/static/ansible-bootstrap/breadcrumbs.less create mode 100644 awx/ui/static/ansible-bootstrap/button-groups.less create mode 100644 awx/ui/static/ansible-bootstrap/buttons.less create mode 100644 awx/ui/static/ansible-bootstrap/carousel.less create mode 100644 awx/ui/static/ansible-bootstrap/close.less create mode 100644 awx/ui/static/ansible-bootstrap/code.less create mode 100644 awx/ui/static/ansible-bootstrap/component-animations.less create mode 100644 awx/ui/static/ansible-bootstrap/dropdowns.less create mode 100644 awx/ui/static/ansible-bootstrap/forms.less create mode 100644 awx/ui/static/ansible-bootstrap/glyphicons.less create mode 100644 awx/ui/static/ansible-bootstrap/grid.less create mode 100644 awx/ui/static/ansible-bootstrap/input-groups.less create mode 100644 awx/ui/static/ansible-bootstrap/jumbotron.less create mode 100644 awx/ui/static/ansible-bootstrap/labels.less create mode 100644 awx/ui/static/ansible-bootstrap/list-group.less create mode 100644 awx/ui/static/ansible-bootstrap/media.less create mode 100644 awx/ui/static/ansible-bootstrap/mixins.less create mode 100644 awx/ui/static/ansible-bootstrap/modals.less create mode 100644 awx/ui/static/ansible-bootstrap/navbar.less create mode 100644 awx/ui/static/ansible-bootstrap/navs.less create mode 100644 awx/ui/static/ansible-bootstrap/normalize.less create mode 100644 awx/ui/static/ansible-bootstrap/pager.less create mode 100644 awx/ui/static/ansible-bootstrap/pagination.less create mode 100644 awx/ui/static/ansible-bootstrap/panels.less create mode 100644 awx/ui/static/ansible-bootstrap/popovers.less create mode 100644 awx/ui/static/ansible-bootstrap/print.less create mode 100644 awx/ui/static/ansible-bootstrap/progress-bars.less create mode 100644 awx/ui/static/ansible-bootstrap/responsive-utilities.less create mode 100644 awx/ui/static/ansible-bootstrap/scaffolding.less create mode 100644 awx/ui/static/ansible-bootstrap/tables.less create mode 100644 awx/ui/static/ansible-bootstrap/theme.less create mode 100644 awx/ui/static/ansible-bootstrap/thumbnails.less create mode 100644 awx/ui/static/ansible-bootstrap/tooltip.less create mode 100644 awx/ui/static/ansible-bootstrap/type.less create mode 100644 awx/ui/static/ansible-bootstrap/utilities.less create mode 100644 awx/ui/static/ansible-bootstrap/variables.less create mode 100644 awx/ui/static/ansible-bootstrap/wells.less create mode 100644 awx/ui/static/img/tower_console_bug_black.png create mode 100644 awx/ui/static/less/breadcrumbs.less create mode 100644 awx/ui/static/less/main-layout.less diff --git a/.gitignore b/.gitignore index d6fbb456b7..cc1c6605b8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ awx/public/media awx/public/static awx/ui/static/js/awx.min.js awx/ui/static/css/awx.min.css +awx/ui/static/css/ansible-bootstrap.min.css awx/main/fixtures awx/tower_warnings.log celerybeat-schedule diff --git a/Gruntfile.js b/Gruntfile.js index 2513d6edac..a49374f67f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -27,10 +27,12 @@ module.exports = function(grunt) { less: { production: { options: { - cleancss: true + cleancss: true, + compress: true }, files: { - "awx/ui/static/css/awx.min.css": "awx/ui/static/less/ansible-ui.less" + "awx/ui/static/css/awx.min.css": "awx/ui/static/less/ansible-ui.less", + "awx/ui/static/css/ansible-bootstrap.min.css": "awx/ui/static/ansible-bootstrap/bootstrap.less" } } } diff --git a/awx/ui/static/ansible-bootstrap/alerts.less b/awx/ui/static/ansible-bootstrap/alerts.less new file mode 100644 index 0000000000..3eab066294 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/alerts.less @@ -0,0 +1,67 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: @alert-padding; + margin-bottom: @line-height-computed; + border: 1px solid transparent; + border-radius: @alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing @headings-color + color: inherit; + } + // Provide class for links that match alerts + .alert-link { + font-weight: @alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + > p + p { + margin-top: 5px; + } +} + +// Dismissable alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable { + padding-right: (@alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); +} +.alert-info { + .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); +} +.alert-warning { + .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); +} +.alert-danger { + .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); +} diff --git a/awx/ui/static/ansible-bootstrap/badges.less b/awx/ui/static/ansible-bootstrap/badges.less new file mode 100644 index 0000000000..56828cab7c --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/badges.less @@ -0,0 +1,55 @@ +// +// Badges +// -------------------------------------------------- + + +// Base classes +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: @font-size-small; + font-weight: @badge-font-weight; + color: @badge-color; + line-height: @badge-line-height; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: @badge-bg; + border-radius: @badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + .btn-xs & { + top: 0; + padding: 1px 5px; + } +} + +// Hover state, but only for links +a.badge { + &:hover, + &:focus { + color: @badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } +} + +// Account for counters in navs +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: @badge-active-color; + background-color: @badge-active-bg; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} diff --git a/awx/ui/static/ansible-bootstrap/bootstrap.less b/awx/ui/static/ansible-bootstrap/bootstrap.less new file mode 100644 index 0000000000..b368b87107 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/bootstrap.less @@ -0,0 +1,49 @@ +// Core variables and mixins +@import "variables.less"; +@import "mixins.less"; + +// Reset +@import "normalize.less"; +@import "print.less"; + +// Core CSS +@import "scaffolding.less"; +@import "type.less"; +@import "code.less"; +@import "grid.less"; +@import "tables.less"; +@import "forms.less"; +@import "buttons.less"; + +// Components +@import "component-animations.less"; +@import "glyphicons.less"; +@import "dropdowns.less"; +@import "button-groups.less"; +@import "input-groups.less"; +@import "navs.less"; +@import "navbar.less"; +@import "breadcrumbs.less"; +@import "pagination.less"; +@import "pager.less"; +@import "labels.less"; +@import "badges.less"; +@import "jumbotron.less"; +@import "thumbnails.less"; +@import "alerts.less"; +@import "progress-bars.less"; +@import "media.less"; +@import "list-group.less"; +@import "panels.less"; +@import "wells.less"; +@import "close.less"; + +// Components w/ JavaScript +@import "modals.less"; +@import "tooltip.less"; +@import "popovers.less"; +@import "carousel.less"; + +// Utility classes +@import "utilities.less"; +@import "responsive-utilities.less"; diff --git a/awx/ui/static/ansible-bootstrap/breadcrumbs.less b/awx/ui/static/ansible-bootstrap/breadcrumbs.less new file mode 100644 index 0000000000..cb01d503fb --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/breadcrumbs.less @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; + margin-bottom: @line-height-computed; + list-style: none; + background-color: @breadcrumb-bg; + border-radius: @border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: @breadcrumb-color; + } + } + + > .active { + color: @breadcrumb-active-color; + } +} diff --git a/awx/ui/static/ansible-bootstrap/button-groups.less b/awx/ui/static/ansible-bootstrap/button-groups.less new file mode 100644 index 0000000000..27eb796b89 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/button-groups.less @@ -0,0 +1,226 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + &:focus { + // Remove focus outline when dropdown JS adds it after closing the menu + outline: none; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + &:extend(.clearfix all); + + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + .border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + .border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child { + > .btn:last-child, + > .dropdown-toggle { + .border-right-radius(0); + } +} +.btn-group > .btn-group:last-child > .btn:first-child { + .border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { &:extend(.btn-xs); } +.btn-group-sm > .btn { &:extend(.btn-sm); } +.btn-group-lg > .btn { &:extend(.btn-lg); } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + .box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: @caret-width-large @caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 @caret-width-large @caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + &:extend(.clearfix all); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + border-top-right-radius: @border-radius-base; + .border-bottom-radius(0); + } + &:last-child:not(:first-child) { + border-bottom-left-radius: @border-radius-base; + .border-top-radius(0); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-top-radius(0); +} + + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } +} + + +// Checkbox and radio options +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; +} diff --git a/awx/ui/static/ansible-bootstrap/buttons.less b/awx/ui/static/ansible-bootstrap/buttons.less new file mode 100644 index 0000000000..d4fc156be6 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/buttons.less @@ -0,0 +1,159 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: @btn-font-weight; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base); + .user-select(none); + + &, + &:active, + &.active { + &:focus { + .tab-focus(); + } + } + + &:hover, + &:focus { + color: @btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + pointer-events: none; // Future-proof disabling of clicks + .opacity(.65); + .box-shadow(none); + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); +} +.btn-primary { + .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); +} +// Success appears as green +.btn-success { + .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); +} +// Info appears as blue-green +.btn-info { + .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); +} +// Warning appears as orange +.btn-warning { + .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: @link-color; + font-weight: normal; + cursor: pointer; + border-radius: 0; + + &, + &:active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + .box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: underline; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} +.btn-xs { + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; + padding-left: 0; + padding-right: 0; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/awx/ui/static/ansible-bootstrap/carousel.less b/awx/ui/static/ansible-bootstrap/carousel.less new file mode 100644 index 0000000000..e3fb8a2cfd --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/carousel.less @@ -0,0 +1,232 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + .transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + &:extend(.img-responsive); + line-height: 1; + } + } + + > .active, + > .next, + > .prev { display: block; } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: @carousel-control-width; + .opacity(@carousel-control-opacity); + font-size: @carousel-control-font-size; + color: @carousel-control-color; + text-align: center; + text-shadow: @carousel-text-shadow; + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: none; + color: @carousel-control-color; + text-decoration: none; + .opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + font-family: serif; + } + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid @carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: @carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: @carousel-caption-color; + text-align: center; + text-shadow: @carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: @screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/awx/ui/static/ansible-bootstrap/close.less b/awx/ui/static/ansible-bootstrap/close.less new file mode 100644 index 0000000000..9b4e74f2b8 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/close.less @@ -0,0 +1,33 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: (@font-size-base * 1.5); + font-weight: @close-font-weight; + line-height: 1; + color: @close-color; + text-shadow: @close-text-shadow; + .opacity(.2); + + &:hover, + &:focus { + color: @close-color; + text-decoration: none; + cursor: pointer; + .opacity(.5); + } + + // Additional properties for button version + // iOS requires the button element instead of an anchor tag. + // If you want the anchor version, it requires `href="#"`. + button& { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + } +} diff --git a/awx/ui/static/ansible-bootstrap/code.less b/awx/ui/static/ansible-bootstrap/code.less new file mode 100644 index 0000000000..3eed26c05b --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/code.less @@ -0,0 +1,63 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: @font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: @code-color; + background-color: @code-bg; + white-space: nowrap; + border-radius: @border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: @kbd-color; + background-color: @kbd-bg; + border-radius: @border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); +} + +// Blocks of code +pre { + display: block; + padding: ((@line-height-computed - 1) / 2); + margin: 0 0 (@line-height-computed / 2); + font-size: (@font-size-base - 1); // 14px to 13px + line-height: @line-height-base; + word-break: break-all; + word-wrap: break-word; + color: @pre-color; + background-color: @pre-bg; + border: 1px solid @pre-border-color; + border-radius: @border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: @pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/awx/ui/static/ansible-bootstrap/component-animations.less b/awx/ui/static/ansible-bootstrap/component-animations.less new file mode 100644 index 0000000000..1efe45e2c3 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/component-animations.less @@ -0,0 +1,29 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552. + +.fade { + opacity: 0; + .transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + &.in { + display: block; + } +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + .transition(height .35s ease); +} diff --git a/awx/ui/static/ansible-bootstrap/dropdowns.less b/awx/ui/static/ansible-bootstrap/dropdowns.less new file mode 100644 index 0000000000..f165165e7a --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/dropdowns.less @@ -0,0 +1,213 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: @caret-width-base solid; + border-right: @caret-width-base solid transparent; + border-left: @caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: @zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: @font-size-base; + background-color: @dropdown-bg; + border: 1px solid @dropdown-fallback-border; // IE8 fallback + border: 1px solid @dropdown-border; + border-radius: @border-radius-base; + .box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + .nav-divider(@dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: @line-height-base; + color: @dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: @dropdown-link-hover-color; + background-color: @dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: @dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: @dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: @dropdown-link-disabled-color; + } +} +// Nuke hover/focus effects +.dropdown-menu > .disabled > a { + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + .reset-filter(); + cursor: not-allowed; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: @font-size-small; + line-height: @line-height-base; + color: @dropdown-header-color; +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: (@zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: @caret-width-base solid; + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: @grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + .dropdown-menu-right(); + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + .dropdown-menu-left(); + } + } +} + diff --git a/awx/ui/static/ansible-bootstrap/forms.less b/awx/ui/static/ansible-bootstrap/forms.less new file mode 100644 index 0000000000..f607b8509a --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/forms.less @@ -0,0 +1,438 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: @line-height-computed; + font-size: (@font-size-base * 1.5); + line-height: inherit; + color: @legend-color; + border: 0; + border-bottom: 1px solid @legend-border-color; +} + +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (* isn't specific enough) +input[type="search"] { + .box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; /* IE8-9 */ + line-height: normal; +} + +// Set the height of file controls to match text inputs +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + .tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: (@padding-base-vertical + 1); + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +// select +// textarea +// input[type="text"] +// input[type="password"] +// input[type="datetime"] +// input[type="datetime-local"] +// input[type="date"] +// input[type="month"] +// input[type="time"] +// input[type="week"] +// input[type="number"] +// input[type="email"] +// input[type="url"] +// input[type="search"] +// input[type="tel"] +// input[type="color"] + +.form-control { + display: block; + width: 100%; + height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid @input-border; + border-radius: @input-border-radius; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); + + // Customize the `:focus` state to imitate native WebKit styles. + .form-control-focus(); + + // Placeholder + .placeholder(); + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: not-allowed; + background-color: @input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content + } + + // Reset height for `textarea`s + textarea& { + height: auto; + } +} + + +// Search inputs in iOS +// +// This overrides the extra rounded corners on search inputs in iOS so that our +// `.form-control` class can properly style them. Note that this cannot simply +// be added to `.form-control` as it's not specific enough. For details, see +// https://github.com/twbs/bootstrap/issues/11586. + +input[type="search"] { + -webkit-appearance: none; +} + + +// Special styles for iOS date input +// +// In Mobile Safari, date inputs require a pixel line-height that matches the +// given height of the input. + +input[type="date"] { + line-height: @input-height-base; +} + + +// Form groups +// +// Designed to help with the organization and spacing of vertical forms. For +// horizontal forms, use the predefined grid classes. + +.form-group { + margin-bottom: 15px; +} + + +// Checkboxes and radios +// +// Indent the labels to position radios/checkboxes as hanging controls. + +.radio, +.checkbox { + display: block; + min-height: @line-height-computed; // clear the floating input if there is no label text + margin-top: 10px; + margin-bottom: 10px; + padding-left: 20px; + label { + display: inline; + font-weight: normal; + cursor: pointer; + } +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing +} + +// Radios and checkboxes on same line +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; // space out consecutive inline controls +} + +// Apply same disabled cursor tweak as for inputs +// +// Note: Neither radios nor checkboxes can be readonly. +input[type="radio"], +input[type="checkbox"], +.radio, +.radio-inline, +.checkbox, +.checkbox-inline { + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + } +} + + +// Form control sizing +// +// Build on `.form-control` with modifier classes to decrease or increase the +// height and font-size of form controls. + +.input-sm { + .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} + +.input-lg { + .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); +} + + +// Form control feedback states +// +// Apply contextual and semantic states to individual form controls. + +.has-feedback { + // Enable absolute positioning + position: relative; + + // Ensure icons don't overlap text + .form-control { + padding-right: (@input-height-base * 1.25); + } + + // Feedback icon (requires .glyphicon classes) + .form-control-feedback { + position: absolute; + top: (@line-height-computed + 5); // Height of the `label` and its margin + right: 0; + display: block; + width: @input-height-base; + height: @input-height-base; + line-height: @input-height-base; + text-align: center; + } +} + +// Feedback states +.has-success { + .form-control-validation(@state-success-text; @state-success-text; @state-success-bg); +} +.has-warning { + .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg); +} +.has-error { + .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); +} + + +// Static form control text +// +// Apply class to a `p` element to make any string of text align with labels in +// a horizontal form layout. + +.form-control-static { + margin-bottom: 0; // Remove default margin from `p` +} + + +// Help text +// +// Apply to any element you wish to create light text for placement immediately +// below a form control. Use for general help, formatting, or instructional text. + +.help-block { + display: block; // account for any element using help-block + margin-top: 5px; + margin-bottom: 10px; + color: lighten(@text-color, 25%); // lighten the text some for contrast +} + + + +// Inline forms +// +// Make forms appear inline(-block) by adding the `.form-inline` class. Inline +// forms begin stacked on extra small (mobile) devices and then go inline when +// viewports reach <768px. +// +// Requires wrapping inputs and labels with `.form-group` for proper display of +// default HTML form controls and our custom form controls (e.g., input groups). +// +// Heads up! This is mixin-ed into `.navbar-form` in navbars.less. + +.form-inline { + + // Kick in the inline + @media (min-width: @screen-sm-min) { + // Inline-block all the things for "inline" + .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + + // In navbar-form, allow folks to *not* use `.form-group` + .form-control { + display: inline-block; + width: auto; // Prevent labels from stacking above inputs in `.form-group` + vertical-align: middle; + } + // Input groups need that 100% width though + .input-group > .form-control { + width: 100%; + } + + .control-label { + margin-bottom: 0; + vertical-align: middle; + } + + // Remove default margin on radios/checkboxes that were used for stacking, and + // then undo the floating of radios and checkboxes to match (which also avoids + // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969). + .radio, + .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + padding-left: 0; + vertical-align: middle; + } + .radio input[type="radio"], + .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .form-control-feedback { + top: 0; + } + } +} + + +// Horizontal forms +// +// Horizontal forms are built on grid classes and allow you to create forms with +// labels on the left and inputs on the right. + +.form-horizontal { + + // Consistent vertical alignment of labels, radios, and checkboxes + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + margin-top: 0; + margin-bottom: 0; + padding-top: (@padding-base-vertical + 1); // Default padding plus a border + } + // Account for padding we're adding to ensure the alignment and of help text + // and other content below items + .radio, + .checkbox { + min-height: (@line-height-computed + (@padding-base-vertical + 1)); + } + + // Make form groups behave like rows + .form-group { + .make-row(); + } + + .form-control-static { + padding-top: (@padding-base-vertical + 1); + } + + // Only right align form labels here when the columns stop stacking + @media (min-width: @screen-sm-min) { + .control-label { + text-align: right; + } + } + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .form-control-feedback { + top: 0; + right: (@grid-gutter-width / 2); + } +} diff --git a/awx/ui/static/ansible-bootstrap/glyphicons.less b/awx/ui/static/ansible-bootstrap/glyphicons.less new file mode 100644 index 0000000000..789c5e7f4a --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/glyphicons.less @@ -0,0 +1,233 @@ +// +// Glyphicons for Bootstrap +// +// Since icons are fonts, they can be placed anywhere text is placed and are +// thus automatically sized to match the surrounding child. To use, create an +// inline element with the appropriate classes, like so: +// +// Star + +// Import the fonts +@font-face { + font-family: 'Glyphicons Halflings'; + src: ~"url('@{icon-font-path}@{icon-font-name}.eot')"; + src: ~"url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype')", + ~"url('@{icon-font-path}@{icon-font-name}.woff') format('woff')", + ~"url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype')", + ~"url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg')"; +} + +// Catchall baseclass +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Individual icons +.glyphicon-asterisk { &:before { content: "\2a"; } } +.glyphicon-plus { &:before { content: "\2b"; } } +.glyphicon-euro { &:before { content: "\20ac"; } } +.glyphicon-minus { &:before { content: "\2212"; } } +.glyphicon-cloud { &:before { content: "\2601"; } } +.glyphicon-envelope { &:before { content: "\2709"; } } +.glyphicon-pencil { &:before { content: "\270f"; } } +.glyphicon-glass { &:before { content: "\e001"; } } +.glyphicon-music { &:before { content: "\e002"; } } +.glyphicon-search { &:before { content: "\e003"; } } +.glyphicon-heart { &:before { content: "\e005"; } } +.glyphicon-star { &:before { content: "\e006"; } } +.glyphicon-star-empty { &:before { content: "\e007"; } } +.glyphicon-user { &:before { content: "\e008"; } } +.glyphicon-film { &:before { content: "\e009"; } } +.glyphicon-th-large { &:before { content: "\e010"; } } +.glyphicon-th { &:before { content: "\e011"; } } +.glyphicon-th-list { &:before { content: "\e012"; } } +.glyphicon-ok { &:before { content: "\e013"; } } +.glyphicon-remove { &:before { content: "\e014"; } } +.glyphicon-zoom-in { &:before { content: "\e015"; } } +.glyphicon-zoom-out { &:before { content: "\e016"; } } +.glyphicon-off { &:before { content: "\e017"; } } +.glyphicon-signal { &:before { content: "\e018"; } } +.glyphicon-cog { &:before { content: "\e019"; } } +.glyphicon-trash { &:before { content: "\e020"; } } +.glyphicon-home { &:before { content: "\e021"; } } +.glyphicon-file { &:before { content: "\e022"; } } +.glyphicon-time { &:before { content: "\e023"; } } +.glyphicon-road { &:before { content: "\e024"; } } +.glyphicon-download-alt { &:before { content: "\e025"; } } +.glyphicon-download { &:before { content: "\e026"; } } +.glyphicon-upload { &:before { content: "\e027"; } } +.glyphicon-inbox { &:before { content: "\e028"; } } +.glyphicon-play-circle { &:before { content: "\e029"; } } +.glyphicon-repeat { &:before { content: "\e030"; } } +.glyphicon-refresh { &:before { content: "\e031"; } } +.glyphicon-list-alt { &:before { content: "\e032"; } } +.glyphicon-lock { &:before { content: "\e033"; } } +.glyphicon-flag { &:before { content: "\e034"; } } +.glyphicon-headphones { &:before { content: "\e035"; } } +.glyphicon-volume-off { &:before { content: "\e036"; } } +.glyphicon-volume-down { &:before { content: "\e037"; } } +.glyphicon-volume-up { &:before { content: "\e038"; } } +.glyphicon-qrcode { &:before { content: "\e039"; } } +.glyphicon-barcode { &:before { content: "\e040"; } } +.glyphicon-tag { &:before { content: "\e041"; } } +.glyphicon-tags { &:before { content: "\e042"; } } +.glyphicon-book { &:before { content: "\e043"; } } +.glyphicon-bookmark { &:before { content: "\e044"; } } +.glyphicon-print { &:before { content: "\e045"; } } +.glyphicon-camera { &:before { content: "\e046"; } } +.glyphicon-font { &:before { content: "\e047"; } } +.glyphicon-bold { &:before { content: "\e048"; } } +.glyphicon-italic { &:before { content: "\e049"; } } +.glyphicon-text-height { &:before { content: "\e050"; } } +.glyphicon-text-width { &:before { content: "\e051"; } } +.glyphicon-align-left { &:before { content: "\e052"; } } +.glyphicon-align-center { &:before { content: "\e053"; } } +.glyphicon-align-right { &:before { content: "\e054"; } } +.glyphicon-align-justify { &:before { content: "\e055"; } } +.glyphicon-list { &:before { content: "\e056"; } } +.glyphicon-indent-left { &:before { content: "\e057"; } } +.glyphicon-indent-right { &:before { content: "\e058"; } } +.glyphicon-facetime-video { &:before { content: "\e059"; } } +.glyphicon-picture { &:before { content: "\e060"; } } +.glyphicon-map-marker { &:before { content: "\e062"; } } +.glyphicon-adjust { &:before { content: "\e063"; } } +.glyphicon-tint { &:before { content: "\e064"; } } +.glyphicon-edit { &:before { content: "\e065"; } } +.glyphicon-share { &:before { content: "\e066"; } } +.glyphicon-check { &:before { content: "\e067"; } } +.glyphicon-move { &:before { content: "\e068"; } } +.glyphicon-step-backward { &:before { content: "\e069"; } } +.glyphicon-fast-backward { &:before { content: "\e070"; } } +.glyphicon-backward { &:before { content: "\e071"; } } +.glyphicon-play { &:before { content: "\e072"; } } +.glyphicon-pause { &:before { content: "\e073"; } } +.glyphicon-stop { &:before { content: "\e074"; } } +.glyphicon-forward { &:before { content: "\e075"; } } +.glyphicon-fast-forward { &:before { content: "\e076"; } } +.glyphicon-step-forward { &:before { content: "\e077"; } } +.glyphicon-eject { &:before { content: "\e078"; } } +.glyphicon-chevron-left { &:before { content: "\e079"; } } +.glyphicon-chevron-right { &:before { content: "\e080"; } } +.glyphicon-plus-sign { &:before { content: "\e081"; } } +.glyphicon-minus-sign { &:before { content: "\e082"; } } +.glyphicon-remove-sign { &:before { content: "\e083"; } } +.glyphicon-ok-sign { &:before { content: "\e084"; } } +.glyphicon-question-sign { &:before { content: "\e085"; } } +.glyphicon-info-sign { &:before { content: "\e086"; } } +.glyphicon-screenshot { &:before { content: "\e087"; } } +.glyphicon-remove-circle { &:before { content: "\e088"; } } +.glyphicon-ok-circle { &:before { content: "\e089"; } } +.glyphicon-ban-circle { &:before { content: "\e090"; } } +.glyphicon-arrow-left { &:before { content: "\e091"; } } +.glyphicon-arrow-right { &:before { content: "\e092"; } } +.glyphicon-arrow-up { &:before { content: "\e093"; } } +.glyphicon-arrow-down { &:before { content: "\e094"; } } +.glyphicon-share-alt { &:before { content: "\e095"; } } +.glyphicon-resize-full { &:before { content: "\e096"; } } +.glyphicon-resize-small { &:before { content: "\e097"; } } +.glyphicon-exclamation-sign { &:before { content: "\e101"; } } +.glyphicon-gift { &:before { content: "\e102"; } } +.glyphicon-leaf { &:before { content: "\e103"; } } +.glyphicon-fire { &:before { content: "\e104"; } } +.glyphicon-eye-open { &:before { content: "\e105"; } } +.glyphicon-eye-close { &:before { content: "\e106"; } } +.glyphicon-warning-sign { &:before { content: "\e107"; } } +.glyphicon-plane { &:before { content: "\e108"; } } +.glyphicon-calendar { &:before { content: "\e109"; } } +.glyphicon-random { &:before { content: "\e110"; } } +.glyphicon-comment { &:before { content: "\e111"; } } +.glyphicon-magnet { &:before { content: "\e112"; } } +.glyphicon-chevron-up { &:before { content: "\e113"; } } +.glyphicon-chevron-down { &:before { content: "\e114"; } } +.glyphicon-retweet { &:before { content: "\e115"; } } +.glyphicon-shopping-cart { &:before { content: "\e116"; } } +.glyphicon-folder-close { &:before { content: "\e117"; } } +.glyphicon-folder-open { &:before { content: "\e118"; } } +.glyphicon-resize-vertical { &:before { content: "\e119"; } } +.glyphicon-resize-horizontal { &:before { content: "\e120"; } } +.glyphicon-hdd { &:before { content: "\e121"; } } +.glyphicon-bullhorn { &:before { content: "\e122"; } } +.glyphicon-bell { &:before { content: "\e123"; } } +.glyphicon-certificate { &:before { content: "\e124"; } } +.glyphicon-thumbs-up { &:before { content: "\e125"; } } +.glyphicon-thumbs-down { &:before { content: "\e126"; } } +.glyphicon-hand-right { &:before { content: "\e127"; } } +.glyphicon-hand-left { &:before { content: "\e128"; } } +.glyphicon-hand-up { &:before { content: "\e129"; } } +.glyphicon-hand-down { &:before { content: "\e130"; } } +.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } +.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } +.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } +.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } +.glyphicon-globe { &:before { content: "\e135"; } } +.glyphicon-wrench { &:before { content: "\e136"; } } +.glyphicon-tasks { &:before { content: "\e137"; } } +.glyphicon-filter { &:before { content: "\e138"; } } +.glyphicon-briefcase { &:before { content: "\e139"; } } +.glyphicon-fullscreen { &:before { content: "\e140"; } } +.glyphicon-dashboard { &:before { content: "\e141"; } } +.glyphicon-paperclip { &:before { content: "\e142"; } } +.glyphicon-heart-empty { &:before { content: "\e143"; } } +.glyphicon-link { &:before { content: "\e144"; } } +.glyphicon-phone { &:before { content: "\e145"; } } +.glyphicon-pushpin { &:before { content: "\e146"; } } +.glyphicon-usd { &:before { content: "\e148"; } } +.glyphicon-gbp { &:before { content: "\e149"; } } +.glyphicon-sort { &:before { content: "\e150"; } } +.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } +.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } +.glyphicon-sort-by-order { &:before { content: "\e153"; } } +.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } +.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } +.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } +.glyphicon-unchecked { &:before { content: "\e157"; } } +.glyphicon-expand { &:before { content: "\e158"; } } +.glyphicon-collapse-down { &:before { content: "\e159"; } } +.glyphicon-collapse-up { &:before { content: "\e160"; } } +.glyphicon-log-in { &:before { content: "\e161"; } } +.glyphicon-flash { &:before { content: "\e162"; } } +.glyphicon-log-out { &:before { content: "\e163"; } } +.glyphicon-new-window { &:before { content: "\e164"; } } +.glyphicon-record { &:before { content: "\e165"; } } +.glyphicon-save { &:before { content: "\e166"; } } +.glyphicon-open { &:before { content: "\e167"; } } +.glyphicon-saved { &:before { content: "\e168"; } } +.glyphicon-import { &:before { content: "\e169"; } } +.glyphicon-export { &:before { content: "\e170"; } } +.glyphicon-send { &:before { content: "\e171"; } } +.glyphicon-floppy-disk { &:before { content: "\e172"; } } +.glyphicon-floppy-saved { &:before { content: "\e173"; } } +.glyphicon-floppy-remove { &:before { content: "\e174"; } } +.glyphicon-floppy-save { &:before { content: "\e175"; } } +.glyphicon-floppy-open { &:before { content: "\e176"; } } +.glyphicon-credit-card { &:before { content: "\e177"; } } +.glyphicon-transfer { &:before { content: "\e178"; } } +.glyphicon-cutlery { &:before { content: "\e179"; } } +.glyphicon-header { &:before { content: "\e180"; } } +.glyphicon-compressed { &:before { content: "\e181"; } } +.glyphicon-earphone { &:before { content: "\e182"; } } +.glyphicon-phone-alt { &:before { content: "\e183"; } } +.glyphicon-tower { &:before { content: "\e184"; } } +.glyphicon-stats { &:before { content: "\e185"; } } +.glyphicon-sd-video { &:before { content: "\e186"; } } +.glyphicon-hd-video { &:before { content: "\e187"; } } +.glyphicon-subtitles { &:before { content: "\e188"; } } +.glyphicon-sound-stereo { &:before { content: "\e189"; } } +.glyphicon-sound-dolby { &:before { content: "\e190"; } } +.glyphicon-sound-5-1 { &:before { content: "\e191"; } } +.glyphicon-sound-6-1 { &:before { content: "\e192"; } } +.glyphicon-sound-7-1 { &:before { content: "\e193"; } } +.glyphicon-copyright-mark { &:before { content: "\e194"; } } +.glyphicon-registration-mark { &:before { content: "\e195"; } } +.glyphicon-cloud-download { &:before { content: "\e197"; } } +.glyphicon-cloud-upload { &:before { content: "\e198"; } } +.glyphicon-tree-conifer { &:before { content: "\e199"; } } +.glyphicon-tree-deciduous { &:before { content: "\e200"; } } diff --git a/awx/ui/static/ansible-bootstrap/grid.less b/awx/ui/static/ansible-bootstrap/grid.less new file mode 100644 index 0000000000..e100655b70 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/grid.less @@ -0,0 +1,84 @@ +// +// Grid system +// -------------------------------------------------- + + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +.container { + .container-fixed(); + + @media (min-width: @screen-sm-min) { + width: @container-sm; + } + @media (min-width: @screen-md-min) { + width: @container-md; + } + @media (min-width: @screen-lg-min) { + width: @container-lg; + } +} + + +// Fluid container +// +// Utilizes the mixin meant for fixed width containers, but without any defined +// width for fluid, full width layouts. + +.container-fluid { + .container-fixed(); +} + + +// Row +// +// Rows contain and clear the floats of your columns. + +.row { + .make-row(); +} + + +// Columns +// +// Common styles for small and large grid columns + +.make-grid-columns(); + + +// Extra small grid +// +// Columns, offsets, pushes, and pulls for extra small devices like +// smartphones. + +.make-grid(xs); + + +// Small grid +// +// Columns, offsets, pushes, and pulls for the small device range, from phones +// to tablets. + +@media (min-width: @screen-sm-min) { + .make-grid(sm); +} + + +// Medium grid +// +// Columns, offsets, pushes, and pulls for the desktop device range. + +@media (min-width: @screen-md-min) { + .make-grid(md); +} + + +// Large grid +// +// Columns, offsets, pushes, and pulls for the large desktop device range. + +@media (min-width: @screen-lg-min) { + .make-grid(lg); +} diff --git a/awx/ui/static/ansible-bootstrap/input-groups.less b/awx/ui/static/ansible-bootstrap/input-groups.less new file mode 100644 index 0000000000..a111474630 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/input-groups.less @@ -0,0 +1,162 @@ +// +// Input groups +// -------------------------------------------------- + +// Base styles +// ------------------------- +.input-group { + position: relative; // For dropdowns + display: table; + border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table + + // Undo padding and float of grid classes + &[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; + } + + .form-control { + // Ensure that the input is always above the *appended* addon button for + // proper border colors. + position: relative; + z-index: 2; + + // IE9 fubars the placeholder attribute in text inputs and the arrows on + // select elements in input groups. To fix it, we float the input. Details: + // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 + float: left; + + width: 100%; + margin-bottom: 0; + } +} + +// Sizing options +// +// Remix the default form control sizing classes into new ones for easier +// manipulation. + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { .input-lg(); } +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { .input-sm(); } + + +// Display as table-cell +// ------------------------- +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } +} +// Addon and addon wrapper for buttons +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; // Match the inputs +} + +// Text input groups +// ------------------------- +.input-group-addon { + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + font-weight: normal; + line-height: 1; + color: @input-color; + text-align: center; + background-color: @input-group-addon-bg; + border: 1px solid @input-group-addon-border-color; + border-radius: @border-radius-base; + + // Sizing + &.input-sm { + padding: @padding-small-vertical @padding-small-horizontal; + font-size: @font-size-small; + border-radius: @border-radius-small; + } + &.input-lg { + padding: @padding-large-vertical @padding-large-horizontal; + font-size: @font-size-large; + border-radius: @border-radius-large; + } + + // Nuke default margins from checkboxes and radios to vertically center within. + input[type="radio"], + input[type="checkbox"] { + margin-top: 0; + } +} + +// Reset rounded corners +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + .border-right-radius(0); +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + .border-left-radius(0); +} +.input-group-addon:last-child { + border-left: 0; +} + +// Button input groups +// ------------------------- +.input-group-btn { + position: relative; + // Jankily prevent input button groups from wrapping with `white-space` and + // `font-size` in combination with `inline-block` on buttons. + font-size: 0; + white-space: nowrap; + + // Negative margin for spacing, position for bringing hovered/focused/actived + // element above the siblings. + > .btn { + position: relative; + + .btn { + margin-left: -1px; + } + // Bring the "active" button to the front + &:hover, + &:focus, + &:active { + z-index: 2; + } + } + + // Negative margin to only have a 1px border between the two + &:first-child { + > .btn, + > .btn-group { + margin-right: -1px; + } + } + &:last-child { + > .btn, + > .btn-group { + margin-left: -1px; + } + } +} diff --git a/awx/ui/static/ansible-bootstrap/jumbotron.less b/awx/ui/static/ansible-bootstrap/jumbotron.less new file mode 100644 index 0000000000..a15e169715 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/jumbotron.less @@ -0,0 +1,44 @@ +// +// Jumbotron +// -------------------------------------------------- + + +.jumbotron { + padding: @jumbotron-padding; + margin-bottom: @jumbotron-padding; + color: @jumbotron-color; + background-color: @jumbotron-bg; + + h1, + .h1 { + color: @jumbotron-heading-color; + } + p { + margin-bottom: (@jumbotron-padding / 2); + font-size: @jumbotron-font-size; + font-weight: 200; + } + + .container & { + border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container + } + + .container { + max-width: 100%; + } + + @media screen and (min-width: @screen-sm-min) { + padding-top: (@jumbotron-padding * 1.6); + padding-bottom: (@jumbotron-padding * 1.6); + + .container & { + padding-left: (@jumbotron-padding * 2); + padding-right: (@jumbotron-padding * 2); + } + + h1, + .h1 { + font-size: (@font-size-base * 4.5); + } + } +} diff --git a/awx/ui/static/ansible-bootstrap/labels.less b/awx/ui/static/ansible-bootstrap/labels.less new file mode 100644 index 0000000000..5db1ed12c0 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/labels.less @@ -0,0 +1,64 @@ +// +// Labels +// -------------------------------------------------- + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: @label-color; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + + // Add hover effects, but only for links + &[href] { + &:hover, + &:focus { + color: @label-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Empty labels collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for labels in buttons + .btn & { + position: relative; + top: -1px; + } +} + +// Colors +// Contextual variations (linked labels get darker on :hover) + +.label-default { + .label-variant(@label-default-bg); +} + +.label-primary { + .label-variant(@label-primary-bg); +} + +.label-success { + .label-variant(@label-success-bg); +} + +.label-info { + .label-variant(@label-info-bg); +} + +.label-warning { + .label-variant(@label-warning-bg); +} + +.label-danger { + .label-variant(@label-danger-bg); +} diff --git a/awx/ui/static/ansible-bootstrap/list-group.less b/awx/ui/static/ansible-bootstrap/list-group.less new file mode 100644 index 0000000000..3343f8e5e2 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/list-group.less @@ -0,0 +1,110 @@ +// +// List groups +// -------------------------------------------------- + + +// Base class +// +// Easily usable on
    ,
      , or
      . + +.list-group { + // No need to set list-style: none; since .list-group-item is block level + margin-bottom: 20px; + padding-left: 0; // reset padding because ul and ol +} + + +// Individual list items +// +// Use on `li`s or `div`s within the `.list-group` parent. + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + // Place the border on the list items and negative margin up for better styling + margin-bottom: -1px; + background-color: @list-group-bg; + border: 1px solid @list-group-border; + + // Round the first and last items + &:first-child { + .border-top-radius(@list-group-border-radius); + } + &:last-child { + margin-bottom: 0; + .border-bottom-radius(@list-group-border-radius); + } + + // Align badges within list items + > .badge { + float: right; + } + > .badge + .badge { + margin-right: 5px; + } +} + + +// Linked list items +// +// Use anchor elements instead of `li`s or `div`s to create linked list items. +// Includes an extra `.active` modifier class for showing selected items. + +a.list-group-item { + color: @list-group-link-color; + + .list-group-item-heading { + color: @list-group-link-heading-color; + } + + // Hover state + &:hover, + &:focus { + text-decoration: none; + background-color: @list-group-hover-bg; + } + + // Active class on item itself, not parent + &.active, + &.active:hover, + &.active:focus { + z-index: 2; // Place active items above their siblings for proper border styling + color: @list-group-active-color; + background-color: @list-group-active-bg; + border-color: @list-group-active-border; + + // Force color to inherit for custom content + .list-group-item-heading { + color: inherit; + } + .list-group-item-text { + color: @list-group-active-text-color; + } + } +} + + +// Contextual variants +// +// Add modifier classes to change text and background color on individual items. +// Organizationally, this must come after the `:hover` states. + +.list-group-item-variant(success; @state-success-bg; @state-success-text); +.list-group-item-variant(info; @state-info-bg; @state-info-text); +.list-group-item-variant(warning; @state-warning-bg; @state-warning-text); +.list-group-item-variant(danger; @state-danger-bg; @state-danger-text); + + +// Custom content options +// +// Extra classes for creating well-formatted content within `.list-group-item`s. + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} diff --git a/awx/ui/static/ansible-bootstrap/media.less b/awx/ui/static/ansible-bootstrap/media.less new file mode 100644 index 0000000000..5ad22cd6d5 --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/media.less @@ -0,0 +1,56 @@ +// Media objects +// Source: http://stubbornella.org/content/?p=497 +// -------------------------------------------------- + + +// Common styles +// ------------------------- + +// Clear the floats +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +// Proper spacing between instances of .media +.media, +.media .media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} + +// For images and videos, set to block +.media-object { + display: block; +} + +// Reset margins on headings for tighter default spacing +.media-heading { + margin: 0 0 5px; +} + + +// Media image alignment +// ------------------------- + +.media { + > .pull-left { + margin-right: 10px; + } + > .pull-right { + margin-left: 10px; + } +} + + +// Media list variation +// ------------------------- + +// Undo default ul/ol styles +.media-list { + padding-left: 0; + list-style: none; +} diff --git a/awx/ui/static/ansible-bootstrap/mixins.less b/awx/ui/static/ansible-bootstrap/mixins.less new file mode 100644 index 0000000000..71723dba4a --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/mixins.less @@ -0,0 +1,929 @@ +// +// Mixins +// -------------------------------------------------- + + +// Utilities +// ------------------------- + +// Clearfix +// Source: http://nicolasgallagher.com/micro-clearfix-hack/ +// +// For modern browsers +// 1. The space content is one way to avoid an Opera bug when the +// contenteditable attribute is included anywhere else in the document. +// Otherwise it causes space to appear at the top and bottom of elements +// that are clearfixed. +// 2. The use of `table` rather than `block` is only necessary if using +// `:before` to contain the top-margins of child elements. +.clearfix() { + &:before, + &:after { + content: " "; // 1 + display: table; // 2 + } + &:after { + clear: both; + } +} + +// WebKit-style focus +.tab-focus() { + // Default + outline: thin dotted; + // WebKit + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +// Center-align a block level element +.center-block() { + display: block; + margin-left: auto; + margin-right: auto; +} + +// Sizing shortcuts +.size(@width; @height) { + width: @width; + height: @height; +} +.square(@size) { + .size(@size; @size); +} + +// Placeholder text +.placeholder(@color: @input-color-placeholder) { + &::-moz-placeholder { color: @color; // Firefox + opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526 + &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ + &::-webkit-input-placeholder { color: @color; } // Safari and Chrome +} + +// Text overflow +// Requires inline-block or block for proper styling +.text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// CSS image replacement +// +// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for +// mixins being reused as classes with the same name, this doesn't hold up. As +// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note +// that we cannot chain the mixins together in Less, so they are repeated. +// +// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 + +// Deprecated as of v3.0.1 (will be removed in v4) +.hide-text() { + font: ~"0/0" a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +// New mixin to use as of v3.0.1 +.text-hide() { + .hide-text(); +} + + + +// CSS3 PROPERTIES +// -------------------------------------------------- + +// Single side border-radius +.border-top-radius(@radius) { + border-top-right-radius: @radius; + border-top-left-radius: @radius; +} +.border-right-radius(@radius) { + border-bottom-right-radius: @radius; + border-top-right-radius: @radius; +} +.border-bottom-radius(@radius) { + border-bottom-right-radius: @radius; + border-bottom-left-radius: @radius; +} +.border-left-radius(@radius) { + border-bottom-left-radius: @radius; + border-top-left-radius: @radius; +} + +// Drop shadows +// +// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's +// supported browsers that have box shadow capabilities now support the +// standard `box-shadow` property. +.box-shadow(@shadow) { + -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 + box-shadow: @shadow; +} + +// Transitions +.transition(@transition) { + -webkit-transition: @transition; + transition: @transition; +} +.transition-property(@transition-property) { + -webkit-transition-property: @transition-property; + transition-property: @transition-property; +} +.transition-delay(@transition-delay) { + -webkit-transition-delay: @transition-delay; + transition-delay: @transition-delay; +} +.transition-duration(@transition-duration) { + -webkit-transition-duration: @transition-duration; + transition-duration: @transition-duration; +} +.transition-transform(@transition) { + -webkit-transition: -webkit-transform @transition; + -moz-transition: -moz-transform @transition; + -o-transition: -o-transform @transition; + transition: transform @transition; +} + +// Transformations +.rotate(@degrees) { + -webkit-transform: rotate(@degrees); + -ms-transform: rotate(@degrees); // IE9 only + transform: rotate(@degrees); +} +.scale(@ratio; @ratio-y...) { + -webkit-transform: scale(@ratio, @ratio-y); + -ms-transform: scale(@ratio, @ratio-y); // IE9 only + transform: scale(@ratio, @ratio-y); +} +.translate(@x; @y) { + -webkit-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); // IE9 only + transform: translate(@x, @y); +} +.skew(@x; @y) { + -webkit-transform: skew(@x, @y); + -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ + transform: skew(@x, @y); +} +.translate3d(@x; @y; @z) { + -webkit-transform: translate3d(@x, @y, @z); + transform: translate3d(@x, @y, @z); +} + +.rotateX(@degrees) { + -webkit-transform: rotateX(@degrees); + -ms-transform: rotateX(@degrees); // IE9 only + transform: rotateX(@degrees); +} +.rotateY(@degrees) { + -webkit-transform: rotateY(@degrees); + -ms-transform: rotateY(@degrees); // IE9 only + transform: rotateY(@degrees); +} +.perspective(@perspective) { + -webkit-perspective: @perspective; + -moz-perspective: @perspective; + perspective: @perspective; +} +.perspective-origin(@perspective) { + -webkit-perspective-origin: @perspective; + -moz-perspective-origin: @perspective; + perspective-origin: @perspective; +} +.transform-origin(@origin) { + -webkit-transform-origin: @origin; + -moz-transform-origin: @origin; + -ms-transform-origin: @origin; // IE9 only + transform-origin: @origin; +} + +// Animations +.animation(@animation) { + -webkit-animation: @animation; + animation: @animation; +} +.animation-name(@name) { + -webkit-animation-name: @name; + animation-name: @name; +} +.animation-duration(@duration) { + -webkit-animation-duration: @duration; + animation-duration: @duration; +} +.animation-timing-function(@timing-function) { + -webkit-animation-timing-function: @timing-function; + animation-timing-function: @timing-function; +} +.animation-delay(@delay) { + -webkit-animation-delay: @delay; + animation-delay: @delay; +} +.animation-iteration-count(@iteration-count) { + -webkit-animation-iteration-count: @iteration-count; + animation-iteration-count: @iteration-count; +} +.animation-direction(@direction) { + -webkit-animation-direction: @direction; + animation-direction: @direction; +} + +// Backface visibility +// Prevent browsers from flickering when using CSS 3D transforms. +// Default value is `visible`, but can be changed to `hidden` +.backface-visibility(@visibility){ + -webkit-backface-visibility: @visibility; + -moz-backface-visibility: @visibility; + backface-visibility: @visibility; +} + +// Box sizing +.box-sizing(@boxmodel) { + -webkit-box-sizing: @boxmodel; + -moz-box-sizing: @boxmodel; + box-sizing: @boxmodel; +} + +// User select +// For selecting text on the page +.user-select(@select) { + -webkit-user-select: @select; + -moz-user-select: @select; + -ms-user-select: @select; // IE10+ + user-select: @select; +} + +// Resize anything +.resizable(@direction) { + resize: @direction; // Options: horizontal, vertical, both + overflow: auto; // Safari fix +} + +// CSS3 Content Columns +.content-columns(@column-count; @column-gap: @grid-gutter-width) { + -webkit-column-count: @column-count; + -moz-column-count: @column-count; + column-count: @column-count; + -webkit-column-gap: @column-gap; + -moz-column-gap: @column-gap; + column-gap: @column-gap; +} + +// Optional hyphenation +.hyphens(@mode: auto) { + word-wrap: break-word; + -webkit-hyphens: @mode; + -moz-hyphens: @mode; + -ms-hyphens: @mode; // IE10+ + -o-hyphens: @mode; + hyphens: @mode; +} + +// Opacity +.opacity(@opacity) { + opacity: @opacity; + // IE8 filter + @opacity-ie: (@opacity * 100); + filter: ~"alpha(opacity=@{opacity-ie})"; +} + + + +// GRADIENTS +// -------------------------------------------------- + +#gradient { + + // Horizontal gradient, from left to right + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down + } + + // Vertical gradient, from top to bottom + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down + } + + .directional(@start-color: #555; @end-color: #333; @deg: 45deg) { + background-repeat: repeat-x; + background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + } + .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .radial(@inner-color: #555; @outer-color: #333) { + background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color); + background-image: radial-gradient(circle, @inner-color, @outer-color); + background-repeat: no-repeat; + } + .striped(@color: rgba(255,255,255,.15); @angle: 45deg) { + background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + } +} + +// Reset filters for IE +// +// When you need to remove a gradient background, do not forget to use this to reset +// the IE filter for IE9 and below. +.reset-filter() { + filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); +} + + + +// Retina images +// +// Short retina mixin for setting background-image and -size + +.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { + background-image: url("@{file-1x}"); + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + background-image: url("@{file-2x}"); + background-size: @width-1x @height-1x; + } +} + + +// Responsive image +// +// Keep images from scaling beyond the width of their parents. + +.img-responsive(@display: block) { + display: @display; + max-width: 100%; // Part 1: Set a maximum relative to the parent + height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching +} + + +// COMPONENT MIXINS +// -------------------------------------------------- + +// Horizontal dividers +// ------------------------- +// Dividers (basically an hr) within dropdowns and nav lists +.nav-divider(@color: #e5e5e5) { + height: 1px; + margin: ((@line-height-computed / 2) - 1) 0; + overflow: hidden; + background-color: @color; +} + +// Panels +// ------------------------- +.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { + border-color: @border; + + & > .panel-heading { + color: @heading-text-color; + background-color: @heading-bg-color; + border-color: @heading-border; + + + .panel-collapse .panel-body { + border-top-color: @border; + } + } + & > .panel-footer { + + .panel-collapse .panel-body { + border-bottom-color: @border; + } + } +} + +// Alerts +// ------------------------- +.alert-variant(@background; @border; @text-color) { + background-color: @background; + border-color: @border; + color: @text-color; + + hr { + border-top-color: darken(@border, 5%); + } + .alert-link { + color: darken(@text-color, 10%); + } +} + +// Tables +// ------------------------- +.table-row-variant(@state; @background) { + // Exact selectors below required to override `.table-striped` and prevent + // inheritance to nested tables. + .table > thead > tr, + .table > tbody > tr, + .table > tfoot > tr { + > td.@{state}, + > th.@{state}, + &.@{state} > td, + &.@{state} > th { + background-color: @background; + } + } + + // Hover states for `.table-hover` + // Note: this is not available for cells or rows within `thead` or `tfoot`. + .table-hover > tbody > tr { + > td.@{state}:hover, + > th.@{state}:hover, + &.@{state}:hover > td, + &.@{state}:hover > th { + background-color: darken(@background, 5%); + } + } +} + +// List Groups +// ------------------------- +.list-group-item-variant(@state; @background; @color) { + .list-group-item-@{state} { + color: @color; + background-color: @background; + + a& { + color: @color; + + .list-group-item-heading { color: inherit; } + + &:hover, + &:focus { + color: @color; + background-color: darken(@background, 5%); + } + &.active, + &.active:hover, + &.active:focus { + color: #fff; + background-color: @color; + border-color: @color; + } + } + } +} + +// Button variants +// ------------------------- +// Easily pump out default styles, as well as :hover, :focus, :active, +// and disabled options for all buttons +.button-variant(@color; @background; @border) { + color: @color; + background-color: @background; + border-color: @border; + + &:hover, + &:focus, + &:active, + &.active, + .open .dropdown-toggle& { + color: @color; + background-color: darken(@background, 8%); + border-color: darken(@border, 12%); + } + &:active, + &.active, + .open .dropdown-toggle& { + background-image: none; + } + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &:active, + &.active { + background-color: @background; + border-color: @border; + } + } + + .badge { + color: @background; + background-color: @color; + } +} + +// Button sizes +// ------------------------- +.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + border-radius: @border-radius; +} + +// Pagination +// ------------------------- +.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) { + > li { + > a, + > span { + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + } + &:first-child { + > a, + > span { + .border-left-radius(@border-radius); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius); + } + } + } +} + +// Labels +// ------------------------- +.label-variant(@color) { + background-color: @color; + &[href] { + &:hover, + &:focus { + background-color: darken(@color, 10%); + } + } +} + +// Contextual backgrounds +// ------------------------- +.bg-variant(@color) { + background-color: @color; + a&:hover { + background-color: darken(@color, 10%); + } +} + +// Typography +// ------------------------- +.text-emphasis-variant(@color) { + color: @color; + a&:hover { + color: darken(@color, 10%); + } +} + +// Navbar vertical align +// ------------------------- +// Vertically center elements in the navbar. +// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. +.navbar-vertical-align(@element-height) { + margin-top: ((@navbar-height - @element-height) / 2); + margin-bottom: ((@navbar-height - @element-height) / 2); +} + +// Progress bars +// ------------------------- +.progress-bar-variant(@color) { + background-color: @color; + .progress-striped & { + #gradient > .striped(); + } +} + +// Responsive utilities +// ------------------------- +// More easily include all the states for responsive-utilities.less. +.responsive-visibility() { + display: block !important; + table& { display: table; } + tr& { display: table-row !important; } + th&, + td& { display: table-cell !important; } +} + +.responsive-invisibility() { + display: none !important; +} + + +// Grid System +// ----------- + +// Centered container element +.container-fixed() { + margin-right: auto; + margin-left: auto; + padding-left: (@grid-gutter-width / 2); + padding-right: (@grid-gutter-width / 2); + &:extend(.clearfix all); +} + +// Creates a wrapper for a series of columns +.make-row(@gutter: @grid-gutter-width) { + margin-left: (@gutter / -2); + margin-right: (@gutter / -2); + &:extend(.clearfix all); +} + +// Generate the extra small columns +.make-xs-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + float: left; + width: percentage((@columns / @grid-columns)); + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); +} +.make-xs-column-offset(@columns) { + @media (min-width: @screen-xs-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-xs-column-push(@columns) { + @media (min-width: @screen-xs-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-xs-column-pull(@columns) { + @media (min-width: @screen-xs-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Generate the small columns +.make-sm-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-sm-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-offset(@columns) { + @media (min-width: @screen-sm-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-push(@columns) { + @media (min-width: @screen-sm-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-pull(@columns) { + @media (min-width: @screen-sm-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Generate the medium columns +.make-md-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-md-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-md-column-offset(@columns) { + @media (min-width: @screen-md-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-push(@columns) { + @media (min-width: @screen-md-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-pull(@columns) { + @media (min-width: @screen-md-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Generate the large columns +.make-lg-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-lg-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-offset(@columns) { + @media (min-width: @screen-lg-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-push(@columns) { + @media (min-width: @screen-lg-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-pull(@columns) { + @media (min-width: @screen-lg-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Framework grid generation +// +// Used only by Bootstrap to generate the correct number of grid classes given +// any value of `@grid-columns`. + +.make-grid-columns() { + // Common styles for all sizes of grid columns, widths 1-12 + .col(@index) when (@index = 1) { // initial + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + position: relative; + // Prevent columns from collapsing when empty + min-height: 1px; + // Inner gutter via padding + padding-left: (@grid-gutter-width / 2); + padding-right: (@grid-gutter-width / 2); + } + } + .col(1); // kickstart it +} + +.float-grid-columns(@class) { + .col(@index) when (@index = 1) { // initial + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + float: left; + } + } + .col(1); // kickstart it +} + +.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { + .col-@{class}-@{index} { + width: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = push) { + .col-@{class}-push-@{index} { + left: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = pull) { + .col-@{class}-pull-@{index} { + right: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = offset) { + .col-@{class}-offset-@{index} { + margin-left: percentage((@index / @grid-columns)); + } +} + +// Basic looping in LESS +.loop-grid-columns(@index, @class, @type) when (@index >= 0) { + .calc-grid-column(@index, @class, @type); + // next iteration + .loop-grid-columns((@index - 1), @class, @type); +} + +// Create grid for specific class +.make-grid(@class) { + .float-grid-columns(@class); + .loop-grid-columns(@grid-columns, @class, width); + .loop-grid-columns(@grid-columns, @class, pull); + .loop-grid-columns(@grid-columns, @class, push); + .loop-grid-columns(@grid-columns, @class, offset); +} + +// Form validation states +// +// Used in forms.less to generate the form validation CSS for warnings, errors, +// and successes. + +.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { + // Color the label and help text + .help-block, + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + color: @text-color; + } + // Set the border and box shadow on specific inputs to match + .form-control { + border-color: @border-color; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken(@border-color, 10%); + @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); + .box-shadow(@shadow); + } + } + // Set validation states also for addons + .input-group-addon { + color: @text-color; + border-color: @border-color; + background-color: @background-color; + } + // Optional feedback icon + .form-control-feedback { + color: @text-color; + } +} + +// Form control focus state +// +// Generate a customized focus state and for any input with the specified color, +// which defaults to the `@input-focus-border` variable. +// +// We highly encourage you to not customize the default value, but instead use +// this to tweak colors on an as-needed basis. This aesthetic change is based on +// WebKit's default styles, but applicable to a wider range of browsers. Its +// usability and accessibility should be taken into account with any change. +// +// Example usage: change the default blue border and shadow to white for better +// contrast against a dark gray background. + +.form-control-focus(@color: @input-border-focus) { + @color-rgba: rgba(red(@color), green(@color), blue(@color), .6); + &:focus { + border-color: @color; + outline: 0; + .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}"); + } +} + +// Form control sizing +// +// Relative text size, padding, and border-radii changes for form controls. For +// horizontal sizing, wrap controls in the predefined grid classes. `` background color +@input-bg: #fff; +//** `` background color +@input-bg-disabled: @gray-lighter; + +//** Text color for ``s +@input-color: @gray; +//** `` border color +@input-border: #ccc; +//** `` border radius +@input-border-radius: @border-radius-base; +//** Border color for inputs on focus +@input-border-focus: #66afe9; + +//** Placeholder text color +@input-color-placeholder: @gray-light; + +//** Default `.form-control` height +@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); +//** Large `.form-control` height +@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); +//** Small `.form-control` height +@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); + +@legend-color: @gray-dark; +@legend-border-color: #e5e5e5; + +//** Background color for textual input addons +@input-group-addon-bg: @gray-lighter; +//** Border color for textual input addons +@input-group-addon-border-color: @input-border; + + +//== Dropdowns +// +//## Dropdown menu container and contents. + +//** Background for the dropdown menu. +@dropdown-bg: #fff; +//** Dropdown menu `border-color`. +@dropdown-border: rgba(0,0,0,.15); +//** Dropdown menu `border-color` **for IE8**. +@dropdown-fallback-border: #ccc; +//** Divider color for between dropdown items. +@dropdown-divider-bg: #e5e5e5; + +//** Dropdown link text color. +@dropdown-link-color: @gray-dark; +//** Hover color for dropdown links. +@dropdown-link-hover-color: darken(@gray-dark, 5%); +//** Hover background for dropdown links. +@dropdown-link-hover-bg: #f5f5f5; + +//** Active dropdown menu item text color. +@dropdown-link-active-color: @component-active-color; +//** Active dropdown menu item background color. +@dropdown-link-active-bg: @component-active-bg; + +//** Disabled dropdown menu item background color. +@dropdown-link-disabled-color: @gray-light; + +//** Text color for headers within dropdown menus. +@dropdown-header-color: @gray-light; + +// Note: Deprecated @dropdown-caret-color as of v3.1.0 +@dropdown-caret-color: #000; + + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// +// Note: These variables are not generated into the Customizer. + +@zindex-navbar: 1000; +@zindex-dropdown: 1000; +@zindex-popover: 1010; +@zindex-tooltip: 1030; +@zindex-navbar-fixed: 1030; +@zindex-modal-background: 1040; +@zindex-modal: 1050; + + +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. + +// Extra small screen / phone +// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1 +@screen-xs: 480px; +@screen-xs-min: @screen-xs; +@screen-phone: @screen-xs-min; + +// Small screen / tablet +// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1 +@screen-sm: 768px; +@screen-sm-min: @screen-sm; +@screen-tablet: @screen-sm-min; + +// Medium screen / desktop +// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1 +@screen-md: 992px; +@screen-md-min: @screen-md; +@screen-desktop: @screen-md-min; + +// Large screen / wide desktop +// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1 +@screen-lg: 1200px; +@screen-lg-min: @screen-lg; +@screen-lg-desktop: @screen-lg-min; + +// So media queries don't overlap when required, provide a maximum +@screen-xs-max: (@screen-sm-min - 1); +@screen-sm-max: (@screen-md-min - 1); +@screen-md-max: (@screen-lg-min - 1); + + +//== Grid system +// +//## Define your custom responsive grid. + +//** Number of columns in the grid. +@grid-columns: 12; +//** Padding between columns. Gets divided in half for the left and right. +@grid-gutter-width: 30px; +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +@grid-float-breakpoint: @screen-sm-min; +//** Point at which the navbar begins collapsing. +@grid-float-breakpoint-max: (@grid-float-breakpoint - 1); + + +//== Container sizes +// +//## Define the maximum width of `.container` for different screen sizes. + +// Small screen / tablet +@container-tablet: ((720px + @grid-gutter-width)); +//** For `@screen-sm-min` and up. +@container-sm: @container-tablet; + +// Medium screen / desktop +@container-desktop: ((940px + @grid-gutter-width)); +//** For `@screen-md-min` and up. +@container-md: @container-desktop; + +// Large screen / wide desktop +@container-large-desktop: ((1140px + @grid-gutter-width)); +//** For `@screen-lg-min` and up. +@container-lg: @container-large-desktop; + + +//== Navbar +// +//## + +// Basics of a navbar +@navbar-height: 50px; +@navbar-margin-bottom: @line-height-computed; +@navbar-border-radius: @border-radius-base; +@navbar-padding-horizontal: floor((@grid-gutter-width / 2)); +@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); +@navbar-collapse-max-height: 340px; + +@navbar-default-color: #777; +@navbar-default-bg: #fff; +@navbar-default-border: darken(@navbar-default-bg, 6.5%); + +// Navbar links +@navbar-default-link-color: #000; +@navbar-default-link-hover-color: #000; +@navbar-default-link-hover-bg: transparent; +@navbar-default-link-active-color: #000; +@navbar-default-link-active-bg: #fff; +@navbar-default-link-disabled-color: #ccc; +@navbar-default-link-disabled-bg: transparent; + +// Navbar brand label +@navbar-default-brand-color: @navbar-default-link-color; +@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); +@navbar-default-brand-hover-bg: transparent; + +// Navbar toggle +@navbar-default-toggle-hover-bg: #ddd; +@navbar-default-toggle-icon-bar-bg: #888; +@navbar-default-toggle-border-color: #ddd; + + +// Inverted navbar +// Reset inverted navbar basics +@navbar-inverse-color: @gray-light; +@navbar-inverse-bg: #222; +@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); + +// Inverted navbar links +@navbar-inverse-link-color: @gray-light; +@navbar-inverse-link-hover-color: #fff; +@navbar-inverse-link-hover-bg: transparent; +@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color; +@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%); +@navbar-inverse-link-disabled-color: #444; +@navbar-inverse-link-disabled-bg: transparent; + +// Inverted navbar brand label +@navbar-inverse-brand-color: @navbar-inverse-link-color; +@navbar-inverse-brand-hover-color: #fff; +@navbar-inverse-brand-hover-bg: transparent; + +// Inverted navbar toggle +@navbar-inverse-toggle-hover-bg: #333; +@navbar-inverse-toggle-icon-bar-bg: #fff; +@navbar-inverse-toggle-border-color: #333; + + +//== Navs +// +//## + +//=== Shared nav styles +@nav-link-padding: 10px 15px; +@nav-link-hover-bg: @gray-lighter; + +@nav-disabled-link-color: @gray-light; +@nav-disabled-link-hover-color: @gray-light; + +@nav-open-link-hover-color: #fff; + +//== Tabs +@nav-tabs-border-color: #ddd; + +@nav-tabs-link-hover-border-color: @gray-lighter; + +@nav-tabs-active-link-hover-bg: @body-bg; +@nav-tabs-active-link-hover-color: @gray; +@nav-tabs-active-link-hover-border-color: #ddd; + +@nav-tabs-justified-link-border-color: #ddd; +@nav-tabs-justified-active-link-border-color: @body-bg; + +//== Pills +@nav-pills-border-radius: @border-radius-base; +@nav-pills-active-link-hover-bg: @component-active-bg; +@nav-pills-active-link-hover-color: @component-active-color; + + +//== Pagination +// +//## + +@pagination-color: @link-color; +@pagination-bg: #fff; +@pagination-border: #ddd; + +@pagination-hover-color: @link-hover-color; +@pagination-hover-bg: @gray-lighter; +@pagination-hover-border: #ddd; + +@pagination-active-color: #fff; +@pagination-active-bg: @brand-primary; +@pagination-active-border: @brand-primary; + +@pagination-disabled-color: @gray-light; +@pagination-disabled-bg: #fff; +@pagination-disabled-border: #ddd; + + +//== Pager +// +//## + +@pager-bg: @pagination-bg; +@pager-border: @pagination-border; +@pager-border-radius: 15px; + +@pager-hover-bg: @pagination-hover-bg; + +@pager-active-bg: @pagination-active-bg; +@pager-active-color: @pagination-active-color; + +@pager-disabled-color: @pagination-disabled-color; + + +//== Jumbotron +// +//## + +@jumbotron-padding: 30px; +@jumbotron-color: inherit; +@jumbotron-bg: @gray-lighter; +@jumbotron-heading-color: inherit; +@jumbotron-font-size: ceil((@font-size-base * 1.5)); + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +@state-success-text: #3c763d; +@state-success-bg: #dff0d8; +@state-success-border: darken(spin(@state-success-bg, -10), 5%); + +@state-info-text: #31708f; +@state-info-bg: #d9edf7; +@state-info-border: darken(spin(@state-info-bg, -10), 7%); + +@state-warning-text: #8a6d3b; +@state-warning-bg: #fcf8e3; +@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); + +@state-danger-text: #a94442; +@state-danger-bg: #f2dede; +@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); + + +//== Tooltips +// +//## + +//** Tooltip max width +@tooltip-max-width: 200px; +//** Tooltip text color +@tooltip-color: #fff; +//** Tooltip background color +@tooltip-bg: #000; +@tooltip-opacity: .9; + +//** Tooltip arrow width +@tooltip-arrow-width: 5px; +//** Tooltip arrow color +@tooltip-arrow-color: @tooltip-bg; + + +//== Popovers +// +//## + +//** Popover body background color +@popover-bg: #fff; +//** Popover maximum width +@popover-max-width: 276px; +//** Popover border color +@popover-border-color: rgba(0,0,0,.2); +//** Popover fallback border color +@popover-fallback-border-color: #ccc; + +//** Popover title background color +@popover-title-bg: darken(@popover-bg, 3%); + +//** Popover arrow width +@popover-arrow-width: 10px; +//** Popover arrow color +@popover-arrow-color: #fff; + +//** Popover outer arrow width +@popover-arrow-outer-width: (@popover-arrow-width + 1); +//** Popover outer arrow color +@popover-arrow-outer-color: fadein(@popover-border-color, 5%); +//** Popover outer arrow fallback color +@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); + + +//== Labels +// +//## + +//** Default label background color +@label-default-bg: @gray-light; +//** Primary label background color +@label-primary-bg: @brand-primary; +//** Success label background color +@label-success-bg: @brand-success; +//** Info label background color +@label-info-bg: @brand-info; +//** Warning label background color +@label-warning-bg: @brand-warning; +//** Danger label background color +@label-danger-bg: @brand-danger; + +//** Default label text color +@label-color: #fff; +//** Default text color of a linked label +@label-link-hover-color: #fff; + + +//== Modals +// +//## + +//** Padding applied to the modal body +@modal-inner-padding: 20px; + +//** Padding applied to the modal title +@modal-title-padding: 15px; +//** Modal title line-height +@modal-title-line-height: @line-height-base; + +//** Background color of modal content area +@modal-content-bg: #fff; +//** Modal content border color +@modal-content-border-color: rgba(0,0,0,.2); +//** Modal content border color **for IE8** +@modal-content-fallback-border-color: #999; + +//** Modal backdrop background color +@modal-backdrop-bg: #000; +//** Modal backdrop opacity +@modal-backdrop-opacity: .5; +//** Modal header border color +@modal-header-border-color: #e5e5e5; +//** Modal footer border color +@modal-footer-border-color: @modal-header-border-color; + +@modal-lg: 900px; +@modal-md: 600px; +@modal-sm: 300px; + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +@alert-padding: 15px; +@alert-border-radius: @border-radius-base; +@alert-link-font-weight: bold; + +@alert-success-bg: @state-success-bg; +@alert-success-text: @state-success-text; +@alert-success-border: @state-success-border; + +@alert-info-bg: @state-info-bg; +@alert-info-text: @state-info-text; +@alert-info-border: @state-info-border; + +@alert-warning-bg: @state-warning-bg; +@alert-warning-text: @state-warning-text; +@alert-warning-border: @state-warning-border; + +@alert-danger-bg: @state-danger-bg; +@alert-danger-text: @state-danger-text; +@alert-danger-border: @state-danger-border; + + +//== Progress bars +// +//## + +//** Background color of the whole progress component +@progress-bg: #f5f5f5; +//** Progress bar text color +@progress-bar-color: #fff; + +//** Default progress bar color +@progress-bar-bg: @brand-primary; +//** Success progress bar color +@progress-bar-success-bg: @brand-success; +//** Warning progress bar color +@progress-bar-warning-bg: @brand-warning; +//** Danger progress bar color +@progress-bar-danger-bg: @brand-danger; +//** Info progress bar color +@progress-bar-info-bg: @brand-info; + + +//== List group +// +//## + +//** Background color on `.list-group-item` +@list-group-bg: #fff; +//** `.list-group-item` border color +@list-group-border: #ddd; +//** List group border radius +@list-group-border-radius: @border-radius-base; + +//** Background color of single list elements on hover +@list-group-hover-bg: #f5f5f5; +//** Text color of active list elements +@list-group-active-color: @component-active-color; +//** Background color of active list elements +@list-group-active-bg: @component-active-bg; +//** Border color of active list elements +@list-group-active-border: @list-group-active-bg; +@list-group-active-text-color: lighten(@list-group-active-bg, 40%); + +@list-group-link-color: #555; +@list-group-link-heading-color: #333; + + +//== Panels +// +//## + +@panel-bg: #fff; +@panel-body-padding: 15px; +@panel-border-radius: @border-radius-base; + +//** Border color for elements within panels +@panel-inner-border: #ddd; +@panel-footer-bg: #f5f5f5; + +@panel-default-text: @gray-dark; +@panel-default-border: #ddd; +@panel-default-heading-bg: #f5f5f5; + +@panel-primary-text: #fff; +@panel-primary-border: @brand-primary; +@panel-primary-heading-bg: @brand-primary; + +@panel-success-text: @state-success-text; +@panel-success-border: @state-success-border; +@panel-success-heading-bg: @state-success-bg; + +@panel-info-text: @state-info-text; +@panel-info-border: @state-info-border; +@panel-info-heading-bg: @state-info-bg; + +@panel-warning-text: @state-warning-text; +@panel-warning-border: @state-warning-border; +@panel-warning-heading-bg: @state-warning-bg; + +@panel-danger-text: @state-danger-text; +@panel-danger-border: @state-danger-border; +@panel-danger-heading-bg: @state-danger-bg; + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +@thumbnail-padding: 4px; +//** Thumbnail background color +@thumbnail-bg: @body-bg; +//** Thumbnail border color +@thumbnail-border: #ddd; +//** Thumbnail border radius +@thumbnail-border-radius: @border-radius-base; + +//** Custom text color for thumbnail captions +@thumbnail-caption-color: @text-color; +//** Padding around the thumbnail caption +@thumbnail-caption-padding: 9px; + + +//== Wells +// +//## + +@well-bg: #f5f5f5; +@well-border: darken(@well-bg, 7%); + + +//== Badges +// +//## + +@badge-color: #fff; +//** Linked badge text color on hover +@badge-link-hover-color: #fff; +@badge-bg: @gray-light; + +//** Badge text color in active nav link +@badge-active-color: @link-color; +//** Badge background color in active nav link +@badge-active-bg: #fff; + +@badge-font-weight: bold; +@badge-line-height: 1; +@badge-border-radius: 10px; + + +//== Breadcrumbs +// +//## + +@breadcrumb-padding-vertical: 8px; +@breadcrumb-padding-horizontal: 15px; +//** Breadcrumb background color +@breadcrumb-bg: #f5f5f5; +//** Breadcrumb text color +@breadcrumb-color: #ccc; +//** Text color of current page in the breadcrumb +@breadcrumb-active-color: @gray-light; +//** Textual separator for between breadcrumb elements +@breadcrumb-separator: "/"; + + +//== Carousel +// +//## + +@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); + +@carousel-control-color: #fff; +@carousel-control-width: 15%; +@carousel-control-opacity: .5; +@carousel-control-font-size: 20px; + +@carousel-indicator-active-bg: #fff; +@carousel-indicator-border-color: #fff; + +@carousel-caption-color: #fff; + + +//== Close +// +//## + +@close-font-weight: bold; +@close-color: #000; +@close-text-shadow: 0 1px 0 #fff; + + +//== Code +// +//## + +@code-color: #c7254e; +@code-bg: #f9f2f4; + +@kbd-color: #fff; +@kbd-bg: #333; + +@pre-bg: #f5f5f5; +@pre-color: @gray-dark; +@pre-border-color: #ccc; +@pre-scrollable-max-height: 340px; + + +//== Type +// +//## + +//** Text muted color +@text-muted: @gray-light; +//** Abbreviations and acronyms border color +@abbr-border-color: @gray-light; +//** Headings small color +@headings-small-color: @gray-light; +//** Blockquote small color +@blockquote-small-color: @gray-light; +//** Blockquote font size +@blockquote-font-size: (@font-size-base * 1.25); +//** Blockquote border color +@blockquote-border-color: @gray-lighter; +//** Page header border color +@page-header-border-color: @gray-lighter; + + +//== Miscellaneous +// +//## + +//** Horizontal line color. +@hr-border: @gray-lighter; + +//** Horizontal offset for forms and lists. +@component-offset-horizontal: 180px; diff --git a/awx/ui/static/ansible-bootstrap/wells.less b/awx/ui/static/ansible-bootstrap/wells.less new file mode 100644 index 0000000000..15d072b0cd --- /dev/null +++ b/awx/ui/static/ansible-bootstrap/wells.less @@ -0,0 +1,29 @@ +// +// Wells +// -------------------------------------------------- + + +// Base class +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: @well-bg; + border: 1px solid @well-border; + border-radius: @border-radius-base; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); + blockquote { + border-color: #ddd; + border-color: rgba(0,0,0,.15); + } +} + +// Sizes +.well-lg { + padding: 24px; + border-radius: @border-radius-large; +} +.well-sm { + padding: 9px; + border-radius: @border-radius-small; +} diff --git a/awx/ui/static/img/tower_console_bug_black.png b/awx/ui/static/img/tower_console_bug_black.png new file mode 100644 index 0000000000000000000000000000000000000000..0fd6b22d5fe1b9d39978afca3078a98a59bbcd8c GIT binary patch literal 851 zcmV-Z1FZasP)Co=pUF4&A|FTTFOIz z_ZA??h2niji-v;#e1M(~_M}g6ICI2LJfHNqmpy|HU(mtfZH{=v6TV|ZJZ5mEX7so_ z1_Kf(RKx(Yfi;EFeJma_YkG?ZG{j*68!N!JS~m)`$r>8W;2kx=Xv{6(owY~}IwWud zEwBo68z{kH6;^Lf^N1CDZhwOT+(8p=VgO|_92q##N)6sH2Rl%zz&}JnDu*{Qh@ec4 zXS{d^-s!=D6VC)fssUB3+?-)zj+M_qg9?+}-hr1)%@b@vxgsw~G=x;P@!EAP@lDsy zfO5?&iJA^J4G*1`X9k|>L8w5w69a|Ji~M~r+tff^w~K@9}8D0xlw&~ z3Hqj8idm>`-&tv|t4E+kJOcibTE?P`$D*^RY}(2W>$UBhRQ3j55{ea!%Jx;+i+oL_ ztNsIKTYH|@@wJ22w)R36i^QDiYIvd|8bK3_iIxj<7Md%gr{B)r^r|fkC*~w}#SRT( zdy~9rm+D8!L-A89EVV%%i#!T_u?x*fl(z3dc%X{aws6HRbeyE^+lf9zc122a8P3c} z9Hs4>5#_O{_esej{H!)gMoRg|YP(R-z9TYvNz;0wD#yrcOu`qDNg0bmhJhLD+Bb?` z0Qr^cFtX&Z^3kpsE};^Ox_q$yIoh=Rj+ li > a { - font-size: 12px; - color: @grey; -} - -.main-menu .nav > li > a:hover, -.main-menu .nav > li > a:focus { - color: @blue; -} - -#main_tabs.nav-tabs>li.active>a, #main_tabs.nav-tabs>li.active>a:hover, #main_tabs.nav-tabs>li.active>a:focus { - color: @blue; - cursor: pointer; -} - .text-justify { text-align: justify; } -.navbar-collapse { - padding-right: 0; -} - -.main-menu .nav >li >a:last-child { - padding-right: 0; - padding-left: 20px; -} - -.navbar>.container-fluid .navbar-brand { - height: 0; - margin-left: 0; - margin-top: 11px; -} - -/* Using inline-block rather than block keeps - brand img from right aligning into the collapse button - on mobile screens */ -.main-menu .navbar-brand { - display: inline-block; - padding: 0; -} - -.main-menu .navbar-brand img { - max-width: 260px; -} - .help-link, .help-link:active, .help-link:visited, diff --git a/awx/ui/static/less/breadcrumbs.less b/awx/ui/static/less/breadcrumbs.less new file mode 100644 index 0000000000..3b0db52af4 --- /dev/null +++ b/awx/ui/static/less/breadcrumbs.less @@ -0,0 +1,73 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * breadcrumbs.less + * + * custom breadcrumbs + * + */ + +.ansible-breadcrumb { + list-style: none; + overflow: hidden; + padding: 0; + margin: 0 0 8px 0; +} +.ansible-breadcrumb li { + float: left; + height: 26px; + margin-top: 3px; + margin-bottom: 3px; +} +.ansible-breadcrumb li a { + color: @white; + font-weight: normal; + text-decoration: none; + padding: 3px 8px 3px 20px; + background: @blue-dark; /* fallback color */ + position: relative; + left: 0; + top: 0; + display: block; + float: left; +} +.ansible-breadcrumb li.active a { + background: @grey; + color: @black; + font-weight: normal; +} +.ansible-breadcrumb li a:after { + content: " "; + display: block; + width: 0; + height: 0; + border-top: 13px dashed transparent; /* Go big on the size, and let overflow hide */ + border-bottom: 13px dashed transparent; + border-left: 11px solid @blue-dark; + position: absolute; + top: 50%; + margin-top: -13px; + left: 100%; + z-index: 2; +} +.ansible-breadcrumb li.active a:after { + border-left: 13px solid @grey; +} +.ansible-breadcrumb li a:before { + content: " "; + display: block; + width: 0; + height: 0; + border-top: 13px dashed transparent; + border-bottom: 13px dashed transparent; + border-left: 11px solid @white; + position: absolute; + top: 50%; + margin-top: -13px; + margin-left: 1px; + left: 100%; + z-index: 1; +} +.ansible-breadcrumb li.active a:before { + border-left: 11px solid @white; +} \ No newline at end of file diff --git a/awx/ui/static/less/inventory-edit.less b/awx/ui/static/less/inventory-edit.less index 9942eb0d1f..dcb192b6b9 100644 --- a/awx/ui/static/less/inventory-edit.less +++ b/awx/ui/static/less/inventory-edit.less @@ -11,70 +11,6 @@ margin-bottom: 8px; } } -.group-breadcrumbs { - list-style: none; - overflow: hidden; - padding: 0; - margin: 0 0 8px 0; -} -.group-breadcrumbs li { - float: left; - height: 26px; - margin-top: 3px; - margin-bottom: 3px; -} -.group-breadcrumbs li a { - color: @white; - font-weight: normal; - text-decoration: none; - padding: 3px 8px 3px 20px; - background: @blue-dark; /* fallback color */ - position: relative; - left: 0; - top: 0; - display: block; - float: left; -} -.group-breadcrumbs li.active a { - background: @grey; - color: @black; - font-weight: normal; -} -.group-breadcrumbs li a:after { - content: " "; - display: block; - width: 0; - height: 0; - border-top: 13px dashed transparent; /* Go big on the size, and let overflow hide */ - border-bottom: 13px dashed transparent; - border-left: 11px solid @blue-dark; - position: absolute; - top: 50%; - margin-top: -13px; - left: 100%; - z-index: 2; -} -.group-breadcrumbs li.active a:after { - border-left: 13px solid @grey; -} -.group-breadcrumbs li a:before { - content: " "; - display: block; - width: 0; - height: 0; - border-top: 13px dashed transparent; - border-bottom: 13px dashed transparent; - border-left: 11px solid @white; - position: absolute; - top: 50%; - margin-top: -13px; - margin-left: 1px; - left: 100%; - z-index: 1; -} -.group-breadcrumbs li.active a:before { - border-left: 11px solid @white; -} #group-copy-dialog, #host-copy-dialog { diff --git a/awx/ui/static/less/main-layout.less b/awx/ui/static/less/main-layout.less new file mode 100644 index 0000000000..64603ecb70 --- /dev/null +++ b/awx/ui/static/less/main-layout.less @@ -0,0 +1,47 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * main-layout.css + * + * primary page layout styles + * + */ + +html, body { height: 100%; } + +html { + background-color: @white; +} + +body { + font-family: 'Source Sans Pro', sans-serif; + font-weight: 400; + padding-top: 65px; + color: @black; +} + +#main-menu-container { + .navbar { + margin-bottom: 0; + } + .navbar-brand { + position: absolute; + top: 3px; + left: 15px; + padding: 0px 15px 0 15px; + img { + width: 42px; + } + } + .navbar-collapse { + margin-left: 42px; + } +} + +#content-container { + margin-top: 40px; +} + +.group-breadcrumbs { + margin-bottom: 20px; +} \ No newline at end of file diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 5b68ce5581..c04ad5ec5c 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -1048,8 +1048,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'Utilities', 'ListGenerator breadCrumbs: function (options, navigation) { var itm, paths, html = ''; - html += "
      \n"; - html += "
        \n"; + html += "
          \n"; html += "
        • {{ crumb.title }}
        • \n"; if (navigation) { @@ -1090,15 +1089,14 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'Utilities', 'ListGenerator } html += "
        \n"; html += "
      \n"; - html += "
      \n"; } else { - html += "
    1. "; + html += "
    2. "; if (options.mode === 'edit') { html += this.form.editTitle; } else { html += this.form.addTitle; } - html += "
    3. \n
\n\n"; + html += "\n \n"; } return html; }, diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 6c6505b4b3..141ece82bb 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -401,8 +401,7 @@ angular.module('GeneratorHelpers', []) mode = params.mode, html = '', itm, navigation; - html += "
\n"; - html += "
    \n"; + html += "
      \n"; html += "
    • {{ crumb.title }}
    • \n"; if (list.navigationLinks) { @@ -443,15 +442,14 @@ angular.module('GeneratorHelpers', []) } html += "
    \n"; html += "
\n"; - html += "\n"; } else { - html += "
  • "; + html += "
  • "; if (mode === 'select') { html += list.selectTitle; } else { html += list.editTitle; } - html += "
  • \n\n\n"; + html += "\n\n"; } return html; diff --git a/awx/ui/static/partials/inventory-edit.html b/awx/ui/static/partials/inventory-edit.html index 7686250658..9e69e32a2d 100644 --- a/awx/ui/static/partials/inventory-edit.html +++ b/awx/ui/static/partials/inventory-edit.html @@ -1,10 +1,9 @@
    -
    - -
    - -
    -
    - +
    -
    +
    No matching plays
    @@ -86,7 +86,7 @@
    @@ -101,7 +101,7 @@
    {{ task.successfulCount }}
    {{ task.changedCount }}
    {{ task.skippedCount }}
    {{ task.failedCount }}
    No matching hosts
    -
    +
    No matching tasks
    From 88c1f13d2772588ad47dafa3013c2a7e87bb8347 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 18 Jun 2014 18:48:17 -0400 Subject: [PATCH 19/32] Job detail page refactor Fixed issue with tracking last event id. --- awx/ui/static/js/controllers/JobDetail.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 31fbf03797..67562b53a1 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -68,6 +68,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, event_socket.on("job_events-" + job_id, function(data) { data.event = data.event_name; + console.log("id: " + data.id + " lastEventId: " + lastEventId); if (api_complete && data.id > lastEventId) { $log.debug('received event: ' + data.id); if (queue.length < 50) { @@ -109,12 +110,17 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } scope.removeAPIComplete = scope.$on('APIComplete', function() { // process any events sitting in the queue - var url, hostId = 0, taskId = 0, playId = 0; + var keys, url, hostId = 0, taskId = 0, playId = 0; // Find the max event.id value in memory - hostId = (scope.hostResults.length > 0) ? scope.hostResults[scope.hostResults.length - 1] : 0; - taskId = (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1] : 0; - playId = (scope.plays.length > 0) ? scope.plays[scope.plays.length - 1] : 0; + hostId = (scope.hostResults.length > 0) ? scope.hostResults[scope.hostResults.length - 1].id : 0; + if (scope.hostResults.length > 0) { + keys = Object.keys(scope.hostResults); + keys.sort(); + hostId = keys[keys.length - 1]; + } + taskId = (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].id : 0; + playId = (scope.plays.length > 0) ? scope.plays[scope.plays.length - 1].id : 0; lastEventId = Math.max(hostId, taskId, playId); api_complete = true; From 22d5f061ddfd9f48b6c5f57448815129606229c4 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 18 Jun 2014 21:17:18 -0400 Subject: [PATCH 20/32] Job detail page refactor Performance adjustment. CPU is melting with all the new changes. --- awx/ui/static/js/controllers/JobDetail.js | 7 +++---- awx/ui/static/js/helpers/JobDetail.js | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 67562b53a1..32d016a8a2 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -68,10 +68,9 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, event_socket.on("job_events-" + job_id, function(data) { data.event = data.event_name; - console.log("id: " + data.id + " lastEventId: " + lastEventId); if (api_complete && data.id > lastEventId) { $log.debug('received event: ' + data.id); - if (queue.length < 50) { + if (queue.length < 20) { queue.unshift(data); } else { @@ -607,7 +606,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; url += (scope.searchAllStatus === 'failed') ? '&failed=true' : ''; - url += 'id__gt=' + scope.tasks[scope.tasks.length - 1].id + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id'; + url += '&id__gt=' + scope.tasks[scope.tasks.length - 1].id + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -685,7 +684,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; url += (scope.searchAllStatus === 'failed') ? '&failed=true' : ''; - url += 'id__lt=' + scope.tasks[scope.tasks[0]].name + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id'; + url += '&id__lt=' + scope.tasks[scope.tasks[0]].name + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id'; Wait('start'); Rest.setUrl(url); Rest.get() diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 96cdc093f6..3d65224989 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -459,7 +459,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge scope.hosts.forEach(function(host, idx){ scope.hostsMap[host.id] = idx; }); - $('#tasks-table-detail').mCustomScrollbar("update"); + $('#hosts-table-detail').mCustomScrollbar("update"); } UpdateTaskStatus({ From 0fc977648e7402d11233fb49ad1232b5b3259f29 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 18 Jun 2014 23:04:12 -0400 Subject: [PATCH 21/32] Job detail page refactor Improved handling of scrollbar refresh. Handling on via scope.$emit rather than inside the http response. Fixed pie chart drawing at job completion so that totaling of stats on playbook_on_stats event matches the way we're counting hosts during event processing. --- awx/ui/static/js/controllers/JobDetail.js | 111 +++++++++--- awx/ui/static/js/helpers/JobDetail.js | 200 +++++++++++++--------- awx/ui/static/partials/job_detail.html | 10 +- 3 files changed, 209 insertions(+), 112 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 32d016a8a2..44770df805 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -8,7 +8,7 @@ 'use strict'; function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, - ProcessErrors, ProcessEventQueue, SelectPlay, SelectTask, Socket, GetElapsed, SelectHost, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, + ProcessErrors, ProcessEventQueue, SelectPlay, SelectTask, Socket, GetElapsed, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished, SetTaskStyles) { ClearScope(); @@ -31,10 +31,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.hostResultsMap = {}; api_complete = false; - scope.hostTableRows = 150; - scope.hostSummaryTableRows = 150; - scope.tasksMaxRows = 150; - scope.playsMaxRows = 150; + scope.hostTableRows = 75; + scope.hostSummaryTableRows = 75; + scope.tasksMaxRows = 75; + scope.playsMaxRows = 75; scope.search_all_tasks = []; scope.search_all_plays = []; @@ -188,8 +188,11 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.removeRefreshJobDetails = scope.$on('LoadJobDetails', function(e, events_url) { // Call to load all the job bits including, plays, tasks, hosts results and host summary - scope.plays = []; - scope.playsMap = {}; + scope.host_summary.ok = 0; + scope.host_summary.changed = 0; + scope.host_summary.unreachable = 0; + scope.host_summary.failed = 0; + scope.host_summary.total = 0; var url = scope.job.url + 'job_plays/?order_by=id'; Rest.setUrl(url); @@ -198,7 +201,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, data.forEach(function(event, idx) { var status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful', start = event.started, - end, elapsed; + end, elapsed, play; if (idx < data.length - 1) { // end date = starting date of the next event @@ -218,18 +221,29 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, elapsed = '00:00:00'; } - scope.plays.push({ - id: event.id, - name: event.play, - created: start, - finished: end, - status: status, - elapsed: elapsed, - playActiveClass: '', - hostCount: 0, - fistTask: null - }); - scope.playsMap[event.id] = scope.plays.length - 1; + if (scope.playsMap[event.id] !== undefined) { + play = scope.plays[scope.playsMap[event.id]]; + play.finished = end; + play.status = status; + play.elapsed = elapsed; + play.playActiveClass = ''; + } + else { + scope.plays.push({ + id: event.id, + name: event.play, + created: start, + finished: end, + status: status, + elapsed: elapsed, + playActiveClass: '', + hostCount: 0, + fistTask: null + }); + if (scope.plays.length > scope.playsMaxRows) { + scope.plays.shift(); + } + } scope.host_summary.ok += (data.ok_count) ? data.ok_count : 0; scope.host_summary.changed += (data.changed_count) ? data.changed_count : 0; @@ -239,7 +253,14 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.host_summary.unreachable + scope.host_summary.failed; }); + //rebuild the index + scope.playsMap = {}; + scope.plays.forEach(function(play, idx) { + scope.playsMap[play.id] = idx; + }); + scope.$emit('PlaysReady', events_url); + scope.$emit('FixPlaysScroll'); }) .error( function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -334,6 +355,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); }); + if (scope.removeRefreshCompleted) { scope.removeRefreshCompleted(); } @@ -349,6 +371,41 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } }); + if (scope.removeFixPlaysScroll) { + scope.removeFixPlaysScroll(); + } + scope.removeFixPlaysScroll = scope.$on('FixPlaysScroll', function() { + $('#plays-table-detail').mCustomScrollbar("update"); + setTimeout( function() { + scope.auto_scroll = true; + $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); + }, 500); + }); + + if (scope.removeFixTasksScroll) { + scope.removeFixTasksScroll(); + } + scope.removeFixTasksScroll = scope.$on('FixTasksScroll', function() { + $('#tasks-table-detail').mCustomScrollbar("update"); + setTimeout( function() { + scope.auto_scroll = true; + $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); + }, 500); + }); + + + if (scope.removeFixHostResultsScroll) { + scope.removeFixHostResultsScroll(); + } + scope.removeFixHostResultsScroll = scope.$on('FixHostResultsScroll', function() { + $('#hosts-table-detail').mCustomScrollbar("update"); + setTimeout( function() { + scope.auto_scroll = true; + $('#hosts-table-detail').mCustomScrollbar("scrollTo", "bottom"); + }, 500); + }); + + scope.adjustSize = function() { var height, ww = $(window).width(); if (ww < 1240) { @@ -512,7 +569,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; - url += 'host__name__gt=' + scope.hostResults[scope.hostResults.length - 1].name + '&host__isnull=false&page_size=' + (scope.hostTableRows / 3) + '&order_by=host__name'; + url += 'host__name__gt=' + scope.hostResults[scope.hostResults.length - 1].name + '&host__isnull=false&page_size=' + scope.hostTableRows + '&order_by=host__name'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -559,7 +616,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; - url += 'host__name__lt=' + scope.hostResults[0].name + '&host__isnull=false&page_size=' + (scope.hostTableRows / 3) + '&order_by=-host__name'; + url += 'host__name__lt=' + scope.hostResults[0].name + '&host__isnull=false&page_size=' + scope.hostTableRows + '&order_by=-host__name'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -606,7 +663,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; url += (scope.searchAllStatus === 'failed') ? '&failed=true' : ''; - url += '&id__gt=' + scope.tasks[scope.tasks.length - 1].id + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id'; + url += '&id__gt=' + scope.tasks[scope.tasks.length - 1].id + '&page_size=' + scope.tasksMaxRows + '&order_by=id'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -684,7 +741,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; url += (scope.searchAllStatus === 'failed') ? '&failed=true' : ''; - url += '&id__lt=' + scope.tasks[scope.tasks[0]].name + '&page_size=' + (scope.tasksMaxRows / 3) + '&order_by=id'; + url += '&id__lt=' + scope.tasks[scope.tasks[0]].id + '&page_size=' + scope.tasksMaxRows + '&order_by=id'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -759,7 +816,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; - url += 'host__name__gt=' + scope.hosts[scope.hosts.length - 1].name + '&page_size=' + (scope.hostSummaryTableRows / 3) + '&order_by=host__name'; + url += 'host__name__gt=' + scope.hosts[scope.hosts.length - 1].name + '&page_size=' + scope.hostSummaryTableRows + '&order_by=host__name'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -803,7 +860,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; - url += 'host__name__lt=' + scope.hosts[0].name + '&page_size=' + (scope.hostSummaryTableRows / 3) + '&order_by=-host__name'; + url += 'host__name__lt=' + scope.hosts[0].name + '&page_size=' + scope.hostSummaryTableRows + '&order_by=-host__name'; Wait('start'); Rest.setUrl(url); Rest.get() @@ -904,6 +961,6 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', - 'Wait', 'Rest', 'ProcessErrors', 'ProcessEventQueue', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'SelectHost', 'FilterAllByHostName', 'DrawGraph', + 'Wait', 'Rest', 'ProcessErrors', 'ProcessEventQueue', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'FilterAllByHostName', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles' ]; diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 3d65224989..9af481396e 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -88,7 +88,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge fistTask: null }); scope.playsMap[event.id] = scope.plays.length -1; - if (scope.activePlay && scope.playsMap[scope.activePlay] !== undefined) { + if (scope.playsMap[scope.activePlay] !== undefined) { scope.plays[scope.playsMap[scope.activePlay]].playActiveClass = ''; } scope.activePlay = event.id; @@ -99,6 +99,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge scope.hostResultsMap = {}; $('#hosts-table-detail').mCustomScrollbar("update"); $('#tasks-table-detail').mCustomScrollbar("update"); + scope.$emit('FixPlaysScroll'); break; case 'playbook_on_setup': @@ -280,12 +281,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge scope.hasRoles = (event.role) ? true : false; $('#hosts-table-detail').mCustomScrollbar("update"); - $('#tasks-table-detail').mCustomScrollbar("update"); - setTimeout( function() { - scope.auto_scroll = true; - $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); - - }, 1500); + scope.$emit('FixTasksScroll'); // Record the first task id UpdatePlayStatus({ @@ -340,7 +336,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge status_text = params.status_text, play; - if (scope.playsMap[id]) { + if (scope.playsMap[id] !== undefined) { play = scope.plays[scope.playsMap[id]]; if (failed) { play.status = 'failed'; @@ -380,7 +376,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge no_hosts = params.no_hosts, task; - if (scope.tasksMap[id]) { + if (scope.tasksMap[id] !== undefined) { task = scope.tasks[scope.tasksMap[id]]; if (no_hosts){ task.status = 'no-matching-hosts'; @@ -582,14 +578,15 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge var scope = params.scope, id = params.id, callback = params.callback, - clear = true; + clear = false; // Determine if the tasks and hostResults arrays should be initialized - //if (scope.search_all_hosts_name || scope.searchAllStatus === 'failed') { - // clear = true; - //} - //else { - // clear = (scope.activePlay === id) ? false : true; //are we moving to a new play? + if (scope.search_all_hosts_name || scope.searchAllStatus === 'failed') { + clear = true; + } + else { + clear = (scope.activePlay === id) ? false : true; //are we moving to a new play? + } if (scope.activePlay && scope.playsMap[scope.activePlay] !== undefined) { scope.plays[scope.playsMap[scope.activePlay]].playActiveClass = ''; @@ -616,10 +613,13 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge return function(params) { var scope = params.scope, callback = params.callback, + clear = params.clear, url; - scope.tasks = []; - scope.tasksMap = {}; + if (clear) { + scope.tasks = []; + scope.tasksMap = {}; + } if (scope.activePlay) { url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; @@ -631,7 +631,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge Rest.get() .success(function(data) { data.results.forEach(function(event, idx) { - var end, elapsed; + var task, end, elapsed; if (!scope.plays[scope.playsMap[scope.activePlay]].firstTask) { scope.plays[scope.playsMap[scope.activePlay]].firstTask = event.id; @@ -657,36 +657,64 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge elapsed = '00:00:00'; } - scope.tasks.push({ - id: event.id, - play_id: scope.activePlay, - name: event.name, - status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), - created: event.created, - modified: event.modified, - finished: end, - elapsed: elapsed, - hostCount: (event.host_count) ? event.host_count : 0, - reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0, - successfulCount: (event.successful_count) ? event.successful_count : 0, - failedCount: (event.failed_count) ? event.failed_count : 0, - changedCount: (event.changed_count) ? event.changed_count : 0, - skippedCount: (event.skipped_count) ? event.skipped_count : 0, - taskActiveClass: '' - }); - scope.tasksMap[event.id] = scope.tasks.length - 1; + if (scope.tasksMap[event.id] !== undefined) { + task = scope.tasks[scope.tasksMap[event.id]]; + task.status = ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ); + task.modified = event.modified; + task.finished = end; + task.elapsed = elapsed; + task.hostCount = (event.host_count) ? event.host_count : 0; + task.reportedHosts = (event.reported_hosts) ? event.reported_hosts : 0; + task.successfulCount = (event.successful_count) ? event.successful_count : 0; + task.failedCount = (event.failed_count) ? event.failed_count : 0; + task.changedCount = (event.changed_count) ? event.changed_count : 0; + task.skippedCount = (event.skipped_count) ? event.skipped_count : 0; + task.taskActiveClass = ''; + } + else { + scope.tasks.push({ + id: event.id, + play_id: scope.activePlay, + name: event.name, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + created: event.created, + modified: event.modified, + finished: end, + elapsed: elapsed, + hostCount: (event.host_count) ? event.host_count : 0, + reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0, + successfulCount: (event.successful_count) ? event.successful_count : 0, + failedCount: (event.failed_count) ? event.failed_count : 0, + changedCount: (event.changed_count) ? event.changed_count : 0, + skippedCount: (event.skipped_count) ? event.skipped_count : 0, + taskActiveClass: '' + }); + scope.tasksMap[event.id] = scope.tasks.length - 1; + if (scope.tasks.length > scope.tasksMaxRows) { + scope.tasks.shift(); + } + } SetTaskStyles({ scope: scope, task_id: event.id }); }); + //rebuild the index; + scope.tasksMap = {}; + scope.tasks.forEach(function(task, idx) { + scope.tasksMap[task.id] = idx; + }); + // set the active task SelectTask({ scope: scope, id: (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].id : null, callback: callback }); + + scope.$emit('FixTasksScroll'); + }) .error(function(data) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -696,6 +724,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge else { scope.tasks = []; scope.tasksMap = {}; + $('#tasks-table-detail').mCustomScrollbar("update"); SelectTask({ scope: scope, id: null, @@ -711,14 +740,14 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge var scope = params.scope, id = params.id, callback = params.callback, - clear=true; + clear=false; - //if (scope.search_all_hosts_name || scope.searchAllStatus === 'failed') { - // clear = true; - //} - //else { - // clear = (scope.activeTask === id) ? false : true; - //} + if (scope.search_all_hosts_name || scope.searchAllStatus === 'failed') { + clear = true; + } + else { + clear = (scope.activeTask === id) ? false : true; + } if (scope.activeTask && scope.tasksMap[scope.activeTask] !== undefined) { scope.tasks[scope.tasksMap[scope.activeTask]].taskActiveClass = ''; @@ -728,13 +757,6 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge } scope.activeTask = id; - $('#tasks-table-detail').mCustomScrollbar("update"); - setTimeout( function() { - scope.auto_scroll = true; - $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); - - }, 1500); - LoadHosts({ scope: scope, callback: callback, @@ -744,7 +766,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge }]) // Refresh the list of hosts -.factory('LoadHosts', ['Rest', 'ProcessErrors', 'SelectHost', function(Rest, ProcessErrors, SelectHost) { +.factory('LoadHosts', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) { return function(params) { var scope = params.scope, callback = params.callback, @@ -766,21 +788,39 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge Rest.get() .success(function(data) { data.results.forEach(function(event) { - scope.hostResults.push({ - id: event.id, - status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), - host_id: event.host, - task_id: event.parent, - name: event.event_data.host, - created: event.created, - msg: ( (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' ) - }); - scope.hostResultsMap[event.id] = scope.hostResults.length - 1; + var result; + if (scope.hostResultsMap[event.id] !== undefined) { + result = scope.hostResults[scope.hostResultsMap[event.id]]; + result.status = ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ); + result.created = event.created; + result.msg = (event.event_data && event.event_data.res) ? event.event_data.res.msg : ''; + } + else { + scope.hostResults.push({ + id: event.id, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + host_id: event.host, + task_id: event.parent, + name: event.event_data.host, + created: event.created, + msg: ( (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' ) + }); + if (scope.hostResults.length > scope.hostTableRows) { + scope.hostResults.shift(); + } + } }); + + // Rebuild the index + scope.hostResultsMap = {}; + scope.hostResults.forEach(function(result, idx) { + scope.hostResultsMap[result.id] = idx; + }); + if (callback) { scope.$emit(callback); } - SelectHost({ scope: scope }); + scope.$emit('FixHostResultsScroll'); }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -793,22 +833,11 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge if (callback) { scope.$emit(callback); } - SelectHost({ scope: scope }); + $('#hosts-table-detail').mCustomScrollbar("update"); } }; }]) -.factory('SelectHost', [ function() { - return function(params) { - var scope = params.scope; - $('#hosts-table-detail').mCustomScrollbar("update"); - setTimeout( function() { - scope.auto_scroll = true; - $('#hosts-table-detail').mCustomScrollbar("scrollTo", "bottom"); - }, 700); - }; -}]) - // Refresh the list of hosts in the hosts summary section .factory('ReloadHostSummaryList', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) { return function(params) { @@ -864,11 +893,24 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge .factory('LoadHostSummary', [ function() { return function(params) { var scope = params.scope, - data = params.data; - scope.host_summary.ok = Object.keys(data.ok).length; - scope.host_summary.changed = Object.keys(data.changed).length; - scope.host_summary.unreachable = Object.keys(data.dark).length; - scope.host_summary.failed = Object.keys(data.failures).length; + data = params.data, + host; + scope.host_summary.ok = 0; + for (host in data.ok) { + scope.host_summary.ok += data.ok[host]; + } + scope.host_summary.changed = 0; + for (host in data.changed) { + scope.host_summary.changed += data.changed[host]; + } + scope.host_summary.unreachable = 0; + for (host in data.dark) { + scope.host_summary.dark += data.dark[host]; + } + scope.host_summary.failed = 0; + for (host in data.failures) { + scope.host_summary.failed += data.failures[host]; + } scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + scope.host_summary.unreachable + scope.host_summary.failed; }; diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html index a70e2d4152..40525d2e36 100644 --- a/awx/ui/static/partials/job_detail.html +++ b/awx/ui/static/partials/job_detail.html @@ -3,12 +3,10 @@
    From b4369d1259fa8607d45db9e4cba344e63c616f8d Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 18 Jun 2014 23:33:12 -0400 Subject: [PATCH 22/32] Job detail page refactor Making sure we never call the endless scroll queries when programmatically scrolling to the bottom of a list. Eliminate repeated calls to lookup credential names. --- awx/ui/static/js/controllers/JobDetail.js | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 44770df805..7586660d43 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -347,7 +347,9 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } scope.setSearchAll('host'); scope.$emit('LoadJobDetails', data.related.job_events); - scope.$emit('GetCredentialNames', data); + if (!scope.credential_name) { + scope.$emit('GetCredentialNames', data); + } }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -375,10 +377,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.removeFixPlaysScroll(); } scope.removeFixPlaysScroll = scope.$on('FixPlaysScroll', function() { + scope.auto_scroll = true; $('#plays-table-detail').mCustomScrollbar("update"); setTimeout( function() { - scope.auto_scroll = true; - $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); + $('#plays-table-detail').mCustomScrollbar("scrollTo", "bottom"); }, 500); }); @@ -386,9 +388,9 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.removeFixTasksScroll(); } scope.removeFixTasksScroll = scope.$on('FixTasksScroll', function() { + scope.auto_scroll = true; $('#tasks-table-detail').mCustomScrollbar("update"); setTimeout( function() { - scope.auto_scroll = true; $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); }, 500); }); @@ -398,9 +400,9 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.removeFixHostResultsScroll(); } scope.removeFixHostResultsScroll = scope.$on('FixHostResultsScroll', function() { + scope.auto_scroll = true; $('#hosts-table-detail').mCustomScrollbar("update"); setTimeout( function() { - scope.auto_scroll = true; $('#hosts-table-detail').mCustomScrollbar("scrollTo", "bottom"); }, 500); }); @@ -564,7 +566,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls down (or forward in time) var url, mcs = arguments[0]; scope.$apply(function() { - if (!scope.auto_scroll && scope.activeTask && scope.hostResults.length) { + if ((!scope.auto_scroll) && scope.activeTask && scope.hostResults.length) { scope.auto_scroll = true; url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; @@ -611,7 +613,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls up (or back in time) var url, mcs = arguments[0]; scope.$apply(function() { - if (!scope.auto_scroll && scope.activeTask && scope.hostResults.length) { + if ((!scope.auto_scroll) && scope.activeTask && scope.hostResults.length) { scope.auto_scroll = true; url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; @@ -658,7 +660,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls down (or forward in time) var url, mcs = arguments[0]; scope.$apply(function() { - if (!scope.auto_scroll && scope.activePlay && scope.tasks.length) { + if ((!scope.auto_scroll) && scope.activePlay && scope.tasks.length) { scope.auto_scroll = true; url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; @@ -676,7 +678,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } else { // no next event (task), get the end time of the play - end = scope.plays[scope.activePlay].finished; + end = scope.plays[scope.playsMap[scope.activePlay]].finished; } if (end) { elapsed = GetElapsed({ @@ -736,7 +738,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls up (or back in time) var url, mcs = arguments[0]; scope.$apply(function() { - if (!scope.auto_scroll && scope.activePlay && scope.tasks.length) { + if ((!scope.auto_scroll) && scope.activePlay && scope.tasks.length) { scope.auto_scroll = true; url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; @@ -754,7 +756,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } else { // no next event (task), get the end time of the play - end = scope.plays[scope.activePlay].finished; + end = scope.plays[scope.playsMap[scope.activePlay]].finished; } if (end) { elapsed = GetElapsed({ @@ -812,7 +814,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.HostSummaryOnTotalScroll = function(mcs) { var url; - if (!scope.auto_scroll && scope.hosts) { + if ((!scope.auto_scroll) && scope.hosts) { url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; @@ -856,7 +858,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.HostSummaryOnTotalScrollBack = function(mcs) { var url; - if (!scope.auto_scroll && scope.hosts) { + if ((!scope.auto_scroll) && scope.hosts) { url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; From 5a461a5471fa0b8d4bb7b63bdeeeea1281b67d9b Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 19 Jun 2014 00:17:05 -0400 Subject: [PATCH 23/32] Job detail page refactor Endless scrolling queries appear to no longer be firing when programmatically scrolling to the end of list. --- awx/ui/static/js/controllers/JobDetail.js | 37 ++++++++++++----------- awx/ui/static/js/helpers/JobDetail.js | 1 + 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 7586660d43..ea166a2dad 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -377,9 +377,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.removeFixPlaysScroll(); } scope.removeFixPlaysScroll = scope.$on('FixPlaysScroll', function() { - scope.auto_scroll = true; + scope.auto_scroll_plays = true; $('#plays-table-detail').mCustomScrollbar("update"); setTimeout( function() { + scope.auto_scroll_plays = true; $('#plays-table-detail').mCustomScrollbar("scrollTo", "bottom"); }, 500); }); @@ -388,26 +389,26 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.removeFixTasksScroll(); } scope.removeFixTasksScroll = scope.$on('FixTasksScroll', function() { - scope.auto_scroll = true; + scope.auto_scroll_tasks = true; $('#tasks-table-detail').mCustomScrollbar("update"); setTimeout( function() { + scope.auto_scroll_tasks = true; $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); }, 500); }); - if (scope.removeFixHostResultsScroll) { scope.removeFixHostResultsScroll(); } scope.removeFixHostResultsScroll = scope.$on('FixHostResultsScroll', function() { - scope.auto_scroll = true; + scope.auto_scroll_results = true; $('#hosts-table-detail').mCustomScrollbar("update"); setTimeout( function() { + scope.auto_scroll_results = true; $('#hosts-table-detail').mCustomScrollbar("scrollTo", "bottom"); }, 500); }); - scope.adjustSize = function() { var height, ww = $(window).width(); if (ww < 1240) { @@ -481,6 +482,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }; scope.selectPlay = function(id) { + scope.auto_scroll_plays = false; SelectPlay({ scope: scope, id: id @@ -488,6 +490,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }; scope.selectTask = function(id) { + scope.auto_scroll_tasks = false; SelectTask({ scope: scope, id: id @@ -566,7 +569,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls down (or forward in time) var url, mcs = arguments[0]; scope.$apply(function() { - if ((!scope.auto_scroll) && scope.activeTask && scope.hostResults.length) { + if ((!scope.auto_scroll_results) && scope.activeTask && scope.hostResults.length) { scope.auto_scroll = true; url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; @@ -604,7 +607,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); } else { - scope.auto_scroll = false; + scope.auto_scroll_results = false; } }); }, 300); @@ -613,7 +616,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls up (or back in time) var url, mcs = arguments[0]; scope.$apply(function() { - if ((!scope.auto_scroll) && scope.activeTask && scope.hostResults.length) { + if ((!scope.auto_scroll_results) && scope.activeTask && scope.hostResults.length) { scope.auto_scroll = true; url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; @@ -651,7 +654,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); } else { - scope.auto_scroll = false; + scope.auto_scroll_results = false; } }); }, 300); @@ -660,7 +663,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls down (or forward in time) var url, mcs = arguments[0]; scope.$apply(function() { - if ((!scope.auto_scroll) && scope.activePlay && scope.tasks.length) { + if ((!scope.auto_scroll_tasks) && scope.activePlay && scope.tasks.length) { scope.auto_scroll = true; url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; @@ -729,7 +732,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); } else { - scope.auto_scroll = false; + scope.auto_scroll_tasks = false; } }); }, 300); @@ -738,7 +741,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, // Called when user scrolls up (or back in time) var url, mcs = arguments[0]; scope.$apply(function() { - if ((!scope.auto_scroll) && scope.activePlay && scope.tasks.length) { + if ((!scope.auto_scroll_tasks) && scope.activePlay && scope.tasks.length) { scope.auto_scroll = true; url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; @@ -807,14 +810,14 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); } else { - scope.auto_scroll = false; + scope.auto_scroll_tasks = false; } }); }, 300); scope.HostSummaryOnTotalScroll = function(mcs) { var url; - if ((!scope.auto_scroll) && scope.hosts) { + if ((!scope.auto_scroll_summary) && scope.hosts) { url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; @@ -852,13 +855,13 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); } else { - scope.auto_scroll = false; + scope.auto_scroll_summary = false; } }; scope.HostSummaryOnTotalScrollBack = function(mcs) { var url; - if ((!scope.auto_scroll) && scope.hosts) { + if ((!scope.auto_scroll_summary) && scope.hosts) { url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; @@ -896,7 +899,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, }); } else { - scope.auto_scroll = false; + scope.auto_scroll_summary = false; } }; diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 9af481396e..adc6838566 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -516,6 +516,7 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge scope.hostResults.forEach(function(result, idx) { scope.hostResultsMap[result.id] = idx; }); + scope.$emit('FixHostResultsScroll'); } // update the task status bar From 4cb7e2301dcc59e4291d3a8cc90c3ee5413a2d29 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 19 Jun 2014 00:32:39 -0400 Subject: [PATCH 24/32] Job detail page refactor Fixing scrolling issue on tasks list. --- awx/ui/static/js/controllers/JobDetail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index ea166a2dad..58ff6b9307 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -746,7 +746,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; url += (scope.search_all_tasks.length > 0) ? '&id__in=' + scope.search_all_tasks.join() : ''; url += (scope.searchAllStatus === 'failed') ? '&failed=true' : ''; - url += '&id__lt=' + scope.tasks[scope.tasks[0]].id + '&page_size=' + scope.tasksMaxRows + '&order_by=id'; + url += '&id__lt=' + scope.tasks[0].id + '&page_size=' + scope.tasksMaxRows + '&order_by=-id'; Wait('start'); Rest.setUrl(url); Rest.get() From cb44949a1d9490da65166c49e11db7aa6a92ad0d Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 19 Jun 2014 01:09:24 -0400 Subject: [PATCH 25/32] Job detail page refactor Experimenting with _.throttle to improve event queue processing. --- awx/ui/static/js/controllers/JobDetail.js | 57 +++++++++++++++-------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 58ff6b9307..f583eb1696 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -9,7 +9,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, ProcessErrors, ProcessEventQueue, SelectPlay, SelectTask, Socket, GetElapsed, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, - JobIsFinished, SetTaskStyles) { + JobIsFinished, SetTaskStyles, DigestEvent) { ClearScope(); @@ -19,7 +19,8 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, api_complete = false, refresh_count = 0, lastEventId = 0, - queue = []; + queue = [], + processEvent; scope.plays = []; scope.playsMap = {}; @@ -66,24 +67,40 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, event_socket.init(); + + processEvent = _.throttle(function() { + var event; + if (queue.length > 0) { + event = queue.pop(); + $log.debug('processing event: ' + event.id); + DigestEvent({ + scope: scope, + event: event + }); + } + }, 200); + event_socket.on("job_events-" + job_id, function(data) { data.event = data.event_name; if (api_complete && data.id > lastEventId) { $log.debug('received event: ' + data.id); - if (queue.length < 20) { - queue.unshift(data); - } - else { - api_complete = false; // stop more events from hitting the queue - $log.debug('queue halted. reloading in 1.'); - setTimeout(function() { - $log.debug('reloading'); - scope.haltEventQueue = true; - queue = []; - scope.$emit('LoadJob'); - }, 1000); - } + queue.unshift(data); + processEvent(); } + //if (queue.length < 20) { + // queue.unshift(data); + //} + //*else { + // api_complete = false; // stop more events from hitting the queue + // $log.debug('queue halted. reloading in 1.'); + // setTimeout(function() { + // $log.debug('reloading'); + // scope.haltEventQueue = true; + // queue = []; + // scope.$emit('LoadJob'); + // }, 1000); + // } + //} }); if ($rootScope.removeJobStatusChange) { @@ -150,10 +167,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, DrawGraph({ scope: scope, resize: true }); } - ProcessEventQueue({ - scope: scope, - eventQueue: queue - }); + //ProcessEventQueue({ + // scope: scope, + // eventQueue: queue + //}); }); @@ -967,5 +984,5 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'ProcessEventQueue', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'FilterAllByHostName', 'DrawGraph', - 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles' + 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent' ]; From 3a6fe9ee4ae746fd351f6570b605e335e0928a1c Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 19 Jun 2014 02:21:13 -0400 Subject: [PATCH 26/32] Job detail page refactor Reverting attempt to use _.throttle and _.defer. They assume we actually want to process every event. We need to stop all processing if the job has finished. --- awx/ui/static/js/controllers/JobDetail.js | 61 +++++++++-------------- awx/ui/static/js/helpers/JobDetail.js | 27 ++++++---- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index f583eb1696..e8d5ee65ce 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -9,7 +9,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, ProcessErrors, ProcessEventQueue, SelectPlay, SelectTask, Socket, GetElapsed, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, - JobIsFinished, SetTaskStyles, DigestEvent) { + JobIsFinished, SetTaskStyles) { ClearScope(); @@ -19,8 +19,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, api_complete = false, refresh_count = 0, lastEventId = 0, - queue = [], - processEvent; + queue = []; scope.plays = []; scope.playsMap = {}; @@ -67,42 +66,27 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, event_socket.init(); - - processEvent = _.throttle(function() { - var event; - if (queue.length > 0) { - event = queue.pop(); - $log.debug('processing event: ' + event.id); - DigestEvent({ - scope: scope, - event: event - }); - } - }, 200); - event_socket.on("job_events-" + job_id, function(data) { data.event = data.event_name; if (api_complete && data.id > lastEventId) { - $log.debug('received event: ' + data.id); - queue.unshift(data); - processEvent(); + if (queue.length < 20) { + queue.unshift(data); + } + else { + api_complete = false; // stop more events from hitting the queue + window.clearInterval($rootScope.jobDetailInterval); + $log.debug('queue halted. reloading...'); + setTimeout(function() { + $log.debug('reload'); + scope.haltEventQueue = true; + queue = []; + scope.$emit('LoadJob'); + }, 300); + } } - //if (queue.length < 20) { - // queue.unshift(data); - //} - //*else { - // api_complete = false; // stop more events from hitting the queue - // $log.debug('queue halted. reloading in 1.'); - // setTimeout(function() { - // $log.debug('reloading'); - // scope.haltEventQueue = true; - // queue = []; - // scope.$emit('LoadJob'); - // }, 1000); - // } - //} }); + if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); } @@ -121,6 +105,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, } }); + if (scope.removeAPIComplete) { scope.removeAPIComplete(); } @@ -167,10 +152,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, DrawGraph({ scope: scope, resize: true }); } - //ProcessEventQueue({ - // scope: scope, - // eventQueue: queue - //}); + ProcessEventQueue({ + scope: scope, + eventQueue: queue + }); }); @@ -984,5 +969,5 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'ProcessEventQueue', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'FilterAllByHostName', 'DrawGraph', - 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent' + 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles' ]; diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index adc6838566..3ea3c3358d 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -39,24 +39,31 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices']) -.factory('ProcessEventQueue', ['$log', 'DigestEvent', 'JobIsFinished', function ($log, DigestEvent, JobIsFinished) { +.factory('ProcessEventQueue', ['$log', '$rootScope', 'DigestEvent', 'JobIsFinished', function ($log, $rootScope, DigestEvent, JobIsFinished) { return function(params) { var scope = params.scope, eventQueue = params.eventQueue, - event; + processing = false; function runTheQ() { - while (eventQueue.length > 0) { + var event; + processing = true; + while (!JobIsFinished(scope) && !scope.haltEventQueue && eventQueue.length > 0) { event = eventQueue.pop(); - $log.debug('read event: ' + event.id); + $log.debug('processing event: ' + event.id); DigestEvent({ scope: scope, event: event }); } - if (!JobIsFinished(scope) && !scope.haltEventQueue) { - setTimeout( function() { - runTheQ(); - }, 300); - } + processing = false; + //if (!JobIsFinished(scope) && !scope.haltEventQueue) { + // setTimeout( function() { + // runTheQ(); + // }, 1000); + //} } - runTheQ(); + $rootScope.jobDetailInterval = window.setInterval(function() { + if (!processing && eventQueue.length > 0) { + runTheQ(); + } + }, 1000); }; }]) From 69add3d62629265ec2529c32960bd50143f2a5da Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 19 Jun 2014 03:34:07 -0400 Subject: [PATCH 27/32] Job detail page refactor Latest iteration on event queue processing. Replaced setTimeout with an interval. --- awx/ui/static/js/app.js | 4 ++++ awx/ui/static/js/controllers/JobDetail.js | 28 +++++++++++++---------- awx/ui/static/js/helpers/JobDetail.js | 16 +++++-------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 6e49defdcf..dff0feb89d 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -470,6 +470,10 @@ angular.module('Tower', [ HideStream(); } + if ($rootScope.jobDetailInterval) { + window.clearInterval($rootScope.jobDetailInterval); + } + // On each navigation request, check that the user is logged in if (!/^\/(login|logout)/.test($location.path())) { // capture most recent URL, excluding login/logout diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index e8d5ee65ce..ab4db9edde 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -45,6 +45,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, scope.searchSummaryHostsEnabled = true; scope.searchAllHostsEnabled = true; scope.haltEventQueue = false; + scope.processing = false; scope.host_summary = {}; scope.host_summary.ok = 0; @@ -69,13 +70,14 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, event_socket.on("job_events-" + job_id, function(data) { data.event = data.event_name; if (api_complete && data.id > lastEventId) { - if (queue.length < 20) { + if (queue.length < 25) { + $log.debug('received event: ' + data.id); queue.unshift(data); } else { api_complete = false; // stop more events from hitting the queue window.clearInterval($rootScope.jobDetailInterval); - $log.debug('queue halted. reloading...'); + $log.debug('halting queue. reloading...'); setTimeout(function() { $log.debug('reload'); scope.haltEventQueue = true; @@ -99,6 +101,7 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, $log.debug('Job completed!'); api_complete = false; scope.haltEventQueue = true; + window.clearInterval($rootScope.jobDetailInterval); queue = []; scope.$emit('LoadJob'); } @@ -124,7 +127,6 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, playId = (scope.plays.length > 0) ? scope.plays[scope.plays.length - 1].id : 0; lastEventId = Math.max(hostId, taskId, playId); - api_complete = true; Wait('stop'); // Draw the graph @@ -147,16 +149,18 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, msg: 'Call to ' + url + '. GET returned: ' + status }); }); } - else if (scope.host_summary.total > 0) { - // Draw the graph based on summary values in memory - DrawGraph({ scope: scope, resize: true }); + else { + if (scope.host_summary.total > 0) { + // Draw the graph based on summary values in memory + DrawGraph({ scope: scope, resize: true }); + } + api_complete = true; + scope.haltEventQueue = false; + ProcessEventQueue({ + scope: scope, + eventQueue: queue + }); } - - ProcessEventQueue({ - scope: scope, - eventQueue: queue - }); - }); if (scope.removeInitialDataLoaded) { diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 3ea3c3358d..8cca5f77d4 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -42,25 +42,21 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices']) .factory('ProcessEventQueue', ['$log', '$rootScope', 'DigestEvent', 'JobIsFinished', function ($log, $rootScope, DigestEvent, JobIsFinished) { return function(params) { var scope = params.scope, - eventQueue = params.eventQueue, - processing = false; + eventQueue = params.eventQueue; function runTheQ() { var event; - processing = true; + scope.processing = true; while (!JobIsFinished(scope) && !scope.haltEventQueue && eventQueue.length > 0) { event = eventQueue.pop(); $log.debug('processing event: ' + event.id); DigestEvent({ scope: scope, event: event }); } - processing = false; - //if (!JobIsFinished(scope) && !scope.haltEventQueue) { - // setTimeout( function() { - // runTheQ(); - // }, 1000); - //} + $log.debug('processing halted'); + scope.processing = false; } $rootScope.jobDetailInterval = window.setInterval(function() { - if (!processing && eventQueue.length > 0) { + $log.debug('checking... processing: ' + scope.processing + ' queue.length: ' + eventQueue.length); + if (!scope.processing && eventQueue.length > 0) { runTheQ(); } }, 1000); From 09ef25a8adb7619f256661237279ddc0792d7152 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Thu, 19 Jun 2014 08:01:57 -0500 Subject: [PATCH 28/32] Re-fixed an error from a merge conflict. --- awx/api/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 2bb2f3165b..b876d76dad 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1602,19 +1602,19 @@ class JobJobTasksList(BaseJobEventsList): task_data['host_count'] += 1 task_data['reported_hosts'] += 1 task_data['failed_count'] += 1 - elif child_event.event == 'runner_on_ok': + elif child_data['event'] == 'runner_on_ok': task_data['host_count'] += 1 task_data['reported_hosts'] += 1 - if child_event.changed: + if child_data['changed']: task_data['changed_count'] += 1 task_data['changed'] = True else: task_data['successful_count'] += 1 - elif child_event.event == 'runner_on_skipped': + elif child_data['event'] == 'runner_on_skipped': task_data['host_count'] += 1 task_data['reported_hosts'] += 1 task_data['skipped_count'] += 1 - elif child_event.event == 'runner_on_error': + elif child_data['event'] == 'runner_on_error': task_data['host_count'] += 1 task_data['reported_hosts'] += 1 task_data['failed'] = True From a2a46d22bcdd4155a43c8290d5e2b350bf4f7301 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 11 Jun 2014 15:56:32 -0400 Subject: [PATCH 29/32] Add play and tasks linkage to job related fields --- awx/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cfa4372e3c..8feb128bb3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1241,6 +1241,8 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): res = super(JobSerializer, self).get_related(obj) res.update(dict( job_events = reverse('api:job_job_events_list', args=(obj.pk,)), + job_plays = reverse('api:job_job_plays_list', args=(obj.pk,)), + job_tasks = reverse('api:job_job_tasks_list', args=(obj.pk,)), job_host_summaries = reverse('api:job_job_host_summaries_list', args=(obj.pk,)), activity_stream = reverse('api:job_activity_stream_list', args=(obj.pk,)), )) From 23b617b580600b966e50bc2c9b1c8ee37ed179f0 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Thu, 19 Jun 2014 13:16:35 -0500 Subject: [PATCH 30/32] Adding a job_tasks test. --- awx/api/tests/job_tasks.py | 56 ++++++++++++++++++++++++++++++++++++++ awx/api/views.py | 3 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 awx/api/tests/job_tasks.py diff --git a/awx/api/tests/job_tasks.py b/awx/api/tests/job_tasks.py new file mode 100644 index 0000000000..17313e1cb2 --- /dev/null +++ b/awx/api/tests/job_tasks.py @@ -0,0 +1,56 @@ +# Copyright (c) 2014 Ansible, Inc. +# All Rights Reserved. + +from datetime import datetime + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import LiveServerTestCase +from django.test.utils import override_settings + +from rest_framework.test import APIClient + +import mock + +from awx.api.views import JobJobTasksList +from awx.main.models import Job, JobTemplate, JobEvent +from awx.main.tests.jobs import BaseJobTestMixin, MIDDLEWARE_CLASSES + + +@override_settings(CELERY_ALWAYS_EAGER=True, + CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, + CALLBACK_CONSUMER_PORT='', + ANSIBLE_TRANSPORT='local', + MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES) +class JobTasksTests(BaseJobTestMixin, LiveServerTestCase): + """A set of tests to ensure that the job_tasks endpoint, available at + `/api/v1/jobs/{id}/job_tasks/`, works as expected. + """ + def setUp(self): + super(JobTasksTests, self).setUp() + settings.INTERNAL_API_URL = self.live_server_url + + def test_tasks_endpoint(self): + """Establish that the `job_tasks` endpoint shows what we expect, + which is a rollup of information about each of the corresponding + job events. + """ + # Create a job + job = self.make_job(self.jt_ops_east_run, self.user_sue, 'new') + job.signal_start() + + # Get the initial job event. + event = job.job_events.get(event='playbook_on_play_start') + + # Actually make the request for the job tasks. + with self.current_user(self.user_sue): + url = '/api/v1/jobs/%d/job_tasks/?event_id=%d' % (job.id, event.id) + response = self.get(url) + + # Test to make sure we got back what we expected. + result = response['results'][0] + self.assertEqual(result['host_count'], 1) + self.assertEqual(result['changed_count'], 1) + self.assertFalse(result['failed']) + self.assertTrue(result['changed']) diff --git a/awx/api/views.py b/awx/api/views.py index b876d76dad..4bbf5bcad4 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1572,7 +1572,8 @@ class JobJobTasksList(BaseJobEventsList): data.setdefault(parent_id, []) data[parent_id].append(line) - # Iterate over the start events and compile information about each one. + # Iterate over the start events and compile information about each one + # using their children. qs = parent_task.children.filter(event__in=STARTING_EVENTS, id__in=data.keys()) for task_start_event in qs: From c8e5892d54f3a10a99bbc7d076927e52e05f999e Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Thu, 19 Jun 2014 08:01:57 -0500 Subject: [PATCH 31/32] Re-fixed an error from a merge conflict. --- awx/api/views.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 4bbf5bcad4..be0f93aec9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1600,24 +1600,24 @@ class JobJobTasksList(BaseJobEventsList): for child_data in data.get(task_start_event.id, []): if child_data['event'] == 'runner_on_failed': task_data['failed'] = True - task_data['host_count'] += 1 - task_data['reported_hosts'] += 1 - task_data['failed_count'] += 1 + task_data['host_count'] += child_data['num'] + task_data['reported_hosts'] += child_data['num'] + task_data['failed_count'] += child_data['num'] elif child_data['event'] == 'runner_on_ok': - task_data['host_count'] += 1 - task_data['reported_hosts'] += 1 + task_data['host_count'] += child_data['num'] + task_data['reported_hosts'] += child_data['num'] if child_data['changed']: - task_data['changed_count'] += 1 + task_data['changed_count'] += child_data['num'] task_data['changed'] = True else: - task_data['successful_count'] += 1 + task_data['successful_count'] += child_data['num'] elif child_data['event'] == 'runner_on_skipped': - task_data['host_count'] += 1 - task_data['reported_hosts'] += 1 - task_data['skipped_count'] += 1 + task_data['host_count'] += child_data['num'] + task_data['reported_hosts'] += child_data['num'] + task_data['skipped_count'] += child_data['num'] elif child_data['event'] == 'runner_on_error': - task_data['host_count'] += 1 - task_data['reported_hosts'] += 1 + task_data['host_count'] += child_data['num'] + task_data['reported_hosts'] += child_data['num'] task_data['failed'] = True task_data['failed_count'] += child_data['num'] elif child_data['event'] == 'runner_on_no_hosts': From fbb408f8171845dc1dcfc66c8711d70714b9ddfd Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Thu, 19 Jun 2014 14:53:02 -0500 Subject: [PATCH 32/32] Rename tools/vagrant to tools/dev_setup Because these playbooks (should) work without Vagrant! --- Vagrantfile | 4 ++-- ansible.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index eecb9d743a..4710edbf2e 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -34,8 +34,8 @@ Vagrant.configure('2') do |config| 'vagrant' => true, 'vagrant_host_user' => ENV['USER'], } - ansible.inventory_path = 'tools/vagrant/inventory' - ansible.playbook = 'tools/vagrant/playbook.yml' + ansible.inventory_path = 'tools/dev_setup/inventory' + ansible.playbook = 'tools/dev_setup/playbook.yml' ansible.verbose = 'v' end end diff --git a/ansible.cfg b/ansible.cfg index 32ffcc5216..f84e3cc638 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -11,5 +11,5 @@ module_name = shell # Paths -roles_path = setup/roles:tools/vagrant/roles +roles_path = setup/roles:tools/dev_setup/roles