diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 732a6625d6..3fd08a0b21 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -94,7 +94,10 @@ angular.module('ansible', [ 'Timezones', 'SchedulesHelper', 'QueuedJobsDefinition', - 'JobsListDefinition' + 'JobsListDefinition', + 'LogViewerStatusDefinition', + 'LogViewerHelper', + 'LogViewerOptionsDefinition' ]) .constant('AngularScheduler.partials', $basePath + 'lib/angular-scheduler/lib/') diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index 919b7a5b07..d2cf9c9f18 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -12,7 +12,7 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, ProjectList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, - ProjectStatus, FormatDate, Refresh, Wait, Stream, GetChoices, Empty, Find) { + ProjectStatus, FormatDate, Refresh, Wait, Stream, GetChoices, Empty, Find, LogViewer) { ClearScope(); @@ -72,7 +72,7 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, break; } - if (project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { $scope.projects[i].statusTip = 'Canceled. Click for details'; } @@ -206,10 +206,15 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, }); } else if (project.related.last_update) { Wait('start'); + LogViewer({ + scope: $scope, + url: project.related.last_update + }); + /* ProjectStatus({ project_id: id, last_update: project.related.last_update - }); + });*/ } else { Alert('No Updates Available', 'There is no SCM update information available for this project. An update has not yet been ' + ' completed. If you have not already done so, start an update for this project.', 'alert-info'); @@ -354,7 +359,8 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', - 'SelectionInit', 'ProjectUpdate', 'ProjectStatus', 'FormatDate', 'Refresh', 'Wait', 'Stream', 'GetChoices', 'Empty', 'Find' + 'SelectionInit', 'ProjectUpdate', 'ProjectStatus', 'FormatDate', 'Refresh', 'Wait', 'Stream', 'GetChoices', 'Empty', 'Find', + 'LogViewer' ]; diff --git a/awx/ui/static/js/forms/LogViewerOptions.js b/awx/ui/static/js/forms/LogViewerOptions.js new file mode 100644 index 0000000000..d23b365977 --- /dev/null +++ b/awx/ui/static/js/forms/LogViewerOptions.js @@ -0,0 +1,102 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * LogViewerOptions.js + * + * Form definition for LogViewer.js helper + * + */ +angular.module('LogViewerOptionsDefinition', []) + .value('LogViewerOptionsForm', { + + name: 'status', + well: false, + + fields: { + "job_args": { + label: "Arguments", + type: "text", + readonly: true + }, + "job_cwd": { + label: "CWD", + type: "text", + readonly: true + }, + "job_type": { + label: "Job Type", + type: "text", + readonly: true + }, + "inventory": { + label: "Inventory", + type: "text", + readonly: true + }, + "project": { + label: "Project", + type: "text", + readonly: true + }, + "playbook": { + label: "Playbook", + type: "text", + readonly: true + }, + "credential": { + label: "Credential", + type: "text", + readonly: true + }, + "cloud credential": { + label: "Cloud Cred.", + type: "text", + readonly: true + }, + "forks": { + label: "Forks", + type: "text", + readonly: true + }, + "limit": { + label: "Limit", + type: "text", + readonly: true + }, + "verbosity": { + label: "Verbosity", + type: "text", + readonly: true + }, + "job_tags": { + label: "Job Tags", + type: "text", + readonly: true + }, + "source": { + label: "Source", + type: "text", + readonly: true + }, + "source_path": { + label: "Source Path", + type: "text", + readonly: true + }, + "source_regions":{ + label: "Regions", + type: "text", + readonly: true + }, + "overwrite": { + label: "Overwrite Vars", + type: "text", + readonly: true + }, + "inventory_source": { + label: "Inv Source", + type: "text", + readonly: true + } + } + }); \ No newline at end of file diff --git a/awx/ui/static/js/forms/LogViewerStatus.js b/awx/ui/static/js/forms/LogViewerStatus.js new file mode 100644 index 0000000000..7b634998fc --- /dev/null +++ b/awx/ui/static/js/forms/LogViewerStatus.js @@ -0,0 +1,68 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * LogViewerStatus.js + * + * Form definition for LogViewer.js helper + * + */ +angular.module('LogViewerStatusDefinition', []) + .value('LogViewerStatusForm', { + + name: 'status', + well: false, + + fields: { + "name": { + label: "Name", + type: "text", + readonly: true, + }, + "created": { + label: "Created", + type: "text", + readonly: true + }, + "modified": { + label: "Modified", + type: "text", + readonly: true + }, + "unified_job_template": { + label: "Job Template", + type: "text", + readonly: true + }, + "launch_type": { + label: "Launch Type", + type: "text", + readonly: true + }, + "status": { + label: "Status", + type: "text", + readonly: true + }, + "license_error": { + label: "License Error", + type: "text", + readonly: true + }, + "started": { + label: "Started", + type: "text", + readonly: true + }, + "finished": { + label: "Finished", + type: "text", + readonly: true + }, + "elapsed": { + label: "Elapsed", + type: "text", + readonly: true + } + } + + }); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index ddb4f31b66..a57ad173b4 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -12,7 +12,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'GroupListDefinition', 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'AuthService', 'GroupsHelper', 'InventoryHelper', 'SelectionHelper', 'JobSubmissionHelper', 'RefreshHelper', 'PromptDialog', 'CredentialsListDefinition', 'InventoryTree', - 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'SourceFormDefinition']) + 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'SourceFormDefinition', 'LogViewerHelper']) .factory('GetSourceTypeOptions', ['Rest', 'ProcessErrors', 'GetBasePath', function (Rest, ProcessErrors, GetBasePath) { @@ -48,15 +48,24 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G ]) -.factory('ViewUpdateStatus', ['Rest', 'ProcessErrors', 'GetBasePath', 'ShowUpdateStatus', 'Alert', 'Wait', 'Empty', 'Find', - function (Rest, ProcessErrors, GetBasePath, ShowUpdateStatus, Alert, Wait, Empty, Find) { +.factory('ViewUpdateStatus', ['Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find', 'LogViewer', + function (Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find, LogViewer) { return function (params) { var scope = params.scope, tree_id = params.tree_id, - group_id = params.group_id, group = Find({ list: scope.groups, key: 'id', val: tree_id }); + if (scope.removeSourceReady) { + scope.removeSourceReady(); + } + scope.removeSourceReady = scope.$on('SourceReady', function(e, url) { + LogViewer({ + scope: scope, + url: url + }); + }); + if (group) { if (Empty(group.source)) { Alert('Missing Configuration', 'The selected group is not configured for inventory sync. ' + @@ -70,22 +79,12 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G Rest.get() .success(function (data) { var url = (data.related.current_update) ? data.related.current_update : data.related.last_update; - ShowUpdateStatus({ - group_name: data.summary_fields.group.name, - last_update: url, - license_error: ((data.summary_fields.last_update && data.summary_fields.last_update.license_error) ? true : false), - tree_id: tree_id, - group_id: group_id, - parent_scope: scope - }); + scope.$emit('SourceReady', url); }) .error(function (data, status) { - Wait('stop'); - ProcessErrors(scope, data, status, null, { - hdr: 'Error!', + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + - ' POST returned status: ' + status - }); + ' POST returned status: ' + status }); }); } } diff --git a/awx/ui/static/js/helpers/LogViewer.js b/awx/ui/static/js/helpers/LogViewer.js new file mode 100644 index 0000000000..817234c485 --- /dev/null +++ b/awx/ui/static/js/helpers/LogViewer.js @@ -0,0 +1,237 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * LogViewer.js + * + */ + +'use strict'; + +angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) + + .factory('LogViewer', ['$compile', 'CreateDialog', 'GetJob', 'Wait', 'GenerateForm', 'LogViewerStatusForm', 'AddTable', 'AddTextarea', + 'LogViewerOptionsForm', 'EnvTable', 'GetBasePath', 'LookUpName', 'Empty', + function($compile, CreateDialog, GetJob, Wait, GenerateForm, LogViewerStatusForm, AddTable, AddTextarea, LogViewerOptionsForm, EnvTable, + GetBasePath, LookUpName, Empty) { + return function(params) { + var parent_scope = params.scope, + url = params.url, + scope = parent_scope.$new(); + + if (scope.removeModalReady) { + scope.removeModalReady(); + } + scope.removeModalReady = scope.$on('ModalReady', function() { + Wait('stop'); + $('#logviewer-modal-dialog').dialog('open'); + }); + + if (scope.removeJobReady) { + scope.removeJobReady(); + } + scope.removeJobReady = scope.$on('JobReady', function(e, data) { + var key, resizeText, elem; + + for (key in data) { + scope[key] = data[key]; + } + + AddTable({ scope: scope, form: LogViewerStatusForm, id: 'status-form-container' }); + AddTable({ scope: scope, form: LogViewerOptionsForm, id: 'options-form-container' }); + + if (data.result_stdout) { + AddTextarea({ + container_id: 'stdout-form-container', + val: data.result_stdout, + fld_id: 'stdout-textarea' + }); + } + else { + $('#logview-tabs li:eq(2)').hide(); + } + + if (data.result_traceback) { + AddTextarea({ + container_id: 'traceback-form-container', + val: data.result_traceback, + fld_id: 'traceback-textarea' + }); + } + else { + $('#logview-tabs li:eq(2)').hide(); + } + + if (data.job_env) { + EnvTable({ + id: 'env-form-container', + vars: data.job_env + }); + } + + if (!Empty(scope.credential)) { + LookUpName({ + scope: scope, + scope_var: 'credential', + url: GetBasePath('credentials') + scope.credential + '/' + }); + } + + if (!Empty(scope.inventory)) { + LookUpName({ + scope: scope, + scope_var: 'inventory', + url: GetBasePath('inventories') + scope.inventory + '/' + }); + } + + if (!Empty(scope.project)) { + LookUpName({ + scope: scope, + scope_var: 'project', + url: GetBasePath('projects') + scope.project + '/' + }); + } + + if (!Empty(scope.cloud_credential)) { + LookUpName({ + scope: scope, + scope_var: 'cloud_credential', + url: GetBasePath('credentials') + scope.cloud_credential + '/' + }); + } + + if (!Empty(scope.inventory_source)) { + LookUpName({ + scope: scope, + scope_var: 'inventory_source', + url: GetBasePath('inventory_sources') + scope.inventory_source + '/' + }); + } + + resizeText = function() { + var u = $('#logview-tabs').outerHeight() + 25, + h = $('#logviewer-modal-dialog').innerHeight(), + rows = Math.floor((h - u) / 20); + rows -= 3; + rows = (rows < 6) ? 6 : rows; + $('#stdout-textarea').attr({ rows: rows }); + $('#traceback-textarea').attr({ rows: rows }); + }; + + elem = angular.element(document.getElementById('logviewer-modal-dialog')); + $compile(elem)(scope); + + CreateDialog({ + scope: scope, + width: 600, + height: 675, + minWidth: 450, + callback: 'ModalReady', + id: 'logviewer-modal-dialog', + onResizeStop: resizeText, + onOpen: function() { + $('#logview-tabs a:first').tab('show'); + resizeText(); + } + }); + }); + + GetJob({ + url: url, + scope: scope + }); + + scope.modalOK = function() { + $('#logviewer-modal-dialog').dialog('close'); + }; + }; + }]) + + .factory('GetJob', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) { + return function(params) { + var url = params.url, + scope = params.scope; + Rest.setUrl(url); + Rest.get() + .success(function(data){ + scope.$emit('JobReady', data); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve ' + url + '. GET returned: ' + status }); + }); + }; + }]) + + .factory('LookUpName', ['Rest', 'ProcessErrors', 'Empty', function(Rest, ProcessErrors, Empty) { + return function(params) { + var url = params.url, + scope_var = params.scope_var, + scope = params.scope; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (!Empty(data.name)) { + scope[scope_var] = data.name; + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve ' + url + '. GET returned: ' + status }); + }); + }; + }]) + + .factory('AddTable', ['Empty', function(Empty) { + return function(params) { + var form = params.form, + id = params.id, + scope = params.scope, + fld, html; + html = "\n"; + for (fld in form.fields) { + if (!Empty(scope[fld])) { + html += "" + + "\n"; + } + } + html += "
" + form.fields[fld].label + ""; + if (fld === "credential" || fld === "project" || fld === "inventory" || fld === "cloud_credential" || + fld === "inventory_source") { + html += "{{ " + fld + " }}"; + } + else { + html += scope[fld]; + } + html += "
\n"; + $('#' + id).empty().html(html); + }; + }]) + + .factory('AddTextarea', [ function() { + return function(params) { + var container_id = params.container_id, + val = params.val, + fld_id = params.fld_id, + html; + html = "
\n" + + "" + + "
\n"; + $('#' + container_id).empty().html(html); + }; + }]) + + .factory('EnvTable', [ function() { + return function(params) { + var id = params.id, + vars = params.vars, + key, html; + html = "\n"; + for (key in vars) { + html += "" + + "\n"; + } + html += "
" + key + "" + vars[key] + "
\n"; + $('#' + id).empty().html(html); + }; + }]); \ No newline at end of file diff --git a/awx/ui/static/js/lists/Schedules.js b/awx/ui/static/js/lists/Schedules.js index 328b8dc80c..2a4d6c8464 100644 --- a/awx/ui/static/js/lists/Schedules.js +++ b/awx/ui/static/js/lists/Schedules.js @@ -23,11 +23,18 @@ angular.module('SchedulesListDefinition', []) name: { key: true, label: 'Name', - ngClick: "editSchedule(schedule.id)" + ngClick: "editSchedule(schedule.id)", + columnClass: "col-md-5 col-sm-3 col-xs-3" }, dtstart: { label: 'Start', - searchable: false + searchable: false, + columnClass: "col-md-2 col-sm-3 col-xs-3" + }, + next_run: { + label: 'Next Run', + searchable: false, + columnClass: "col-md-2 col-sm-3 col-xs-3" }, dtend: { label: 'End', diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 993d848478..4dc36584c9 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -32,6 +32,7 @@ @import "jquery-ui-overrides.less"; @import "codemirror.less"; @import "angular-scheduler.less"; +@import "log-viewer.less"; html, body { height: 100%; } @@ -305,20 +306,6 @@ td.actions { border-top-color: #696969; } -.dropdown-toggle, - -/* -.dropdown-toggle:hover, -.btn-default:visited, -.btn-default:hover, -.btn-default:active - { - color: #333; - background-color: #bbb; - border-color: #bbb; -} -*/ - .btn-light { color: #333; background-color: #ddd; diff --git a/awx/ui/static/less/log-viewer.less b/awx/ui/static/less/log-viewer.less new file mode 100644 index 0000000000..ab9824baf4 --- /dev/null +++ b/awx/ui/static/less/log-viewer.less @@ -0,0 +1,23 @@ +/********************************************* + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * log-viewer.css + * + * custom styles for LogViewer.js helper + * + */ + +#logviewer-modal-dialog { + + textarea { + overflow: auto; + } +} + +table.logviewer-status { + margin-top: 20px; + + .fld-label { + font-weight: bold; + } +} \ No newline at end of file diff --git a/awx/ui/static/lib/ansible/Modal.js b/awx/ui/static/lib/ansible/Modal.js index 58e9ab147d..e36bd30c2c 100644 --- a/awx/ui/static/lib/ansible/Modal.js +++ b/awx/ui/static/lib/ansible/Modal.js @@ -163,6 +163,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper']) textareaId = params.textareaId, modalId = params.modalId, formId = params.formId, + parse = (params.parse === undefined) ? true : params.parse, textarea, formHeight, model, windowHeight, offset, rows; @@ -189,6 +190,8 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper']) rows--; textarea.attr('rows', rows); } - ParseTypeChange({ scope: scope, field_id: textareaId, onReady: waitStop }); + if (parse) { + ParseTypeChange({ scope: scope, field_id: textareaId, onReady: waitStop }); + } }; }]); \ No newline at end of file diff --git a/awx/ui/static/partials/inventory-edit.html b/awx/ui/static/partials/inventory-edit.html index 8927f6f7d2..93f7889259 100644 --- a/awx/ui/static/partials/inventory-edit.html +++ b/awx/ui/static/partials/inventory-edit.html @@ -49,6 +49,6 @@ - +
\ No newline at end of file diff --git a/awx/ui/static/partials/logviewer.html b/awx/ui/static/partials/logviewer.html new file mode 100644 index 0000000000..1620074395 --- /dev/null +++ b/awx/ui/static/partials/logviewer.html @@ -0,0 +1,27 @@ + + \ No newline at end of file diff --git a/awx/ui/static/partials/projects.html b/awx/ui/static/partials/projects.html index a034df92bc..59059d79f9 100644 --- a/awx/ui/static/partials/projects.html +++ b/awx/ui/static/partials/projects.html @@ -1,4 +1,4 @@
-
+
\ No newline at end of file diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index be087d8aed..6db7986a3a 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -98,6 +98,8 @@ + + @@ -147,6 +149,7 @@ +