diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index c140d2e113..fa1e139501 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -1,19 +1,110 @@ - @import '../shared/branding/colors.less'; @import '../shared/branding/colors.default.less'; +@import '../shared/layouts/one-plus-two.less'; +@breakpoint-md: 1200px; +@breakpoint-sm: 623px; + +.JobResults { + .OnePlusTwo-container(100%, @breakpoint-md); + + &.fullscreen { + .JobResults-rightSide { + max-width: 100%; + } + } +} + +.JobResults-leftSide { + .OnePlusTwo-left--panel(100%, @breakpoint-md); +} + +.JobResults-rightSide { + .OnePlusTwo-right--panel(100%, @breakpoint-md); + + @media (max-width: @breakpoint-md - 1px) { + padding-right: 15px; + } +} + +.JobResults-stdoutActionButton--active { + display: none; + visibility: hidden; + flex:none; + width:0px; + padding-right: 0px; +} + +.JobResults-panelHeader { + display: flex; + height: 30px; +} + +.JobResults-panelHeaderText { + color: @default-interface-txt; + flex: 1 0 auto; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; +} + +.JobResults-resultRow { + width: 100%; + display: flex; + padding-bottom: 10px; + flex-wrap: wrap; +} + +.JobResults-resultRow--variables { + flex-direction: column; +} + +.JobResults-resultRowLabel { + text-transform: uppercase; + color: @default-interface-txt; + font-size: 14px; + font-weight: normal!important; + width: 30%; + margin-right: 20px; + + @media screen and (max-width: @breakpoint-md) { + flex: 2.5 0 auto; + } +} + +.JobResults-resultRowLabel--fullWidth { + width: 100%; + margin-right: 0px; +} + +.JobResults-resultRowText { + width: ~"calc(70% - 20px)"; + flex: 1 0 auto; + text-transform: none; + word-wrap: break-word; +} + +.JobResults-resultRowText--fullWidth { + width: 100%; +} + +.JobResults-statusResultIcon { + padding-left: 0px; + padding-right: 10px; +} + +.JobResults-badgeRow { + display: flex; + align-items: center; + margin-right: 5px; +} .JobResults-badgeTitle{ - color: #707070; + color: @default-interface-txt; font-size: 14px; margin-right: 10px; font-weight: normal; text-transform: uppercase; margin-left: 20px; } - -.JobResults-badgeRow{ - display:flex; - align-items: center; - margin-right: 5px; -} diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js new file mode 100644 index 0000000000..843d5936a6 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -0,0 +1,74 @@ +export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService) { + var getTowerLinks = function() { + var getTowerLink = function(key) { + if ($scope.job.related[key]) { + return '/#/' + $scope.job.related[key] + .split('api/v1/')[1]; + } else { + return null; + } + }; + + $scope.job_template_link = getTowerLink('job_template'); + $scope.created_by_link = getTowerLink('created_by'); + $scope.inventory_link = getTowerLink('inventory'); + $scope.project_link = getTowerLink('project'); + $scope.machine_credential_link = getTowerLink('credential'); + $scope.cloud_credential_link = getTowerLink('cloud_credential'); + $scope.network_credential_link = getTowerLink('network_credential'); + }; + + var getTowerLabels = function() { + var getTowerLabel = function(key) { + if ($scope.jobOptions && $scope.jobOptions[key]) { + return $scope.jobOptions[key].choices + .filter(val => val[0] === $scope.job[key]) + .map(val => val[1])[0]; + } else { + return null; + } + }; + + $scope.status_label = getTowerLabel('status'); + $scope.type_label = getTowerLabel('job_type'); + $scope.verbosity_label = getTowerLabel('verbosity'); + }; + + // put initially resolved request data on scope + $scope.job = jobData; + $scope.jobOptions = jobDataOptions.actions.GET; + $scope.labels = jobLabels; + + // turn related api browser routes into tower routes + getTowerLinks(); + + // use options labels to manipulate display of details + getTowerLabels(); + + // set up a read only code mirror for extra vars + $scope.variables = ParseVariableString($scope.job.extra_vars); + $scope.parseType = 'yaml'; + ParseTypeChange({ scope: $scope, + field_id: 'pre-formatted-variables', + readOnly: true }); + + // Click binding for the expand/collapse button on the standard out log + $scope.stdoutFullScreen = false; + $scope.toggleStdoutFullscreen = function() { + $scope.stdoutFullScreen = !$scope.stdoutFullScreen; + }; + + $scope.deleteJob = function() { + jobResultsService.deleteJob($scope.job); + }; + + $scope.cancelJob = function() { + jobResultsService.cancelJob($scope.job); + }; + + // Set the job status + // TODO: pull from websockets + $scope.job_status = {"status": ""}; + $scope.job_status.status = (jobData.status === 'waiting' || + jobData.status === 'new') ? 'pending' : jobData.status; +}]; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 32a429b2c8..038998ef14 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -1,27 +1,28 @@
- diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 8728b8af3d..eb6d72b1a7 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -6,6 +6,8 @@ import {templateUrl} from '../shared/template-url/template-url.factory'; +import jobResultsController from './job-results.controller'; + export default { name: 'jobDetail', url: '/jobs/:id', @@ -75,68 +77,5 @@ export default { }] }, templateUrl: templateUrl('job-results/job-results'), - controller: ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString) { - - var getTowerLinks = function() { - var getTowerLink = function(key) { - if ($scope.job.related[key]) { - return '/#/' + $scope.job.related[key] - .split('api/v1/')[1]; - } else { - return null; - } - }; - - $scope.job_template_link = getTowerLink('job_template'); - $scope.created_by_link = getTowerLink('created_by'); - $scope.inventory_link = getTowerLink('inventory'); - $scope.project_link = getTowerLink('project'); - $scope.machine_credential_link = getTowerLink('credential'); - $scope.cloud_credential_link = getTowerLink('cloud_credential'); - $scope.network_credential_link = getTowerLink('network_credential'); - }; - - var getTowerLabels = function() { - var getTowerLabel = function(key) { - if ($scope.jobOptions && $scope.jobOptions[key]) { - return $scope.jobOptions[key].choices - .filter(val => val[0] === $scope.job[key]) - .map(val => val[1])[0]; - } else { - return null; - } - }; - - $scope.status_label = getTowerLabel('status'); - $scope.type_label = getTowerLabel('job_type'); - $scope.verbosity_label = getTowerLabel('verbosity'); - }; - - // put initially resolved request data on scope - $scope.job = jobData; - $scope.jobOptions = jobDataOptions.actions.GET; - $scope.labels = jobLabels; - $scope.stdoutFullScreen = false; - - $scope.job_status = {"status": ""}; - $scope.job_status.status = (jobData.status === 'waiting' || jobData.status === 'new') ? 'pending' : jobData.status; - - // turn related api browser routes into tower routes - getTowerLinks(); - - // use options labels to manipulate display of details - getTowerLabels(); - - // set up a read only code mirror for extra vars - $scope.variables = ParseVariableString($scope.job.extra_vars); - $scope.parseType = 'yaml'; - ParseTypeChange({ scope: $scope, - field_id: 'pre-formatted-variables', - readOnly: true }); - - // Click binding for the expand/collapse button on the standard out log - $scope.toggleStdoutFullscreen = function() { - $scope.stdoutFullScreen = !$scope.stdoutFullScreen; - }; - }] + controller: jobResultsController }; diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js new file mode 100644 index 0000000000..fd4a10f588 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -0,0 +1,103 @@ +/************************************************* +* Copyright (c) 2016 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + + +export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors) { + return { + deleteJob: function(job) { + Prompt({ + hdr: 'Delete Job', + body: `
+ Are you sure you want to delete the job below? +
+
+ #${job.id} ${$filter('sanitize')(job.name)} +
`, + action: function() { + Wait('start'); + Rest.setUrl(job.url); + Rest.destroy() + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + $state.go('jobs'); + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + ProcessErrors(null, obj, status, null, { + hdr: 'Error!', + msg: `Could not delete job. + Returned status: ${status}` + }); + }); + }, + actionText: 'DELETE' + }); + }, + cancelJob: function(job) { + var doCancel = function() { + Rest.setUrl(job.url + 'cancel'); + Rest.post({}) + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + ProcessErrors(null, obj, status, null, { + hdr: 'Error!', + msg: `Could not cancel job. + Returned status: ${status}` + }); + }); + }; + + Prompt({ + hdr: 'Cancel Job', + body: `
+ Are you sure you want to cancel the job below? +
+
+ #${job.id} ${$filter('sanitize')(job.name)} +
`, + action: function() { + Wait('start'); + Rest.setUrl(job.url + 'cancel'); + Rest.get() + .success(function(data) { + if (data.can_cancel === true) { + doCancel(); + } else { + $('#prompt-modal').modal('hide'); + ProcessErrors(null, data, null, null, { + hdr: 'Error!', + msg: `Job has completed, + unabled to be canceled.` + }); + } + }); + Rest.destroy() + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + ProcessErrors(null, obj, status, null, { + hdr: 'Error!', + msg: `Could not cancel job. + Returned status: ${status}` + }); + }); + }, + actionText: 'CANCEL' + }); + } + }; +}]; diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index 71e2e13c7c..6a67cd000f 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -5,9 +5,11 @@ *************************************************/ import route from './job-results.route.js'; +import jobResultsService from './job-results.service'; export default angular.module('jobResults', []) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); - }]); + }]) + .service('jobResultsService', jobResultsService);