1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-02 01:21:21 +03:00

Created a job viewing widget that provides a common modal dialog for viewing job output. Implemented on projects and inventory groups.

This commit is contained in:
Chris Houseknecht 2014-03-28 06:03:48 -04:00
parent 52ab418abb
commit 7237d313d2
14 changed files with 506 additions and 41 deletions

View File

@ -94,7 +94,10 @@ angular.module('ansible', [
'Timezones',
'SchedulesHelper',
'QueuedJobsDefinition',
'JobsListDefinition'
'JobsListDefinition',
'LogViewerStatusDefinition',
'LogViewerHelper',
'LogViewerOptionsDefinition'
])
.constant('AngularScheduler.partials', $basePath + 'lib/angular-scheduler/lib/')

View File

@ -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'
];

View File

@ -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
}
}
});

View File

@ -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
}
}
});

View File

@ -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 });
});
}
}

View File

@ -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 = "<table class=\"table logviewer-status\">\n";
for (fld in form.fields) {
if (!Empty(scope[fld])) {
html += "<tr><td class=\"fld-label col-md-3 col-sm-3 col-xs-3\">" + form.fields[fld].label + "</td>" +
"<td>";
if (fld === "credential" || fld === "project" || fld === "inventory" || fld === "cloud_credential" ||
fld === "inventory_source") {
html += "{{ " + fld + " }}";
}
else {
html += scope[fld];
}
html += "</td></tr>\n";
}
}
html += "</table>\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 = "<div class=\"form-group\">\n" +
"<textarea id=\"" + fld_id + "\" class=\"form-control nowrap mono-space\" rows=\"12\" readonly>" + val + "</textarea>" +
"</div>\n";
$('#' + container_id).empty().html(html);
};
}])
.factory('EnvTable', [ function() {
return function(params) {
var id = params.id,
vars = params.vars,
key, html;
html = "<table class=\"table logviewer-status\">\n";
for (key in vars) {
html += "<tr><td class=\"fld-label col-md-4 col-sm-3 col-xs-3 break\">" + key + "</td>" +
"<td class=\"break\">" + vars[key] + "</td></tr>\n";
}
html += "</table>\n";
$('#' + id).empty().html(html);
};
}]);

View File

@ -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',

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 });
}
};
}]);

View File

@ -49,6 +49,6 @@
</div>
<div id="host-modal-dialog" style="display: none;" class="dialog-content"></div>
<div ng-include="'/static/partials/logviewer.html'"></div>
</div>
</div>

View File

@ -0,0 +1,27 @@
<div id="logviewer-modal-dialog" title="Log View" style="display: none;">
<ul id="logview-tabs" class="nav nav-tabs">
<li class="active"><a href="#status" id="status-link" data-toggle="tab" ng-click="toggleTab($event, 'status-link', 'logview-tabs')">Status</a></li>
<li><a href="#stdout" id="stdout-link" data-toggle="tab" ng-click="toggleTab($event, 'stdout-link', 'logview-tabs')">Std Out</a></li>
<li><a href="#traceback" id="traceback-link" data-toggle="tab" ng-click="toggleTab($event, 'traceback-link', 'logview-tabs')">Traceback</a></li>
<li><a href="#options" id="options-link" data-toggle="tab" ng-click="toggleTab($event, 'options-link', 'logview-tabs')">Options</a></li>
<li><a href="#env" id="env-link" data-toggle="tab" ng-click="toggleTab($event, 'env-link', 'logview-tabs')">Env</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="status">
<div id="status-form-container"></div>
</div>
<div class="tab-pane" id="stdout">
<div id="stdout-form-container"></div>
</div>
<div class="tab-pane" id="traceback">
<div id="traceback-form-container"></div>
</div>
<div class="tab-pane" id="options">
<div id="options-form-container"></div>
</div>
<div class="tab-pane" id="env">
<div id="env-form-container"></div>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div class="tab-pane" id="projects">
<div ng-cloak id="htmlTemplate"></div>
<div id="projects-modal-container"></div>
<div ng-include="'/static/partials/logviewer.html'"></div>
</div>

View File

@ -98,6 +98,8 @@
<script src="{{ STATIC_URL }}js/forms/JobSummary.js"></script>
<script src="{{ STATIC_URL }}js/forms/LicenseForm.js"></script>
<script src="{{ STATIC_URL }}js/forms/Source.js"></script>
<script src="{{ STATIC_URL }}js/forms/LogViewerStatus.js"></script>
<script src="{{ STATIC_URL }}js/forms/LogViewerOptions.js"></script>
<script src="{{ STATIC_URL }}js/lists/Users.js"></script>
<script src="{{ STATIC_URL }}js/lists/Organizations.js"></script>
<script src="{{ STATIC_URL }}js/lists/Admins.js"></script>
@ -147,6 +149,7 @@
<script src="{{ STATIC_URL }}js/helpers/Hosts.js"></script>
<script src="{{ STATIC_URL }}js/helpers/Variables.js"></script>
<script src="{{ STATIC_URL }}js/helpers/Schedules.js"></script>
<script src="{{ STATIC_URL }}js/helpers/LogViewer.js"></script>
<script src="{{ STATIC_URL }}js/widgets/JobStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/InventorySyncStatus.js"></script>
<script src="{{ STATIC_URL }}js/widgets/SCMSyncStatus.js"></script>