mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Merge pull request #1163 from ansible/job-results
job results / job event output
This commit is contained in:
commit
dd0e7e2751
@ -20,16 +20,16 @@ const watch = {
|
||||
output: {
|
||||
filename: OUTPUT
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackHarddiskPlugin(),
|
||||
new HardSourceWebpackPlugin({
|
||||
@ -53,25 +53,37 @@ const watch = {
|
||||
host: '127.0.0.1',
|
||||
https: true,
|
||||
port: 3000,
|
||||
https: true,
|
||||
proxy: {
|
||||
'/': {
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: false,
|
||||
bypass: req => req.originalUrl.includes('hot-update.json')
|
||||
},
|
||||
'/websocket': {
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
'/network_ui': {
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true
|
||||
clientLogLevel: 'none',
|
||||
proxy: [{
|
||||
context: (pathname, req) => !(pathname === '/api/login/' && req.method === 'POST'),
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: false,
|
||||
bypass: req => req.originalUrl.includes('hot-update.json')
|
||||
},
|
||||
{
|
||||
context: '/api/login/',
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: false,
|
||||
headers: {
|
||||
Host: `localhost:${TARGET_PORT}`,
|
||||
Origin: TARGET,
|
||||
Referer: `${TARGET}/`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
context: '/websocket',
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
{
|
||||
context: '/network_ui',
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
@import 'credentials/_index';
|
||||
@import 'output/_index';
|
||||
@import 'users/tokens/_index';
|
||||
|
||||
@ -4,6 +4,7 @@ import atLibModels from '~models';
|
||||
|
||||
import atFeaturesApplications from '~features/applications';
|
||||
import atFeaturesCredentials from '~features/credentials';
|
||||
import atFeaturesOutput from '~features/output';
|
||||
import atFeaturesTemplates from '~features/templates';
|
||||
import atFeaturesUsers from '~features/users';
|
||||
import atFeaturesJobs from '~features/jobs';
|
||||
@ -18,7 +19,9 @@ angular.module(MODULE_NAME, [
|
||||
atFeaturesCredentials,
|
||||
atFeaturesTemplates,
|
||||
atFeaturesUsers,
|
||||
atFeaturesJobs
|
||||
atFeaturesJobs,
|
||||
atFeaturesOutput,
|
||||
atFeaturesTemplates
|
||||
]);
|
||||
|
||||
export default MODULE_NAME;
|
||||
|
||||
@ -52,19 +52,19 @@ function ListJobsController (
|
||||
|
||||
switch (type) {
|
||||
case 'job':
|
||||
link = `/#/jobs/${id}`;
|
||||
link = `/#/jobz/playbook/${id}`;
|
||||
break;
|
||||
case 'ad_hoc_command':
|
||||
link = `/#/ad_hoc_commands/${id}`;
|
||||
link = `/#/jobz/command/${id}`;
|
||||
break;
|
||||
case 'system_job':
|
||||
link = `/#/management_jobs/${id}`;
|
||||
link = `/#/jobz/system/${id}`;
|
||||
break;
|
||||
case 'project_update':
|
||||
link = `/#/scm_update/${id}`;
|
||||
link = `/#/jobz/project/${id}`;
|
||||
break;
|
||||
case 'inventory_update':
|
||||
link = `/#/inventory_sync/${id}`;
|
||||
link = `/#/jobz/inventory/${id}`;
|
||||
break;
|
||||
case 'workflow_job':
|
||||
link = `/#/workflows/${id}`;
|
||||
|
||||
534
awx/ui/client/features/output/_index.less
Normal file
534
awx/ui/client/features/output/_index.less
Normal file
@ -0,0 +1,534 @@
|
||||
@import 'host-event/_index';
|
||||
.at-Stdout {
|
||||
&-menuTop {
|
||||
color: @at-gray-848992;
|
||||
border: 1px solid @at-gray-b7;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
|
||||
& > div {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuBottom {
|
||||
color: @at-gray-848992;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
bottom: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIconGroup {
|
||||
& > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > p:first-child {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
& > p:last-child {
|
||||
margin-top: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIcon {
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIcon--lg {
|
||||
font-size: 22px;
|
||||
line-height: 12px;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIcon--active {
|
||||
font-size: 22px;
|
||||
line-height: 12px;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: @at-blue;
|
||||
}
|
||||
|
||||
&-toggle {
|
||||
color: @at-gray-848992;
|
||||
background-color: @at-gray-eb;
|
||||
font-size: 18px;
|
||||
line-height: 12px;
|
||||
|
||||
& > i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
padding: 0 10px 0 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-line {
|
||||
color: @at-gray-161b1f;
|
||||
background-color: @at-gray-eb;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
padding-right: 5px;
|
||||
border-right: 1px solid @at-gray-b7;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-event {
|
||||
.at-mixin-event();
|
||||
}
|
||||
|
||||
&-event--host {
|
||||
.at-mixin-event();
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-time {
|
||||
padding-right: 2ch;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
width: 11ch;
|
||||
|
||||
& > span {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
font-family: monospace;
|
||||
height: calc(~"100vh - 240px");
|
||||
overflow-y: scroll;
|
||||
font-size: 15px;
|
||||
border: 1px solid @at-gray-b7;
|
||||
background-color: @at-gray-f2;
|
||||
color: @at-gray-161b1f;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
|
||||
& > table {
|
||||
table-layout: fixed;
|
||||
|
||||
tr:hover > td {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.at-mixin-event() {
|
||||
padding: 0 10px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
}
|
||||
|
||||
// Search ---------------------------------------------------------------------------------
|
||||
@at-jobz-top-search-key: @at-space-2x;
|
||||
@at-jobz-bottom-search-key: @at-space-3x;
|
||||
|
||||
.jobz-searchKeyPaneContainer {
|
||||
margin-top: @at-jobz-top-search-key;
|
||||
margin-bottom: @at-jobz-bottom-search-key;
|
||||
}
|
||||
|
||||
.jobz-searchKeyPane {
|
||||
// background-color: @at-gray-f6;
|
||||
background-color: @login-notice-bg;
|
||||
color: @login-notice-text;
|
||||
border-radius: @at-border-radius;
|
||||
border: 1px solid @at-gray-b7;
|
||||
// color: @at-gray-848992;
|
||||
padding: 6px @at-padding-input 6px @at-padding-input;
|
||||
}
|
||||
|
||||
.jobz-searchClearAll {
|
||||
font-size: 10px;
|
||||
padding-bottom: @at-space;
|
||||
}
|
||||
|
||||
.jobz-Button-searchKey {
|
||||
.at-mixin-Button();
|
||||
|
||||
background-color: @at-blue;
|
||||
border-color: at-color-button-border-default;
|
||||
color: @at-white;
|
||||
|
||||
&:hover, &:active {
|
||||
color: @at-white;
|
||||
background-color: @at-blue-hover;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: @at-white;
|
||||
}
|
||||
}
|
||||
|
||||
.jobz-tagz {
|
||||
margin-top: @at-space;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
// Status Bar -----------------------------------------------------------------------------
|
||||
.HostStatusBar {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.HostStatusBar-ok,
|
||||
.HostStatusBar-changed,
|
||||
.HostStatusBar-dark,
|
||||
.HostStatusBar-failed,
|
||||
.HostStatusBar-skipped,
|
||||
.HostStatusBar-noData {
|
||||
height: 15px;
|
||||
border-top: 5px solid @default-bg;
|
||||
border-bottom: 5px solid @default-bg;
|
||||
}
|
||||
|
||||
.HostStatusBar-ok {
|
||||
background-color: @default-succ;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-changed {
|
||||
background-color: @default-warning;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-dark {
|
||||
background-color: @default-unreachable;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-failed {
|
||||
background-color: @default-err;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-skipped {
|
||||
background-color: @default-link;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-noData {
|
||||
background-color: @default-icon-hov;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipLabel {
|
||||
text-transform: uppercase;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge {
|
||||
border-radius: 5px;
|
||||
border: 1px solid @default-bg;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--ok {
|
||||
background-color: @default-succ;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--dark {
|
||||
background-color: @default-unreachable;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--skipped {
|
||||
background-color: @default-link;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--changed {
|
||||
background-color: @default-warning;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--failed {
|
||||
background-color: @default-err;
|
||||
|
||||
}
|
||||
|
||||
// Job Details ---------------------------------------------------------------------------------
|
||||
|
||||
@breakpoint-md: 1200px;
|
||||
|
||||
.JobResults {
|
||||
.OnePlusTwo-container(100%, @breakpoint-md);
|
||||
|
||||
&.fullscreen {
|
||||
.JobResults-rightSide {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-leftSide {
|
||||
.OnePlusTwo-left--panel(100%, @breakpoint-md);
|
||||
max-width: 30%;
|
||||
height: ~"calc(100vh - 177px)";
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-rightSide {
|
||||
.OnePlusTwo-right--panel(100%, @breakpoint-md);
|
||||
height: ~"calc(100vh - 177px)";
|
||||
|
||||
@media (max-width: @breakpoint-md - 1px) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-detailsPanel{
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.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-panelHeaderButtonActions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.JobResults-resultRow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.JobResults-resultRow--variables {
|
||||
flex-direction: column;
|
||||
|
||||
#cm-variables-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-resultRowLabel {
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
font-size: 12px;
|
||||
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-expandArrow {
|
||||
color: #D7D7D7;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-transform: uppercase;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.JobResults-resultRowText--instanceGroup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.JobResults-isolatedBadge {
|
||||
align-items: center;
|
||||
background-color: @default-list-header-bg;
|
||||
border-radius: 5px;
|
||||
color: @default-stdout-txt;
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
margin: 3px 0 0 10px;
|
||||
padding: 0 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.JobResults-statusResultIcon {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.JobResults-badgeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.JobResults-badgeTitle{
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: @breakpoint-md) {
|
||||
.JobResults-detailsPanel {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.JobResults-rightSide {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-timeBadge {
|
||||
float:right;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
padding: 1px 10px;
|
||||
height: 14px;
|
||||
margin: 3px 15px;
|
||||
width: 80px;
|
||||
background-color: @default-bg;
|
||||
border-radius: 5px;
|
||||
color: @default-interface-txt;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.JobResults-panelRight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.JobResults-panelRight .SmartSearch-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.JobResults-panelRightTitle{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.JobResults-panelRightTitleText{
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.JobResults-badgeAndActionRow{
|
||||
display:flex;
|
||||
flex: 1 0 auto;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.StandardOut-panelHeader {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
.StandardOut-panelHeader--jobIsRunning {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
host-status-bar {
|
||||
flex: initial;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
smart-search {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
job-results-standard-out {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
height: ~"calc(100% - 800px)";
|
||||
display: flex;
|
||||
border: 1px solid @d7grey;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
job-results-standard-out {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-extraVarsHelp {
|
||||
margin-left: 10px;
|
||||
color: @default-icon;
|
||||
}
|
||||
|
||||
.JobResults-seeMoreLess {
|
||||
color: #337AB7;
|
||||
margin: 4px 0px;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 0px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
577
awx/ui/client/features/output/details.directive.js
Normal file
577
awx/ui/client/features/output/details.directive.js
Normal file
@ -0,0 +1,577 @@
|
||||
const templateUrl = require('~features/output/details.partial.html');
|
||||
|
||||
let $http;
|
||||
let $filter;
|
||||
let $scope;
|
||||
let $state;
|
||||
|
||||
let error;
|
||||
let parse;
|
||||
let prompt;
|
||||
let resource;
|
||||
let strings;
|
||||
let status;
|
||||
let wait;
|
||||
|
||||
let vm;
|
||||
|
||||
function mapChoices (choices) {
|
||||
if (!choices) return {};
|
||||
return Object.assign(...choices.map(([k, v]) => ({ [k]: v })));
|
||||
}
|
||||
|
||||
function getStatusDetails (jobStatus) {
|
||||
const unmapped = jobStatus || resource.model.get('status');
|
||||
|
||||
if (!unmapped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choices = mapChoices(resource.model.options('actions.GET.status.choices'));
|
||||
|
||||
const label = 'Status';
|
||||
const icon = `fa icon-job-${unmapped}`;
|
||||
const value = choices[unmapped];
|
||||
|
||||
return { label, icon, value };
|
||||
}
|
||||
|
||||
function getStartDetails (started) {
|
||||
const unfiltered = started || resource.model.get('started');
|
||||
|
||||
const label = 'Started';
|
||||
|
||||
let value;
|
||||
|
||||
if (unfiltered) {
|
||||
value = $filter('longDate')(unfiltered);
|
||||
} else {
|
||||
value = 'Not Started';
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getFinishDetails (finished) {
|
||||
const unfiltered = finished || resource.model.get('finished');
|
||||
|
||||
const label = 'Finished';
|
||||
|
||||
let value;
|
||||
|
||||
if (unfiltered) {
|
||||
value = $filter('longDate')(unfiltered);
|
||||
} else {
|
||||
value = 'Not Finished';
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getJobTypeDetails () {
|
||||
const unmapped = resource.model.get('job_type');
|
||||
|
||||
if (!unmapped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choices = mapChoices(resource.model.options('actions.GET.job_type.choices'));
|
||||
|
||||
const label = 'Job Type';
|
||||
const value = choices[unmapped];
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getVerbosityDetails () {
|
||||
const verbosity = resource.model.get('verbosity');
|
||||
|
||||
if (!verbosity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choices = mapChoices(resource.model.options('actions.GET.verbosity.choices'));
|
||||
|
||||
const label = 'Verbosity';
|
||||
const value = choices[verbosity];
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getSourceWorkflowJobDetails () {
|
||||
const sourceWorkflowJob = resource.model.get('summary_fields.source_workflow_job');
|
||||
|
||||
if (!sourceWorkflowJob) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const link = `/#/workflows/${sourceWorkflowJob.id}`;
|
||||
|
||||
return { link };
|
||||
}
|
||||
|
||||
function getJobTemplateDetails () {
|
||||
const jobTemplate = resource.model.get('summary_fields.job_template');
|
||||
|
||||
if (!jobTemplate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Job Template';
|
||||
const link = `/#/templates/job_template/${jobTemplate.id}`;
|
||||
const value = $filter('sanitize')(jobTemplate.name);
|
||||
|
||||
return { label, link, value };
|
||||
}
|
||||
|
||||
function getLaunchedByDetails () {
|
||||
const createdBy = resource.model.get('summary_fields.created_by');
|
||||
const jobTemplate = resource.model.get('summary_fields.job_template');
|
||||
|
||||
const relatedSchedule = resource.model.get('related.schedule');
|
||||
const schedule = resource.model.get('summary_fields.schedule');
|
||||
|
||||
if (!createdBy && !schedule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Launched By';
|
||||
|
||||
let link;
|
||||
let tooltip;
|
||||
let value;
|
||||
|
||||
if (createdBy) {
|
||||
tooltip = 'Edit the User';
|
||||
link = `/#/users/${createdBy.id}`;
|
||||
value = $filter('sanitize')(createdBy.username);
|
||||
} else if (relatedSchedule && jobTemplate) {
|
||||
tooltip = 'Edit the Schedule';
|
||||
link = `/#/templates/job_template/${jobTemplate.id}/schedules/${schedule.id}`;
|
||||
value = $filter('sanitize')(schedule.name);
|
||||
} else {
|
||||
tooltip = null;
|
||||
link = null;
|
||||
value = $filter('sanitize')(schedule.name);
|
||||
}
|
||||
|
||||
return { label, link, tooltip, value };
|
||||
}
|
||||
|
||||
function getInventoryDetails () {
|
||||
const inventory = resource.model.get('summary_fields.inventory');
|
||||
|
||||
if (!inventory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Inventory';
|
||||
const tooltip = 'Edit the inventory';
|
||||
const value = $filter('sanitize')(inventory.name);
|
||||
|
||||
let link;
|
||||
|
||||
if (inventory.kind === 'smart') {
|
||||
link = `/#/inventories/smart/${inventory.id}`;
|
||||
} else {
|
||||
link = `/#/inventories/inventory/${inventory.id}`;
|
||||
}
|
||||
|
||||
return { label, link, tooltip, value };
|
||||
}
|
||||
|
||||
function getProjectDetails () {
|
||||
const project = resource.model.get('summary_fields.project');
|
||||
const projectUpdate = resource.model.get('summary_fields.project_update');
|
||||
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Project';
|
||||
const link = `/#/projects/${project.id}`;
|
||||
const value = $filter('sanitize')(project.name);
|
||||
|
||||
if (projectUpdate) {
|
||||
const update = {
|
||||
link: `/#/jobz/project/${projectUpdate.id}`,
|
||||
tooltip: 'View project checkout results',
|
||||
status: projectUpdate.status,
|
||||
};
|
||||
|
||||
return { label, link, value, update };
|
||||
}
|
||||
|
||||
return { label, link, value };
|
||||
}
|
||||
|
||||
function getSCMRevisionDetails () {
|
||||
const label = 'Revision';
|
||||
const value = resource.model.get('scm_revision');
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getPlaybookDetails () {
|
||||
const label = 'Playbook';
|
||||
const value = resource.model.get('playbook');
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getJobExplanationDetails () {
|
||||
const jobExplanation = resource.model.get('job_explanation');
|
||||
|
||||
if (!jobExplanation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = null;
|
||||
|
||||
return { value };
|
||||
}
|
||||
|
||||
function getResultTracebackDetails () {
|
||||
const previousTaskFailed = false;
|
||||
const resultTraceback = resource.model.get('result_traceback');
|
||||
|
||||
if (!resultTraceback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!previousTaskFailed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Results Traceback';
|
||||
const value = null;
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getCredentialDetails () {
|
||||
const credential = resource.model.get('summary_fields.credential');
|
||||
|
||||
if (!credential) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let label = 'Credential';
|
||||
|
||||
if (resource.type === 'playbook') {
|
||||
label = 'Machine Credential';
|
||||
}
|
||||
|
||||
if (resource.type === 'inventory') {
|
||||
label = 'Source Credential';
|
||||
}
|
||||
|
||||
const link = `/#/credentials/${credential.id}`;
|
||||
const tooltip = 'Edit the Credential';
|
||||
const value = $filter('sanitize')(credential.name);
|
||||
|
||||
return { label, link, tooltip, value };
|
||||
}
|
||||
|
||||
function getForkDetails () {
|
||||
const label = 'Forks';
|
||||
const value = resource.model.get('forks');
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getLimitDetails () {
|
||||
const label = 'Limit';
|
||||
const value = resource.model.get('limit');
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getInstanceGroupDetails () {
|
||||
const instanceGroup = resource.model.get('summary_fields.instance_group');
|
||||
|
||||
if (!instanceGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Instance Group';
|
||||
const value = $filter('sanitize')(instanceGroup.name);
|
||||
|
||||
let isolated = null;
|
||||
|
||||
if (instanceGroup.controller_id) {
|
||||
isolated = 'Isolated';
|
||||
}
|
||||
|
||||
return { label, value, isolated };
|
||||
}
|
||||
|
||||
function getJobTagDetails () {
|
||||
const label = 'Job Tags';
|
||||
const value = resource.model.get('job_tags');
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getSkipTagDetails () {
|
||||
const label = 'Skip Tags';
|
||||
const value = resource.model.get('skip_tags');
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function getExtraVarsDetails () {
|
||||
const extraVars = resource.model.get('extra_vars');
|
||||
|
||||
if (!extraVars) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Extra Variables';
|
||||
const tooltip = 'Read-only view of extra variables added to the job template.';
|
||||
const value = parse(extraVars);
|
||||
|
||||
return { label, tooltip, value };
|
||||
}
|
||||
|
||||
function getLabelDetails () {
|
||||
const jobLabels = _.get(resource.model.get('related.labels'), 'results', []);
|
||||
|
||||
if (jobLabels.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = 'Labels';
|
||||
const more = false;
|
||||
|
||||
const value = jobLabels.map(({ name }) => name).map($filter('sanitize'));
|
||||
|
||||
return { label, more, value };
|
||||
}
|
||||
|
||||
function createErrorHandler (path, action) {
|
||||
return res => {
|
||||
const hdr = strings.get('error.HEADER');
|
||||
const msg = strings.get('error.CALL', { path, action, status: res.status });
|
||||
|
||||
error($scope, res.data, res.status, null, { hdr, msg });
|
||||
};
|
||||
}
|
||||
|
||||
const ELEMENT_LABELS = '#job-results-labels';
|
||||
const ELEMENT_PROMPT_MODAL = '#prompt-modal';
|
||||
const LABELS_SLIDE_DISTANCE = 200;
|
||||
|
||||
function toggleLabels () {
|
||||
if (!this.labels.more) {
|
||||
$(ELEMENT_LABELS).slideUp(LABELS_SLIDE_DISTANCE);
|
||||
this.labels.more = true;
|
||||
} else {
|
||||
$(ELEMENT_LABELS).slideDown(LABELS_SLIDE_DISTANCE);
|
||||
this.labels.more = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelJob () {
|
||||
const actionText = strings.get('warnings.CANCEL_ACTION');
|
||||
const hdr = strings.get('warnings.CANCEL_HEADER');
|
||||
const warning = strings.get('warnings.CANCEL_BODY');
|
||||
|
||||
const id = resource.model.get('id');
|
||||
const name = $filter('sanitize')(resource.model.get('name'));
|
||||
|
||||
const body = `<div class="Prompt-bodyQuery">${warning}</div>`;
|
||||
const resourceName = `#${id} ${name}`;
|
||||
|
||||
const method = 'POST';
|
||||
const url = `${resource.model.path}/${id}/cancel/`;
|
||||
|
||||
const errorHandler = createErrorHandler('cancel job', method);
|
||||
|
||||
const action = () => {
|
||||
wait('start');
|
||||
$http({ method, url })
|
||||
.catch(errorHandler)
|
||||
.finally(() => {
|
||||
$(ELEMENT_PROMPT_MODAL).modal('hide');
|
||||
wait('stop');
|
||||
});
|
||||
};
|
||||
|
||||
prompt({ hdr, resourceName, body, actionText, action });
|
||||
}
|
||||
|
||||
function deleteJob () {
|
||||
const actionText = strings.get('DELETE');
|
||||
const hdr = strings.get('warnings.DELETE_HEADER');
|
||||
const warning = strings.get('warnings.DELETE_BODY');
|
||||
|
||||
const id = resource.model.get('id');
|
||||
const name = $filter('sanitize')(resource.model.get('name'));
|
||||
|
||||
const body = `<div class="Prompt-bodyQuery">${warning}</div>`;
|
||||
const resourceName = `#${id} ${name}`;
|
||||
|
||||
const method = 'DELETE';
|
||||
const url = `${resource.model.path}/${id}/`;
|
||||
|
||||
const errorHandler = createErrorHandler('delete job', method);
|
||||
|
||||
const action = () => {
|
||||
wait('start');
|
||||
$http({ method, url })
|
||||
.then(() => $state.go('jobs'))
|
||||
.catch(errorHandler)
|
||||
.finally(() => {
|
||||
$(ELEMENT_PROMPT_MODAL).modal('hide');
|
||||
wait('stop');
|
||||
});
|
||||
};
|
||||
|
||||
prompt({ hdr, resourceName, body, actionText, action });
|
||||
}
|
||||
|
||||
function AtJobDetailsController (
|
||||
_$http_,
|
||||
_$filter_,
|
||||
_$state_,
|
||||
_error_,
|
||||
_prompt_,
|
||||
_strings_,
|
||||
_status_,
|
||||
_wait_,
|
||||
ParseTypeChange,
|
||||
ParseVariableString,
|
||||
) {
|
||||
vm = this || {};
|
||||
|
||||
$http = _$http_;
|
||||
$filter = _$filter_;
|
||||
$state = _$state_;
|
||||
|
||||
error = _error_;
|
||||
parse = ParseVariableString;
|
||||
prompt = _prompt_;
|
||||
strings = _strings_;
|
||||
status = _status_;
|
||||
wait = _wait_;
|
||||
|
||||
vm.init = _$scope_ => {
|
||||
$scope = _$scope_;
|
||||
resource = $scope.resource; // eslint-disable-line prefer-destructuring
|
||||
|
||||
vm.status = getStatusDetails();
|
||||
vm.started = getStartDetails();
|
||||
vm.finished = getFinishDetails();
|
||||
vm.jobType = getJobTypeDetails();
|
||||
vm.jobTemplate = getJobTemplateDetails();
|
||||
vm.sourceWorkflowJob = getSourceWorkflowJobDetails();
|
||||
vm.inventory = getInventoryDetails();
|
||||
vm.project = getProjectDetails();
|
||||
vm.scmRevision = getSCMRevisionDetails();
|
||||
vm.playbook = getPlaybookDetails();
|
||||
vm.resultTraceback = getResultTracebackDetails();
|
||||
vm.launchedBy = getLaunchedByDetails();
|
||||
vm.jobExplanation = getJobExplanationDetails();
|
||||
vm.verbosity = getVerbosityDetails();
|
||||
vm.credential = getCredentialDetails();
|
||||
vm.forks = getForkDetails();
|
||||
vm.limit = getLimitDetails();
|
||||
vm.instanceGroup = getInstanceGroupDetails();
|
||||
vm.jobTags = getJobTagDetails();
|
||||
vm.skipTags = getSkipTagDetails();
|
||||
vm.extraVars = getExtraVarsDetails();
|
||||
vm.labels = getLabelDetails();
|
||||
|
||||
// Relaunch and Delete Components
|
||||
vm.job = _.get(resource.model, 'model.GET', {});
|
||||
vm.canDelete = resource.model.get('summary_fields.user_capabilities.delete');
|
||||
|
||||
// XX - Codemirror
|
||||
if (vm.extraVars) {
|
||||
const cm = {
|
||||
parseType: 'yaml',
|
||||
$apply: $scope.$apply,
|
||||
variables: vm.extraVars.value,
|
||||
};
|
||||
|
||||
ParseTypeChange({
|
||||
field_id: 'cm-extra-vars',
|
||||
readOnly: true,
|
||||
scope: cm,
|
||||
});
|
||||
}
|
||||
|
||||
vm.cancelJob = cancelJob;
|
||||
vm.deleteJob = deleteJob;
|
||||
vm.toggleLabels = toggleLabels;
|
||||
|
||||
$scope.$watch(status.getStarted, value => { vm.started = getStartDetails(value); });
|
||||
$scope.$watch(status.getJobStatus, value => { vm.status = getStatusDetails(value); });
|
||||
$scope.$watch(status.getFinished, value => { vm.finished = getFinishDetails(value); });
|
||||
|
||||
$scope.$watch(status.getProjectStatus, value => {
|
||||
if (!value) return;
|
||||
|
||||
vm.project.update = vm.project.update || {};
|
||||
vm.project.update.status = value;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
AtJobDetailsController.$inject = [
|
||||
'$http',
|
||||
'$filter',
|
||||
'$state',
|
||||
'ProcessErrors',
|
||||
'Prompt',
|
||||
'JobStrings',
|
||||
'JobStatusService',
|
||||
'Wait',
|
||||
'ParseTypeChange',
|
||||
'ParseVariableString',
|
||||
];
|
||||
|
||||
function atJobDetailsLink (scope, el, attrs, controllers) {
|
||||
const [atDetailsController] = controllers;
|
||||
|
||||
atDetailsController.init(scope);
|
||||
}
|
||||
|
||||
function atJobDetails () {
|
||||
return {
|
||||
templateUrl,
|
||||
restrict: 'E',
|
||||
require: ['atJobDetails'],
|
||||
controllerAs: 'vm',
|
||||
link: atJobDetailsLink,
|
||||
controller: AtJobDetailsController,
|
||||
scope: { resource: '=', },
|
||||
};
|
||||
}
|
||||
|
||||
export default atJobDetails;
|
||||
241
awx/ui/client/features/output/details.partial.html
Normal file
241
awx/ui/client/features/output/details.partial.html
Normal file
@ -0,0 +1,241 @@
|
||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||
<div ui-view></div>
|
||||
<div class="JobResults-panelHeader">
|
||||
<div class="JobResults-panelHeaderText" translate> DETAILS</div>
|
||||
<!-- LEFT PANE HEADER ACTIONS -->
|
||||
<div class="JobResults-panelHeaderButtonActions">
|
||||
<!-- RELAUNCH ACTION -->
|
||||
<at-relaunch job="vm.job"></at-relaunch>
|
||||
|
||||
<!-- CANCEL ACTION -->
|
||||
<button
|
||||
class="List-actionButton List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="vm.cancelJob()"
|
||||
ng-show="vm.status.value === 'Running'|| vm.status.value ==='Pending'"
|
||||
aw-tool-tip="{{'Cancel' | translate }}"
|
||||
data-original-title=""
|
||||
title="">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
|
||||
<!-- DELETE ACTION -->
|
||||
<button
|
||||
class="List-actionButton List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="vm.deleteJob()"
|
||||
ng-hide="!vm.canDelete
|
||||
|| vm.status.value === 'Running'
|
||||
|| vm.status.value === 'Pending'"
|
||||
aw-tool-tip="{{ 'Delete' | translate }}"
|
||||
data-original-title=""
|
||||
title="">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEFT PANE DETAILS GROUP -->
|
||||
<div>
|
||||
<!-- STATUS DETAIL -->
|
||||
<div class="JobResults-resultRow">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.status.label}}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<i class="JobResults-statusResultIcon {{ vm.status.icon }}"></i>
|
||||
{{ vm.status.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- START TIME DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.started">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.started.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ vm.started.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FINISHED TIME DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-show="vm.started">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.finished.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ vm.finished.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RESULTS TRACEBACK DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.resultTraceback">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.resultTraceback.label }}</label>
|
||||
<div class="JobResults-resultRowText" ng-bind-html="vm.resultTraceback.value"></div>
|
||||
</div>
|
||||
|
||||
<!-- TEMPLATE DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-show="vm.jobTemplate">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.jobTemplate.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ vm.jobTemplate.link }}" aw-tool-tip="{{'Edit the job template' | translate}}" data-placement="top">
|
||||
{{ vm.jobTemplate.value }}
|
||||
</a>
|
||||
|
||||
<a href="{{ vm.sourceWorkflowJob.link }}" ng-if="vm.sourceWorkflowJob" aw-tool-tip="{{'View workflow results' | translate}}" data-placement="top" data-original-title="" title="">
|
||||
<i class="WorkflowBadge"> W</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JOB TYPE DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.jobType">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.jobType.label }}</label>
|
||||
<div class="JobResults-resultRowText">{{ vm.jobType.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- LAUNCHED BY DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.launchedBy">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.launchedBy.label }}</label>
|
||||
<div ng-if="vm.launchedBy.link" class="JobResults-resultRowText">
|
||||
<a href="{{ vm.launchedBy.link }}" aw-tool-tip="{{ vm.launchedBy.tooltip }}" data-placement="top">
|
||||
{{ vm.launchedBy.value }}
|
||||
</a>
|
||||
</div>
|
||||
<div ng-if="!vm.launchedBy.link" class="jobResults-resultRowText">
|
||||
{{ vm.launchedBy.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INVENTORY DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.inventory">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.inventory.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ vm.inventory.link }}" aw-tool-tip="{{ vm.inventory.tooltip }}" data-placement="top">
|
||||
{{ vm.inventory.value }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.project">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.project.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ vm.project.update.link }}"
|
||||
ng-if="vm.project.update"
|
||||
aw-tool-tip="{{ vm.project.update.tooltip }}"
|
||||
data-placement="top">
|
||||
<i class="JobResults-statusResultIcon fa icon-job-{{ vm.project.update.status }}"></i>
|
||||
</a>
|
||||
<a href="{{ vm.project.link }}"
|
||||
aw-tool-tip="{{ vm.project.tooltip }}"
|
||||
data-placement="top">
|
||||
{{ vm.project.value }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REVISION DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.scmRevision">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.scmRevision.label }}</label>
|
||||
<at-truncate string="{{ vm.scmRevision.value }}" maxLength="7" class="JobResults-resultRowText"></at-truncate>
|
||||
</div>
|
||||
|
||||
<!-- PLAYBOOK DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.playbook">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.playbook.label }}</label>
|
||||
<div class="JobResults-resultRowText">{{ vm.playbook.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- CREDENTIAL DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-show="vm.credential">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.credential.label }}</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ vm.credential.link }}"
|
||||
aw-tool-tip="{{ vm.credential.tooltip }}"
|
||||
data-placement="top">
|
||||
{{ vm.credential.value }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FORKS DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.forks">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.forks.label }}</label>
|
||||
<div class="JobResults-resultRowText">{{ vm.forks.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- LIMIT DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.limit">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.limit.label }}</label>
|
||||
<div class="JobResults-resultRowText">{{ vm.limit.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- VERBOSITY DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.verbosity">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.verbosity.label }}</label>
|
||||
<div class="JobResults-resultRowText">{{ vm.verbosity.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- IG DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.instanceGroup">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.instanceGroup.label }}</label>
|
||||
<div class="JobResults-resultRowText JobResults-resultRowText--instanceGroup">
|
||||
{{ vm.instanceGroup.value }}
|
||||
<span class="JobResults-isolatedBadge" ng-if="vm.instanceGroup.isolated">
|
||||
{{ vm.instanceGroup.isolated }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAGS DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.jobTags">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.jobTags.label }}</label>
|
||||
<div class="JobResults-resultRowText">{{ vm.jobTags.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- SKIP TAGS DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-if="vm.skipTags">
|
||||
<label class="JobResults-resultRowLabel">{{ vm.skipTags.label }}</label>
|
||||
<div class="JobResults-resultRowText"> {{ vm.skipTags.value }}</div>
|
||||
</div>
|
||||
|
||||
<!-- EXTRA VARIABLES DETAIL -->
|
||||
<div class="JobResults-resultRow JobResults-resultRow--variables" ng-show="vm.extraVars">
|
||||
<label class="JobResults-resultRowLabel JobResults-resultRowLabel--fullWidth">
|
||||
<span>{{ vm.extraVars.label }}</span>
|
||||
<i class="JobResults-extraVarsHelp fa fa-question-circle"
|
||||
aw-tool-tip="{{ vm.extraVars.tooltip }}"
|
||||
data-placement="top">
|
||||
</i>
|
||||
</label>
|
||||
<textarea
|
||||
disabled="disabled"
|
||||
rows="6"
|
||||
ng-model="vm.extraVars.value"
|
||||
name="variables"
|
||||
class="form-control Form-textArea Form-textAreaLabel Form-formGroup--fullWidth"
|
||||
id="cm-extra-vars">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<!-- LABELS DETAIL -->
|
||||
<div class="JobResults-resultRow" ng-show="vm.labels">
|
||||
<div class="JobResults-resultRow">
|
||||
<a class="JobResults-resultRowLabel JobResults-resultRowLabel--fullWidth"
|
||||
ng-show="vm.labels.more"
|
||||
href=""
|
||||
ng-click="vm.toggleLabels()">
|
||||
<span translate>Labels</span>
|
||||
<i class="JobResults-expandArrow fa fa-caret-right"></i>
|
||||
</a>
|
||||
<a class="JobResults-resultRowLabel JobResults-resultRowLabel--fullWidth"
|
||||
ng-show="!vm.labels.more"
|
||||
href=""
|
||||
ng-click="vm.toggleLabels()">
|
||||
<span translate>Labels</span>
|
||||
<i class="JobResults-expandArrow fa fa-caret-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div id="job-results-labels" class="LabelList JobResults-resultRowText JobResults-resultRowText--fullWidth">
|
||||
<div ng-repeat="label in vm.labels.value" class="LabelList-tagContainer">
|
||||
<div class="LabelList-tag"><div class="LabelList-name">{{ label }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
226
awx/ui/client/features/output/engine.service.js
Normal file
226
awx/ui/client/features/output/engine.service.js
Normal file
@ -0,0 +1,226 @@
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
const MAX_LAG = 120;
|
||||
|
||||
function JobEventEngine ($q) {
|
||||
this.init = ({ resource, scroll, page, onEventFrame, onStart, onStop }) => {
|
||||
this.resource = resource;
|
||||
this.scroll = scroll;
|
||||
this.page = page;
|
||||
|
||||
this.lag = 0;
|
||||
this.count = 0;
|
||||
this.pageCount = 0;
|
||||
this.chain = $q.resolve();
|
||||
this.factors = this.getBatchFactors(this.resource.page.size);
|
||||
|
||||
this.state = {
|
||||
started: false,
|
||||
paused: false,
|
||||
pausing: false,
|
||||
resuming: false,
|
||||
ending: false,
|
||||
ended: false,
|
||||
counting: false,
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
onEventFrame,
|
||||
onStart,
|
||||
onStop,
|
||||
};
|
||||
|
||||
this.lines = {
|
||||
used: [],
|
||||
missing: [],
|
||||
ready: false,
|
||||
min: 0,
|
||||
max: 0
|
||||
};
|
||||
};
|
||||
|
||||
this.getBatchFactors = size => {
|
||||
const factors = [1];
|
||||
|
||||
for (let i = 2; i <= size / 2; i++) {
|
||||
if (size % i === 0) {
|
||||
factors.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
factors.push(size);
|
||||
|
||||
return factors;
|
||||
};
|
||||
|
||||
this.getBatchFactorIndex = () => {
|
||||
const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
|
||||
|
||||
return index > this.factors.length - 1 ? this.factors.length - 1 : index;
|
||||
};
|
||||
|
||||
this.setBatchFrameCount = () => {
|
||||
const index = this.getBatchFactorIndex();
|
||||
|
||||
this.framesPerRender = this.factors[index];
|
||||
};
|
||||
|
||||
this.buffer = data => {
|
||||
const pageAdded = this.page.addToBuffer(data);
|
||||
|
||||
this.pageCount++;
|
||||
|
||||
if (pageAdded) {
|
||||
this.setBatchFrameCount();
|
||||
|
||||
if (this.isPausing()) {
|
||||
this.pause(true);
|
||||
} else if (this.isResuming()) {
|
||||
this.resume(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.checkLines = data => {
|
||||
for (let i = data.start_line; i < data.end_line; i++) {
|
||||
if (i > this.lines.max) {
|
||||
this.lines.max = i;
|
||||
}
|
||||
|
||||
this.lines.used.push(i);
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
for (let i = this.lines.min; i < this.lines.max; i++) {
|
||||
if (this.lines.used.indexOf(i) === -1) {
|
||||
missing.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length === 0) {
|
||||
this.lines.ready = true;
|
||||
this.lines.min = this.lines.max + 1;
|
||||
this.lines.used = [];
|
||||
} else {
|
||||
this.lines.ready = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.pushJobEvent = data => {
|
||||
this.lag++;
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => {
|
||||
if (!this.isActive()) {
|
||||
this.start();
|
||||
} else if (data.event === JOB_END) {
|
||||
if (this.isPaused()) {
|
||||
this.end(true);
|
||||
} else {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
this.checkLines(data);
|
||||
this.buffer(data);
|
||||
this.count++;
|
||||
|
||||
if (!this.isReadyToRender()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const events = this.page.emptyBuffer();
|
||||
this.count -= events.length;
|
||||
|
||||
return this.renderFrame(events);
|
||||
})
|
||||
.then(() => --this.lag);
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
|
||||
this.renderFrame = events => this.hooks.onEventFrame(events)
|
||||
.then(() => {
|
||||
if (this.scroll.isLocked()) {
|
||||
this.scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
if (this.isEnding()) {
|
||||
const lastEvents = this.page.emptyBuffer();
|
||||
|
||||
if (lastEvents.length) {
|
||||
return this.renderFrame(lastEvents);
|
||||
}
|
||||
|
||||
this.end(true);
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
this.resume = done => {
|
||||
if (done) {
|
||||
this.state.resuming = false;
|
||||
this.state.paused = false;
|
||||
} else if (!this.isTransitioning()) {
|
||||
this.scroll.pause();
|
||||
this.scroll.lock();
|
||||
this.scroll.scrollToBottom();
|
||||
this.state.resuming = true;
|
||||
this.page.removeBookmark();
|
||||
}
|
||||
};
|
||||
|
||||
this.pause = done => {
|
||||
if (done) {
|
||||
this.state.pausing = false;
|
||||
this.state.paused = true;
|
||||
this.scroll.resume();
|
||||
} else if (!this.isTransitioning()) {
|
||||
this.scroll.pause();
|
||||
this.scroll.unlock();
|
||||
this.state.pausing = true;
|
||||
this.page.setBookmark();
|
||||
}
|
||||
};
|
||||
|
||||
this.start = () => {
|
||||
if (!this.state.ending && !this.state.ended) {
|
||||
this.state.started = true;
|
||||
this.scroll.pause();
|
||||
this.scroll.lock();
|
||||
|
||||
this.hooks.onStart();
|
||||
}
|
||||
};
|
||||
|
||||
this.end = done => {
|
||||
if (done) {
|
||||
this.state.ending = false;
|
||||
this.state.ended = true;
|
||||
this.scroll.unlock();
|
||||
this.scroll.resume();
|
||||
|
||||
this.hooks.onStop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.ending = true;
|
||||
};
|
||||
|
||||
this.isReadyToRender = () => this.isDone() ||
|
||||
(!this.isPaused() && this.hasAllLines() && this.isBatchFull());
|
||||
this.hasAllLines = () => this.lines.ready;
|
||||
this.isBatchFull = () => this.count % this.framesPerRender === 0;
|
||||
this.isPaused = () => this.state.paused;
|
||||
this.isPausing = () => this.state.pausing;
|
||||
this.isResuming = () => this.state.resuming;
|
||||
this.isTransitioning = () => this.isActive() && (this.state.pausing || this.state.resuming);
|
||||
this.isActive = () => this.state.started && !this.state.ended;
|
||||
this.isEnding = () => this.state.ending;
|
||||
this.isDone = () => this.state.ended;
|
||||
}
|
||||
|
||||
JobEventEngine.$inject = ['$q'];
|
||||
|
||||
export default JobEventEngine;
|
||||
@ -15,6 +15,7 @@
|
||||
}
|
||||
.HostEvent .CodeMirror{
|
||||
overflow-x: hidden;
|
||||
max-height: none!important;
|
||||
}
|
||||
|
||||
.HostEvent-close:hover{
|
||||
@ -40,19 +40,19 @@
|
||||
|
||||
<div class="HostEvent-nav">
|
||||
<!-- view navigation buttons -->
|
||||
<button ui-sref="jobResult.host-event.json" type="button"
|
||||
<button ui-sref="jobz.host-event.json" type="button"
|
||||
class="btn btn-sm btn-default HostEvent-tab"
|
||||
ng-class="{'HostEvent-tab--selected' : isActiveState('jobResult.host-event.json')}">
|
||||
ng-class="{'HostEvent-tab--selected' : isActiveState('jobz.host-event.json')}">
|
||||
JSON
|
||||
</button>
|
||||
<button ng-if="stdout" ui-sref="jobResult.host-event.stdout"
|
||||
<button ng-if="stdout" ui-sref="jobz.host-event.stdout"
|
||||
type="button" class="btn btn-sm btn-default HostEvent-tab"
|
||||
ng-class="{'HostEvent-tab--selected' : isActiveState('jobResult.host-event.stdout')}">
|
||||
ng-class="{'HostEvent-tab--selected' : isActiveState('jobz.host-event.stdout')}">
|
||||
Standard Out
|
||||
</button>
|
||||
<button ng-if="stderr" ui-sref="jobResult.host-event.stderr"
|
||||
<button ng-if="stderr" ui-sref="jobz.host-event.stderr"
|
||||
type="button" class="btn btn-sm btn-default HostEvent-tab"
|
||||
ng-class="{'HostEvent-tab--selected' : isActiveState('jobResult.host-event.stderr')}">
|
||||
ng-class="{'HostEvent-tab--selected' : isActiveState('jobz.host-event.stderr')}">
|
||||
Standard Error
|
||||
</button>
|
||||
|
||||
@ -0,0 +1,170 @@
|
||||
function HostEventsController (
|
||||
$scope,
|
||||
$state,
|
||||
HostEventService,
|
||||
hostEvent
|
||||
) {
|
||||
$scope.processEventStatus = HostEventService.processEventStatus;
|
||||
$scope.processResults = processResults;
|
||||
$scope.isActiveState = isActiveState;
|
||||
$scope.getActiveHostIndex = getActiveHostIndex;
|
||||
$scope.closeHostEvent = closeHostEvent;
|
||||
|
||||
function init () {
|
||||
hostEvent.event_name = hostEvent.event;
|
||||
$scope.event = _.cloneDeep(hostEvent);
|
||||
|
||||
// grab standard out & standard error if present from the host
|
||||
// event's 'res' object, for things like Ansible modules. Small
|
||||
// wrinkle in this implementation is that the stdout/stderr tabs
|
||||
// should be shown if the `res` object has stdout/stderr keys, even
|
||||
// if they're a blank string. The presence of these keys is
|
||||
// potentially significant to a user.
|
||||
if (_.has(hostEvent.event_data, 'task_action')) {
|
||||
$scope.module_name = hostEvent.event_data.task_action;
|
||||
} else if (!_.has(hostEvent.event_data, 'task_action')) {
|
||||
$scope.module_name = 'No result found';
|
||||
}
|
||||
|
||||
if (_.has(hostEvent.event_data, 'res.result.stdout')) {
|
||||
if (hostEvent.event_data.res.stdout === '') {
|
||||
$scope.stdout = ' ';
|
||||
} else {
|
||||
$scope.stdout = hostEvent.event_data.res.stdout;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.has(hostEvent.event_data, 'res.result.stderr')) {
|
||||
if (hostEvent.event_data.res.stderr === '') {
|
||||
$scope.stderr = ' ';
|
||||
} else {
|
||||
$scope.stderr = hostEvent.event_data.res.stderr;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.has(hostEvent.event_data, 'res')) {
|
||||
$scope.json = hostEvent.event_data.res;
|
||||
}
|
||||
|
||||
if ($scope.module_name === 'debug' &&
|
||||
_.has(hostEvent.event_data, 'res.result.stdout')) {
|
||||
$scope.stdout = hostEvent.event_data.res.result.stdout;
|
||||
}
|
||||
if ($scope.module_name === 'yum' &&
|
||||
_.has(hostEvent.event_data, 'res.results') &&
|
||||
_.isArray(hostEvent.event_data.res.results)) {
|
||||
const event = hostEvent.event_data.res.results;
|
||||
$scope.stdout = event[0];// eslint-disable-line prefer-destructuring
|
||||
}
|
||||
// instantiate Codemirror
|
||||
if ($state.current.name === 'jobz.host-event.json') {
|
||||
try {
|
||||
if (_.has(hostEvent.event_data, 'res')) {
|
||||
initCodeMirror(
|
||||
'HostEvent-codemirror',
|
||||
JSON.stringify($scope.json, null, 4),
|
||||
{ name: 'javascript', json: true }
|
||||
);
|
||||
resize();
|
||||
} else {
|
||||
$scope.no_json = true;
|
||||
}
|
||||
} catch (err) {
|
||||
// element with id HostEvent-codemirror is not the view
|
||||
// controlled by this instance of HostEventController
|
||||
}
|
||||
} else if ($state.current.name === 'jobz.host-event.stdout') {
|
||||
try {
|
||||
resize();
|
||||
} catch (err) {
|
||||
// element with id HostEvent-codemirror is not the view
|
||||
// controlled by this instance of HostEventController
|
||||
}
|
||||
} else if ($state.current.name === 'jobz.host-event.stderr') {
|
||||
try {
|
||||
resize();
|
||||
} catch (err) {
|
||||
// element with id HostEvent-codemirror is not the view
|
||||
// controlled by this instance of HostEventController
|
||||
}
|
||||
}
|
||||
$('#HostEvent').modal('show');
|
||||
$('.modal-content').resizable({
|
||||
minHeight: 523,
|
||||
minWidth: 600
|
||||
});
|
||||
$('.modal-dialog').draggable({
|
||||
cancel: '.HostEvent-view--container'
|
||||
});
|
||||
|
||||
function resize () {
|
||||
if ($state.current.name === 'jobz.host-event.json') {
|
||||
const editor = $('.CodeMirror')[0].CodeMirror;
|
||||
const height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120;
|
||||
editor.setSize('100%', height);
|
||||
} else if ($state.current.name === 'jobz.host-event.stdout' || $state.current.name === 'jobz.host-event.stderr') {
|
||||
const height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120;
|
||||
$('.HostEvent-stdout').width('100%');
|
||||
$('.HostEvent-stdout').height(height);
|
||||
$('.HostEvent-stdoutContainer').height(height);
|
||||
$('.HostEvent-numberColumnPreload').height(height);
|
||||
}
|
||||
}
|
||||
|
||||
$('.modal-dialog').on('resize', resize);
|
||||
|
||||
$('#HostEvent').on('hidden.bs.modal', $scope.closeHostEvent);
|
||||
}
|
||||
|
||||
function processResults (value) {
|
||||
if (typeof value === 'object') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function initCodeMirror (el, data, mode) {
|
||||
const container = document.getElementById(el);
|
||||
const options = {};
|
||||
options.lineNumbers = true;
|
||||
options.mode = mode;
|
||||
options.readOnly = true;
|
||||
options.scrollbarStyle = null;
|
||||
const editor = CodeMirror.fromTextArea(// eslint-disable-line no-undef
|
||||
container,
|
||||
options
|
||||
);
|
||||
editor.setSize('100%', 200);
|
||||
editor.getDoc().setValue(data);
|
||||
}
|
||||
|
||||
function isActiveState (name) {
|
||||
return $state.current.name === name;
|
||||
}
|
||||
|
||||
function getActiveHostIndex () {
|
||||
function hostResultfilter (obj) {
|
||||
return obj.id === $scope.event.id;
|
||||
}
|
||||
const result = $scope.hostResults.filter(hostResultfilter);
|
||||
return $scope.hostResults.indexOf(result[0]);
|
||||
}
|
||||
|
||||
function closeHostEvent () {
|
||||
// Unbind the listener so it doesn't fire when we close the modal via navigation
|
||||
$('#HostEvent').off('hidden.bs.modal');
|
||||
$('#HostEvent').modal('hide');
|
||||
$state.go('jobz');
|
||||
}
|
||||
$scope.init = init;
|
||||
$scope.init();
|
||||
}
|
||||
|
||||
HostEventsController.$inject = [
|
||||
'$scope',
|
||||
'$state',
|
||||
'HostEventService',
|
||||
'hostEvent',
|
||||
];
|
||||
|
||||
module.exports = HostEventsController;
|
||||
72
awx/ui/client/features/output/host-event/host-event.route.js
Normal file
72
awx/ui/client/features/output/host-event/host-event.route.js
Normal file
@ -0,0 +1,72 @@
|
||||
const HostEventModalTemplate = require('~features/output/host-event/host-event-modal.partial.html');
|
||||
const HostEventCodeMirrorTemplate = require('~features/output/host-event/host-event-codemirror.partial.html');
|
||||
const HostEventStdoutTemplate = require('~features/output/host-event/host-event-stdout.partial.html');
|
||||
const HostEventStderrTemplate = require('~features/output/host-event/host-event-stderr.partial.html');
|
||||
|
||||
function exit () {
|
||||
// close the modal
|
||||
// using an onExit event to handle cases where the user navs away
|
||||
// using the url bar / back and not modal "X"
|
||||
$('#HostEvent').modal('hide');
|
||||
// hacky way to handle user browsing away via URL bar
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
|
||||
function HostEventResolve (HostEventService, $stateParams) {
|
||||
return HostEventService.getRelatedJobEvents($stateParams.id, {
|
||||
id: $stateParams.eventId
|
||||
}).then((response) => response.data.results[0]);
|
||||
}
|
||||
|
||||
HostEventResolve.$inject = [
|
||||
'HostEventService',
|
||||
'$stateParams',
|
||||
];
|
||||
|
||||
const hostEventModal = {
|
||||
name: 'jobz.host-event',
|
||||
url: '/host-event/:eventId',
|
||||
controller: 'HostEventsController',
|
||||
templateUrl: HostEventModalTemplate,
|
||||
abstract: false,
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
resolve: {
|
||||
hostEvent: HostEventResolve
|
||||
},
|
||||
onExit: exit
|
||||
};
|
||||
|
||||
const hostEventJson = {
|
||||
name: 'jobz.host-event.json',
|
||||
url: '/json',
|
||||
controller: 'HostEventsController',
|
||||
templateUrl: HostEventCodeMirrorTemplate,
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
};
|
||||
|
||||
const hostEventStdout = {
|
||||
name: 'jobz.host-event.stdout',
|
||||
url: '/stdout',
|
||||
controller: 'HostEventsController',
|
||||
templateUrl: HostEventStdoutTemplate,
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
};
|
||||
|
||||
const hostEventStderr = {
|
||||
name: 'jobz.host-event.stderr',
|
||||
url: '/stderr',
|
||||
controller: 'HostEventsController',
|
||||
templateUrl: HostEventStderrTemplate,
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
};
|
||||
|
||||
export { hostEventJson, hostEventModal, hostEventStdout, hostEventStderr };
|
||||
@ -0,0 +1,69 @@
|
||||
function HostEventService (
|
||||
Rest,
|
||||
ProcessErrors,
|
||||
GetBasePath,
|
||||
$rootScope
|
||||
) {
|
||||
// GET events related to a job run
|
||||
// e.g.
|
||||
// ?event=playbook_on_stats
|
||||
// ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter
|
||||
this.getRelatedJobEvents = (id, params) => {
|
||||
let url = GetBasePath('jobs');
|
||||
url = `${url}${id}/job_events/?${this.stringifyParams(params)}`;
|
||||
Rest.setUrl(url);
|
||||
return Rest.get()
|
||||
.then(response => response)
|
||||
.catch(({ data, status }) => {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: `Call to ${url}. GET returned: ${status}` });
|
||||
});
|
||||
};
|
||||
|
||||
this.stringifyParams = params => {
|
||||
function reduceFunction (result, value, key) {
|
||||
return `${result}${key}=${value}&`;
|
||||
}
|
||||
return _.reduce(params, reduceFunction, '');
|
||||
};
|
||||
|
||||
// Generate a helper class for job_event statuses
|
||||
// the stack for which status to display is
|
||||
// unreachable > failed > changed > ok
|
||||
// uses the API's runner events and convenience properties .failed .changed to determine status.
|
||||
// see: job_event_callback.py for more filters to support
|
||||
this.processEventStatus = event => {
|
||||
const obj = {};
|
||||
if (event.event === 'runner_on_unreachable') {
|
||||
obj.class = 'HostEvent-status--unreachable';
|
||||
obj.status = 'unreachable';
|
||||
}
|
||||
// equiv to 'runner_on_error' && 'runner on failed'
|
||||
if (event.failed) {
|
||||
obj.class = 'HostEvent-status--failed';
|
||||
obj.status = 'failed';
|
||||
}
|
||||
// catch the changed case before ok, because both can be true
|
||||
if (event.changed) {
|
||||
obj.class = 'HostEvent-status--changed';
|
||||
obj.status = 'changed';
|
||||
}
|
||||
if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok') {
|
||||
obj.class = 'HostEvent-status--ok';
|
||||
obj.status = 'ok';
|
||||
}
|
||||
if (event.event === 'runner_on_skipped') {
|
||||
obj.class = 'HostEvent-status--skipped';
|
||||
obj.status = 'skipped';
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
HostEventService.$inject = [
|
||||
'Rest',
|
||||
'ProcessErrors',
|
||||
'GetBasePath',
|
||||
'$rootScope'
|
||||
];
|
||||
export default HostEventService;
|
||||
26
awx/ui/client/features/output/host-event/index.js
Normal file
26
awx/ui/client/features/output/host-event/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import {
|
||||
hostEventModal,
|
||||
hostEventJson,
|
||||
hostEventStdout,
|
||||
hostEventStderr
|
||||
} from './host-event.route';
|
||||
import controller from './host-event.controller';
|
||||
import service from './host-event.service';
|
||||
|
||||
const MODULE_NAME = 'hostEvents';
|
||||
|
||||
function hostEventRun ($stateExtender) {
|
||||
$stateExtender.addState(hostEventModal);
|
||||
$stateExtender.addState(hostEventJson);
|
||||
$stateExtender.addState(hostEventStdout);
|
||||
$stateExtender.addState(hostEventStderr);
|
||||
}
|
||||
hostEventRun.$inject = [
|
||||
'$stateExtender'
|
||||
];
|
||||
|
||||
angular.module(MODULE_NAME, [])
|
||||
.controller('HostEventsController', controller)
|
||||
.service('HostEventService', service)
|
||||
.run(hostEventRun);
|
||||
export default MODULE_NAME;
|
||||
336
awx/ui/client/features/output/index.controller.js
Normal file
336
awx/ui/client/features/output/index.controller.js
Normal file
@ -0,0 +1,336 @@
|
||||
let $compile;
|
||||
let $q;
|
||||
let $scope;
|
||||
let page;
|
||||
let render;
|
||||
let resource;
|
||||
let scroll;
|
||||
let engine;
|
||||
let status;
|
||||
|
||||
let vm;
|
||||
|
||||
function JobsIndexController (
|
||||
_resource_,
|
||||
_page_,
|
||||
_scroll_,
|
||||
_render_,
|
||||
_engine_,
|
||||
_$scope_,
|
||||
_$compile_,
|
||||
_$q_,
|
||||
_status_,
|
||||
) {
|
||||
vm = this || {};
|
||||
|
||||
$compile = _$compile_;
|
||||
$scope = _$scope_;
|
||||
$q = _$q_;
|
||||
resource = _resource_;
|
||||
|
||||
page = _page_;
|
||||
scroll = _scroll_;
|
||||
render = _render_;
|
||||
engine = _engine_;
|
||||
status = _status_;
|
||||
|
||||
// Development helper(s)
|
||||
vm.clear = devClear;
|
||||
|
||||
// Expand/collapse
|
||||
// vm.toggle = toggle;
|
||||
// vm.expand = expand;
|
||||
vm.isExpanded = true;
|
||||
|
||||
// Panel
|
||||
vm.resource = resource;
|
||||
vm.title = resource.model.get('name');
|
||||
|
||||
// Stdout Navigation
|
||||
vm.scroll = {
|
||||
showBackToTop: false,
|
||||
home: scrollHome,
|
||||
end: scrollEnd,
|
||||
down: scrollPageDown,
|
||||
up: scrollPageUp
|
||||
};
|
||||
|
||||
render.requestAnimationFrame(() => init());
|
||||
}
|
||||
|
||||
function init () {
|
||||
status.init({
|
||||
resource,
|
||||
});
|
||||
|
||||
page.init({
|
||||
resource,
|
||||
});
|
||||
|
||||
render.init({
|
||||
get: () => resource.model.get(`related.${resource.related}.results`),
|
||||
compile: html => $compile(html)($scope),
|
||||
isStreamActive: engine.isActive,
|
||||
});
|
||||
|
||||
scroll.init({
|
||||
isAtRest: scrollIsAtRest,
|
||||
previous,
|
||||
next,
|
||||
});
|
||||
|
||||
engine.init({
|
||||
page,
|
||||
scroll,
|
||||
resource,
|
||||
onEventFrame (events) {
|
||||
return shift().then(() => append(events, true));
|
||||
},
|
||||
onStart () {
|
||||
status.resetCounts();
|
||||
status.setJobStatus('running');
|
||||
},
|
||||
onStop () {
|
||||
status.updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on(resource.ws.events, handleSocketEvent);
|
||||
$scope.$on(resource.ws.status, handleStatusEvent);
|
||||
|
||||
if (!status.isRunning()) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusEvent (scope, data) {
|
||||
status.pushStatusEvent(data);
|
||||
}
|
||||
|
||||
function handleSocketEvent (scope, data) {
|
||||
engine.pushJobEvent(data);
|
||||
|
||||
status.pushJobEvent(data);
|
||||
}
|
||||
|
||||
function devClear (pageMode) {
|
||||
init(pageMode);
|
||||
render.clear();
|
||||
}
|
||||
|
||||
function next () {
|
||||
return page.next()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return shift()
|
||||
.then(() => append(events))
|
||||
.then(() => {
|
||||
if (scroll.isMissing()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function previous () {
|
||||
const initialPosition = scroll.getScrollPosition();
|
||||
let postPopHeight;
|
||||
|
||||
return page.previous()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return pop()
|
||||
.then(() => {
|
||||
postPopHeight = scroll.getScrollHeight();
|
||||
|
||||
return prepend(events);
|
||||
})
|
||||
.then(() => {
|
||||
const currentHeight = scroll.getScrollHeight();
|
||||
scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function append (events, eng) {
|
||||
return render.append(events)
|
||||
.then(count => {
|
||||
page.updateLineCount(count, eng);
|
||||
});
|
||||
}
|
||||
|
||||
function prepend (events) {
|
||||
return render.prepend(events)
|
||||
.then(count => {
|
||||
page.updateLineCount(count);
|
||||
});
|
||||
}
|
||||
|
||||
function pop () {
|
||||
if (!page.isOverCapacity()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const lines = page.trim();
|
||||
|
||||
return render.pop(lines);
|
||||
}
|
||||
|
||||
function shift () {
|
||||
if (!page.isOverCapacity()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const lines = page.trim(true);
|
||||
|
||||
return render.shift(lines);
|
||||
}
|
||||
|
||||
function scrollHome () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return page.first()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return render.clear()
|
||||
.then(() => prepend(events))
|
||||
.then(() => {
|
||||
scroll.resetScrollPosition();
|
||||
scroll.resume();
|
||||
})
|
||||
.then(() => {
|
||||
if (scroll.isMissing()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollEnd () {
|
||||
if (engine.isActive()) {
|
||||
if (engine.isTransitioning()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (engine.isPaused()) {
|
||||
engine.resume();
|
||||
} else {
|
||||
engine.pause();
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
} else if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return page.last()
|
||||
.then(events => {
|
||||
if (!events) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return render.clear()
|
||||
.then(() => append(events))
|
||||
.then(() => {
|
||||
scroll.setScrollPosition(scroll.getScrollHeight());
|
||||
scroll.resume();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollPageUp () {
|
||||
if (scroll.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scroll.pageUp();
|
||||
}
|
||||
|
||||
function scrollPageDown () {
|
||||
if (scroll.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scroll.pageDown();
|
||||
}
|
||||
|
||||
function scrollIsAtRest (isAtRest) {
|
||||
vm.scroll.showBackToTop = !isAtRest;
|
||||
}
|
||||
|
||||
// function expand () {
|
||||
// vm.toggle(parent, true);
|
||||
// }
|
||||
|
||||
// function showHostDetails (id) {
|
||||
// jobEvent.request('get', id)
|
||||
// .then(() => {
|
||||
// const title = jobEvent.get('host_name');
|
||||
|
||||
// vm.host = {
|
||||
// menu: true,
|
||||
// stdout: jobEvent.get('stdout')
|
||||
// };
|
||||
|
||||
// $scope.jobs.modal.show(title);
|
||||
// });
|
||||
// }
|
||||
|
||||
// function toggle (uuid, menu) {
|
||||
// const lines = $(`.child-of-${uuid}`);
|
||||
// let icon = $(`#${uuid} .at-Stdout-toggle > i`);
|
||||
|
||||
// if (menu || record[uuid].level === 1) {
|
||||
// vm.isExpanded = !vm.isExpanded;
|
||||
// }
|
||||
|
||||
// if (record[uuid].children) {
|
||||
// icon = icon.add($(`#${record[uuid].children.join(', #')}`)
|
||||
// .find('.at-Stdout-toggle > i'));
|
||||
// }
|
||||
|
||||
// if (icon.hasClass('fa-angle-down')) {
|
||||
// icon.addClass('fa-angle-right');
|
||||
// icon.removeClass('fa-angle-down');
|
||||
|
||||
// lines.addClass('hidden');
|
||||
// } else {
|
||||
// icon.addClass('fa-angle-down');
|
||||
// icon.removeClass('fa-angle-right');
|
||||
|
||||
// lines.removeClass('hidden');
|
||||
// }
|
||||
// }
|
||||
|
||||
JobsIndexController.$inject = [
|
||||
'resource',
|
||||
'JobPageService',
|
||||
'JobScrollService',
|
||||
'JobRenderService',
|
||||
'JobEventEngine',
|
||||
'$scope',
|
||||
'$compile',
|
||||
'$q',
|
||||
'JobStatusService',
|
||||
];
|
||||
|
||||
module.exports = JobsIndexController;
|
||||
229
awx/ui/client/features/output/index.js
Normal file
229
awx/ui/client/features/output/index.js
Normal file
@ -0,0 +1,229 @@
|
||||
import atLibModels from '~models';
|
||||
import atLibComponents from '~components';
|
||||
|
||||
import Strings from '~features/output/jobs.strings';
|
||||
import Controller from '~features/output/index.controller';
|
||||
import PageService from '~features/output/page.service';
|
||||
import RenderService from '~features/output/render.service';
|
||||
import ScrollService from '~features/output/scroll.service';
|
||||
import EngineService from '~features/output/engine.service';
|
||||
import StatusService from '~features/output/status.service';
|
||||
|
||||
import DetailsDirective from '~features/output/details.directive';
|
||||
import SearchDirective from '~features/output/search.directive';
|
||||
import StatsDirective from '~features/output/stats.directive';
|
||||
import HostEvent from './host-event/index';
|
||||
|
||||
const Template = require('~features/output/index.view.html');
|
||||
|
||||
const MODULE_NAME = 'at.features.output';
|
||||
|
||||
const PAGE_CACHE = true;
|
||||
const PAGE_LIMIT = 5;
|
||||
const PAGE_SIZE = 50;
|
||||
const WS_PREFIX = 'ws';
|
||||
|
||||
function resolveResource (
|
||||
Job,
|
||||
ProjectUpdate,
|
||||
AdHocCommand,
|
||||
SystemJob,
|
||||
WorkflowJob,
|
||||
InventoryUpdate,
|
||||
$stateParams,
|
||||
qs,
|
||||
Wait
|
||||
) {
|
||||
const { id, type, job_event_search } = $stateParams; // eslint-disable-line camelcase
|
||||
const { name, key } = getWebSocketResource(type);
|
||||
|
||||
let Resource;
|
||||
let related = 'events';
|
||||
|
||||
switch (type) {
|
||||
case 'project':
|
||||
Resource = ProjectUpdate;
|
||||
break;
|
||||
case 'playbook':
|
||||
Resource = Job;
|
||||
related = 'job_events';
|
||||
break;
|
||||
case 'command':
|
||||
Resource = AdHocCommand;
|
||||
break;
|
||||
case 'system':
|
||||
Resource = SystemJob;
|
||||
break;
|
||||
case 'inventory':
|
||||
Resource = InventoryUpdate;
|
||||
break;
|
||||
// case 'workflow':
|
||||
// todo: integrate workflow chart components into this view
|
||||
// break;
|
||||
default:
|
||||
// Redirect
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = { page_size: PAGE_SIZE, order_by: 'start_line' };
|
||||
const config = { pageCache: PAGE_CACHE, pageLimit: PAGE_LIMIT, params };
|
||||
|
||||
if (job_event_search) { // eslint-disable-line camelcase
|
||||
const queryParams = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
||||
|
||||
Object.assign(config.params, queryParams);
|
||||
}
|
||||
|
||||
Wait('start');
|
||||
return new Resource(['get', 'options'], [id, id])
|
||||
.then(model => {
|
||||
const promises = [model.getStats()];
|
||||
|
||||
if (model.has('related.labels')) {
|
||||
promises.push(model.extend('get', 'labels'));
|
||||
}
|
||||
|
||||
promises.push(model.extend('get', related, config));
|
||||
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(([stats, model]) => ({
|
||||
id,
|
||||
type,
|
||||
stats,
|
||||
model,
|
||||
related,
|
||||
ws: {
|
||||
events: `${WS_PREFIX}-${key}-${id}`,
|
||||
status: `${WS_PREFIX}-${name}`,
|
||||
},
|
||||
page: {
|
||||
cache: PAGE_CACHE,
|
||||
size: PAGE_SIZE,
|
||||
pageLimit: PAGE_LIMIT
|
||||
}
|
||||
}))
|
||||
.catch(({ data, status }) => qs.error(data, status))
|
||||
.finally(() => Wait('stop'));
|
||||
}
|
||||
|
||||
function resolveWebSocketConnection ($stateParams, SocketService) {
|
||||
const { type, id } = $stateParams;
|
||||
const { name, key } = getWebSocketResource(type);
|
||||
|
||||
const state = {
|
||||
data: {
|
||||
socket: {
|
||||
groups: {
|
||||
[name]: ['status_changed', 'summary'],
|
||||
[key]: []
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return SocketService.addStateResolve(state, id);
|
||||
}
|
||||
|
||||
function resolveBreadcrumb (strings) {
|
||||
return {
|
||||
label: strings.get('state.TITLE')
|
||||
};
|
||||
}
|
||||
|
||||
function getWebSocketResource (type) {
|
||||
let name;
|
||||
let key;
|
||||
|
||||
switch (type) {
|
||||
case 'system':
|
||||
name = 'jobs';
|
||||
key = 'system_job_events';
|
||||
break;
|
||||
case 'project':
|
||||
name = 'jobs';
|
||||
key = 'project_update_events';
|
||||
break;
|
||||
case 'command':
|
||||
name = 'jobs';
|
||||
key = 'ad_hoc_command_events';
|
||||
break;
|
||||
case 'inventory':
|
||||
name = 'jobs';
|
||||
key = 'inventory_update_events';
|
||||
break;
|
||||
case 'playbook':
|
||||
name = 'jobs';
|
||||
key = 'job_events';
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported WebSocket type');
|
||||
}
|
||||
|
||||
return { name, key };
|
||||
}
|
||||
|
||||
function JobsRun ($stateRegistry) {
|
||||
const state = {
|
||||
name: 'jobz',
|
||||
url: '/jobz/:type/:id?job_event_search',
|
||||
route: '/jobz/:type/:id?job_event_search',
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'jobs'
|
||||
},
|
||||
views: {
|
||||
'@': {
|
||||
templateUrl: Template,
|
||||
controller: Controller,
|
||||
controllerAs: 'vm'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
resource: [
|
||||
'JobModel',
|
||||
'ProjectUpdateModel',
|
||||
'AdHocCommandModel',
|
||||
'SystemJobModel',
|
||||
'WorkflowJobModel',
|
||||
'InventoryUpdateModel',
|
||||
'$stateParams',
|
||||
'QuerySet',
|
||||
'Wait',
|
||||
resolveResource
|
||||
],
|
||||
ncyBreadcrumb: [
|
||||
'JobStrings',
|
||||
resolveBreadcrumb
|
||||
],
|
||||
webSocketConnection: [
|
||||
'$stateParams',
|
||||
'SocketService',
|
||||
resolveWebSocketConnection
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistry.register(state);
|
||||
}
|
||||
|
||||
JobsRun.$inject = ['$stateRegistry'];
|
||||
|
||||
angular
|
||||
.module(MODULE_NAME, [
|
||||
atLibModels,
|
||||
atLibComponents,
|
||||
HostEvent
|
||||
])
|
||||
.service('JobStrings', Strings)
|
||||
.service('JobPageService', PageService)
|
||||
.service('JobScrollService', ScrollService)
|
||||
.service('JobRenderService', RenderService)
|
||||
.service('JobEventEngine', EngineService)
|
||||
.service('JobStatusService', StatusService)
|
||||
.directive('atJobDetails', DetailsDirective)
|
||||
.directive('atJobSearch', SearchDirective)
|
||||
.directive('atJobStats', StatsDirective)
|
||||
.run(JobsRun);
|
||||
|
||||
export default MODULE_NAME;
|
||||
49
awx/ui/client/features/output/index.view.html
Normal file
49
awx/ui/client/features/output/index.view.html
Normal file
@ -0,0 +1,49 @@
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-4">
|
||||
<at-panel>
|
||||
<at-job-details resource="vm.resource"></at-job-details>
|
||||
</at-panel>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<at-panel class="at-Stdout">
|
||||
<div class="at-Panel-headingTitle">{{ vm.title }}</div>
|
||||
<at-job-stats resource="vm.resource"></at-job-stats>
|
||||
<at-job-search></at-job-search>
|
||||
|
||||
<div class="at-Stdout-menuTop">
|
||||
<div class="pull-left" ng-click="vm.expand()">
|
||||
<i class="at-Stdout-menuIcon fa"
|
||||
ng-class="{ 'fa-minus': vm.isExpanded, 'fa-plus': !vm.isExpanded }"></i>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" ng-click="vm.scroll.end()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||
ng-class=" { 'at-Stdout-menuIcon--active': vm.scroll.isLocked }"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.scroll.home()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.scroll.down()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.scroll.up()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
|
||||
</div>
|
||||
|
||||
<div class="at-u-clear"></div>
|
||||
</div>
|
||||
|
||||
<pre class="at-Stdout-container"><table><thead><tr><th class="at-Stdout-toggle"> </th><th class="at-Stdout-line"></th><th class="at-Stdout-event"></th></tr></thead><tbody id="atStdoutResultTable"></tbody></table></pre>
|
||||
|
||||
<div ng-show="vm.scroll.showBackToTop" class="at-Stdout-menuBottom">
|
||||
<div class="at-Stdout-menuIconGroup" ng-click="vm.scroll.home()">
|
||||
<p class="pull-left"><i class="fa fa-angle-double-up"></i></p>
|
||||
<p class="pull-right">Back to Top</p>
|
||||
</div>
|
||||
|
||||
<div class="at-u-clear"></div>
|
||||
</div>
|
||||
</at-panel>
|
||||
</div>
|
||||
</div>
|
||||
27
awx/ui/client/features/output/jobs.strings.js
Normal file
27
awx/ui/client/features/output/jobs.strings.js
Normal file
@ -0,0 +1,27 @@
|
||||
function JobsStrings (BaseString) {
|
||||
BaseString.call(this, 'jobs');
|
||||
|
||||
const { t } = this;
|
||||
const ns = this.jobs;
|
||||
|
||||
ns.state = {
|
||||
TITLE: t.s('JOBZ')
|
||||
};
|
||||
|
||||
ns.warnings = {
|
||||
CANCEL_ACTION: t.s('PROCEED'),
|
||||
CANCEL_BODY: t.s('Are you sure you want to cancel this job?'),
|
||||
CANCEL_HEADER: t.s('Cancel Job'),
|
||||
DELETE_BODY: t.s('Are you sure you want to delete this job?'),
|
||||
DELETE_HEADER: t.s('Delete Job'),
|
||||
};
|
||||
|
||||
ns.status = {
|
||||
RUNNING: t.s('The host status bar will update when the job is complete.'),
|
||||
UNAVAILABLE: t.s('Host status information for this job unavailable.'),
|
||||
};
|
||||
}
|
||||
|
||||
JobsStrings.$inject = ['BaseStringService'];
|
||||
|
||||
export default JobsStrings;
|
||||
298
awx/ui/client/features/output/page.service.js
Normal file
298
awx/ui/client/features/output/page.service.js
Normal file
@ -0,0 +1,298 @@
|
||||
function JobPageService ($q) {
|
||||
this.init = ({ resource }) => {
|
||||
this.resource = resource;
|
||||
|
||||
this.page = {
|
||||
limit: this.resource.page.pageLimit,
|
||||
size: this.resource.page.size,
|
||||
cache: [],
|
||||
state: {
|
||||
count: 0,
|
||||
current: 0,
|
||||
first: 0,
|
||||
last: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.bookmark = {
|
||||
pending: false,
|
||||
set: false,
|
||||
cache: [],
|
||||
state: {
|
||||
count: 0,
|
||||
first: 0,
|
||||
last: 0,
|
||||
current: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.result = {
|
||||
limit: this.page.limit * this.page.size,
|
||||
count: 0
|
||||
};
|
||||
|
||||
this.buffer = {
|
||||
count: 0
|
||||
};
|
||||
};
|
||||
|
||||
this.addPage = (number, events, push, reference) => {
|
||||
const page = { number, events, lines: 0 };
|
||||
reference = reference || this.getActiveReference();
|
||||
|
||||
if (push) {
|
||||
reference.cache.push(page);
|
||||
reference.state.last = page.number;
|
||||
reference.state.first = reference.cache[0].number;
|
||||
} else {
|
||||
reference.cache.unshift(page);
|
||||
reference.state.first = page.number;
|
||||
reference.state.last = reference.cache[reference.cache.length - 1].number;
|
||||
}
|
||||
|
||||
reference.state.current = page.number;
|
||||
reference.state.count++;
|
||||
};
|
||||
|
||||
this.addToBuffer = event => {
|
||||
const reference = this.getReference();
|
||||
const index = reference.cache.length - 1;
|
||||
let pageAdded = false;
|
||||
|
||||
if (this.result.count % this.page.size === 0) {
|
||||
this.addPage(reference.state.current + 1, [event], true, reference);
|
||||
|
||||
if (this.isBookmarkPending()) {
|
||||
this.setBookmark();
|
||||
}
|
||||
|
||||
this.trimBuffer();
|
||||
|
||||
pageAdded = true;
|
||||
} else {
|
||||
reference.cache[index].events.push(event);
|
||||
}
|
||||
|
||||
this.buffer.count++;
|
||||
this.result.count++;
|
||||
|
||||
return pageAdded;
|
||||
};
|
||||
|
||||
this.trimBuffer = () => {
|
||||
const reference = this.getReference();
|
||||
const diff = reference.cache.length - this.page.limit;
|
||||
|
||||
if (diff <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < diff; i++) {
|
||||
if (reference.cache[i].events) {
|
||||
this.buffer.count -= reference.cache[i].events.length;
|
||||
reference.cache[i].events.splice(0, reference.cache[i].events.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.isBufferFull = () => {
|
||||
if (this.buffer.count === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.emptyBuffer = () => {
|
||||
const reference = this.getReference();
|
||||
let data = [];
|
||||
|
||||
for (let i = 0; i < reference.cache.length; i++) {
|
||||
const count = reference.cache[i].events.length;
|
||||
|
||||
if (count > 0) {
|
||||
this.buffer.count -= count;
|
||||
data = data.concat(reference.cache[i].events.splice(0, count));
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
this.emptyCache = number => {
|
||||
const reference = this.getActiveReference();
|
||||
|
||||
number = number || reference.state.current;
|
||||
|
||||
reference.state.first = number;
|
||||
reference.state.last = number;
|
||||
reference.state.current = number;
|
||||
reference.cache.splice(0, reference.cache.length);
|
||||
};
|
||||
|
||||
this.isOverCapacity = () => {
|
||||
const reference = this.getActiveReference();
|
||||
|
||||
return (reference.cache.length - this.page.limit) > 0;
|
||||
};
|
||||
|
||||
this.trim = left => {
|
||||
const reference = this.getActiveReference();
|
||||
const excess = reference.cache.length - this.page.limit;
|
||||
|
||||
let ejected;
|
||||
|
||||
if (left) {
|
||||
ejected = reference.cache.splice(0, excess);
|
||||
reference.state.first = reference.cache[0].number;
|
||||
} else {
|
||||
ejected = reference.cache.splice(-excess);
|
||||
reference.state.last = reference.cache[reference.cache.length - 1].number;
|
||||
}
|
||||
|
||||
return ejected.reduce((total, page) => total + page.lines, 0);
|
||||
};
|
||||
|
||||
this.isPageBookmarked = number => number >= this.page.bookmark.first &&
|
||||
number <= this.page.bookmark.last;
|
||||
|
||||
this.updateLineCount = (lines, engine) => {
|
||||
let reference;
|
||||
|
||||
if (engine) {
|
||||
reference = this.getReference();
|
||||
} else {
|
||||
reference = this.getActiveReference();
|
||||
}
|
||||
|
||||
const index = reference.cache.findIndex(item => item.number === reference.state.current);
|
||||
|
||||
reference.cache[index].lines += lines;
|
||||
};
|
||||
|
||||
this.isBookmarkPending = () => this.bookmark.pending;
|
||||
this.isBookmarkSet = () => this.bookmark.set;
|
||||
|
||||
this.setBookmark = () => {
|
||||
if (this.isBookmarkSet()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isBookmarkPending()) {
|
||||
this.bookmark.pending = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.bookmark.state.first = this.page.state.first;
|
||||
this.bookmark.state.last = this.page.state.last - 1;
|
||||
this.bookmark.state.current = this.page.state.current - 1;
|
||||
this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache));
|
||||
this.bookmark.set = true;
|
||||
this.bookmark.pending = false;
|
||||
};
|
||||
|
||||
this.removeBookmark = () => {
|
||||
this.bookmark.set = false;
|
||||
this.bookmark.pending = false;
|
||||
this.bookmark.cache.splice(0, this.bookmark.cache.length);
|
||||
this.bookmark.state.first = 0;
|
||||
this.bookmark.state.last = 0;
|
||||
this.bookmark.state.current = 0;
|
||||
};
|
||||
|
||||
this.next = () => {
|
||||
const reference = this.getActiveReference();
|
||||
const config = this.buildRequestConfig(reference.state.last + 1);
|
||||
|
||||
return this.resource.model.goToPage(config)
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.addPage(data.page, [], true);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.previous = () => {
|
||||
const reference = this.getActiveReference();
|
||||
const config = this.buildRequestConfig(reference.state.first - 1);
|
||||
|
||||
return this.resource.model.goToPage(config)
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.addPage(data.page, [], false);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.last = () => {
|
||||
const config = this.buildRequestConfig('last');
|
||||
|
||||
return this.resource.model.goToPage(config)
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.emptyCache(data.page);
|
||||
this.addPage(data.page, [], true);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.first = () => {
|
||||
const config = this.buildRequestConfig('first');
|
||||
|
||||
return this.resource.model.goToPage(config)
|
||||
.then(data => {
|
||||
if (!data || !data.results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
this.emptyCache(data.page);
|
||||
this.addPage(data.page, [], false);
|
||||
|
||||
return data.results;
|
||||
});
|
||||
};
|
||||
|
||||
this.buildRequestConfig = number => ({
|
||||
page: number,
|
||||
related: this.resource.related,
|
||||
params: {
|
||||
order_by: 'start_line'
|
||||
}
|
||||
});
|
||||
|
||||
this.getActiveReference = () => (this.isBookmarkSet() ?
|
||||
this.getReference(true) : this.getReference());
|
||||
|
||||
this.getReference = (bookmark) => {
|
||||
if (bookmark) {
|
||||
return {
|
||||
bookmark: true,
|
||||
cache: this.bookmark.cache,
|
||||
state: this.bookmark.state
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bookmark: false,
|
||||
cache: this.page.cache,
|
||||
state: this.page.state
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
JobPageService.$inject = ['$q'];
|
||||
|
||||
export default JobPageService;
|
||||
288
awx/ui/client/features/output/render.service.js
Normal file
288
awx/ui/client/features/output/render.service.js
Normal file
@ -0,0 +1,288 @@
|
||||
import Ansi from 'ansi-to-html';
|
||||
import Entities from 'html-entities';
|
||||
|
||||
const ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
const EVENT_START_TASK = 'playbook_on_task_start';
|
||||
const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||
const EVENT_STATS_PLAY = 'playbook_on_stats';
|
||||
|
||||
const EVENT_GROUPS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY
|
||||
];
|
||||
|
||||
const TIME_EVENTS = [
|
||||
EVENT_START_TASK,
|
||||
EVENT_START_PLAY,
|
||||
EVENT_STATS_PLAY
|
||||
];
|
||||
|
||||
const ansi = new Ansi();
|
||||
const entities = new Entities.AllHtmlEntities();
|
||||
|
||||
// https://github.com/chalk/ansi-regex
|
||||
const pattern = [
|
||||
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)',
|
||||
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))'
|
||||
].join('|');
|
||||
|
||||
const re = new RegExp(pattern);
|
||||
const hasAnsi = input => re.test(input);
|
||||
|
||||
function JobRenderService ($q, $sce, $window) {
|
||||
this.init = ({ compile, apply, isStreamActive }) => {
|
||||
this.parent = null;
|
||||
this.record = {};
|
||||
this.el = $(ELEMENT_TBODY);
|
||||
this.hooks = { isStreamActive, compile, apply };
|
||||
};
|
||||
|
||||
this.sortByLineNumber = (a, b) => {
|
||||
if (a.start_line > b.start_line) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.start_line < b.start_line) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
this.transformEventGroup = events => {
|
||||
let lines = 0;
|
||||
let html = '';
|
||||
|
||||
events.sort(this.sortByLineNumber);
|
||||
|
||||
events.forEach(event => {
|
||||
const line = this.transformEvent(event);
|
||||
|
||||
html += line.html;
|
||||
lines += line.count;
|
||||
});
|
||||
|
||||
return { html, lines };
|
||||
};
|
||||
|
||||
this.transformEvent = event => {
|
||||
if (!event || !event.stdout) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const stdout = this.sanitize(event.stdout);
|
||||
const lines = stdout.split('\r\n');
|
||||
|
||||
let count = lines.length;
|
||||
let ln = event.start_line;
|
||||
|
||||
const current = this.createRecord(ln, lines, event);
|
||||
|
||||
const html = lines.reduce((concat, line, i) => {
|
||||
ln++;
|
||||
|
||||
const isLastLine = i === lines.length - 1;
|
||||
|
||||
let row = this.createRow(current, ln, line);
|
||||
|
||||
if (current && current.isTruncated && isLastLine) {
|
||||
row += this.createRow(current);
|
||||
count++;
|
||||
}
|
||||
|
||||
return `${concat}${row}`;
|
||||
}, '');
|
||||
|
||||
return { html, count };
|
||||
};
|
||||
|
||||
this.createRecord = (ln, lines, event) => {
|
||||
if (!event.uuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = {
|
||||
id: event.id,
|
||||
line: ln + 1,
|
||||
uuid: event.uuid,
|
||||
level: event.event_level,
|
||||
start: event.start_line,
|
||||
end: event.end_line,
|
||||
isTruncated: (event.end_line - event.start_line) > lines.length,
|
||||
isHost: typeof event.host === 'number'
|
||||
};
|
||||
|
||||
if (event.parent_uuid) {
|
||||
info.parents = this.getParentEvents(event.parent_uuid);
|
||||
}
|
||||
|
||||
if (info.isTruncated) {
|
||||
info.truncatedAt = event.start_line + lines.length;
|
||||
}
|
||||
|
||||
if (EVENT_GROUPS.includes(event.event)) {
|
||||
info.isParent = true;
|
||||
|
||||
if (event.event_level === 1) {
|
||||
this.parent = event.uuid;
|
||||
}
|
||||
|
||||
if (event.parent_uuid) {
|
||||
if (this.record[event.parent_uuid]) {
|
||||
if (this.record[event.parent_uuid].children &&
|
||||
!this.record[event.parent_uuid].children.includes(event.uuid)) {
|
||||
this.record[event.parent_uuid].children.push(event.uuid);
|
||||
} else {
|
||||
this.record[event.parent_uuid].children = [event.uuid];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TIME_EVENTS.includes(event.event)) {
|
||||
info.time = this.getTimestamp(event.created);
|
||||
info.line++;
|
||||
}
|
||||
|
||||
this.record[event.uuid] = info;
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
this.createRow = (current, ln, content) => {
|
||||
let id = '';
|
||||
let timestamp = '';
|
||||
let tdToggle = '';
|
||||
let tdEvent = '';
|
||||
let classList = '';
|
||||
|
||||
content = content || '';
|
||||
|
||||
if (hasAnsi(content)) {
|
||||
content = ansi.toHtml(content);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
if (!this.hooks.isStreamActive() && current.isParent && current.line === ln) {
|
||||
id = current.uuid;
|
||||
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
|
||||
}
|
||||
|
||||
if (current.isHost) {
|
||||
tdEvent = `<td class="at-Stdout-event--host" ui-sref="jobz.host-event.json({eventId: ${current.id}, taskUuid: '${current.uuid}' })">${content}</td>`;
|
||||
}
|
||||
|
||||
if (current.time && current.line === ln) {
|
||||
timestamp = `<span>${current.time}</span>`;
|
||||
}
|
||||
|
||||
if (current.parents) {
|
||||
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!tdEvent) {
|
||||
tdEvent = `<td class="at-Stdout-event">${content}</td>`;
|
||||
}
|
||||
|
||||
if (!tdToggle) {
|
||||
tdToggle = '<td class="at-Stdout-toggle"></td>';
|
||||
}
|
||||
|
||||
if (!ln) {
|
||||
ln = '...';
|
||||
}
|
||||
|
||||
return `
|
||||
<tr id="${id}" class="${classList}">
|
||||
${tdToggle}
|
||||
<td class="at-Stdout-line">${ln}</td>
|
||||
${tdEvent}
|
||||
<td class="at-Stdout-time">${timestamp}</td>
|
||||
</tr>`;
|
||||
};
|
||||
|
||||
this.getTimestamp = created => {
|
||||
const date = new Date(created);
|
||||
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
|
||||
const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
|
||||
const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds();
|
||||
|
||||
return `${hour}:${minute}:${second}`;
|
||||
};
|
||||
|
||||
this.getParentEvents = (uuid, list) => {
|
||||
list = list || [];
|
||||
|
||||
if (this.record[uuid]) {
|
||||
list.push(uuid);
|
||||
|
||||
if (this.record[uuid].parents) {
|
||||
list = list.concat(this.record[uuid].parents);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
this.getEvents = () => this.hooks.get();
|
||||
|
||||
this.insert = (events, insert) => {
|
||||
const result = this.transformEventGroup(events);
|
||||
const html = this.trustHtml(result.html);
|
||||
|
||||
return this.requestAnimationFrame(() => insert(html))
|
||||
.then(() => this.compile(html))
|
||||
.then(() => result.lines);
|
||||
};
|
||||
|
||||
this.remove = elements => this.requestAnimationFrame(() => {
|
||||
elements.remove();
|
||||
});
|
||||
|
||||
this.requestAnimationFrame = fn => $q(resolve => {
|
||||
$window.requestAnimationFrame(() => {
|
||||
if (fn) {
|
||||
fn();
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
this.compile = html => {
|
||||
html = $(this.el);
|
||||
this.hooks.compile(html);
|
||||
|
||||
return this.requestAnimationFrame();
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
const elements = this.el.children();
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.shift = lines => {
|
||||
const elements = this.el.children().slice(0, lines);
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.pop = lines => {
|
||||
const elements = this.el.children().slice(-lines);
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.prepend = events => this.insert(events, html => this.el.prepend(html));
|
||||
|
||||
this.append = events => this.insert(events, html => this.el.append(html));
|
||||
|
||||
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
|
||||
|
||||
this.sanitize = html => entities.encode(html);
|
||||
}
|
||||
|
||||
JobRenderService.$inject = ['$q', '$sce', '$window'];
|
||||
|
||||
export default JobRenderService;
|
||||
167
awx/ui/client/features/output/scroll.service.js
Normal file
167
awx/ui/client/features/output/scroll.service.js
Normal file
@ -0,0 +1,167 @@
|
||||
const ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||
const ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
const DELAY = 100;
|
||||
const THRESHOLD = 0.1;
|
||||
|
||||
function JobScrollService ($q, $timeout) {
|
||||
this.init = (hooks) => {
|
||||
this.el = $(ELEMENT_CONTAINER);
|
||||
this.timer = null;
|
||||
|
||||
this.position = {
|
||||
previous: 0,
|
||||
current: 0
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
isAtRest: hooks.isAtRest,
|
||||
next: hooks.next,
|
||||
previous: hooks.previous
|
||||
};
|
||||
|
||||
this.state = {
|
||||
locked: false,
|
||||
paused: false,
|
||||
top: true
|
||||
};
|
||||
|
||||
this.el.scroll(this.listen);
|
||||
};
|
||||
|
||||
this.listen = () => {
|
||||
if (this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timer) {
|
||||
$timeout.cancel(this.timer);
|
||||
}
|
||||
|
||||
this.timer = $timeout(this.register, DELAY);
|
||||
};
|
||||
|
||||
this.register = () => {
|
||||
this.pause();
|
||||
|
||||
const current = this.getScrollPosition();
|
||||
const downward = current > this.position.previous;
|
||||
|
||||
let promise;
|
||||
|
||||
if (downward && this.isBeyondThreshold(downward, current)) {
|
||||
promise = this.hooks.next;
|
||||
} else if (!downward && this.isBeyondThreshold(downward, current)) {
|
||||
promise = this.hooks.previous;
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
this.setScrollPosition(current);
|
||||
this.isAtRest();
|
||||
this.resume();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return promise()
|
||||
.then(() => {
|
||||
this.setScrollPosition(this.getScrollPosition());
|
||||
this.isAtRest();
|
||||
this.resume();
|
||||
});
|
||||
};
|
||||
|
||||
this.isBeyondThreshold = (downward, current) => {
|
||||
const height = this.getScrollHeight();
|
||||
|
||||
if (downward) {
|
||||
current += this.getViewableHeight();
|
||||
|
||||
if (current >= height || ((height - current) / height) < THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
} else if (current <= 0 || (current / height) < THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.pageUp = () => {
|
||||
if (this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top = this.getScrollPosition();
|
||||
const height = this.getViewableHeight();
|
||||
|
||||
this.setScrollPosition(top - height);
|
||||
};
|
||||
|
||||
this.pageDown = () => {
|
||||
if (this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top = this.getScrollPosition();
|
||||
const height = this.getViewableHeight();
|
||||
|
||||
this.setScrollPosition(top + height);
|
||||
};
|
||||
|
||||
this.getScrollHeight = () => this.el[0].scrollHeight;
|
||||
this.getViewableHeight = () => this.el[0].offsetHeight;
|
||||
this.getScrollPosition = () => this.el[0].scrollTop;
|
||||
|
||||
this.setScrollPosition = position => {
|
||||
this.position.previous = this.position.current;
|
||||
this.position.current = position;
|
||||
this.el[0].scrollTop = position;
|
||||
this.isAtRest();
|
||||
};
|
||||
|
||||
this.resetScrollPosition = () => {
|
||||
this.position.previous = 0;
|
||||
this.position.current = 0;
|
||||
this.el[0].scrollTop = 0;
|
||||
this.isAtRest();
|
||||
};
|
||||
|
||||
this.scrollToBottom = () => {
|
||||
this.setScrollPosition(this.getScrollHeight());
|
||||
};
|
||||
|
||||
this.isAtRest = () => {
|
||||
if (this.position.current === 0 && !this.state.top) {
|
||||
this.state.top = true;
|
||||
this.hooks.isAtRest(true);
|
||||
} else if (this.position.current > 0 && this.state.top) {
|
||||
this.state.top = false;
|
||||
this.hooks.isAtRest(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
this.state.paused = false;
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.state.paused = true;
|
||||
};
|
||||
|
||||
this.isPaused = () => this.state.paused;
|
||||
|
||||
this.lock = () => {
|
||||
this.state.locked = true;
|
||||
};
|
||||
|
||||
this.unlock = () => {
|
||||
this.state.locked = false;
|
||||
};
|
||||
|
||||
this.isLocked = () => this.state.locked;
|
||||
this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
}
|
||||
|
||||
JobScrollService.$inject = ['$q', '$timeout'];
|
||||
|
||||
export default JobScrollService;
|
||||
129
awx/ui/client/features/output/search.directive.js
Normal file
129
awx/ui/client/features/output/search.directive.js
Normal file
@ -0,0 +1,129 @@
|
||||
const templateUrl = require('~features/output/search.partial.html');
|
||||
|
||||
const searchReloadOptions = { reload: true, inherit: false };
|
||||
const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01'];
|
||||
const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play'];
|
||||
|
||||
const PLACEHOLDER_RUNNING = 'CANNOT SEARCH RUNNING JOB';
|
||||
const PLACEHOLDER_DEFAULT = 'SEARCH';
|
||||
|
||||
let $state;
|
||||
let status;
|
||||
let qs;
|
||||
|
||||
let vm;
|
||||
|
||||
function toggleSearchKey () {
|
||||
vm.key = !vm.key;
|
||||
}
|
||||
|
||||
function getCurrentQueryset () {
|
||||
const { job_event_search } = $state.params; // eslint-disable-line camelcase
|
||||
|
||||
return qs.decodeArr(job_event_search);
|
||||
}
|
||||
|
||||
function getSearchTags (queryset) {
|
||||
return qs.createSearchTagsFromQueryset(queryset)
|
||||
.filter(tag => !tag.startsWith('event'))
|
||||
.filter(tag => !tag.startsWith('-event'))
|
||||
.filter(tag => !tag.startsWith('page_size'))
|
||||
.filter(tag => !tag.startsWith('order_by'));
|
||||
}
|
||||
|
||||
function removeSearchTag (index) {
|
||||
const searchTerm = vm.tags[index];
|
||||
|
||||
const currentQueryset = getCurrentQueryset();
|
||||
const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm);
|
||||
|
||||
vm.tags = getSearchTags(modifiedQueryset);
|
||||
vm.disabled = true;
|
||||
|
||||
$state.params.job_event_search = qs.encodeArr(modifiedQueryset);
|
||||
$state.transitionTo($state.current, $state.params, searchReloadOptions);
|
||||
}
|
||||
|
||||
function submitSearch () {
|
||||
const searchInputQueryset = qs.getSearchInputQueryset(vm.value);
|
||||
|
||||
const currentQueryset = getCurrentQueryset();
|
||||
const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset);
|
||||
|
||||
vm.tags = getSearchTags(modifiedQueryset);
|
||||
vm.disabled = true;
|
||||
|
||||
$state.params.job_event_search = qs.encodeArr(modifiedQueryset);
|
||||
$state.transitionTo($state.current, $state.params, searchReloadOptions);
|
||||
}
|
||||
|
||||
function clearSearch () {
|
||||
vm.tags = [];
|
||||
vm.disabled = true;
|
||||
|
||||
$state.params.job_event_search = '';
|
||||
$state.transitionTo($state.current, $state.params, searchReloadOptions);
|
||||
}
|
||||
|
||||
function atJobSearchLink (scope, el, attrs, controllers) {
|
||||
const [atJobSearchController] = controllers;
|
||||
|
||||
atJobSearchController.init(scope);
|
||||
}
|
||||
|
||||
function AtJobSearchController (_$state_, _status_, _qs_) {
|
||||
$state = _$state_;
|
||||
status = _status_;
|
||||
qs = _qs_;
|
||||
|
||||
vm = this || {};
|
||||
|
||||
vm.value = '';
|
||||
vm.key = false;
|
||||
vm.rejected = false;
|
||||
vm.disabled = true;
|
||||
vm.tags = getSearchTags(getCurrentQueryset());
|
||||
|
||||
vm.clearSearch = clearSearch;
|
||||
vm.searchKeyExamples = searchKeyExamples;
|
||||
vm.searchKeyFields = searchKeyFields;
|
||||
vm.toggleSearchKey = toggleSearchKey;
|
||||
vm.removeSearchTag = removeSearchTag;
|
||||
vm.submitSearch = submitSearch;
|
||||
|
||||
vm.init = scope => {
|
||||
vm.examples = scope.examples || searchKeyExamples;
|
||||
vm.fields = scope.fields || searchKeyFields;
|
||||
vm.placeholder = PLACEHOLDER_DEFAULT;
|
||||
vm.relatedFields = scope.relatedFields || [];
|
||||
|
||||
scope.$watch(status.isRunning, value => {
|
||||
vm.disabled = value;
|
||||
vm.placeholder = value ? PLACEHOLDER_RUNNING : PLACEHOLDER_DEFAULT;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
AtJobSearchController.$inject = [
|
||||
'$state',
|
||||
'JobStatusService',
|
||||
'QuerySet',
|
||||
];
|
||||
|
||||
function atJobSearch () {
|
||||
return {
|
||||
templateUrl,
|
||||
restrict: 'E',
|
||||
require: ['atJobSearch'],
|
||||
controllerAs: 'vm',
|
||||
link: atJobSearchLink,
|
||||
controller: AtJobSearchController,
|
||||
scope: {
|
||||
examples: '=',
|
||||
fields: '=',
|
||||
relatedFields: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default atJobSearch;
|
||||
61
awx/ui/client/features/output/search.partial.html
Normal file
61
awx/ui/client/features/output/search.partial.html
Normal file
@ -0,0 +1,61 @@
|
||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||
<form ng-submit="vm.submitSearch()">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control at-Input"
|
||||
ng-class="{ 'at-Input--rejected': vm.rejected }"
|
||||
ng-model="vm.value"
|
||||
ng-attr-placeholder="{{ vm.placeholder }}"
|
||||
ng-disabled="vm.disabled">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||
ng-click="vm.submitSearch()"
|
||||
ng-disabled="vm.disabled"
|
||||
type="button">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
<button class="btn jobz-Button-searchKey"
|
||||
ng-if="vm.key"
|
||||
ng-disabled="vm.disabled"
|
||||
ng-click="vm.toggleSearchKey()"
|
||||
type="button"> key
|
||||
</button>
|
||||
<button class="btn at-ButtonHollow--default at-Input-button"
|
||||
ng-if="!vm.key"
|
||||
ng-disabled="vm.disabled"
|
||||
ng-click="vm.toggleSearchKey()"
|
||||
type="button"> key
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="jobz-tagz">
|
||||
<div class="LabelList-tagContainer" ng-repeat="tag in vm.tags track by $index">
|
||||
<div class="LabelList-tag LabelList-tag--deletable"><span class="LabelList-name">{{ tag }}</span></div>
|
||||
<div class="LabelList-deleteContainer" ng-click="vm.removeSearchTag($index)">
|
||||
<i class="fa fa-times LabelList-tagDelete"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div><a href class="jobz-searchClearAll" ng-click="vm.clearSearch()" ng-show="!(vm.tags | isEmpty)">CLEAR ALL</a></div>
|
||||
</div>
|
||||
|
||||
<div class="jobz-searchKeyPaneContainer" ng-show="vm.key">
|
||||
<div class="jobz-searchKeyPane">
|
||||
<div class="SmartSearch-keyRow">
|
||||
<div class="SmartSearch-examples">
|
||||
<div class="SmartSearch-examples--title"><b>EXAMPLES:</b></div>
|
||||
<div class="SmartSearch-examples--search" ng-repeat="tag in vm.examples"> {{ tag }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="SmartSearch-keyRow">
|
||||
<b>FIELDS:</b>
|
||||
<span ng-repeat="field in vm.fields">{{ field }}<span ng-if="!$last">, </span></span>
|
||||
</div>
|
||||
<div class="SmartSearch-keyRow">
|
||||
<b>ADDITIONAL INFORMATION:</b>
|
||||
For additional information on advanced search search syntax please see the Ansible Tower
|
||||
<a ng-attr-href="undefined" target="_blank">documentation</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
76
awx/ui/client/features/output/stats.directive.js
Normal file
76
awx/ui/client/features/output/stats.directive.js
Normal file
@ -0,0 +1,76 @@
|
||||
const templateUrl = require('~features/output/stats.partial.html');
|
||||
|
||||
let status;
|
||||
let strings;
|
||||
|
||||
function createStatsBarTooltip (key, count) {
|
||||
const label = `<span class='HostStatusBar-tooltipLabel'>${key}</span>`;
|
||||
const badge = `<span class='badge HostStatusBar-tooltipBadge HostStatusBar-tooltipBadge--${key}'>${count}</span>`;
|
||||
|
||||
return `${label}${badge}`;
|
||||
}
|
||||
|
||||
function atJobStatsLink (scope, el, attrs, controllers) {
|
||||
const [atJobStatsController] = controllers;
|
||||
|
||||
atJobStatsController.init(scope);
|
||||
}
|
||||
|
||||
function AtJobStatsController (_strings_, _status_) {
|
||||
status = _status_;
|
||||
strings = _strings_;
|
||||
|
||||
const vm = this || {};
|
||||
|
||||
vm.tooltips = {
|
||||
running: strings.get('status.RUNNING'),
|
||||
unavailable: strings.get('status.UNAVAILABLE'),
|
||||
};
|
||||
|
||||
vm.init = scope => {
|
||||
const { resource } = scope;
|
||||
|
||||
vm.download = resource.model.get('related.stdout');
|
||||
|
||||
vm.setHostStatusCounts(status.getHostStatusCounts());
|
||||
|
||||
scope.$watch(status.getPlayCount, value => { vm.plays = value; });
|
||||
scope.$watch(status.getTaskCount, value => { vm.tasks = value; });
|
||||
scope.$watch(status.getElapsed, value => { vm.elapsed = value; });
|
||||
scope.$watch(status.getHostCount, value => { vm.hosts = value; });
|
||||
scope.$watch(status.isRunning, value => { vm.running = value; });
|
||||
|
||||
scope.$watchCollection(status.getHostStatusCounts, vm.setHostStatusCounts);
|
||||
};
|
||||
|
||||
vm.setHostStatusCounts = counts => {
|
||||
Object.keys(counts).forEach(key => {
|
||||
const count = counts[key];
|
||||
const statusBarElement = $(`.HostStatusBar-${key}`);
|
||||
|
||||
statusBarElement.css('flex', `${count} 0 auto`);
|
||||
|
||||
vm.tooltips[key] = createStatsBarTooltip(key, count);
|
||||
});
|
||||
|
||||
vm.statsAreAvailable = Boolean(status.getStatsEvent());
|
||||
};
|
||||
}
|
||||
|
||||
function atJobStats () {
|
||||
return {
|
||||
templateUrl,
|
||||
restrict: 'E',
|
||||
require: ['atJobStats'],
|
||||
controllerAs: 'vm',
|
||||
link: atJobStatsLink,
|
||||
controller: [
|
||||
'JobStrings',
|
||||
'JobStatusService',
|
||||
AtJobStatsController
|
||||
],
|
||||
scope: { resource: '=', },
|
||||
};
|
||||
}
|
||||
|
||||
export default atJobStats;
|
||||
81
awx/ui/client/features/output/stats.partial.html
Normal file
81
awx/ui/client/features/output/stats.partial.html
Normal file
@ -0,0 +1,81 @@
|
||||
<!-- todo: styling, css etc. - disposition according to project lib conventions -->
|
||||
<div class="at-u-floatRight">
|
||||
<span class="at-Panel-label">plays</span>
|
||||
<span ng-show="!vm.plays" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="vm.plays" class="at-Panel-headingTitleBadge">{{ vm.plays }}</span>
|
||||
|
||||
<span class="at-Panel-label">tasks</span>
|
||||
<span ng-show="!vm.tasks" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="vm.tasks" class="at-Panel-headingTitleBadge">{{ vm.tasks }}</span>
|
||||
|
||||
<span class="at-Panel-label">hosts</span>
|
||||
<span ng-show="!vm.hosts" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="vm.hosts" class="at-Panel-headingTitleBadge">{{ vm.hosts }}</span>
|
||||
|
||||
<span class="at-Panel-label">elapsed</span>
|
||||
<span ng-show="!vm.elapsed" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="vm.elapsed" class="at-Panel-headingTitleBadge">
|
||||
{{ vm.elapsed * 1000 | duration: "hh:mm:ss" }}
|
||||
</span>
|
||||
|
||||
<a ng-show="vm.download && !vm.running" href="{{ vm.download }}?format=txt_download">
|
||||
<button class="btn at-Input-button at-u-noBorder"
|
||||
aw-tool-tip="{{ standardOutTooltip }}"
|
||||
data-tip-watch="standardOutTooltip"
|
||||
data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<button class="btn at-Input-button at-u-noBorder"
|
||||
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
|
||||
data-tip-watch="toggleStdoutFullscreenTooltip"
|
||||
data-placement="top"
|
||||
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
|
||||
ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="HostStatusBar">
|
||||
<div class="HostStatusBar-ok"
|
||||
ng-show="!vm.running"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{ vm.tooltips.ok }}"
|
||||
data-tip-watch="vm.tooltips.ok">
|
||||
</div>
|
||||
<div class="HostStatusBar-skipped"
|
||||
ng-show="!vm.running"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{ vm.tooltips.skipped }}"
|
||||
data-tip-watch="vm.tooltips.skipped">
|
||||
</div>
|
||||
<div class="HostStatusBar-changed"
|
||||
ng-show="!vm.running"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{ vm.tooltips.changed }}"
|
||||
data-tip-watch="vm.tooltips.changed">
|
||||
</div>
|
||||
<div class="HostStatusBar-failures"
|
||||
ng-show="!vm.running"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{ vm.tooltips.failures }}"
|
||||
data-tip-watch="vm.tooltips.failures">
|
||||
</div>
|
||||
<div class="HostStatusBar-dark"
|
||||
ng-show="!vm.running"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{ vm.tooltips.dark }}"
|
||||
data-tip-watch="vm.tooltips.dark">
|
||||
</div>
|
||||
<div class="HostStatusBar-noData"
|
||||
ng-show="vm.running"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{:: vm.tooltips.running }}">
|
||||
</div>
|
||||
<div class="HostStatusBar-noData"
|
||||
ng-show="!vm.running && !vm.statsAreAvailable"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{:: vm.tooltips.unavailable }}">
|
||||
</div>
|
||||
</div>
|
||||
149
awx/ui/client/features/output/status.service.js
Normal file
149
awx/ui/client/features/output/status.service.js
Normal file
@ -0,0 +1,149 @@
|
||||
const JOB_START = 'playbook_on_start';
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
const PLAY_START = 'playbook_on_play_start';
|
||||
const TASK_START = 'playbook_on_task_start';
|
||||
const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped'];
|
||||
|
||||
let moment;
|
||||
|
||||
function JobStatusService (_moment_) {
|
||||
moment = _moment_;
|
||||
|
||||
this.init = ({ resource }) => {
|
||||
this.counter = -1;
|
||||
|
||||
this.created = resource.model.get('created');
|
||||
this.job = resource.model.get('id');
|
||||
this.project = resource.model.get('project');
|
||||
this.elapsed = resource.model.get('elapsed');
|
||||
this.started = resource.model.get('started');
|
||||
this.finished = resource.model.get('finished');
|
||||
this.jobStatus = resource.model.get('status');
|
||||
this.projectStatus = resource.model.get('summary_fields.project_update.status');
|
||||
|
||||
this.playCount = null;
|
||||
this.taskCount = null;
|
||||
this.hostCount = null;
|
||||
this.active = false;
|
||||
this.hostStatusCounts = {};
|
||||
|
||||
this.statsEvent = resource.stats;
|
||||
this.updateStats();
|
||||
};
|
||||
|
||||
this.pushStatusEvent = data => {
|
||||
const isJobEvent = (this.job === data.unified_job_id);
|
||||
const isProjectEvent = (this.project && (this.project === data.project_id));
|
||||
|
||||
if (isJobEvent) {
|
||||
this.setJobStatus(data.status);
|
||||
} else if (isProjectEvent) {
|
||||
this.setProjectStatus(data.status);
|
||||
}
|
||||
};
|
||||
|
||||
this.pushJobEvent = data => {
|
||||
const isLatest = ((!this.counter) || (data.counter > this.counter));
|
||||
|
||||
if (!this.active && !(data.event === JOB_END)) {
|
||||
this.active = true;
|
||||
this.setJobStatus('running');
|
||||
}
|
||||
|
||||
if (isLatest) {
|
||||
this.counter = data.counter;
|
||||
this.elapsed = moment(data.created).diff(this.created, 'seconds');
|
||||
this.jobStatus = _.get(data, ['summary_fields', 'job', 'status']);
|
||||
}
|
||||
|
||||
if (data.event === JOB_START) {
|
||||
this.started = data.created;
|
||||
}
|
||||
|
||||
if (data.event === PLAY_START) {
|
||||
this.playCount++;
|
||||
}
|
||||
|
||||
if (data.event === TASK_START) {
|
||||
this.taskCount++;
|
||||
}
|
||||
|
||||
if (data.event === JOB_END) {
|
||||
this.statsEvent = data;
|
||||
}
|
||||
};
|
||||
|
||||
this.updateHostCounts = () => {
|
||||
const countedHostNames = [];
|
||||
|
||||
const counts = Object.assign(...HOST_STATUS_KEYS.map(key => ({ [key]: 0 })));
|
||||
|
||||
HOST_STATUS_KEYS.forEach(key => {
|
||||
const hostData = _.get(this.statsEvent, ['event_data', key], {});
|
||||
|
||||
Object.keys(hostData).forEach(hostName => {
|
||||
const isAlreadyCounted = (countedHostNames.indexOf(hostName) > -1);
|
||||
const shouldBeCounted = ((!isAlreadyCounted) && hostData[hostName] > 0);
|
||||
|
||||
if (shouldBeCounted) {
|
||||
countedHostNames.push(hostName);
|
||||
counts[key]++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.hostCount = countedHostNames.length;
|
||||
this.hostStatusCounts = counts;
|
||||
};
|
||||
|
||||
this.updateStats = () => {
|
||||
if (!this.statsEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateHostCounts();
|
||||
|
||||
this.setFinished(this.statsEvent.created);
|
||||
this.setJobStatus(this.statsEvent.failed ? 'failed' : 'successful');
|
||||
};
|
||||
|
||||
this.isRunning = () => (Boolean(this.started) && !this.finished) ||
|
||||
(this.jobStatus === 'running') ||
|
||||
(this.jobStatus === 'pending') ||
|
||||
(this.jobStatus === 'waiting');
|
||||
|
||||
this.getPlayCount = () => this.playCount;
|
||||
this.getTaskCount = () => this.taskCount;
|
||||
this.getHostCount = () => this.hostCount;
|
||||
this.getHostStatusCounts = () => this.hostStatusCounts || {};
|
||||
this.getJobStatus = () => this.jobStatus;
|
||||
this.getProjectStatus = () => this.projectStatus;
|
||||
this.getElapsed = () => this.elapsed;
|
||||
this.getStatsEvent = () => this.statsEvent;
|
||||
this.getStarted = () => this.started;
|
||||
this.getFinished = () => this.finished;
|
||||
|
||||
this.setJobStatus = status => {
|
||||
this.jobStatus = status;
|
||||
};
|
||||
|
||||
this.setProjectStatus = status => {
|
||||
this.projectStatus = status;
|
||||
};
|
||||
|
||||
this.setFinished = time => {
|
||||
this.finished = time;
|
||||
};
|
||||
|
||||
this.resetCounts = () => {
|
||||
this.playCount = 0;
|
||||
this.taskCount = 0;
|
||||
this.hostCount = 0;
|
||||
};
|
||||
}
|
||||
|
||||
JobStatusService.$inject = [
|
||||
'moment',
|
||||
];
|
||||
|
||||
export default JobStatusService;
|
||||
@ -41,7 +41,7 @@ function atLaunchTemplateCtrl (
|
||||
selectedJobTemplate
|
||||
.postLaunch({ id: vm.template.id })
|
||||
.then(({ data }) => {
|
||||
$state.go('jobResult', { id: data.job }, { reload: true });
|
||||
$state.go('jobz', { id: data.job, type: 'playbook' }, { reload: true });
|
||||
});
|
||||
} else {
|
||||
const promptData = {
|
||||
@ -138,7 +138,7 @@ function atLaunchTemplateCtrl (
|
||||
id: vm.promptData.template,
|
||||
launchData: jobLaunchData
|
||||
}).then((launchRes) => {
|
||||
$state.go('jobResult', { id: launchRes.data.job }, { reload: true });
|
||||
$state.go('jobz', { id: launchRes.data.job, type: 'playbook' }, { reload: true });
|
||||
}).catch(createErrorHandler('launch job template', 'POST'));
|
||||
} else if (vm.promptData.templateType === 'workflow_job_template') {
|
||||
workflowTemplate.create().postLaunch({
|
||||
|
||||
@ -12,7 +12,7 @@ function atModalLink (scope, el, attrs, controllers) {
|
||||
});
|
||||
}
|
||||
|
||||
function AtModalController (eventService, strings) {
|
||||
function AtModalController ($timeout, eventService, strings) {
|
||||
const vm = this;
|
||||
|
||||
let overlay;
|
||||
@ -58,6 +58,7 @@ function AtModalController (eventService, strings) {
|
||||
}
|
||||
|
||||
AtModalController.$inject = [
|
||||
'$timeout',
|
||||
'EventService',
|
||||
'ComponentsStrings'
|
||||
];
|
||||
|
||||
@ -44,3 +44,15 @@
|
||||
text-align: center;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.at-Panel-label {
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
font-size: 12px;
|
||||
font-weight: normal!important;
|
||||
width: 30%;
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
flex: 2.5 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +114,8 @@ function atRelaunchCtrl (
|
||||
jobObj.postRelaunch(launchParams)
|
||||
.then((launchRes) => {
|
||||
if (!$state.includes('jobs')) {
|
||||
$state.go('jobResult', { id: launchRes.data.id }, { reload: true });
|
||||
const relaunchType = launchRes.data.type === 'job' ? 'playbook' : launchRes.data.type;
|
||||
$state.go('jobz', { id: launchRes.data.id, type: relaunchType }, { reload: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -162,7 +163,7 @@ function atRelaunchCtrl (
|
||||
inventorySource.postUpdate(vm.job.inventory_source)
|
||||
.then((postUpdateRes) => {
|
||||
if (!$state.includes('jobs')) {
|
||||
$state.go('inventorySyncStdout', { id: postUpdateRes.data.id }, { reload: true });
|
||||
$state.go('jobz', { id: postUpdateRes.data.id, type: 'inventory' }, { reload: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -181,7 +182,7 @@ function atRelaunchCtrl (
|
||||
project.postUpdate(vm.job.project)
|
||||
.then((postUpdateRes) => {
|
||||
if (!$state.includes('jobs')) {
|
||||
$state.go('scmUpdateStdout', { id: postUpdateRes.data.id }, { reload: true });
|
||||
$state.go('jobz', { id: postUpdateRes.data.id, type: 'project' }, { reload: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -217,7 +218,7 @@ function atRelaunchCtrl (
|
||||
id: vm.job.id
|
||||
}).then((launchRes) => {
|
||||
if (!$state.includes('jobs')) {
|
||||
$state.go('adHocJobStdout', { id: launchRes.data.id }, { reload: true });
|
||||
$state.go('jobz', { id: launchRes.data.id, type: 'command' }, { reload: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -237,7 +238,7 @@ function atRelaunchCtrl (
|
||||
relaunchData: PromptService.bundlePromptDataForRelaunch(vm.promptData)
|
||||
}).then((launchRes) => {
|
||||
if (!$state.includes('jobs')) {
|
||||
$state.go('jobResult', { id: launchRes.data.job }, { reload: true });
|
||||
$state.go('jobz', { id: launchRes.data.job, type: 'playbook' }, { reload: true });
|
||||
}
|
||||
}).catch(({ data, status }) => {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
let Base;
|
||||
let $http;
|
||||
let BaseModel;
|
||||
|
||||
function getRelaunch (params) {
|
||||
const req = {
|
||||
@ -19,26 +19,31 @@ function postRelaunch (params) {
|
||||
return $http(req);
|
||||
}
|
||||
|
||||
function getStats () {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function AdHocCommandModel (method, resource, config) {
|
||||
Base.call(this, 'ad_hoc_commands');
|
||||
BaseModel.call(this, 'ad_hoc_commands');
|
||||
|
||||
this.Constructor = AdHocCommandModel;
|
||||
this.postRelaunch = postRelaunch.bind(this);
|
||||
this.getRelaunch = getRelaunch.bind(this);
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function AdHocCommandModelLoader (BaseModel, _$http_) {
|
||||
Base = BaseModel;
|
||||
function AdHocCommandModelLoader (_$http_, _BaseModel_) {
|
||||
$http = _$http_;
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return AdHocCommandModel;
|
||||
}
|
||||
|
||||
AdHocCommandModelLoader.$inject = [
|
||||
'$http',
|
||||
'BaseModel',
|
||||
'$http'
|
||||
];
|
||||
|
||||
export default AdHocCommandModelLoader;
|
||||
|
||||
@ -104,6 +104,25 @@ function httpGet (config = {}) {
|
||||
|
||||
if (config.params) {
|
||||
req.params = config.params;
|
||||
|
||||
if (config.params.page_size) {
|
||||
this.page.size = config.params.page_size;
|
||||
this.page.current = 1;
|
||||
|
||||
if (config.pageCache) {
|
||||
this.page.cachedPages = this.page.cachedPages || {};
|
||||
this.page.cache = this.page.cache || {};
|
||||
this.page.limit = config.pageLimit || false;
|
||||
|
||||
if (!_.has(this.page.cachedPages, 'root')) {
|
||||
this.page.cachedPages.root = [];
|
||||
}
|
||||
|
||||
if (!_.has(this.page.cache, 'root')) {
|
||||
this.page.cache.root = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof config.resource === 'object') {
|
||||
@ -118,6 +137,13 @@ function httpGet (config = {}) {
|
||||
.then(res => {
|
||||
this.model.GET = res.data;
|
||||
|
||||
if (config.pageCache) {
|
||||
this.page.cache.root[this.page.current] = res.data.results;
|
||||
this.page.cachedPages.root.push(this.page.current);
|
||||
this.page.count = res.data.count;
|
||||
this.page.last = Math.ceil(res.data.count / this.page.size);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
@ -328,24 +354,42 @@ function has (method, keys) {
|
||||
}
|
||||
|
||||
function extend (method, related, config = {}) {
|
||||
if (!related) {
|
||||
related = method;
|
||||
method = 'GET';
|
||||
} else {
|
||||
method = method.toUpperCase();
|
||||
const req = this.parseRequestConfig(method.toUpperCase(), config);
|
||||
|
||||
if (_.get(config, 'params.page_size')) {
|
||||
this.page.size = config.params.page_size;
|
||||
this.page.current = 1;
|
||||
|
||||
if (config.pageCache) {
|
||||
this.page.cachedPages = this.page.cachedPages || {};
|
||||
this.page.cache = this.page.cache || {};
|
||||
this.page.limit = config.pageLimit || false;
|
||||
|
||||
if (!_.has(this.page.cachedPages, `related.${related}`)) {
|
||||
_.set(this.page.cachedPages, `related.${related}`, []);
|
||||
}
|
||||
|
||||
if (!_.has(this.page.cache, `related.${related}`)) {
|
||||
_.set(this.page.cache, `related.${related}`, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.has(method, `related.${related}`)) {
|
||||
const req = {
|
||||
method,
|
||||
url: this.get(`related.${related}`)
|
||||
};
|
||||
if (this.has(req.method, `related.${related}`)) {
|
||||
req.url = this.get(`related.${related}`);
|
||||
|
||||
Object.assign(req, config);
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
this.set(method, `related.${related}`, data);
|
||||
this.set(req.method, `related.${related}`, data);
|
||||
|
||||
if (config.pageCache) {
|
||||
this.page.cache.related[related][this.page.current] = data.results;
|
||||
this.page.cachedPages.related[related].push(this.page.current);
|
||||
this.page.count = data.count;
|
||||
this.page.last = Math.ceil(data.count / this.page.size);
|
||||
}
|
||||
|
||||
return this;
|
||||
});
|
||||
@ -354,6 +398,97 @@ function extend (method, related, config = {}) {
|
||||
return Promise.reject(new Error(`No related property, ${related}, exists`));
|
||||
}
|
||||
|
||||
function goToPage (config) {
|
||||
const params = config.params || {};
|
||||
const { page } = config;
|
||||
|
||||
let url;
|
||||
let key;
|
||||
let pageNumber;
|
||||
let pageCache;
|
||||
let pagesInCache;
|
||||
|
||||
if (config.related) {
|
||||
url = `${this.endpoint}${config.related}/`;
|
||||
key = `related.${config.related}`;
|
||||
} else {
|
||||
url = this.endpoint;
|
||||
key = 'root';
|
||||
}
|
||||
|
||||
params.page_size = this.page.size;
|
||||
|
||||
if (page === 'next') {
|
||||
pageNumber = this.page.current + 1;
|
||||
} else if (page === 'previous') {
|
||||
pageNumber = this.page.current - 1;
|
||||
} else if (page === 'first') {
|
||||
pageNumber = 1;
|
||||
} else if (page === 'last') {
|
||||
pageNumber = this.page.last;
|
||||
} else {
|
||||
pageNumber = page;
|
||||
}
|
||||
|
||||
if (pageNumber < 1 || pageNumber > this.page.last) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
this.page.current = pageNumber;
|
||||
|
||||
if (this.page.cache) {
|
||||
pageCache = _.get(this.page.cache, key);
|
||||
pagesInCache = _.get(this.page.cachedPages, key);
|
||||
|
||||
if (_.has(pageCache, pageNumber)) {
|
||||
return Promise.resolve({
|
||||
results: pageCache[pageNumber],
|
||||
page: pageNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
params.page_size = this.page.size;
|
||||
params.page = pageNumber;
|
||||
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url,
|
||||
params
|
||||
};
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
if (pageCache) {
|
||||
pageCache[pageNumber] = data.results;
|
||||
pagesInCache.push(pageNumber);
|
||||
|
||||
if (pagesInCache.length > this.page.limit) {
|
||||
const pageToDelete = pagesInCache.shift();
|
||||
|
||||
delete pageCache[pageToDelete];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
results: data.results,
|
||||
page: pageNumber
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function next (config = {}) {
|
||||
config.page = 'next';
|
||||
|
||||
return this.goToPage(config);
|
||||
}
|
||||
|
||||
function prev (config = {}) {
|
||||
config.page = 'previous';
|
||||
|
||||
return this.goToPage(config);
|
||||
}
|
||||
|
||||
function normalizePath (resource) {
|
||||
const version = '/api/v2/';
|
||||
|
||||
@ -463,6 +598,10 @@ function create (method, resource, config) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (req.resource) {
|
||||
this.setEndpoint(req.resource);
|
||||
}
|
||||
|
||||
this.promise = this.request(req);
|
||||
|
||||
if (req.graft) {
|
||||
@ -473,6 +612,14 @@ function create (method, resource, config) {
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
function setEndpoint (resource) {
|
||||
if (Array.isArray(resource)) {
|
||||
this.endpoint = `${this.path}${resource[0]}/`;
|
||||
} else {
|
||||
this.endpoint = `${this.path}${resource}/`;
|
||||
}
|
||||
}
|
||||
|
||||
function parseRequestConfig (method, resource, config) {
|
||||
if (!method) {
|
||||
return null;
|
||||
@ -525,19 +672,23 @@ function BaseModel (resource, settings) {
|
||||
this.create = create;
|
||||
this.find = find;
|
||||
this.get = get;
|
||||
this.goToPage = goToPage;
|
||||
this.graft = graft;
|
||||
this.has = has;
|
||||
this.isEditable = isEditable;
|
||||
this.isCacheable = isCacheable;
|
||||
this.isCreatable = isCreatable;
|
||||
this.match = match;
|
||||
this.next = next;
|
||||
this.normalizePath = normalizePath;
|
||||
this.options = options;
|
||||
this.parseRequestConfig = parseRequestConfig;
|
||||
this.prev = prev;
|
||||
this.request = request;
|
||||
this.requestWithCache = requestWithCache;
|
||||
this.search = search;
|
||||
this.set = set;
|
||||
this.setEndpoint = setEndpoint;
|
||||
this.unset = unset;
|
||||
this.extend = extend;
|
||||
this.copy = copy;
|
||||
@ -552,6 +703,7 @@ function BaseModel (resource, settings) {
|
||||
delete: httpDelete.bind(this)
|
||||
};
|
||||
|
||||
this.page = {};
|
||||
this.model = {};
|
||||
this.path = this.normalizePath(resource);
|
||||
this.label = strings.get(`${resource}.LABEL`);
|
||||
|
||||
27
awx/ui/client/lib/models/InventoryUpdate.js
Normal file
27
awx/ui/client/lib/models/InventoryUpdate.js
Normal file
@ -0,0 +1,27 @@
|
||||
let BaseModel;
|
||||
|
||||
function getStats () {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function InventoryUpdateModel (method, resource, config) {
|
||||
BaseModel.call(this, 'inventory_updates');
|
||||
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
this.Constructor = InventoryUpdateModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function InventoryUpdateModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return InventoryUpdateModel;
|
||||
}
|
||||
|
||||
InventoryUpdateModelLoader.$inject = [
|
||||
'BaseModel'
|
||||
];
|
||||
|
||||
export default InventoryUpdateModelLoader;
|
||||
@ -1,5 +1,5 @@
|
||||
let Base;
|
||||
let $http;
|
||||
let BaseModel;
|
||||
|
||||
function getRelaunch (params) {
|
||||
const req = {
|
||||
@ -23,26 +23,53 @@ function postRelaunch (params) {
|
||||
return $http(req);
|
||||
}
|
||||
|
||||
function getStats () {
|
||||
if (!this.has('GET', 'id')) {
|
||||
return Promise.reject(new Error('No property, id, exists'));
|
||||
}
|
||||
|
||||
if (!this.has('GET', 'related.job_events')) {
|
||||
return Promise.reject(new Error('No related property, job_events, exists'));
|
||||
}
|
||||
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url: `${this.path}${this.get('id')}/job_events/`,
|
||||
params: { event: 'playbook_on_stats' },
|
||||
};
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
if (data.results.length > 0) {
|
||||
return data.results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function JobModel (method, resource, config) {
|
||||
Base.call(this, 'jobs');
|
||||
BaseModel.call(this, 'jobs');
|
||||
|
||||
this.Constructor = JobModel;
|
||||
|
||||
this.postRelaunch = postRelaunch.bind(this);
|
||||
this.getRelaunch = getRelaunch.bind(this);
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function JobModelLoader (BaseModel, _$http_) {
|
||||
Base = BaseModel;
|
||||
function JobModelLoader (_$http_, _BaseModel_) {
|
||||
$http = _$http_;
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return JobModel;
|
||||
}
|
||||
|
||||
JobModelLoader.$inject = [
|
||||
'$http',
|
||||
'BaseModel',
|
||||
'$http'
|
||||
];
|
||||
|
||||
export default JobModelLoader;
|
||||
|
||||
19
awx/ui/client/lib/models/JobEvent.js
Normal file
19
awx/ui/client/lib/models/JobEvent.js
Normal file
@ -0,0 +1,19 @@
|
||||
let BaseModel;
|
||||
|
||||
function JobEventModel (method, resource, config) {
|
||||
BaseModel.call(this, 'job_events');
|
||||
|
||||
this.Constructor = JobEventModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function JobEventModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return JobEventModel;
|
||||
}
|
||||
|
||||
JobEventModel.$inject = ['BaseModel'];
|
||||
|
||||
export default JobEventModelLoader;
|
||||
51
awx/ui/client/lib/models/ProjectUpdate.js
Normal file
51
awx/ui/client/lib/models/ProjectUpdate.js
Normal file
@ -0,0 +1,51 @@
|
||||
let $http;
|
||||
let BaseModel;
|
||||
|
||||
function getStats () {
|
||||
if (!this.has('GET', 'id')) {
|
||||
return Promise.reject(new Error('No property, id, exists'));
|
||||
}
|
||||
|
||||
if (!this.has('GET', 'related.events')) {
|
||||
return Promise.reject(new Error('No related property, events, exists'));
|
||||
}
|
||||
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url: `${this.path}${this.get('id')}/events/`,
|
||||
params: { event: 'playbook_on_stats' },
|
||||
};
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
if (data.results.length > 0) {
|
||||
return data.results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function ProjectUpdateModel (method, resource, config) {
|
||||
BaseModel.call(this, 'project_updates');
|
||||
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
this.Constructor = ProjectUpdateModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function ProjectUpdateModelLoader (_$http_, _BaseModel_) {
|
||||
$http = _$http_;
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return ProjectUpdateModel;
|
||||
}
|
||||
|
||||
ProjectUpdateModelLoader.$inject = [
|
||||
'$http',
|
||||
'BaseModel'
|
||||
];
|
||||
|
||||
export default ProjectUpdateModelLoader;
|
||||
25
awx/ui/client/lib/models/SystemJob.js
Normal file
25
awx/ui/client/lib/models/SystemJob.js
Normal file
@ -0,0 +1,25 @@
|
||||
let BaseModel;
|
||||
|
||||
function getStats () {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function SystemJobModel (method, resource, config) {
|
||||
BaseModel.call(this, 'system_jobs');
|
||||
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
this.Constructor = SystemJobModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function SystemJobModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return SystemJobModel;
|
||||
}
|
||||
|
||||
SystemJobModelLoader.$inject = ['BaseModel'];
|
||||
|
||||
export default SystemJobModelLoader;
|
||||
@ -11,20 +11,25 @@ import InstanceGroup from '~models/InstanceGroup';
|
||||
import Inventory from '~models/Inventory';
|
||||
import InventoryScript from '~models/InventoryScript';
|
||||
import InventorySource from '~models/InventorySource';
|
||||
import InventoryUpdate from '~models/InventoryUpdate';
|
||||
import Job from '~models/Job';
|
||||
import JobEvent from '~models/JobEvent';
|
||||
import JobTemplate from '~models/JobTemplate';
|
||||
import Me from '~models/Me';
|
||||
import ModelsStrings from '~models/models.strings';
|
||||
import NotificationTemplate from '~models/NotificationTemplate';
|
||||
import Organization from '~models/Organization';
|
||||
import Project from '~models/Project';
|
||||
import Schedule from '~models/Schedule';
|
||||
import ProjectUpdate from '~models/ProjectUpdate';
|
||||
import SystemJob from '~models/SystemJob';
|
||||
import UnifiedJobTemplate from '~models/UnifiedJobTemplate';
|
||||
import WorkflowJob from '~models/WorkflowJob';
|
||||
import WorkflowJobTemplate from '~models/WorkflowJobTemplate';
|
||||
import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode';
|
||||
import UnifiedJob from '~models/UnifiedJob';
|
||||
|
||||
import ModelsStrings from '~models/models.strings';
|
||||
|
||||
const MODULE_NAME = 'at.lib.models';
|
||||
|
||||
angular
|
||||
@ -42,18 +47,22 @@ angular
|
||||
.service('InventoryModel', Inventory)
|
||||
.service('InventoryScriptModel', InventoryScript)
|
||||
.service('InventorySourceModel', InventorySource)
|
||||
.service('InventoryUpdateModel', InventoryUpdate)
|
||||
.service('JobEventModel', JobEvent)
|
||||
.service('JobModel', Job)
|
||||
.service('JobTemplateModel', JobTemplate)
|
||||
.service('MeModel', Me)
|
||||
.service('ModelsStrings', ModelsStrings)
|
||||
.service('NotificationTemplate', NotificationTemplate)
|
||||
.service('OrganizationModel', Organization)
|
||||
.service('ProjectModel', Project)
|
||||
.service('ScheduleModel', Schedule)
|
||||
.service('UnifiedJobModel', UnifiedJob)
|
||||
.service('ProjectUpdateModel', ProjectUpdate)
|
||||
.service('SystemJobModel', SystemJob)
|
||||
.service('UnifiedJobTemplateModel', UnifiedJobTemplate)
|
||||
.service('WorkflowJobModel', WorkflowJob)
|
||||
.service('WorkflowJobTemplateModel', WorkflowJobTemplate)
|
||||
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode);
|
||||
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode)
|
||||
.service('ModelsStrings', ModelsStrings);
|
||||
|
||||
export default MODULE_NAME;
|
||||
|
||||
@ -50,6 +50,17 @@
|
||||
font-size: @at-font-size-body;
|
||||
}
|
||||
|
||||
.at-ButtonIcon-noborder {
|
||||
padding: 4px @at-padding-button-horizontal;
|
||||
font-size: @at-font-size-body;
|
||||
.at-mixin-Button();
|
||||
.at-mixin-ButtonHollow(
|
||||
'at-color-default',
|
||||
'at-color-default',
|
||||
'at-color-button-text-default'
|
||||
);
|
||||
}
|
||||
|
||||
.at-Button--expand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -16,3 +16,15 @@
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.at-u-clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.at-u-noBorder {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.at-u-floatRight {
|
||||
float: right
|
||||
}
|
||||
|
||||
@ -81,10 +81,6 @@
|
||||
@import '../../src/inventories-hosts/inventories/inventories.block.less';
|
||||
@import '../../src/inventories-hosts/shared/associate-groups/associate-groups.block.less';
|
||||
@import '../../src/inventories-hosts/shared/associate-hosts/associate-hosts.block.less';
|
||||
@import '../../src/job-results/host-event/host-event.block.less';
|
||||
@import '../../src/job-results/host-status-bar/host-status-bar.block.less';
|
||||
@import '../../src/job-results/job-results-stdout/job-results-stdout.block.less';
|
||||
@import '../../src/job-results/job-results.block.less';
|
||||
@import '../../src/job-submission/job-submission.block.less';
|
||||
@import '../../src/license/license.block.less';
|
||||
@import '../../src/login/loginModal/thirdPartySignOn/thirdPartySignOn.block.less';
|
||||
@ -117,7 +113,7 @@
|
||||
@import '../../src/shared/text-label';
|
||||
@import '../../src/shared/upgrade/upgrade.block.less';
|
||||
@import '../../src/smart-status/smart-status.block.less';
|
||||
@import '../../src/standard-out/standard-out.block.less';
|
||||
@import '../../src/workflow-results/standard-out.block.less';
|
||||
@import '../../src/system-tracking/date-picker/date-picker.block.less';
|
||||
@import '../../src/system-tracking/fact-data-table/fact-data-table.block.less';
|
||||
@import '../../src/system-tracking/fact-module-filter.block.less';
|
||||
|
||||
@ -19,7 +19,6 @@ import credentialTypes from './credential-types/main';
|
||||
import organizations from './organizations/main';
|
||||
import managementJobs from './management-jobs/main';
|
||||
import workflowResults from './workflow-results/main';
|
||||
import jobResults from './job-results/main';
|
||||
import jobSubmission from './job-submission/main';
|
||||
import notifications from './notifications/main';
|
||||
import about from './about/main';
|
||||
@ -30,7 +29,6 @@ import configuration from './configuration/main';
|
||||
import home from './home/main';
|
||||
import login from './login/main';
|
||||
import activityStream from './activity-stream/main';
|
||||
import standardOut from './standard-out/main';
|
||||
import Templates from './templates/main';
|
||||
import teams from './teams/main';
|
||||
import users from './users/main';
|
||||
@ -67,7 +65,6 @@ angular
|
||||
'gettext',
|
||||
'Timezones',
|
||||
'lrInfiniteScroll',
|
||||
|
||||
about.name,
|
||||
access.name,
|
||||
license.name,
|
||||
@ -86,10 +83,8 @@ angular
|
||||
login.name,
|
||||
activityStream.name,
|
||||
workflowResults.name,
|
||||
jobResults.name,
|
||||
jobSubmission.name,
|
||||
notifications.name,
|
||||
standardOut.name,
|
||||
Templates.name,
|
||||
portalMode.name,
|
||||
teams.name,
|
||||
@ -242,23 +237,6 @@ angular
|
||||
$rootScope.crumbCache = [];
|
||||
|
||||
$transitions.onStart({}, function(trans) {
|
||||
// Remove any lingering intervals
|
||||
// except on jobResults.* states
|
||||
var jobResultStates = [
|
||||
'jobResult',
|
||||
'jobResult.host-summary',
|
||||
'jobResult.host-event.details',
|
||||
'jobResult.host-event.json',
|
||||
'jobResult.host-events',
|
||||
'jobResult.host-event.stdout'
|
||||
];
|
||||
if ($rootScope.jobResultInterval && !_.includes(jobResultStates, trans.to().name) ) {
|
||||
window.clearInterval($rootScope.jobResultInterval);
|
||||
}
|
||||
if ($rootScope.jobStdOutInterval && !_.includes(jobResultStates, trans.to().name) ) {
|
||||
window.clearInterval($rootScope.jobStdOutInterval);
|
||||
}
|
||||
|
||||
$rootScope.flashMessage = null;
|
||||
|
||||
$('#form-modal2 .modal-body').empty();
|
||||
|
||||
@ -28,8 +28,17 @@ export default
|
||||
function createList(list) {
|
||||
// detailsUrl, status, name, time
|
||||
scope.jobs = _.map(list, function(job){
|
||||
|
||||
let detailsUrl;
|
||||
|
||||
if (job.type === 'workflow_job') {
|
||||
detailsUrl = `/#/workflows/${job.id}`;
|
||||
} else {
|
||||
detailsUrl = `/#/jobz/playbook/${job.id}`;
|
||||
}
|
||||
|
||||
return {
|
||||
detailsUrl: job.type && job.type === 'workflow_job' ? job.url.replace(/api\/v\d+\/workflow_jobs/, "#/workflows") : job.url.replace(/api\/v\d+/, "#"),
|
||||
detailsUrl,
|
||||
status: job.status,
|
||||
name: job.name,
|
||||
id: job.id,
|
||||
|
||||
@ -241,7 +241,7 @@ function adhocController($q, $scope, $stateParams,
|
||||
Rest.post(data)
|
||||
.then(({data}) => {
|
||||
Wait('stop');
|
||||
$state.go('adHocJobStdout', {id: data.id});
|
||||
$state.go('jobz', {id: data.id, type: 'command'});
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, adhocForm, {
|
||||
|
||||
@ -23,7 +23,7 @@ export default [ '$scope', 'Empty', 'Wait', 'GetBasePath', 'Rest', 'ProcessError
|
||||
};
|
||||
|
||||
$scope.viewJob = function(jobId) {
|
||||
$state.go('jobResult', {id: jobId});
|
||||
$state.go('jobz', { id: jobId, type: 'playbook' });
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export default [ '$scope', 'Wait', 'Empty', 'Rest', 'ProcessErrors', '$state',
|
||||
$scope.viewJob = function(url) {
|
||||
// Pull the id out of the URL
|
||||
var id = url.replace(/^\//, '').split('/')[3];
|
||||
$state.go('inventorySyncStdout', {id: id});
|
||||
$state.go('jobz', { id, type: 'inventory' } );
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ export default
|
||||
// Get the ID from the correct summary field
|
||||
var update_id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id;
|
||||
|
||||
$state.go('inventorySyncStdout', {id: update_id});
|
||||
$state.go('jobz', { id: update_id, type: 'inventory' });
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['jobResultsService', 'parseStdoutService', function(jobResultsService, parseStdoutService){
|
||||
var val = {};
|
||||
|
||||
val = {
|
||||
populateDefers: {},
|
||||
queue: {},
|
||||
// munge the raw event from the backend into the event_queue's format
|
||||
munge: function(event) {
|
||||
// basic data needed in the munged event
|
||||
var mungedEvent = {
|
||||
counter: event.counter,
|
||||
id: event.id,
|
||||
processed: false,
|
||||
name: event.event_name,
|
||||
changes: []
|
||||
};
|
||||
|
||||
// the interface for grabbing standard out is generalized and
|
||||
// present across many types of events, so go ahead and check for
|
||||
// updates to it
|
||||
if (event.stdout) {
|
||||
mungedEvent.stdout = parseStdoutService.parseStdout(event);
|
||||
mungedEvent.start_line = event.start_line + 1;
|
||||
mungedEvent.end_line = event.end_line + 1;
|
||||
mungedEvent.actual_end_line = parseStdoutService.actualEndLine(event) + 1;
|
||||
mungedEvent.changes.push('stdout');
|
||||
}
|
||||
|
||||
// for different types of events, you need different types of data
|
||||
if (event.event_name === 'playbook_on_start') {
|
||||
mungedEvent.startTime = event.modified;
|
||||
mungedEvent.changes.push('startTime');
|
||||
} if (event.event_name === 'playbook_on_stats') {
|
||||
// get the data for populating the host status bar
|
||||
mungedEvent.count = jobResultsService
|
||||
.getCountsFromStatsEvent(event.event_data);
|
||||
mungedEvent.finishedTime = event.modified;
|
||||
mungedEvent.changes.push('count');
|
||||
mungedEvent.changes.push('countFinished');
|
||||
mungedEvent.changes.push('finishedTime');
|
||||
}
|
||||
return mungedEvent;
|
||||
},
|
||||
// reinitializes the event queue value for the job results page
|
||||
initialize: function() {
|
||||
val.queue = {};
|
||||
val.populateDefers = {};
|
||||
},
|
||||
// populates the event queue
|
||||
populate: function(event) {
|
||||
if (event) {
|
||||
val.queue[event.counter] = val.munge(event);
|
||||
|
||||
if (!val.queue[event.counter].processed) {
|
||||
return val.munge(event);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// the event has been processed in the view and should be marked as
|
||||
// completed in the queue
|
||||
markProcessed: function(event) {
|
||||
val.queue[event.counter].processed = true;
|
||||
}
|
||||
};
|
||||
|
||||
return val;
|
||||
}];
|
||||
@ -1,143 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default
|
||||
['$scope', '$state', 'jobResultsService', 'hostEvent',
|
||||
function($scope, $state, jobResultsService, hostEvent){
|
||||
|
||||
$scope.processEventStatus = jobResultsService.processEventStatus;
|
||||
$scope.processResults = function(value){
|
||||
if (typeof value === 'object'){return false;}
|
||||
else {return true;}
|
||||
};
|
||||
|
||||
var initCodeMirror = function(el, data, mode){
|
||||
var container = document.getElementById(el);
|
||||
var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line
|
||||
lineNumbers: true,
|
||||
mode: mode,
|
||||
readOnly: true,
|
||||
scrollbarStyle: null
|
||||
});
|
||||
editor.setSize("100%", 200);
|
||||
editor.getDoc().setValue(data);
|
||||
};
|
||||
/*ignore jslint end*/
|
||||
$scope.isActiveState = function(name){
|
||||
return $state.current.name === name;
|
||||
};
|
||||
|
||||
$scope.getActiveHostIndex = function(){
|
||||
var result = $scope.hostResults.filter(function( obj ) {
|
||||
return obj.id === $scope.event.id;
|
||||
});
|
||||
return $scope.hostResults.indexOf(result[0]);
|
||||
};
|
||||
|
||||
$scope.closeHostEvent = function() {
|
||||
// Unbind the listener so it doesn't fire when we close the modal via navigation
|
||||
$('#HostEvent').off('hidden.bs.modal');
|
||||
$('#HostEvent').modal('hide');
|
||||
$state.go('jobResult');
|
||||
};
|
||||
|
||||
var init = function(){
|
||||
hostEvent.event_name = hostEvent.event;
|
||||
$scope.event = _.cloneDeep(hostEvent);
|
||||
|
||||
// grab standard out & standard error if present from the host
|
||||
// event's "res" object, for things like Ansible modules. Small
|
||||
// wrinkle in this implementation is that the stdout/stderr tabs
|
||||
// should be shown if the `res` object has stdout/stderr keys, even
|
||||
// if they're a blank string. The presence of these keys is
|
||||
// potentially significant to a user.
|
||||
try{
|
||||
$scope.module_name = hostEvent.event_data.task_action || "No result found";
|
||||
$scope.stdout = hostEvent.event_data.res.stdout ? hostEvent.event_data.res.stdout : hostEvent.event_data.res.stdout === "" ? " " : undefined;
|
||||
$scope.stderr = hostEvent.event_data.res.stderr ? hostEvent.event_data.res.stderr : hostEvent.event_data.res.stderr === "" ? " " : undefined;
|
||||
$scope.json = hostEvent.event_data.res;
|
||||
}
|
||||
catch(err){
|
||||
// do nothing, no stdout/stderr for this module
|
||||
}
|
||||
if($scope.module_name === "debug" &&
|
||||
_.has(hostEvent.event_data, "res.result.stdout")){
|
||||
$scope.stdout = hostEvent.event_data.res.result.stdout;
|
||||
}
|
||||
if($scope.module_name === "yum" &&
|
||||
_.has(hostEvent.event_data, "res.results") &&
|
||||
_.isArray(hostEvent.event_data.res.results)){
|
||||
$scope.stdout = hostEvent.event_data.res.results[0];
|
||||
}
|
||||
// instantiate Codemirror
|
||||
// try/catch pattern prevents the abstract-state controller from complaining about element being null
|
||||
if ($state.current.name === 'jobResult.host-event.json'){
|
||||
try{
|
||||
if(_.has(hostEvent.event_data, "res")){
|
||||
initCodeMirror('HostEvent-codemirror', JSON.stringify($scope.json, null, 4), {name: "javascript", json: true});
|
||||
resize();
|
||||
}
|
||||
else{
|
||||
$scope.no_json = true;
|
||||
}
|
||||
|
||||
}
|
||||
catch(err){
|
||||
// element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController
|
||||
}
|
||||
}
|
||||
else if ($state.current.name === 'jobResult.host-event.stdout'){
|
||||
try{
|
||||
resize();
|
||||
}
|
||||
catch(err){
|
||||
// element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController
|
||||
}
|
||||
}
|
||||
else if ($state.current.name === 'jobResult.host-event.stderr'){
|
||||
try{
|
||||
resize();
|
||||
}
|
||||
catch(err){
|
||||
// element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController
|
||||
}
|
||||
}
|
||||
$('#HostEvent').modal('show');
|
||||
$('.modal-content').resizable({
|
||||
minHeight: 523,
|
||||
minWidth: 600
|
||||
});
|
||||
$('.modal-dialog').draggable({
|
||||
cancel: '.HostEvent-view--container'
|
||||
});
|
||||
|
||||
function resize(){
|
||||
if ($state.current.name === 'jobResult.host-event.json'){
|
||||
let editor = $('.CodeMirror')[0].CodeMirror;
|
||||
let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120;
|
||||
editor.setSize("100%", height);
|
||||
}
|
||||
else if($state.current.name === 'jobResult.host-event.stdout' || $state.current.name === 'jobResult.host-event.stderr'){
|
||||
let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120;
|
||||
$(".HostEvent-stdout").width("100%");
|
||||
$(".HostEvent-stdout").height(height);
|
||||
$(".HostEvent-stdoutContainer").height(height);
|
||||
$(".HostEvent-numberColumnPreload").height(height);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$('.modal-dialog').on('resize', function(){
|
||||
resize();
|
||||
});
|
||||
|
||||
$('#HostEvent').on('hidden.bs.modal', function () {
|
||||
$scope.closeHostEvent();
|
||||
});
|
||||
};
|
||||
init();
|
||||
}];
|
||||
@ -1,66 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import { templateUrl } from '../../shared/template-url/template-url.factory';
|
||||
|
||||
var hostEventModal = {
|
||||
name: 'jobResult.host-event',
|
||||
url: '/host-event/:eventId',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-results/host-event/host-event-modal'),
|
||||
'abstract': false,
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
resolve: {
|
||||
hostEvent: ['jobResultsService', '$stateParams', function(jobResultsService, $stateParams) {
|
||||
return jobResultsService.getRelatedJobEvents($stateParams.id, {
|
||||
id: $stateParams.eventId
|
||||
}).then((response) => response.data.results[0]);
|
||||
}]
|
||||
},
|
||||
onExit: function() {
|
||||
// close the modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
$('#HostEvent').modal('hide');
|
||||
// hacky way to handle user browsing away via URL bar
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
};
|
||||
|
||||
var hostEventJson = {
|
||||
name: 'jobResult.host-event.json',
|
||||
url: '/json',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-results/host-event/host-event-codemirror'),
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
};
|
||||
|
||||
var hostEventStdout = {
|
||||
name: 'jobResult.host-event.stdout',
|
||||
url: '/stdout',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-results/host-event/host-event-stdout'),
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
};
|
||||
|
||||
var hostEventStderr = {
|
||||
name: 'jobResult.host-event.stderr',
|
||||
url: '/stderr',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-results/host-event/host-event-stderr'),
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export { hostEventJson, hostEventModal, hostEventStdout, hostEventStderr };
|
||||
@ -1,20 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {hostEventModal,
|
||||
hostEventJson, hostEventStdout, hostEventStderr} from './host-event.route';
|
||||
import controller from './host-event.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobResults.hostEvent', [])
|
||||
.controller('HostEventController', controller)
|
||||
|
||||
.run(['$stateExtender', function($stateExtender){
|
||||
$stateExtender.addState(hostEventModal);
|
||||
$stateExtender.addState(hostEventJson);
|
||||
$stateExtender.addState(hostEventStdout);
|
||||
$stateExtender.addState(hostEventStderr);
|
||||
}]);
|
||||
@ -1,79 +0,0 @@
|
||||
.HostStatusBar {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.HostStatusBar-ok,
|
||||
.HostStatusBar-changed,
|
||||
.HostStatusBar-unreachable,
|
||||
.HostStatusBar-failures,
|
||||
.HostStatusBar-skipped,
|
||||
.HostStatusBar-noData {
|
||||
height: 15px;
|
||||
border-top: 5px solid @default-bg;
|
||||
border-bottom: 5px solid @default-bg;
|
||||
}
|
||||
|
||||
.HostStatusBar-ok {
|
||||
background-color: @default-succ;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-changed {
|
||||
background-color: @default-warning;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-unreachable {
|
||||
background-color: @default-unreachable;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-failures {
|
||||
background-color: @default-err;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-skipped {
|
||||
background-color: @default-link;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-noData {
|
||||
background-color: @default-icon-hov;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipLabel {
|
||||
text-transform: uppercase;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge {
|
||||
border-radius: 5px;
|
||||
border: 1px solid @default-bg;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--ok {
|
||||
background-color: @default-succ;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--unreachable {
|
||||
background-color: @default-unreachable;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--skipped {
|
||||
background-color: @default-link;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--changed {
|
||||
background-color: @default-warning;
|
||||
}
|
||||
|
||||
.HostStatusBar-tooltipBadge--failures {
|
||||
background-color: @default-err;
|
||||
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
// import hostStatusBarController from './host-status-bar.controller';
|
||||
export default [ 'templateUrl',
|
||||
function(templateUrl) {
|
||||
return {
|
||||
scope: true,
|
||||
templateUrl: templateUrl('job-results/host-status-bar/host-status-bar'),
|
||||
restrict: 'E',
|
||||
// controller: standardOutLogController,
|
||||
link: function(scope) {
|
||||
// as count is changed by event data coming in,
|
||||
// update the host status bar
|
||||
var toDestroy = scope.$watch('count', function(val) {
|
||||
if (val) {
|
||||
Object.keys(val).forEach(key => {
|
||||
// reposition the hosts status bar by setting
|
||||
// the various flex values to the count of
|
||||
// those hosts
|
||||
$(`.HostStatusBar-${key}`)
|
||||
.css('flex', `${val[key]} 0 auto`);
|
||||
|
||||
// set the tooltip to give how many hosts of
|
||||
// each type
|
||||
if (val[key] > 0) {
|
||||
scope[`${key}CountTip`] = `<span class='HostStatusBar-tooltipLabel'>${key}</span><span class='badge HostStatusBar-tooltipBadge HostStatusBar-tooltipBadge--${key}'>${val[key]}</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
// if there are any hosts that have finished, don't
|
||||
// show default grey bar
|
||||
scope.hasCount = (Object
|
||||
.keys(val)
|
||||
.filter(key => (val[key] > 0)).length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
toDestroy();
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -1,30 +0,0 @@
|
||||
<div class="HostStatusBar">
|
||||
<div class="HostStatusBar-ok"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{okCountTip}}"
|
||||
data-tip-watch="okCountTip"></div>
|
||||
<div class="HostStatusBar-changed"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{changedCountTip}}"
|
||||
data-tip-watch="changedCountTip"></div>
|
||||
<div class="HostStatusBar-failures"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{failuresCountTip}}"
|
||||
data-tip-watch="failuresCountTip"></div>
|
||||
<div class="HostStatusBar-unreachable"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{unreachableCountTip}}"
|
||||
data-tip-watch="unreachableCountTip"></div>
|
||||
<div class="HostStatusBar-skipped"
|
||||
data-placement="top"
|
||||
aw-tool-tip="{{skippedCountTip}}"
|
||||
data-tip-watch="skippedCountTip"></div>
|
||||
<div class="HostStatusBar-noData"
|
||||
aw-tool-tip="The host status bar will update when the job is complete."
|
||||
ng-show="!hasCount && !jobFinished"
|
||||
data-placement="top"></div>
|
||||
<div class="HostStatusBar-noData"
|
||||
aw-tool-tip="No host status data was given for this job."
|
||||
ng-show="!hasCount && jobFinished"
|
||||
data-placement="top"></div>
|
||||
</div>
|
||||
@ -1,11 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import hostStatusBar from './host-status-bar.directive';
|
||||
|
||||
export default
|
||||
angular.module('hostStatusBarDirective', [])
|
||||
.directive('hostStatusBar', hostStatusBar);
|
||||
@ -1,271 +0,0 @@
|
||||
@breakpoint-md: 1200px;
|
||||
|
||||
.JobResultsStdOut {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-toolbar {
|
||||
flex: initial;
|
||||
display: flex;
|
||||
border-bottom: 0px;
|
||||
border-radius: 5px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-toolbarNumberColumn {
|
||||
background-color: @default-list-header-bg;
|
||||
color: @b7grey;
|
||||
flex: initial;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 70px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-top: 10px;
|
||||
border-top-left-radius: 5px;
|
||||
z-index: 1;
|
||||
border-right: 1px solid @d7grey;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-expandAllButton {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
padding-left: 4px;
|
||||
padding-top: 1px;
|
||||
border-radius: 50%;
|
||||
background-color: @default-bg;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: #848992;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-expandAllButton:hover .JobResultsStdOut-expandAllIcon,
|
||||
.JobResultsStdOut-expandAllIcon:hover {
|
||||
color: @default-data-txt;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-toolbarStdoutColumn {
|
||||
white-space: normal;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-right: 10px;
|
||||
background-color: @default-secondary-bg;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followButton {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
padding: 1px 0 0 4px;
|
||||
border-radius: 50%;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
background-color: @default-icon;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followIcon {
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followButton:hover {
|
||||
background-color: @default-data-txt;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followButton.is-engaged {
|
||||
background-color: @default-link;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followButton.is-engaged .JobResultsStdOut-followIcon {
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followButton.is-engaged:hover {
|
||||
background-color: @default-icon;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followButton.is-engaged:hover .JobResultsStdOut-followIcon,
|
||||
.JobResultsStdOut-followButton.is-engaged .JobResultsStdOut-followIcon:hover {
|
||||
color: @default-border;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutContainer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: @default-secondary-bg;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-numberColumnPreload {
|
||||
background-color: @default-list-header-bg;
|
||||
border-right: 1px solid @d7grey;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-aLineOfStdOut {
|
||||
display: flex;
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-lineExpander {
|
||||
text-align: left;
|
||||
padding-left: 11px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-lineExpanderIcon {
|
||||
font-size: 19px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-lineExpanderIcon:hover {
|
||||
color: @default-data-txt;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-lineNumberColumn {
|
||||
display: flex;
|
||||
background-color: @default-list-header-bg;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
width: 75px;
|
||||
flex: 1 0 70px;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
z-index: 1;
|
||||
border-right: 1px solid @d7grey;
|
||||
color: @default-icon;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutColumn {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
color: @default-interface-txt;
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
width:100%;
|
||||
background-color: @default-secondary-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutColumn--tooMany {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: @default-err;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutColumn--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-aLineOfStdOut:hover,
|
||||
.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-lineNumberColumn,
|
||||
.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-stdoutColumn {
|
||||
background-color: @default-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-lineNumberColumn {
|
||||
border-right: 1px solid @default-bg;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-footer {
|
||||
height: 20px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
background-color: @default-secondary-bg;
|
||||
border-top: 0px;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
overflow: hidden;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-footerNumberColumn {
|
||||
background-color: @default-list-header-bg;
|
||||
width: 70px;
|
||||
height: 100%;
|
||||
border-right: 1px solid @d7grey;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followAnchor {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-toTop {
|
||||
color: @default-icon;
|
||||
cursor: pointer;
|
||||
font-family: monaco;
|
||||
font-size: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResultsStdOut-toTop--numberColumn {
|
||||
background: @default-list-header-bg;
|
||||
height: 40px;
|
||||
width: 70px;
|
||||
border-right: 1px solid #D7D7D7;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-toTop:hover {
|
||||
color: @default-data-txt;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-cappedLine {
|
||||
color: @b7grey;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: @breakpoint-md) {
|
||||
.JobResultsStdOut-numberColumnPreload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-topAnchor {
|
||||
position: static;
|
||||
width: 100%;
|
||||
top: -20px;
|
||||
margin-top: -250px;
|
||||
margin-bottom: 250px;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-followAnchor {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutContainer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-lineAnchor {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@ -1,415 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
// import hostStatusBarController from './host-status-bar.controller';
|
||||
export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
function(templateUrl, $timeout, $location, $anchorScroll) {
|
||||
return {
|
||||
scope: false,
|
||||
templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'),
|
||||
restrict: 'E',
|
||||
link: function(scope, element) {
|
||||
var toDestroy = [],
|
||||
resizer,
|
||||
scrollWatcher;
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
$(window).off("resize", resizer);
|
||||
$(window).off("scroll", scrollWatcher);
|
||||
$(".JobResultsStdOut-stdoutContainer").off('scroll',
|
||||
scrollWatcher);
|
||||
toDestroy.forEach(closureFunc => closureFunc());
|
||||
});
|
||||
|
||||
scope.stdoutContainerAvailable.resolve("container available");
|
||||
// utility function used to find the top visible line and
|
||||
// parent header in the pane
|
||||
//
|
||||
// note that while this function is called when in mobile width
|
||||
// the line anchor is not displayed in the css so calls
|
||||
// to lineAnchor do nothing
|
||||
var findTopLines = function() {
|
||||
var $container = $('.JobResultsStdOut-stdoutContainer');
|
||||
|
||||
// get the first visible line's head element
|
||||
var getHeadElement = function (line) {
|
||||
var lineHasHeaderClass = !!(line
|
||||
.hasClass("header_play") ||
|
||||
line.hasClass("header_task"));
|
||||
var lineClassList;
|
||||
var lineUUIDClass;
|
||||
|
||||
if (lineHasHeaderClass) {
|
||||
// find head element when the first visible
|
||||
// line is a header
|
||||
|
||||
lineClassList = line.attr("class")
|
||||
.split(" ");
|
||||
|
||||
// get the header class w task uuid...
|
||||
lineUUIDClass = lineClassList
|
||||
.filter(n => n
|
||||
.indexOf("header_task_") > -1)[0];
|
||||
|
||||
// ...if that doesn't exist get the one
|
||||
// w play uuid
|
||||
if (!lineUUIDClass) {
|
||||
lineUUIDClass = lineClassList
|
||||
.filter(n => n.
|
||||
indexOf("header_play_") > -1)[0];
|
||||
}
|
||||
|
||||
// get the header line (not their might
|
||||
// be more than one, so get the one with
|
||||
// the actual header class)
|
||||
//
|
||||
// TODO it might be better in this case to just
|
||||
// return `line` (less jumping with a cowsay
|
||||
// case)
|
||||
return $(".actual_header." +
|
||||
lineUUIDClass);
|
||||
} else {
|
||||
// find head element when the first visible
|
||||
// line is not a header
|
||||
|
||||
lineClassList = line.attr("class")
|
||||
.split(" ");
|
||||
|
||||
// get the class w task uuid...
|
||||
lineUUIDClass = lineClassList
|
||||
.filter(n => n
|
||||
.indexOf("task_") > -1)[0];
|
||||
|
||||
// ...if that doesn't exist get the one
|
||||
// w play uuid
|
||||
if (!lineUUIDClass) {
|
||||
lineUUIDClass = lineClassList
|
||||
.filter(n => n
|
||||
.indexOf("play_") > -1)[0];
|
||||
}
|
||||
|
||||
// get the header line (not their might
|
||||
// be more than one, so get the one with
|
||||
// the actual header class)
|
||||
return $(".actual_header.header_" +
|
||||
lineUUIDClass);
|
||||
}
|
||||
};
|
||||
|
||||
var visItem,
|
||||
parentItem;
|
||||
|
||||
// iterate through each line of standard out
|
||||
$container.find('.JobResultsStdOut-aLineOfStdOut:visible')
|
||||
.each( function () {
|
||||
var $this = $(this);
|
||||
|
||||
// check to see if the line is the first visible
|
||||
// line in the viewport...
|
||||
if ($this.position().top >= 0) {
|
||||
|
||||
// ...if it is, return the line number
|
||||
// for this line
|
||||
visItem = parseInt($($this
|
||||
.children()[0])
|
||||
.text());
|
||||
|
||||
// as well as the line number for it's
|
||||
// closest parent header line
|
||||
var $head = getHeadElement($this);
|
||||
parentItem = parseInt($($head
|
||||
.children()[0])
|
||||
.text());
|
||||
|
||||
// stop iterating over the standard out
|
||||
// lines once the first one has been
|
||||
// found
|
||||
|
||||
$this = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this = null;
|
||||
});
|
||||
|
||||
$container = null;
|
||||
|
||||
return {
|
||||
visLine: visItem,
|
||||
parentVisLine: parentItem
|
||||
};
|
||||
};
|
||||
|
||||
// find if window is initially mobile or desktop width
|
||||
if (window.innerWidth <= 1200) {
|
||||
scope.isMobile = true;
|
||||
} else {
|
||||
scope.isMobile = false;
|
||||
}
|
||||
|
||||
resizer = function() {
|
||||
// and update the isMobile var accordingly
|
||||
if (window.innerWidth <= 1200 && !scope.isMobile) {
|
||||
scope.isMobile = true;
|
||||
} else if (window.innerWidth > 1200 & scope.isMobile) {
|
||||
scope.isMobile = false;
|
||||
}
|
||||
};
|
||||
// watch changes to the window size
|
||||
$(window).resize(resizer);
|
||||
|
||||
var lastScrollTop;
|
||||
|
||||
var initScrollTop = function() {
|
||||
lastScrollTop = 0;
|
||||
};
|
||||
scrollWatcher = function() {
|
||||
var st = $(this).scrollTop();
|
||||
var netScroll = st + $(this).innerHeight();
|
||||
var fullHeight;
|
||||
|
||||
if (st < lastScrollTop){
|
||||
// user up scrolled, so disengage follow
|
||||
scope.followEngaged = false;
|
||||
}
|
||||
|
||||
if (scope.isMobile) {
|
||||
// for mobile the height is the body of the entire
|
||||
// page
|
||||
fullHeight = $("body").height();
|
||||
} else {
|
||||
// for desktop the height is the body of the
|
||||
// stdoutContainer, minus the "^ TOP" indicator
|
||||
fullHeight = $(this)[0].scrollHeight - 25;
|
||||
}
|
||||
|
||||
if(netScroll >= fullHeight) {
|
||||
// user scrolled all the way to bottom, so engage
|
||||
// follow
|
||||
scope.followEngaged = true;
|
||||
}
|
||||
|
||||
// pane is now overflowed, show top indicator.
|
||||
if (st > 0) {
|
||||
scope.stdoutOverflowed = true;
|
||||
}
|
||||
|
||||
lastScrollTop = st;
|
||||
|
||||
st = null;
|
||||
netScroll = null;
|
||||
fullHeight = null;
|
||||
};
|
||||
|
||||
// update scroll watchers when isMobile changes based on
|
||||
// window resize
|
||||
toDestroy.push(scope.$watch('isMobile', function(val) {
|
||||
if (val === true) {
|
||||
// make sure ^ TOP always shown for mobile
|
||||
scope.stdoutOverflowed = true;
|
||||
|
||||
// unbind scroll watcher on standard out container
|
||||
$(".JobResultsStdOut-stdoutContainer")
|
||||
.unbind('scroll');
|
||||
|
||||
// init scroll watcher on window
|
||||
initScrollTop();
|
||||
$(window).on('scroll', scrollWatcher);
|
||||
|
||||
} else if (val === false) {
|
||||
// unbind scroll watcher on window
|
||||
$(window).unbind('scroll');
|
||||
|
||||
// init scroll watcher on standard out container
|
||||
initScrollTop();
|
||||
$(".JobResultsStdOut-stdoutContainer").on('scroll',
|
||||
scrollWatcher);
|
||||
}
|
||||
}));
|
||||
|
||||
// called to scroll to follow anchor
|
||||
scope.followScroll = function() {
|
||||
// a double-check to make sure the follow anchor is at
|
||||
// the bottom of the standard out container
|
||||
$(".JobResultsStdOut-followAnchor")
|
||||
.appendTo(".JobResultsStdOut-stdoutContainer");
|
||||
|
||||
$location.hash('followAnchor');
|
||||
$anchorScroll();
|
||||
};
|
||||
|
||||
// called to scroll to top of standard out (when "^ TOP" is
|
||||
// clicked)
|
||||
scope.toTop = function() {
|
||||
$location.hash('topAnchor');
|
||||
$anchorScroll();
|
||||
};
|
||||
|
||||
// called to scroll to the current line anchor
|
||||
// when expand all/collapse all/filtering is engaged
|
||||
//
|
||||
// note that while this function can be called when in mobile
|
||||
// width the line anchor is not displayed in the css so those
|
||||
// calls do nothing
|
||||
scope.lineAnchor = function() {
|
||||
$location.hash('lineAnchor');
|
||||
$anchorScroll();
|
||||
};
|
||||
|
||||
// if following becomes active, go ahead and get to the bottom
|
||||
// of the standard out pane
|
||||
toDestroy.push(scope.$watch('followEngaged', function(val) {
|
||||
// scroll to follow point if followEngaged is true
|
||||
if (val) {
|
||||
scope.followScroll();
|
||||
}
|
||||
|
||||
// set up tooltip changes for not finsihed job
|
||||
if (!scope.jobFinished) {
|
||||
if (val) {
|
||||
scope.followTooltip = "Currently following standard out as it comes in. Click to unfollow.";
|
||||
} else {
|
||||
scope.followTooltip = "Click to follow standard out as it comes in.";
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// follow button ng-click function
|
||||
scope.followToggleClicked = function() {
|
||||
if (scope.jobFinished) {
|
||||
// when the job is finished engage followScroll
|
||||
scope.followScroll();
|
||||
} else {
|
||||
// when the job is not finished toggle followEngaged
|
||||
// which is watched above
|
||||
scope.followEngaged = !scope.followEngaged;
|
||||
}
|
||||
};
|
||||
|
||||
// expand all/collapse all ng-click function
|
||||
scope.toggleAllStdout = function(type) {
|
||||
// find the top visible line in the container currently,
|
||||
// as well as the header parent of that line
|
||||
var topLines = findTopLines();
|
||||
|
||||
if (type === 'expand') {
|
||||
// for expand prepend the lineAnchor to the visible
|
||||
// line
|
||||
$(".line_num_" + topLines.visLine)
|
||||
.prepend($("#lineAnchor"));
|
||||
} else {
|
||||
// for collapse prepent the lineAnchor to the
|
||||
// visible line's parent header
|
||||
$(".line_num_" + topLines.parentVisLine)
|
||||
.prepend($("#lineAnchor"));
|
||||
}
|
||||
|
||||
var expandClass;
|
||||
if (type === 'expand') {
|
||||
// for expand all, you'll need to find all the
|
||||
// collapsed headers to expand them
|
||||
expandClass = "fa-caret-right";
|
||||
} else {
|
||||
// for collapse all, you'll need to find all the
|
||||
// expanded headers to collapse them
|
||||
expandClass = "fa-caret-down";
|
||||
}
|
||||
|
||||
// find all applicable task headers that need to be
|
||||
// toggled
|
||||
element.find(".expanderizer--task."+expandClass)
|
||||
.each((i, val) => {
|
||||
// and trigger their expansion/collapsing
|
||||
$timeout(function(){
|
||||
// TODO change to a direct call of the
|
||||
// toggleLine function
|
||||
angular.element(val).trigger('click');
|
||||
// TODO only call lineAnchor for those
|
||||
// that are above the first visible line
|
||||
scope.lineAnchor();
|
||||
});
|
||||
});
|
||||
|
||||
// find all applicable play headers that need to be
|
||||
// toggled
|
||||
element.find(".expanderizer--play."+expandClass)
|
||||
.each((i, val) => {
|
||||
// for collapse all, only collapse play
|
||||
// headers that do not have children task
|
||||
// headers
|
||||
if(angular.element("." +
|
||||
angular.element(val).attr("data-uuid"))
|
||||
.find(".expanderizer--task")
|
||||
.length === 0 ||
|
||||
type !== 'collapse') {
|
||||
|
||||
// trigger their expansion/
|
||||
// collapsing
|
||||
$timeout(function(){
|
||||
// TODO change to a direct
|
||||
// call of the
|
||||
// toggleLine function
|
||||
angular.element(val)
|
||||
.trigger('click');
|
||||
// TODO only call lineAnchor
|
||||
// for those that are above
|
||||
// the first visible line
|
||||
scope.lineAnchor();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// expand/collapse triangle ng-click function
|
||||
scope.toggleLine = function($event, id) {
|
||||
// if the section is currently expanded
|
||||
if ($($event.currentTarget).hasClass("fa-caret-down")) {
|
||||
// hide all the children lines
|
||||
$(id).hide();
|
||||
|
||||
// and change the triangle for the header to collapse
|
||||
$($event.currentTarget)
|
||||
.removeClass("fa-caret-down");
|
||||
$($event.currentTarget)
|
||||
.addClass("fa-caret-right");
|
||||
} else {
|
||||
// if the section is currently collapsed
|
||||
|
||||
// show all the children lines
|
||||
$(id).show();
|
||||
|
||||
// and change the triangle for the header to expanded
|
||||
$($event.currentTarget)
|
||||
.removeClass("fa-caret-right");
|
||||
$($event.currentTarget)
|
||||
.addClass("fa-caret-down");
|
||||
|
||||
// if the section you expanded is a play
|
||||
if ($($event.currentTarget)
|
||||
.hasClass("expanderizer--play")) {
|
||||
// find all children task headers and
|
||||
// expand them too
|
||||
$("." + $($event.currentTarget)
|
||||
.attr("data-uuid"))
|
||||
.find(".expanderizer--task")
|
||||
.each((i, val) => {
|
||||
if ($(val)
|
||||
.hasClass("fa-caret-right")) {
|
||||
$timeout(function(){
|
||||
// TODO change to a
|
||||
// direct call of the
|
||||
// toggleLine function
|
||||
angular.element(val)
|
||||
.trigger('click');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -1,65 +0,0 @@
|
||||
<div class="JobResultsStdOut">
|
||||
<div class="JobResultsStdOut-toolbar">
|
||||
<div class="JobResultsStdOut-toolbarNumberColumn">
|
||||
<div class="JobResultsStdOut-expandAllButton"
|
||||
ng-click="toggleAllStdout('expand')"
|
||||
aw-tool-tip="Expand all lines of standard out."
|
||||
data-placement="top">
|
||||
<i class ="JobResultsStdOut-expandAllIcon fa fa-plus">
|
||||
</i>
|
||||
</div>
|
||||
<div class="JobResultsStdOut-expandAllButton"
|
||||
ng-click="toggleAllStdout('collapse')"
|
||||
aw-tool-tip="Collapse all lines of standard out except play and task headers."
|
||||
data-placement="top">
|
||||
<i class ="JobResultsStdOut-expandAllIcon fa fa-minus">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="JobResultsStdOut-toolbarStdoutColumn">
|
||||
<div class="JobResultsStdOut-followButton"
|
||||
ng-class="{'is-engaged': followEngaged && !jobFinished}"
|
||||
aw-tool-tip="{{ followTooltip }}"
|
||||
data-tip-watch="followTooltip"
|
||||
data-placement="left"
|
||||
data-trigger="hover"
|
||||
data-container="body"
|
||||
ng-click="followToggleClicked()">
|
||||
<i class="JobResultsStdOut-followIcon fa fa-arrow-down">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="JobResultsStdOut-stdoutContainer">
|
||||
<div id="topAnchor" class="JobResultsStdOut-topAnchor"></div>
|
||||
<div class="JobResultsStdOut-numberColumnPreload"></div>
|
||||
<div id='lineAnchor' class="JobResultsStdOut-lineAnchor"></div>
|
||||
<div class="JobResultsStdOut-aLineOfStdOut"
|
||||
ng-show="tooManyEvents || tooManyPastEvents || showLegacyJobErrorMessage">
|
||||
<div class="JobResultsStdOut-lineNumberColumn">
|
||||
<span class="JobResultsStdOut-lineExpander"> </span>
|
||||
</div>
|
||||
<div class="JobResultsStdOut-stdoutColumn JobResultsStdOut-stdoutColumn--tooMany"
|
||||
ng-show="tooManyEvents" translate>The standard output is too large to display. Please specify additional filters to narrow the standard out.</div>
|
||||
<div class="JobResultsStdOut-stdoutColumn JobResultsStdOut-stdoutColumn--tooMany"
|
||||
ng-show="tooManyPastEvents" translate>Too much previous output to display. Showing running standard output.</div>
|
||||
<div class="JobResultsStdOut-stdoutColumn JobResultsStdOut-stdoutColumn--tooMany"
|
||||
ng-show="showLegacyJobErrorMessage" translate>Job details are not available for this job. Please download to view standard out.</div>
|
||||
</div>
|
||||
<!-- next is 1 is a hack to get the first line to be put in the pane in the
|
||||
right place -->
|
||||
<div class="next_is_1"></div>
|
||||
<div id="followAnchor"
|
||||
class="JobResultsStdOut-followAnchor">
|
||||
</div>
|
||||
</div>
|
||||
<div class="JobResultsStdOut-footer">
|
||||
<div class="JobResultsStdOut-toTop"
|
||||
ng-show="stdoutOverflowed">
|
||||
<div class="JobResultsStdOut-toTop--numberColumn">
|
||||
</div>
|
||||
<span ng-click="toTop()">^ <span translate>TOP</span></span>
|
||||
</div>
|
||||
<div class="JobResultsStdOut-footerNumberColumn"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,11 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import jobResultsStdOut from './job-results-stdout.directive';
|
||||
|
||||
export default
|
||||
angular.module('jobResultStdOutDirective', [])
|
||||
.directive('jobResultsStandardOut', jobResultsStdOut);
|
||||
@ -1,248 +0,0 @@
|
||||
@breakpoint-md: 1200px;
|
||||
|
||||
.JobResults {
|
||||
.OnePlusTwo-container(100%, @breakpoint-md);
|
||||
|
||||
&.fullscreen {
|
||||
.JobResults-rightSide {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-leftSide {
|
||||
.OnePlusTwo-left--panel(100%, @breakpoint-md);
|
||||
max-width: 30%;
|
||||
height: ~"calc(100vh - 177px)";
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-rightSide {
|
||||
.OnePlusTwo-right--panel(100%, @breakpoint-md);
|
||||
height: ~"calc(100vh - 177px)";
|
||||
|
||||
@media (max-width: @breakpoint-md - 1px) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-detailsPanel{
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.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-panelHeaderButtonActions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.JobResults-resultRow {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.JobResults-resultRow--variables {
|
||||
flex-direction: column;
|
||||
|
||||
#cm-variables-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-resultRowLabel {
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
font-size: 12px;
|
||||
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-expandArrow {
|
||||
color: #D7D7D7;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-transform: uppercase;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.JobResults-resultRowText--instanceGroup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.JobResults-isolatedBadge {
|
||||
align-items: center;
|
||||
background-color: @default-list-header-bg;
|
||||
border-radius: 5px;
|
||||
color: @default-stdout-txt;
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
margin: 3px 0 0 10px;
|
||||
padding: 0 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.JobResults-statusResultIcon {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.JobResults-badgeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.JobResults-badgeTitle{
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: @breakpoint-md) {
|
||||
.JobResults-detailsPanel {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.JobResults-rightSide {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-timeBadge {
|
||||
float:right;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
padding: 1px 10px;
|
||||
height: 14px;
|
||||
margin: 3px 15px;
|
||||
width: 80px;
|
||||
background-color: @default-bg;
|
||||
border-radius: 5px;
|
||||
color: @default-interface-txt;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.JobResults-panelRight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.JobResults-panelRight .SmartSearch-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.JobResults-panelRightTitle{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.JobResults-panelRightTitleText{
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.JobResults-badgeAndActionRow{
|
||||
display:flex;
|
||||
flex: 1 0 auto;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.StandardOut-panelHeader {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
.StandardOut-panelHeader--jobIsRunning {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
host-status-bar {
|
||||
flex: initial;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
smart-search {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
job-results-standard-out {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
height: ~"calc(100% - 800px)";
|
||||
display: flex;
|
||||
border: 1px solid @d7grey;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
job-results-standard-out {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.JobResults-extraVarsHelp {
|
||||
margin-left: 10px;
|
||||
color: @default-icon;
|
||||
}
|
||||
|
||||
.JobResults-seeMoreLess {
|
||||
color: #337AB7;
|
||||
margin: 4px 0px;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 0px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
@ -1,784 +0,0 @@
|
||||
export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange',
|
||||
'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', 'Dataset', '$q',
|
||||
'QuerySet', '$rootScope', 'moment', '$stateParams', 'i18n', 'fieldChoices', 'fieldLabels',
|
||||
'workflowResultsService', 'statusSocket', 'GetBasePath', '$state', 'jobExtraCredentials',
|
||||
function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange,
|
||||
ParseVariableString, jobResultsService, eventQueue, $compile, $log, Dataset, $q,
|
||||
QuerySet, $rootScope, moment, $stateParams, i18n, fieldChoices, fieldLabels,
|
||||
workflowResultsService, statusSocket, GetBasePath, $state, jobExtraCredentials) {
|
||||
|
||||
var toDestroy = [];
|
||||
var cancelRequests = false;
|
||||
var runTimeElapsedTimer = null;
|
||||
|
||||
// download stdout tooltip text
|
||||
$scope.standardOutTooltip = i18n._('Download Output');
|
||||
|
||||
// stdout full screen toggle tooltip text
|
||||
$scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output");
|
||||
|
||||
// this allows you to manage the timing of rest-call based events as
|
||||
// filters are updated. see processPage for more info
|
||||
var currentContext = 1;
|
||||
$scope.firstCounterFromSocket = -1;
|
||||
|
||||
$scope.explanationLimit = 150;
|
||||
|
||||
// if the user enters the page mid-run, reset the search to include a param
|
||||
// to only grab events less than the first counter from the websocket events
|
||||
toDestroy.push($scope.$watch('firstCounterFromSocket', function(counter) {
|
||||
if (counter > -1) {
|
||||
// make it so that the search include a counter less than the
|
||||
// first counter from the socket
|
||||
let params = _.cloneDeep($stateParams.job_event_search);
|
||||
params.counter__lte = "" + counter;
|
||||
|
||||
Dataset = QuerySet.search(jobData.related.job_events,
|
||||
params);
|
||||
|
||||
Dataset.then(function(actualDataset) {
|
||||
$scope.job_event_dataset = actualDataset.data;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// used for tag search
|
||||
$scope.job_event_dataset = Dataset.data;
|
||||
|
||||
// used for tag search
|
||||
$scope.list = {
|
||||
basePath: jobData.related.job_events,
|
||||
name: 'job_events'
|
||||
};
|
||||
|
||||
// used for tag search
|
||||
$scope.job_events = $scope.job_event_dataset.results;
|
||||
|
||||
$scope.jobExtraCredentials = jobExtraCredentials;
|
||||
|
||||
var getLinks = function() {
|
||||
var getLink = function(key) {
|
||||
if(key === 'schedule') {
|
||||
if($scope.job.related.schedule) {
|
||||
return '/#/templates/job_template/' + $scope.job.job_template + '/schedules' + $scope.job.related.schedule.split(/api\/v\d+\/schedules/)[1];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if(key === 'inventory') {
|
||||
if($scope.job.summary_fields.inventory && $scope.job.summary_fields.inventory.id) {
|
||||
if($scope.job.summary_fields.inventory.kind && $scope.job.summary_fields.inventory.kind === 'smart') {
|
||||
return '/#/inventories/smart/' + $scope.job.summary_fields.inventory.id;
|
||||
}
|
||||
else {
|
||||
return '/#/inventories/inventory/' + $scope.job.summary_fields.inventory.id;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($scope.job.related[key]) {
|
||||
return '/#/' + $scope.job.related[key]
|
||||
.split(/api\/v\d+\//)[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.created_by_link = getLink('created_by');
|
||||
$scope.scheduled_by_link = getLink('schedule');
|
||||
$scope.inventory_link = getLink('inventory');
|
||||
$scope.project_link = getLink('project');
|
||||
$scope.machine_credential_link = getLink('credential');
|
||||
$scope.cloud_credential_link = getLink('cloud_credential');
|
||||
$scope.network_credential_link = getLink('network_credential');
|
||||
$scope.vault_credential_link = getLink('vault_credential');
|
||||
$scope.schedule_link = getLink('schedule');
|
||||
};
|
||||
|
||||
// uses options to set scope variables to their readable string
|
||||
// value
|
||||
var getLabels = function() {
|
||||
var getLabel = 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.type_label = getLabel('job_type');
|
||||
$scope.verbosity_label = getLabel('verbosity');
|
||||
};
|
||||
|
||||
var getTotalHostCount = function(count) {
|
||||
return Object
|
||||
.keys(count).reduce((acc, i) => acc += count[i], 0);
|
||||
};
|
||||
|
||||
// put initially resolved request data on scope
|
||||
$scope.job = jobData;
|
||||
$scope.jobOptions = jobDataOptions.actions.GET;
|
||||
$scope.labels = jobLabels;
|
||||
$scope.jobFinished = jobFinished;
|
||||
|
||||
// update label in left pane and tooltip in right pane when the job_status
|
||||
// changes
|
||||
toDestroy.push($scope.$watch('job_status', function(status) {
|
||||
if (status) {
|
||||
$scope.status_label = $scope.jobOptions.status.choices
|
||||
.filter(val => val[0] === status)
|
||||
.map(val => val[1])[0];
|
||||
$scope.status_tooltip = "Job " + $scope.status_label;
|
||||
}
|
||||
}));
|
||||
|
||||
$scope.previousTaskFailed = false;
|
||||
|
||||
toDestroy.push($scope.$watch('job.job_explanation', function(explanation) {
|
||||
if (explanation && explanation.split(":")[0] === "Previous Task Failed") {
|
||||
$scope.previousTaskFailed = true;
|
||||
|
||||
var taskObj = JSON.parse(explanation.substring(explanation.split(":")[0].length + 1));
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var fieldChoice = fieldChoices({
|
||||
$scope: $scope,
|
||||
url: GetBasePath('unified_jobs'),
|
||||
field: 'type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
fieldChoice.then(function (choices) {
|
||||
choices =
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
$scope.explanation_fail_type = choices[taskObj.job_type];
|
||||
$scope.explanation_fail_name = taskObj.job_name;
|
||||
$scope.explanation_fail_id = taskObj.job_id;
|
||||
$scope.task_detail = $scope.explanation_fail_type + " failed for " + $scope.explanation_fail_name + " with ID " + $scope.explanation_fail_id + ".";
|
||||
});
|
||||
} else {
|
||||
$scope.previousTaskFailed = false;
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// update the job_status value. Use the cached rootScope value if there
|
||||
// is one. This is a workaround when the rest call for the jobData is
|
||||
// made before some socket events come in for the job status
|
||||
if ($rootScope['lastSocketStatus' + jobData.id]) {
|
||||
$scope.job_status = $rootScope['lastSocketStatus' + jobData.id];
|
||||
delete $rootScope['lastSocketStatus' + jobData.id];
|
||||
} else {
|
||||
$scope.job_status = jobData.status;
|
||||
}
|
||||
|
||||
// turn related api browser routes into front end routes
|
||||
getLinks();
|
||||
|
||||
// the links below can't be set in getLinks because the
|
||||
// links on the UI don't directly match the corresponding URL
|
||||
// on the API browser
|
||||
if(jobData.summary_fields && jobData.summary_fields.job_template &&
|
||||
jobData.summary_fields.job_template.id){
|
||||
$scope.job_template_link = `/#/templates/job_template/${$scope.job.summary_fields.job_template.id}`;
|
||||
}
|
||||
if(jobData.summary_fields && jobData.summary_fields.project_update &&
|
||||
jobData.summary_fields.project_update.status){
|
||||
$scope.project_status = jobData.summary_fields.project_update.status;
|
||||
}
|
||||
if(jobData.summary_fields && jobData.summary_fields.project_update &&
|
||||
jobData.summary_fields.project_update.id){
|
||||
$scope.project_update_link = `/#/scm_update/${jobData.summary_fields.project_update.id}`;
|
||||
}
|
||||
if(jobData.summary_fields && jobData.summary_fields.source_workflow_job &&
|
||||
jobData.summary_fields.source_workflow_job.id){
|
||||
$scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`;
|
||||
}
|
||||
if(jobData.result_traceback) {
|
||||
$scope.job.result_traceback = jobData.result_traceback.trim().split('\n').join('<br />');
|
||||
}
|
||||
|
||||
// use options labels to manipulate display of details
|
||||
getLabels();
|
||||
|
||||
// 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;
|
||||
|
||||
if ($scope.stdoutFullScreen === true) {
|
||||
$scope.toggleStdoutFullscreenTooltip = i18n._("Collapse Output");
|
||||
} else if ($scope.stdoutFullScreen === false) {
|
||||
$scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output");
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteJob = function() {
|
||||
jobResultsService.deleteJob($scope.job);
|
||||
};
|
||||
|
||||
$scope.cancelJob = function() {
|
||||
jobResultsService.cancelJob($scope.job);
|
||||
};
|
||||
|
||||
$scope.lessLabels = false;
|
||||
$scope.toggleLessLabels = function() {
|
||||
if (!$scope.lessLabels) {
|
||||
$('#job-results-labels').slideUp(200);
|
||||
$scope.lessLabels = true;
|
||||
}
|
||||
else {
|
||||
$('#job-results-labels').slideDown(200);
|
||||
$scope.lessLabels = false;
|
||||
}
|
||||
};
|
||||
|
||||
// get initial count from resolve
|
||||
$scope.count = count.val;
|
||||
$scope.hostCount = getTotalHostCount(count.val);
|
||||
$scope.countFinished = count.countFinished;
|
||||
|
||||
// if the job is still running engage following of the last line in the
|
||||
// standard out pane
|
||||
$scope.followEngaged = !$scope.jobFinished;
|
||||
|
||||
// follow button for completed job should specify that the
|
||||
// button will jump to the bottom of the standard out pane,
|
||||
// not follow lines as they come in
|
||||
if ($scope.jobFinished) {
|
||||
$scope.followTooltip = i18n._("Jump to last line of standard out.");
|
||||
} else {
|
||||
$scope.followTooltip = i18n._("Currently following standard out as it comes in. Click to unfollow.");
|
||||
}
|
||||
|
||||
$scope.events = {};
|
||||
|
||||
function updateJobElapsedTimer(time) {
|
||||
$scope.job.elapsed = time;
|
||||
}
|
||||
|
||||
// For elapsed time while a job is running, compute the differnce in seconds,
|
||||
// from the time the job started until now. Moment() returns the current
|
||||
// time as a moment object.
|
||||
if ($scope.job.started !== null && $scope.job.status === 'running') {
|
||||
runTimeElapsedTimer = workflowResultsService.createOneSecondTimer($scope.job.started, updateJobElapsedTimer);
|
||||
}
|
||||
|
||||
// EVENT STUFF BELOW
|
||||
var linesInPane = [];
|
||||
|
||||
function addToLinesInPane(event) {
|
||||
var arr = _.range(event.start_line, event.actual_end_line);
|
||||
linesInPane = linesInPane.concat(arr);
|
||||
linesInPane = linesInPane.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
function appendToBottom (event){
|
||||
// if we get here then the event type was either a
|
||||
// header line, recap line, or one of the additional
|
||||
// event types, so we append it to the bottom.
|
||||
// These are the event types for captured
|
||||
// stdout not directly related to playbook or runner
|
||||
// events:
|
||||
// (0, 'debug', _('Debug'), False),
|
||||
// (0, 'verbose', _('Verbose'), False),
|
||||
// (0, 'deprecated', _('Deprecated'), False),
|
||||
// (0, 'warning', _('Warning'), False),
|
||||
// (0, 'system_warning', _('System Warning'), False),
|
||||
// (0, 'error', _('Error'), True),
|
||||
angular
|
||||
.element(".JobResultsStdOut-stdoutContainer")
|
||||
.append($compile(event
|
||||
.stdout)($scope.events[event
|
||||
.counter]));
|
||||
}
|
||||
|
||||
function putInCorrectPlace(event) {
|
||||
if (linesInPane.length) {
|
||||
for (var i = linesInPane.length - 1; i >= 0; i--) {
|
||||
if (event.start_line > linesInPane[i]) {
|
||||
$(`.line_num_${linesInPane[i]}`)
|
||||
.after($compile(event
|
||||
.stdout)($scope.events[event
|
||||
.counter]));
|
||||
i = -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appendToBottom(event);
|
||||
}
|
||||
}
|
||||
|
||||
// This is where the async updates to the UI actually happen.
|
||||
// Flow is event queue munging in the service -> $scope setting in here
|
||||
var processEvent = function(event, context) {
|
||||
// only care about filter context checking when the event comes
|
||||
// as a rest call
|
||||
if (context && context !== currentContext) {
|
||||
return;
|
||||
}
|
||||
// put the event in the queue
|
||||
var mungedEvent = eventQueue.populate(event);
|
||||
|
||||
// make changes to ui based on the event returned from the queue
|
||||
if (mungedEvent.changes) {
|
||||
mungedEvent.changes.forEach(change => {
|
||||
// we've got a change we need to make to the UI!
|
||||
// update the necessary scope and make the change
|
||||
if (change === 'startTime' && !$scope.job.start) {
|
||||
$scope.job.start = mungedEvent.startTime;
|
||||
}
|
||||
|
||||
if (change === 'count' && !$scope.countFinished) {
|
||||
// for all events that affect the host count,
|
||||
// update the status bar as well as the host
|
||||
// count badge
|
||||
$scope.count = mungedEvent.count;
|
||||
$scope.hostCount = getTotalHostCount(mungedEvent
|
||||
.count);
|
||||
}
|
||||
|
||||
if (change === 'finishedTime' && !$scope.job.finished) {
|
||||
$scope.job.finished = mungedEvent.finishedTime;
|
||||
$scope.jobFinished = true;
|
||||
$scope.followTooltip = i18n._("Jump to last line of standard out.");
|
||||
if ($scope.followEngaged) {
|
||||
if (!$scope.followScroll) {
|
||||
$scope.followScroll = function() {
|
||||
$log.error("follow scroll undefined, standard out directive not loaded yet?");
|
||||
};
|
||||
}
|
||||
$scope.followScroll();
|
||||
}
|
||||
}
|
||||
|
||||
if (change === 'countFinished') {
|
||||
// the playbook_on_stats event actually lets
|
||||
// us know that we don't need to iteratively
|
||||
// look at event to update the host counts
|
||||
// any more.
|
||||
$scope.countFinished = true;
|
||||
}
|
||||
|
||||
if(change === 'stdout'){
|
||||
if (!$scope.events[mungedEvent.counter]) {
|
||||
// line hasn't been put in the pane yet
|
||||
|
||||
// create new child scope
|
||||
$scope.events[mungedEvent.counter] = $scope.$new();
|
||||
$scope.events[mungedEvent.counter]
|
||||
.event = mungedEvent;
|
||||
|
||||
// let's see if we have a specific place to put it in
|
||||
// the pane
|
||||
let $prevElem = $(`.next_is_${mungedEvent.start_line}`);
|
||||
if ($prevElem && $prevElem.length) {
|
||||
// if so, put it there
|
||||
$(`.next_is_${mungedEvent.start_line}`)
|
||||
.after($compile(mungedEvent
|
||||
.stdout)($scope.events[mungedEvent
|
||||
.counter]));
|
||||
addToLinesInPane(mungedEvent);
|
||||
} else {
|
||||
var putIn;
|
||||
var classList = $("div",
|
||||
"<div>"+mungedEvent.stdout+"</div>")
|
||||
.attr("class").split(" ");
|
||||
if (classList
|
||||
.filter(v => v.indexOf("task_") > -1)
|
||||
.length) {
|
||||
putIn = classList
|
||||
.filter(v => v.indexOf("task_") > -1)[0];
|
||||
} else if(classList
|
||||
.filter(v => v.indexOf("play_") > -1)
|
||||
.length) {
|
||||
putIn = classList
|
||||
.filter(v => v.indexOf("play_") > -1)[0];
|
||||
}
|
||||
|
||||
var putAfter;
|
||||
var isDup = false;
|
||||
|
||||
if ($(".header_" + putIn + ",." + putIn).length === 0) {
|
||||
putInCorrectPlace(mungedEvent);
|
||||
addToLinesInPane(mungedEvent);
|
||||
} else {
|
||||
$(".header_" + putIn + ",." + putIn)
|
||||
.each((i, v) => {
|
||||
if (angular.element(v).scope()
|
||||
.event.start_line < mungedEvent
|
||||
.start_line) {
|
||||
putAfter = v;
|
||||
} else if (angular.element(v).scope()
|
||||
.event.start_line === mungedEvent
|
||||
.start_line) {
|
||||
isDup = true;
|
||||
return false;
|
||||
} else if (angular.element(v).scope()
|
||||
.event.start_line > mungedEvent
|
||||
.start_line) {
|
||||
return false;
|
||||
} else {
|
||||
appendToBottom(mungedEvent);
|
||||
addToLinesInPane(mungedEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDup && putAfter) {
|
||||
addToLinesInPane(mungedEvent);
|
||||
$(putAfter).after($compile(mungedEvent
|
||||
.stdout)($scope.events[mungedEvent
|
||||
.counter]));
|
||||
}
|
||||
|
||||
|
||||
classList = null;
|
||||
putIn = null;
|
||||
}
|
||||
|
||||
// delete ref to the elem because it might leak scope
|
||||
// if you don't
|
||||
$prevElem = null;
|
||||
}
|
||||
|
||||
// move the followAnchor to the bottom of the
|
||||
// container
|
||||
$(".JobResultsStdOut-followAnchor")
|
||||
.appendTo(".JobResultsStdOut-stdoutContainer");
|
||||
}
|
||||
});
|
||||
|
||||
// the changes have been processed in the ui, mark it in the
|
||||
// queue
|
||||
eventQueue.markProcessed(event);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.stdoutContainerAvailable = $q.defer();
|
||||
$scope.hasSkeleton = $q.defer();
|
||||
|
||||
eventQueue.initialize();
|
||||
|
||||
$scope.playCount = 0;
|
||||
$scope.taskCount = 0;
|
||||
|
||||
|
||||
// used to show a message to just download for old jobs
|
||||
// remove in 3.2.0
|
||||
$scope.isOld = 0;
|
||||
$scope.showLegacyJobErrorMessage = false;
|
||||
|
||||
toDestroy.push($scope.$watch('isOld', function (val) {
|
||||
if (val >= 2) {
|
||||
$scope.showLegacyJobErrorMessage = true;
|
||||
}
|
||||
}));
|
||||
|
||||
// get header and recap lines
|
||||
var skeletonPlayCount = 0;
|
||||
var skeletonTaskCount = 0;
|
||||
var getSkeleton = function(url) {
|
||||
jobResultsService.getEvents(url)
|
||||
.then(events => {
|
||||
events.results.forEach(event => {
|
||||
if (event.start_line === 0 && event.end_line === 0) {
|
||||
$scope.isOld++;
|
||||
}
|
||||
// get the name in the same format as the data
|
||||
// coming over the websocket
|
||||
event.event_name = event.event;
|
||||
delete event.event;
|
||||
|
||||
// increment play and task count
|
||||
if (event.event_name === "playbook_on_play_start") {
|
||||
skeletonPlayCount++;
|
||||
} else if (event.event_name === "playbook_on_task_start") {
|
||||
skeletonTaskCount++;
|
||||
}
|
||||
|
||||
processEvent(event);
|
||||
});
|
||||
if (events.next) {
|
||||
getSkeleton(events.next);
|
||||
} else {
|
||||
// after the skeleton requests have completed,
|
||||
// put the play and task count into the dom
|
||||
$scope.playCount = skeletonPlayCount;
|
||||
$scope.taskCount = skeletonTaskCount;
|
||||
$scope.hasSkeleton.resolve("skeleton resolved");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stdoutContainerAvailable.promise.then(() => {
|
||||
getSkeleton(jobData.related.job_events + "?order_by=start_line&or__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats");
|
||||
});
|
||||
|
||||
var getEvents;
|
||||
|
||||
var processPage = function(events, context) {
|
||||
// currentContext is the context of the filter when this request
|
||||
// to processPage was made
|
||||
//
|
||||
// currentContext is the context of the filter currently
|
||||
//
|
||||
// if they are not the same, make sure to stop process events/
|
||||
// making rest calls for next pages/etc. (you can see context is
|
||||
// also passed into getEvents and processEvent and similar checks
|
||||
// exist in these functions)
|
||||
//
|
||||
// also, if the page doesn't contain results (i.e.: the response
|
||||
// returns an error), don't process the page
|
||||
if (context !== currentContext || events === undefined ||
|
||||
events.results === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
events.results.forEach(event => {
|
||||
// get the name in the same format as the data
|
||||
// coming over the websocket
|
||||
event.event_name = event.event;
|
||||
delete event.event;
|
||||
|
||||
processEvent(event, context);
|
||||
});
|
||||
if (events.next && !cancelRequests) {
|
||||
getEvents(events.next, context);
|
||||
} else {
|
||||
// put those paused events into the pane
|
||||
$scope.gotPreviouslyRanEvents.resolve("");
|
||||
}
|
||||
};
|
||||
|
||||
// grab non-header recap lines
|
||||
getEvents = function(url, context) {
|
||||
if (context !== currentContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
jobResultsService.getEvents(url)
|
||||
.then(events => {
|
||||
processPage(events, context);
|
||||
});
|
||||
};
|
||||
|
||||
// grab non-header recap lines
|
||||
toDestroy.push($scope.$watch('job_event_dataset', function(val) {
|
||||
if (val) {
|
||||
eventQueue.initialize();
|
||||
|
||||
Object.keys($scope.events)
|
||||
.forEach(v => {
|
||||
// dont destroy scope events for skeleton lines
|
||||
let name = $scope.events[v].event.name;
|
||||
|
||||
if (!(name === "playbook_on_play_start" ||
|
||||
name === "playbook_on_task_start" ||
|
||||
name === "playbook_on_stats")) {
|
||||
$scope.events[v].$destroy();
|
||||
$scope.events[v] = null;
|
||||
delete $scope.events[v];
|
||||
}
|
||||
});
|
||||
|
||||
// pause websocket events from coming in to the pane
|
||||
$scope.gotPreviouslyRanEvents = $q.defer();
|
||||
currentContext += 1;
|
||||
|
||||
let context = currentContext;
|
||||
|
||||
$( ".JobResultsStdOut-aLineOfStdOut.not_skeleton" ).remove();
|
||||
$scope.hasSkeleton.promise.then(() => {
|
||||
if (val.count > parseInt(val.maxEvents)) {
|
||||
$(".header_task").hide();
|
||||
$(".header_play").hide();
|
||||
$scope.standardOutTooltip = '<div class="JobResults-downloadTooLarge"><div>' +
|
||||
i18n._('The output is too large to display. Please download.') +
|
||||
'</div>' +
|
||||
'<div class="JobResults-downloadTooLarge--icon">' +
|
||||
'<span class="fa-stack fa-lg">' +
|
||||
'<i class="fa fa-circle fa-stack-1x"></i>' +
|
||||
'<i class="fa fa-stack-1x icon-job-stdout-download-tooltip"></i>' +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if ($scope.job_status === "successful" ||
|
||||
$scope.job_status === "failed" ||
|
||||
$scope.job_status === "error" ||
|
||||
$scope.job_status === "canceled") {
|
||||
$scope.tooManyEvents = true;
|
||||
$scope.tooManyPastEvents = false;
|
||||
} else {
|
||||
$scope.tooManyPastEvents = true;
|
||||
$scope.tooManyEvents = false;
|
||||
$scope.gotPreviouslyRanEvents.resolve("");
|
||||
}
|
||||
} else {
|
||||
$(".header_task").show();
|
||||
$(".header_play").show();
|
||||
$scope.tooManyEvents = false;
|
||||
$scope.tooManyPastEvents = false;
|
||||
processPage(val, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
var buffer = [];
|
||||
|
||||
var processBuffer = function() {
|
||||
var follow = function() {
|
||||
// if follow is engaged,
|
||||
// scroll down to the followAnchor
|
||||
if ($scope.followEngaged) {
|
||||
if (!$scope.followScroll) {
|
||||
$scope.followScroll = function() {
|
||||
$log.error("follow scroll undefined, standard out directive not loaded yet?");
|
||||
};
|
||||
}
|
||||
$scope.followScroll();
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
processEvent(buffer[i]);
|
||||
buffer.splice(i, 1);
|
||||
}
|
||||
|
||||
follow();
|
||||
};
|
||||
|
||||
var bufferInterval;
|
||||
|
||||
// Processing of job_events messages from the websocket
|
||||
toDestroy.push($scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) {
|
||||
if (!bufferInterval) {
|
||||
bufferInterval = setInterval(function(){
|
||||
processBuffer();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// use the lowest counter coming over the socket to retrigger pull data
|
||||
// to only be for stuff lower than that id
|
||||
//
|
||||
// only do this for entering the jobs page mid-run (thus the
|
||||
// data.counter is 1 conditional
|
||||
if (data.counter === 1) {
|
||||
$scope.firstCounterFromSocket = -2;
|
||||
}
|
||||
|
||||
if ($scope.firstCounterFromSocket !== -2 &&
|
||||
$scope.firstCounterFromSocket === -1 ||
|
||||
data.counter < $scope.firstCounterFromSocket) {
|
||||
$scope.firstCounterFromSocket = data.counter;
|
||||
}
|
||||
|
||||
$q.all([$scope.gotPreviouslyRanEvents.promise,
|
||||
$scope.hasSkeleton.promise]).then(() => {
|
||||
// put the line in the
|
||||
// standard out pane (and increment play and task
|
||||
// count if applicable)
|
||||
if (data.event_name === "playbook_on_play_start") {
|
||||
$scope.playCount++;
|
||||
} else if (data.event_name === "playbook_on_task_start") {
|
||||
$scope.taskCount++;
|
||||
}
|
||||
buffer.push(data);
|
||||
});
|
||||
}));
|
||||
|
||||
// get previously set up socket messages from resolve
|
||||
if (statusSocket && statusSocket[0] && statusSocket[0].job_status) {
|
||||
$scope.job_status = statusSocket[0].job_status;
|
||||
}
|
||||
if ($scope.job_status === "running" && !$scope.job.elapsed) {
|
||||
runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateJobElapsedTimer);
|
||||
}
|
||||
|
||||
// Processing of job-status messages from the websocket
|
||||
toDestroy.push($scope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) ===
|
||||
parseInt($scope.job.id,10)) {
|
||||
// controller is defined, so set the job_status
|
||||
$scope.job_status = data.status;
|
||||
if(_.has(data, 'instance_group_name')){
|
||||
$scope.job.instance_group = true;
|
||||
$scope.job.summary_fields.instance_group = {
|
||||
"name": data.instance_group_name
|
||||
};
|
||||
}
|
||||
if (data.status === "running") {
|
||||
if (!runTimeElapsedTimer) {
|
||||
runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateJobElapsedTimer);
|
||||
}
|
||||
} else if (data.status === "successful" ||
|
||||
data.status === "failed" ||
|
||||
data.status === "error" ||
|
||||
data.status === "canceled") {
|
||||
workflowResultsService.destroyTimer(runTimeElapsedTimer);
|
||||
|
||||
// When the fob is finished retrieve the job data to
|
||||
// correct anything that was out of sync from the job run
|
||||
jobResultsService.getJobData($scope.job.id).then(function(data){
|
||||
$scope.job = data;
|
||||
$scope.jobFinished = true;
|
||||
});
|
||||
}
|
||||
} else if (parseInt(data.project_id, 10) ===
|
||||
parseInt($scope.job.project,10)) {
|
||||
// this is a project status update message, so set the
|
||||
// project status in the left pane
|
||||
$scope.project_status = data.status;
|
||||
$scope.project_update_link = `/#/scm_update/${data
|
||||
.unified_job_id}`;
|
||||
} else {
|
||||
// controller was previously defined, but is not yet defined
|
||||
// for this job. cache the socket status on root scope
|
||||
$rootScope['lastSocketStatus' + data.unified_job_id] = data.status;
|
||||
}
|
||||
}));
|
||||
|
||||
if (statusSocket && statusSocket[1]) {
|
||||
statusSocket[1]();
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', function(){
|
||||
if (statusSocket && statusSocket[1]) {
|
||||
statusSocket[1]();
|
||||
}
|
||||
$( ".JobResultsStdOut-aLineOfStdOut" ).remove();
|
||||
cancelRequests = true;
|
||||
eventQueue.initialize();
|
||||
Object.keys($scope.events)
|
||||
.forEach(v => {
|
||||
$scope.events[v].$destroy();
|
||||
$scope.events[v] = null;
|
||||
});
|
||||
$scope.events = {};
|
||||
workflowResultsService.destroyTimer(runTimeElapsedTimer);
|
||||
if (bufferInterval) {
|
||||
clearInterval(bufferInterval);
|
||||
}
|
||||
toDestroy.forEach(closureFunc => closureFunc());
|
||||
});
|
||||
}];
|
||||
@ -1,566 +0,0 @@
|
||||
<div class="tab-pane" id="job-results">
|
||||
<div ng-cloak
|
||||
id="htmlTemplate"
|
||||
class="JobResults"
|
||||
ng-class="{'fullscreen': stdoutFullScreen}">
|
||||
<div ui-view></div>
|
||||
|
||||
<!-- LEFT PANE -->
|
||||
<div class="JobResults-leftSide"
|
||||
ng-class="{'JobResults-stdoutActionButton--active': stdoutFullScreen}">
|
||||
<div class="JobResults-detailsPanel Panel"
|
||||
ng-show="!stdoutFullScreen">
|
||||
|
||||
<!-- LEFT PANE HEADER -->
|
||||
<div class="JobResults-panelHeader">
|
||||
<div
|
||||
class="JobResults-panelHeaderText" translate>
|
||||
DETAILS
|
||||
</div>
|
||||
|
||||
<!-- LEFT PANE HEADER ACTIONS -->
|
||||
<div class="JobResults-panelHeaderButtonActions">
|
||||
|
||||
<!-- RELAUNCH ACTION -->
|
||||
<at-relaunch job="job"></at-relaunch>
|
||||
|
||||
<!-- CANCEL ACTION -->
|
||||
<button class="List-actionButton
|
||||
List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="cancelJob()"
|
||||
ng-show="job_status == 'running' ||
|
||||
job_status=='pending' "
|
||||
aw-tool-tip="{{'Cancel' | translate}}"
|
||||
data-original-title="" title="">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
|
||||
<!-- DELETE ACTION -->
|
||||
<button class="List-actionButton
|
||||
List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="deleteJob()"
|
||||
ng-hide="job_status == 'running' ||
|
||||
job_status == 'pending' || !job.summary_fields.user_capabilities.delete"
|
||||
aw-tool-tip="{{'Delete' | translate}}"
|
||||
data-original-title=""
|
||||
title="">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEFT PANE DETAILS GROUP -->
|
||||
<div>
|
||||
|
||||
<!-- STATUS DETAIL -->
|
||||
<div class="JobResults-resultRow">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Status
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<i class="JobResults-statusResultIcon
|
||||
fa
|
||||
icon-job-{{ job_status }}">
|
||||
</i> {{ status_label | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXPLANATION DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.job_explanation">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Explanation
|
||||
</label>
|
||||
<div class="JobResults-resultRowText" ng-show="!previousTaskFailed">
|
||||
{{ job.job_explanation }}
|
||||
</div>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{task_detail | limitTo:explanationLimit}}
|
||||
<span ng-show="previousTaskFailed && explanationLimit && task_detail.length > explanationLimit">
|
||||
<span>... </span>
|
||||
<span class="JobResults-seeMoreLess" ng-click="explanationLimit=undefined">Show More</span>
|
||||
</span>
|
||||
<span ng-show="explanationLimit === undefined" class="JobResults-seeMoreLess" ng-click="explanationLimit=150">Show Less</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- START TIME DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.started">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Started
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ job.started | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FINISHED TIME DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.started">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Finished
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ (job.finished |
|
||||
longDate) || "Not Finished" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RESULTS TRACEBACK DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.result_traceback && !previousTaskFailed">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Results Traceback
|
||||
</label>
|
||||
<div class="JobResults-resultRowText"
|
||||
ng-bind-html="job.result_traceback">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- TEMPLATE DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.job_template.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Template
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ job_template_link }}"
|
||||
aw-tool-tip="{{'Edit the job template' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.job_template.name }}
|
||||
</a>
|
||||
<a href="{{ workflow_result_link }}"
|
||||
aw-tool-tip="{{'View workflow results' | translate}}"
|
||||
data-placement="top"
|
||||
data-original-title="" title="">
|
||||
<i class="WorkflowBadge"
|
||||
ng-show="job.launch_type === 'workflow' ">
|
||||
W
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JOB TYPE DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.job_type">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Job Type
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ type_label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CREATED BY DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.created_by.username">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Launched By
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ created_by_link }}"
|
||||
aw-tool-tip="{{'Edit the User' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.created_by.username }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SCHEDULED BY DETAIL -->
|
||||
<div class="JobResults-resultRow toggle-show"
|
||||
ng-show="job.summary_fields.schedule.name">
|
||||
<label
|
||||
class="JobResults-resultRowLabel" translate>
|
||||
Launched By
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ scheduled_by_link }}"
|
||||
aw-tool-tip="{{'Edit the Schedule' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.schedule.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INVENTORY DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.inventory.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Inventory
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ inventory_link }}"
|
||||
aw-tool-tip="{{'Edit the inventory' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.inventory.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.project.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Project
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ project_update_link }}"
|
||||
ng-hide="job.summary_fields.project.scm_type==='' || !project_status"
|
||||
aw-tool-tip="{{'View project checkout results' | translate}}"
|
||||
data-placement="top">
|
||||
<i class="JobResults-statusResultIcon
|
||||
fa icon-job-{{ project_status }}">
|
||||
</i>
|
||||
</a>
|
||||
<a href="{{ project_link }}"
|
||||
aw-tool-tip="{{'Edit the project' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.project.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REVISION DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-if="job.scm_revision">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Revision
|
||||
</label>
|
||||
<at-truncate string="{{job.scm_revision}}" maxLength="7" class="JobResults-resultRowText">
|
||||
</at-truncate>
|
||||
</div>
|
||||
|
||||
<!-- PLAYBOOK DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.playbook">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Playbook
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ job.playbook }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MACHINE CREDENTIAL DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.credential.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Machine Credential
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ machine_credential_link }}"
|
||||
aw-tool-tip="{{'Edit the credential' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.credential.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXTRA CREDENTIALS DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="jobExtraCredentials.length > 0">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Extra Credentials
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<span ng-repeat="extraCredential in jobExtraCredentials">
|
||||
<a ui-sref="credentials.edit({credential_id: extraCredential.id})" aw-tool-tip="{{'Edit the credential' | translate}}" data-placement="top">
|
||||
{{ extraCredential.name }}
|
||||
</a>
|
||||
{{$last ? '' : ', '}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CLOUD CREDENTIAL DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.cloud_credential.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Cloud Credential
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ cloud_credential_link }}"
|
||||
aw-tool-tip="{{'Edit the credential' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.cloud_credential.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NETWORK CREDENTAIL DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.network_credential.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Network Credential
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ network_credential_link }}"
|
||||
aw-tool-tip="{{'Edit the credential' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.network_credential.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VAULT CREDENTAIL DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.summary_fields.vault_credential.name">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Vault Credential
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
<a href="{{ vault_credential_link }}"
|
||||
aw-tool-tip="{{'Edit the credential' | translate}}"
|
||||
data-placement="top">
|
||||
{{ job.summary_fields.vault_credential.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FORKS DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.forks !== undefined">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Forks
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ job.forks }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIMIT DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.limit">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Limit
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ job.limit }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VERBOSITY DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.verbosity !== undefined">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Verbosity
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ verbosity_label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IG DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.instance_group">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Instance Group
|
||||
</label>
|
||||
<div class="JobResults-resultRowText JobResults-resultRowText--instanceGroup">
|
||||
{{ job.summary_fields.instance_group.name }}
|
||||
<span class="JobResults-isolatedBadge"
|
||||
ng-if="job.summary_fields.instance_group && job.summary_fields.instance_group.controller_id">
|
||||
Isolated
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAGS DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.job_tags">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Job Tags
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ job.job_tags }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SKIP TAGS DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="job.skip_tags">
|
||||
<label class="JobResults-resultRowLabel" translate>
|
||||
Skip Tags
|
||||
</label>
|
||||
<div class="JobResults-resultRowText">
|
||||
{{ job.skip_tags }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXTRA VARIABLES DETAIL -->
|
||||
<div class="JobResults-resultRow
|
||||
JobResults-resultRow--variables"
|
||||
ng-show="variables">
|
||||
<label class="JobResults-resultRowLabel
|
||||
JobResults-resultRowLabel--fullWidth">
|
||||
<span translate> Extra Variables </span>
|
||||
<i class="JobResults-extraVarsHelp fa fa-question-circle"
|
||||
aw-tool-tip="{{'Read only view of extra variables added to the job template.' | translate}}"
|
||||
data-placement="top">
|
||||
</i>
|
||||
</label>
|
||||
<textarea
|
||||
rows="6"
|
||||
ng-model="variables"
|
||||
name="variables"
|
||||
class="form-control Form-textArea Form-textAreaLabel Form-formGroup--fullWidth"
|
||||
id="pre-formatted-variables"
|
||||
disabled="disabled">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<!-- LABELS DETAIL -->
|
||||
<div class="JobResults-resultRow"
|
||||
ng-show="labels && labels.length > 0">
|
||||
<div class="JobResults-resultRow">
|
||||
<a class="JobResults-resultRowLabel
|
||||
JobResults-resultRowLabel--fullWidth"
|
||||
ng-show="lessLabels"
|
||||
href=""
|
||||
ng-click="toggleLessLabels()">
|
||||
<span translate>Labels</span>
|
||||
<i class="JobResults-expandArrow
|
||||
fa fa-caret-right"></i>
|
||||
</a>
|
||||
<a class="JobResults-resultRowLabel
|
||||
JobResults-resultRowLabel--fullWidth"
|
||||
ng-show="!lessLabels"
|
||||
href=""
|
||||
ng-click="toggleLessLabels()">
|
||||
<span translate>Labels</span>
|
||||
<i class="JobResults-expandArrow
|
||||
fa fa-caret-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div id="job-results-labels" class="LabelList
|
||||
JobResults-resultRowText
|
||||
JobResults-resultRowText--fullWidth">
|
||||
<div ng-repeat="label in labels"
|
||||
class="LabelList-tagContainer">
|
||||
<div class="LabelList-tag">
|
||||
<div class="LabelList-name">
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT PANE -->
|
||||
<div class="JobResults-rightSide">
|
||||
<div class="Panel JobResults-panelRight">
|
||||
|
||||
<!-- RIGHT PANE HEADER -->
|
||||
<div class="StandardOut-panelHeader JobResults-panelRightTitle">
|
||||
<div class="StandardOut-panelHeaderText JobResults-panelRightTitleText">
|
||||
<i class="JobResults-statusResultIcon
|
||||
fa icon-job-{{ job_status }}"
|
||||
ng-show="stdoutFullScreen"
|
||||
aw-tool-tip="Job {{status_label}}"
|
||||
data-tip-watch="status_tooltip"
|
||||
aw-tip-placement="top"
|
||||
data-original-title>
|
||||
</i>
|
||||
{{ job.name }}
|
||||
</div>
|
||||
<div class="JobResults-badgeAndActionRow">
|
||||
<!-- HEADER COUNTS -->
|
||||
<div class="JobResults-badgeRow">
|
||||
<!-- PLAYS COUNT -->
|
||||
<div class="JobResults-badgeTitle" translate>
|
||||
Plays
|
||||
</div>
|
||||
<span class="badge List-titleBadge">
|
||||
{{ playCount || 0}}
|
||||
</span>
|
||||
|
||||
<!-- TASKS COUNT -->
|
||||
<div class="JobResults-badgeTitle" translate>
|
||||
Tasks
|
||||
</div>
|
||||
<span class="badge List-titleBadge">
|
||||
{{ taskCount || 0}}
|
||||
</span>
|
||||
|
||||
<!-- HOSTS COUNT -->
|
||||
<div class="JobResults-badgeTitle" translate>
|
||||
Hosts
|
||||
</div>
|
||||
<span class="badge List-titleBadge"
|
||||
ng-if="jobFinished">
|
||||
{{ hostCount || 0}}
|
||||
</span>
|
||||
|
||||
<span class="badge List-titleBadge"
|
||||
aw-tool-tip="{{'The host count will update when the job is complete.' | translate}}"
|
||||
data-placement="top"
|
||||
ng-if="!jobFinished">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</span>
|
||||
|
||||
<!-- ELAPSED TIME -->
|
||||
<div class="JobResults-badgeTitle" translate>
|
||||
Elapsed
|
||||
</div>
|
||||
<span class="badge List-titleBadge">
|
||||
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- HEADER ACTIONS -->
|
||||
<div class="StandardOut-panelHeaderActions">
|
||||
|
||||
<!-- FULL-SCREEN TOGGLE ACTION -->
|
||||
<button class="StandardOut-actionButton"
|
||||
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
|
||||
data-tip-watch="toggleStdoutFullscreenTooltip"
|
||||
data-placement="top"
|
||||
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
|
||||
ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
|
||||
<!-- DOWNLOAD ACTION -->
|
||||
<a ng-show="job.status === 'failed' ||
|
||||
job.status === 'successful' ||
|
||||
job.status === 'canceled'"
|
||||
href="/api/v2/jobs/{{ job.id }}/stdout?format=txt_download">
|
||||
<button class="StandardOut-actionButton"
|
||||
aw-tool-tip="{{ standardOutTooltip }}"
|
||||
data-tip-watch="standardOutTooltip"
|
||||
data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<host-status-bar></host-status-bar>
|
||||
<smart-search
|
||||
django-model="job_events"
|
||||
base-path="{{list.basePath}}"
|
||||
iterator="job_event"
|
||||
list="list"
|
||||
collection="job_events"
|
||||
dataset="job_event_dataset"
|
||||
search-tags="searchTags"
|
||||
disable-search="job_status == 'running' ||
|
||||
job_status=='pending'">
|
||||
</smart-search>
|
||||
<job-results-standard-out></job-results-standard-out>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,187 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {templateUrl} from '../shared/template-url/template-url.factory';
|
||||
|
||||
const defaultParams = {
|
||||
page_size: "200",
|
||||
order_by: 'start_line',
|
||||
not__event__in: 'playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'jobResult',
|
||||
url: '/jobs/{id: int}',
|
||||
searchPrefix: 'job_event',
|
||||
ncyBreadcrumb: {
|
||||
parent: 'jobs',
|
||||
label: '{{ job.id }} - {{ job.name }}'
|
||||
},
|
||||
data: {
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
}
|
||||
},
|
||||
params: {
|
||||
job_event_search: {
|
||||
value: defaultParams,
|
||||
dynamic: true,
|
||||
squash: ''
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
statusSocket: ['$rootScope', '$stateParams', function($rootScope, $stateParams) {
|
||||
var preScope = {};
|
||||
var eventOn = $rootScope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) ===
|
||||
parseInt($stateParams.id,10)) {
|
||||
preScope.job_status = data.status;
|
||||
}
|
||||
});
|
||||
return [preScope, eventOn];
|
||||
}],
|
||||
// the GET for the particular job
|
||||
jobData: ['jobResultsService', '$stateParams', function(jobResultsService, $stateParams) {
|
||||
return jobResultsService.getJobData($stateParams.id);
|
||||
}],
|
||||
Dataset: ['QuerySet', '$stateParams', 'jobData',
|
||||
function(qs, $stateParams, jobData) {
|
||||
let path = jobData.related.job_events;
|
||||
return qs.search(path, $stateParams[`job_event_search`]);
|
||||
}
|
||||
],
|
||||
// used to signify if job is completed or still running
|
||||
jobFinished: ['jobData', function(jobData) {
|
||||
if (jobData.finished) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}],
|
||||
// after the GET for the job, this helps us keep the status bar from
|
||||
// flashing as rest data comes in. If the job is finished and
|
||||
// there's a playbook_on_stats event, go ahead and resolve the count
|
||||
// so you don't get that flashing!
|
||||
count: ['jobData', 'jobResultsService', 'Rest', '$q', '$stateParams', '$state', function(jobData, jobResultsService, Rest, $q, $stateParams, $state) {
|
||||
var defer = $q.defer();
|
||||
if (jobData.finished) {
|
||||
// if the job is finished, grab the playbook_on_stats
|
||||
// role to get the final count
|
||||
Rest.setUrl(jobData.related.job_events +
|
||||
"?event=playbook_on_stats");
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
if(!data.results[0]){
|
||||
defer.resolve({val: {
|
||||
ok: 0,
|
||||
skipped: 0,
|
||||
unreachable: 0,
|
||||
failures: 0,
|
||||
changed: 0
|
||||
}, countFinished: false});
|
||||
}
|
||||
else {
|
||||
defer.resolve({
|
||||
val: jobResultsService
|
||||
.getCountsFromStatsEvent(data
|
||||
.results[0].event_data),
|
||||
countFinished: true});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
defer.resolve({val: {
|
||||
ok: 0,
|
||||
skipped: 0,
|
||||
unreachable: 0,
|
||||
failures: 0,
|
||||
changed: 0
|
||||
}, countFinished: false});
|
||||
});
|
||||
} else {
|
||||
// make sure to not include any extra
|
||||
// search params for a running job (because we can't filter
|
||||
// incoming job events)
|
||||
if (!_.isEqual($stateParams.job_event_search, defaultParams)) {
|
||||
let params = _.cloneDeep($stateParams);
|
||||
params.job_event_search = defaultParams;
|
||||
$state.go('.', params, { reload: true });
|
||||
}
|
||||
|
||||
// job isn't finished so just send an empty count and read
|
||||
// from events
|
||||
defer.resolve({val: {
|
||||
ok: 0,
|
||||
skipped: 0,
|
||||
unreachable: 0,
|
||||
failures: 0,
|
||||
changed: 0
|
||||
}, countFinished: false});
|
||||
}
|
||||
return defer.promise;
|
||||
}],
|
||||
// GET for the particular jobs labels to be displayed in the
|
||||
// left-hand pane
|
||||
jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
|
||||
var getNext = function(data, arr, resolve) {
|
||||
Rest.setUrl(data.next);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
if (data.next) {
|
||||
getNext(data, arr.concat(data.results), resolve);
|
||||
} else {
|
||||
resolve.resolve(arr.concat(data.results)
|
||||
.map(val => val.name));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var seeMoreResolve = $q.defer();
|
||||
|
||||
Rest.setUrl(GetBasePath('jobs') + $stateParams.id + '/labels/');
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
if (data.next) {
|
||||
getNext(data, data.results, seeMoreResolve);
|
||||
} else {
|
||||
seeMoreResolve.resolve(data.results
|
||||
.map(val => val.name));
|
||||
}
|
||||
});
|
||||
|
||||
return seeMoreResolve.promise;
|
||||
}],
|
||||
// OPTIONS request for the job. Used to make things like the
|
||||
// verbosity data in the left-hand pane prettier than just an
|
||||
// integer
|
||||
jobDataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
|
||||
Rest.setUrl(GetBasePath('jobs') + $stateParams.id);
|
||||
var val = $q.defer();
|
||||
Rest.options()
|
||||
.then(function(data) {
|
||||
val.resolve(data.data);
|
||||
}, function(data) {
|
||||
val.reject(data);
|
||||
});
|
||||
return val.promise;
|
||||
}],
|
||||
jobExtraCredentials: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
|
||||
Rest.setUrl(GetBasePath('jobs') + $stateParams.id + '/extra_credentials');
|
||||
var val = $q.defer();
|
||||
Rest.get()
|
||||
.then(function(res) {
|
||||
val.resolve(res.data.results);
|
||||
}, function(res) {
|
||||
val.reject(res);
|
||||
});
|
||||
return val.promise;
|
||||
}]
|
||||
},
|
||||
templateUrl: templateUrl('job-results/job-results'),
|
||||
controller: 'jobResultsController'
|
||||
};
|
||||
@ -1,269 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'GetBasePath', 'Alert', '$rootScope', 'i18n',
|
||||
function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, GetBasePath, Alert, $rootScope, i18n) {
|
||||
var val = {
|
||||
// the playbook_on_stats event returns the count data in a weird format.
|
||||
// format to what we need!
|
||||
getCountsFromStatsEvent: function(event_data) {
|
||||
var hosts = {},
|
||||
hostsArr;
|
||||
|
||||
// iterate over the event_data and populate an object with hosts
|
||||
// and their status data
|
||||
Object.keys(event_data).forEach(key => {
|
||||
// failed passes boolean not integer
|
||||
if (key === "changed" ||
|
||||
key === "dark" ||
|
||||
key === "failures" ||
|
||||
key === "ok" ||
|
||||
key === "skipped") {
|
||||
// array of hosts from each type ("changed", "dark", etc.)
|
||||
hostsArr = Object.keys(event_data[key]);
|
||||
hostsArr.forEach(host => {
|
||||
if (!hosts[host]) {
|
||||
// host has not been added to hosts object
|
||||
// add now
|
||||
hosts[host] = {};
|
||||
}
|
||||
|
||||
if (!hosts[host][key]) {
|
||||
// host doesn't have key
|
||||
hosts[host][key] = 0;
|
||||
}
|
||||
hosts[host][key] += event_data[key][host];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var total_hosts_by_state = {
|
||||
ok: 0,
|
||||
skipped: 0,
|
||||
unreachable: 0,
|
||||
failures: 0,
|
||||
changed: 0
|
||||
};
|
||||
|
||||
// each host belongs in at most *one* of these states depending on
|
||||
// the state of its tasks
|
||||
_.each(hosts, function(host) {
|
||||
if (host.dark > 0){
|
||||
total_hosts_by_state.unreachable++;
|
||||
} else if (host.failures > 0){
|
||||
total_hosts_by_state.failures++;
|
||||
} else if (host.changed > 0){
|
||||
total_hosts_by_state.changed++;
|
||||
} else if (host.ok > 0){
|
||||
total_hosts_by_state.ok++;
|
||||
} else if (host.skipped > 0){
|
||||
total_hosts_by_state.skipped++;
|
||||
}
|
||||
});
|
||||
|
||||
return total_hosts_by_state;
|
||||
},
|
||||
// rest call to grab previously complete job_events
|
||||
getEvents: function(url) {
|
||||
var val = $q.defer();
|
||||
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
val.resolve({results: data.results,
|
||||
next: data.next});
|
||||
})
|
||||
.catch(({obj, status}) => {
|
||||
ProcessErrors(null, obj, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: `Could not get job events.
|
||||
Returned status: ${status}`
|
||||
});
|
||||
val.reject(obj);
|
||||
});
|
||||
|
||||
return val.promise;
|
||||
},
|
||||
deleteJob: function(job) {
|
||||
Prompt({
|
||||
hdr: i18n._("Delete Job"),
|
||||
resourceName: `#${job.id} ` + $filter('sanitize')(job.name),
|
||||
body: `<div class='Prompt-bodyQuery'>
|
||||
${i18n._("Are you sure you want to delete this job?")}
|
||||
</div>`,
|
||||
action: function() {
|
||||
Wait('start');
|
||||
Rest.setUrl(job.url);
|
||||
Rest.destroy()
|
||||
.then(() => {
|
||||
Wait('stop');
|
||||
$('#prompt-modal').modal('hide');
|
||||
$state.go('jobs');
|
||||
})
|
||||
.catch(({obj, status}) => {
|
||||
Wait('stop');
|
||||
$('#prompt-modal').modal('hide');
|
||||
ProcessErrors(null, obj, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: `Could not delete job.
|
||||
Returned status: ${status}`
|
||||
});
|
||||
});
|
||||
},
|
||||
actionText: i18n._('DELETE')
|
||||
});
|
||||
},
|
||||
cancelJob: function(job) {
|
||||
var doCancel = function() {
|
||||
Rest.setUrl(job.url + 'cancel');
|
||||
Rest.post({})
|
||||
.then(() => {
|
||||
Wait('stop');
|
||||
$('#prompt-modal').modal('hide');
|
||||
})
|
||||
.catch(({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: i18n._('Cancel Job'),
|
||||
resourceName: `#${job.id} ` + $filter('sanitize')(job.name),
|
||||
body: `<div class='Prompt-bodyQuery' translate>
|
||||
${i18n._("Are you sure you want to cancel this job?")}
|
||||
</div>`,
|
||||
action: function() {
|
||||
Wait('start');
|
||||
Rest.setUrl(job.url + 'cancel');
|
||||
Rest.get()
|
||||
.then(({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.`
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
actionText: i18n._('PROCEED')
|
||||
});
|
||||
},
|
||||
getJobData: function(id){
|
||||
var val = $q.defer();
|
||||
|
||||
Rest.setUrl(GetBasePath('jobs') + id );
|
||||
Rest.get()
|
||||
.then(function(data) {
|
||||
val.resolve(data.data);
|
||||
}, function(data) {
|
||||
val.reject(data);
|
||||
|
||||
if (data.status === 404) {
|
||||
Alert('Job Not Found', 'Cannot find job.', 'alert-info');
|
||||
} else if (data.status === 403) {
|
||||
Alert('Insufficient Permissions', 'You do not have permission to view this job.', 'alert-info');
|
||||
}
|
||||
|
||||
$state.go('jobs');
|
||||
});
|
||||
|
||||
return val.promise;
|
||||
},
|
||||
// Generate a helper class for job_event statuses
|
||||
// the stack for which status to display is
|
||||
// unreachable > failed > changed > ok
|
||||
// uses the API's runner events and convenience properties .failed .changed to determine status.
|
||||
// see: job_event_callback.py for more filters to support
|
||||
processEventStatus: function(event){
|
||||
if (event.event === 'runner_on_unreachable'){
|
||||
return {
|
||||
class: 'HostEvent-status--unreachable',
|
||||
status: 'unreachable'
|
||||
};
|
||||
}
|
||||
// equiv to 'runner_on_error' && 'runner on failed'
|
||||
if (event.failed){
|
||||
return {
|
||||
class: 'HostEvent-status--failed',
|
||||
status: 'failed'
|
||||
};
|
||||
}
|
||||
// catch the changed case before ok, because both can be true
|
||||
if (event.changed){
|
||||
return {
|
||||
class: 'HostEvent-status--changed',
|
||||
status: 'changed'
|
||||
};
|
||||
}
|
||||
if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){
|
||||
return {
|
||||
class: 'HostEvent-status--ok',
|
||||
status: 'ok'
|
||||
};
|
||||
}
|
||||
if (event.event === 'runner_on_skipped'){
|
||||
return {
|
||||
class: 'HostEvent-status--skipped',
|
||||
status: 'skipped'
|
||||
};
|
||||
}
|
||||
},
|
||||
// GET events related to a job run
|
||||
// e.g.
|
||||
// ?event=playbook_on_stats
|
||||
// ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter
|
||||
getRelatedJobEvents: function(id, params){
|
||||
var url = GetBasePath('jobs');
|
||||
url = url + id + '/job_events/?' + this.stringifyParams(params);
|
||||
Rest.setUrl(url);
|
||||
return Rest.get()
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
},
|
||||
stringifyParams: function(params){
|
||||
return _.reduce(params, (result, value, key) => {
|
||||
return result + key + '=' + value + '&';
|
||||
}, '');
|
||||
},
|
||||
// the the API passes through Ansible's event_data response
|
||||
// we need to massage away the verbose & redundant stdout/stderr properties
|
||||
processJson: function(data){
|
||||
// configure fields to ignore
|
||||
var ignored = [
|
||||
'type',
|
||||
'event_data',
|
||||
'related',
|
||||
'summary_fields',
|
||||
'url',
|
||||
'ansible_facts',
|
||||
];
|
||||
// remove ignored properties
|
||||
var result = _.chain(data).cloneDeep().forEach(function(value, key, collection){
|
||||
if (ignored.indexOf(key) > -1){
|
||||
delete collection[key];
|
||||
}
|
||||
}).value();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
return val;
|
||||
}];
|
||||
@ -1,27 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import hostStatusBar from './host-status-bar/main';
|
||||
import jobResultsStdOut from './job-results-stdout/main';
|
||||
import hostEvent from './host-event/main';
|
||||
|
||||
import route from './job-results.route.js';
|
||||
|
||||
import jobResultsController from './job-results.controller';
|
||||
|
||||
import jobResultsService from './job-results.service';
|
||||
import eventQueueService from './event-queue.service';
|
||||
import parseStdoutService from './parse-stdout.service';
|
||||
|
||||
export default
|
||||
angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name, hostEvent.name, 'angularMoment'])
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
$stateExtender.addState(route);
|
||||
}])
|
||||
.controller('jobResultsController', jobResultsController)
|
||||
.service('jobResultsService', jobResultsService)
|
||||
.service('eventQueue', eventQueueService)
|
||||
.service('parseStdoutService', parseStdoutService);
|
||||
@ -1,293 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['$log', 'moment', 'i18n', function($log, moment, i18n){
|
||||
var val = {
|
||||
// parses stdout string from api and formats various codes to the
|
||||
// correct dom structure
|
||||
prettify: function(line, unstyled){
|
||||
line = line
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
// TODO: remove once Chris's fixes to the [K lines comes in
|
||||
if (line.indexOf("[K") > -1) {
|
||||
$log.error(line);
|
||||
}
|
||||
|
||||
if(!unstyled){
|
||||
// add span tags with color styling
|
||||
line = line.replace(/u001b/g, '');
|
||||
|
||||
// ansi classes
|
||||
/* jshint ignore:start */
|
||||
line = line.replace(/(|)\[1;im/g, '<span class="JobResultsStdOut-cappedLine">');
|
||||
line = line.replace(/(|)\[0;30m/g, '<span class="ansi30">');
|
||||
line = line.replace(/(|)\[1;30m/g, '<span class="ansi1 ansi30">');
|
||||
line = line.replace(/(|)\[[0,1];31m/g, '<span class="ansi1 ansi31">');
|
||||
line = line.replace(/(|)\[0;32m(=|)/g, '<span class="ansi32">');
|
||||
line = line.replace(/(|)\[0;32m1/g, '<span class="ansi36">');
|
||||
line = line.replace(/(|)\[0;33m/g, '<span class="ansi33">');
|
||||
line = line.replace(/(|)\[0;34m/g, '<span class="ansi34">');
|
||||
line = line.replace(/(|)\[[0,1];35m/g, '<span class="ansi35">');
|
||||
line = line.replace(/(|)\[0;36m/g, '<span class="ansi36">');
|
||||
line = line.replace(/(<host.*?>)\s/g, '$1');
|
||||
|
||||
//end span
|
||||
line = line.replace(/(|)\[0m/g, '</span>');
|
||||
/* jshint ignore:end */
|
||||
} else {
|
||||
// For the host event modal in the standard out tab,
|
||||
// the styling isn't necessary
|
||||
line = line.replace(/u001b/g, '');
|
||||
|
||||
// ansi classes
|
||||
/* jshint ignore:start */
|
||||
line = line.replace(/(|)\[[0,1];3[0-9]m(1|=|)/g, '');
|
||||
line = line.replace(/(<host.*?>)\s/g, '$1');
|
||||
|
||||
//end span
|
||||
line = line.replace(/(|)\[0m/g, '');
|
||||
/* jshint ignore:end */
|
||||
}
|
||||
|
||||
return line;
|
||||
},
|
||||
// adds anchor tags and tooltips to host status lines
|
||||
getAnchorTags: function(event){
|
||||
if(event.event_name.indexOf("runner_") === -1){
|
||||
return `"`;
|
||||
}
|
||||
else{
|
||||
return ` JobResultsStdOut-stdoutColumn--clickable" ui-sref="jobResult.host-event.json({eventId: ${event.id}, taskUuid: '${event.event_data.task_uuid}' })" aw-tool-tip="${i18n._("Event ID")}: ${event.id} <br>${i18n._("Status")}: ${event.event_display} <br>${i18n._("Click for details")}" data-placement="top"`;
|
||||
}
|
||||
|
||||
},
|
||||
// this adds classes based on event data to the
|
||||
// .JobResultsStdOut-aLineOfStdOut element
|
||||
getLineClasses: function(event, line, lineNum) {
|
||||
var string = "";
|
||||
|
||||
if (lineNum === event.end_line) {
|
||||
// used to tell you where to put stuff in the pane
|
||||
string += ` next_is_${event.end_line + 1}`;
|
||||
}
|
||||
|
||||
if (event.event_name === "playbook_on_play_start") {
|
||||
// play header classes
|
||||
string += " header_play";
|
||||
string += " header_play_" + event.event_data.play_uuid;
|
||||
|
||||
// give the actual header class to the line with the
|
||||
// actual header info (think cowsay)
|
||||
if (line.indexOf("PLAY") > -1) {
|
||||
string += " actual_header";
|
||||
}
|
||||
} else if (event.event_name === "playbook_on_task_start") {
|
||||
// task header classes
|
||||
string += " header_task";
|
||||
string += " header_task_" + event.event_data.task_uuid;
|
||||
|
||||
// give the actual header class to the line with the
|
||||
// actual header info (think cowsay)
|
||||
if (line.indexOf("TASK") > -1 ||
|
||||
line.indexOf("RUNNING HANDLER") > -1) {
|
||||
string += " actual_header";
|
||||
}
|
||||
|
||||
// task headers also get classed by their parent play
|
||||
// if applicable
|
||||
if (event.event_data.play_uuid) {
|
||||
string += " play_" + event.event_data.play_uuid;
|
||||
}
|
||||
} else if (event.event_name !== "playbook_on_stats"){
|
||||
string += " not_skeleton";
|
||||
// host status or debug line
|
||||
|
||||
// these get classed by their parent play if applicable
|
||||
if (event.event_data.play_uuid) {
|
||||
string += " play_" + event.event_data.play_uuid;
|
||||
}
|
||||
// as well as their parent task if applicable
|
||||
if (event.event_data.task_uuid) {
|
||||
string += " task_" + event.event_data.task_uuid;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: adding this line_num_XX class is hacky because the
|
||||
// line number is availabe in children of this dom element
|
||||
string += " line_num_" + lineNum;
|
||||
|
||||
return string;
|
||||
},
|
||||
getStartTimeBadge: function(event, line){
|
||||
// This will return a div with the badge class
|
||||
// for the start time to show at the right hand
|
||||
// side of each stdout header line.
|
||||
// returns an empty string if not a header line
|
||||
var emptySpan = "", time;
|
||||
if ((event.event_name === "playbook_on_play_start" ||
|
||||
event.event_name === "playbook_on_task_start") &&
|
||||
line !== "") {
|
||||
time = moment(event.created).format('HH:mm:ss');
|
||||
return `<div class="badge JobResults-timeBadge ng-binding">${time}</div>`;
|
||||
}
|
||||
else if(event.event_name === "playbook_on_stats" && line.indexOf("PLAY") > -1){
|
||||
time = moment(event.created).format('HH:mm:ss');
|
||||
return `<div class="badge JobResults-timeBadge ng-binding">${time}</div>`;
|
||||
}
|
||||
else {
|
||||
return emptySpan;
|
||||
}
|
||||
|
||||
},
|
||||
// used to add expand/collapse icon next to line numbers of headers
|
||||
getCollapseIcon: function(event, line) {
|
||||
var clickClass,
|
||||
expanderizerSpecifier;
|
||||
|
||||
var emptySpan = `
|
||||
<span class="JobResultsStdOut-lineExpander"></span>`;
|
||||
|
||||
if ((event.event_name === "playbook_on_play_start" ||
|
||||
event.event_name === "playbook_on_task_start") &&
|
||||
line !== "") {
|
||||
if (event.event_name === "playbook_on_play_start" &&
|
||||
line.indexOf("PLAY") > -1) {
|
||||
// play header specific attrs
|
||||
expanderizerSpecifier = "play";
|
||||
clickClass = "play_" +
|
||||
event.event_data.play_uuid;
|
||||
} else if (line.indexOf("TASK") > -1 ||
|
||||
line.indexOf("RUNNING HANDLER") > -1) {
|
||||
// task header specific attrs
|
||||
expanderizerSpecifier = "task";
|
||||
clickClass = "task_" +
|
||||
event.event_data.task_uuid;
|
||||
} else {
|
||||
// header lines that don't have PLAY, TASK,
|
||||
// or RUNNING HANDLER in them don't get
|
||||
// expand icon.
|
||||
// This provides cowsay support.
|
||||
return emptySpan;
|
||||
}
|
||||
|
||||
|
||||
var expandDom = `
|
||||
<span class="JobResultsStdOut-lineExpander">
|
||||
<i class="JobResultsStdOut-lineExpanderIcon fa fa-caret-down expanderizer
|
||||
expanderizer--${expanderizerSpecifier} expanded"
|
||||
ng-click="toggleLine($event, '.${clickClass}')"
|
||||
data-uuid="${clickClass}">
|
||||
</i>
|
||||
</span>`;
|
||||
return expandDom;
|
||||
} else {
|
||||
// non-header lines don't get an expander
|
||||
return emptySpan;
|
||||
}
|
||||
},
|
||||
distributeColors: function(lines) {
|
||||
var colorCode;
|
||||
return lines.map(line => {
|
||||
|
||||
if (colorCode) {
|
||||
line = colorCode + line;
|
||||
}
|
||||
|
||||
if (line.indexOf("[0m") === -1) {
|
||||
if (line.indexOf("[1;31m") > -1) {
|
||||
colorCode = "[1;31m";
|
||||
} else if (line.indexOf("[1;30m") > -1) {
|
||||
colorCode = "[1;30m";
|
||||
} else if (line.indexOf("[0;31m") > -1) {
|
||||
colorCode = "[0;31m";
|
||||
} else if (line.indexOf("[0;32m=") > -1) {
|
||||
colorCode = "[0;32m=";
|
||||
} else if (line.indexOf("[0;32m1") > -1) {
|
||||
colorCode = "[0;32m1";
|
||||
} else if (line.indexOf("[0;32m") > -1) {
|
||||
colorCode = "[0;32m";
|
||||
} else if (line.indexOf("[0;33m") > -1) {
|
||||
colorCode = "[0;33m";
|
||||
} else if (line.indexOf("[0;34m") > -1) {
|
||||
colorCode = "[0;34m";
|
||||
} else if (line.indexOf("[0;35m") > -1) {
|
||||
colorCode = "[0;35m";
|
||||
} else if (line.indexOf("[1;35m") > -1) {
|
||||
colorCode = "[1;35m";
|
||||
} else if (line.indexOf("[0;36m") > -1) {
|
||||
colorCode = "[0;36m";
|
||||
}
|
||||
} else {
|
||||
colorCode = null;
|
||||
}
|
||||
|
||||
return line;
|
||||
});
|
||||
},
|
||||
getLineArr: function(event) {
|
||||
let lineNums = _.range(event.start_line + 1,
|
||||
event.end_line + 1);
|
||||
|
||||
// hack around no-carriage return issues
|
||||
if (!lineNums.length) {
|
||||
lineNums = [event.start_line + 1];
|
||||
}
|
||||
|
||||
let lines = event.stdout
|
||||
.replace("\t", " ")
|
||||
.split("\r\n");
|
||||
|
||||
if (lineNums.length > lines.length) {
|
||||
lineNums = lineNums.slice(0, lines.length);
|
||||
}
|
||||
|
||||
lines = this.distributeColors(lines);
|
||||
|
||||
// hack around no-carriage return issues
|
||||
if (lineNums.length === lines.length) {
|
||||
return _.zip(lineNums, lines);
|
||||
}
|
||||
|
||||
return _.zip(lineNums, lines).slice(0, -1);
|
||||
},
|
||||
actualEndLine: function(event) {
|
||||
return event.start_line + this.getLineArr(event).length;
|
||||
},
|
||||
// public function that provides the parsed stdout line, given a
|
||||
// job_event
|
||||
parseStdout: function(event){
|
||||
// this utilizes the start/end lines and stdout blob
|
||||
// to create an array in the format:
|
||||
// [
|
||||
// [lineNum, lineText],
|
||||
// [lineNum, lineText],
|
||||
// ]
|
||||
var lineArr = this.getLineArr(event);
|
||||
|
||||
// this takes each `[lineNum: lineText]` element and calls the
|
||||
// relevant helper functions in this service to build the
|
||||
// parsed line of standard out
|
||||
lineArr = lineArr
|
||||
.map(lineArr => {
|
||||
return `
|
||||
<div class="JobResultsStdOut-aLineOfStdOut${this.getLineClasses(event, lineArr[1], lineArr[0])}">
|
||||
<div class="JobResultsStdOut-lineNumberColumn">${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}</div>
|
||||
<div class="JobResultsStdOut-stdoutColumn${this.getAnchorTags(event)}><span ng-non-bindable>${this.prettify(lineArr[1])}</span>${this.getStartTimeBadge(event, lineArr[1])}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// this joins all the lines for this job_event together and
|
||||
// returns to the mungeEvent function
|
||||
return lineArr.join("");
|
||||
}
|
||||
};
|
||||
return val;
|
||||
}];
|
||||
@ -89,7 +89,7 @@
|
||||
.then(({data}) => {
|
||||
Wait('stop');
|
||||
if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') {
|
||||
$state.go('adHocJobStdout', {id: data.id});
|
||||
$state.go('jobz', { id: data.id, type: 'command' });
|
||||
}
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
|
||||
@ -149,8 +149,8 @@ export default
|
||||
if(base !== 'portal' && Empty(data.system_job) || (base === 'home')){
|
||||
// use $state.go with reload: true option to re-instantiate sockets in
|
||||
|
||||
var goTojobResults = function(state) {
|
||||
$state.go(state, {id: job}, {reload:true});
|
||||
var goTojobResults = function(type) {
|
||||
$state.go('jobz', {id: job, type}, {reload:true});
|
||||
};
|
||||
|
||||
if($state.includes('jobs')) {
|
||||
@ -159,23 +159,23 @@ export default
|
||||
|
||||
else {
|
||||
if(_.has(data, 'job')) {
|
||||
goTojobResults('jobResult');
|
||||
goTojobResults('playbook');
|
||||
} else if(data.type && data.type === 'workflow_job') {
|
||||
job = data.id;
|
||||
goTojobResults('workflowResults');
|
||||
goTojobResults('workflow_job');
|
||||
}
|
||||
else if(_.has(data, 'ad_hoc_command')) {
|
||||
goTojobResults('adHocJobStdout');
|
||||
goTojobResults('ad_hoc_command');
|
||||
}
|
||||
else if(_.has(data, 'system_job')) {
|
||||
goTojobResults('managementJobStdout');
|
||||
goTojobResults('system_job');
|
||||
}
|
||||
else if(_.has(data, 'project_update')) {
|
||||
// If we are on the projects list or any child state of that list
|
||||
// then we want to stay on that page. Otherwise go to the stdout
|
||||
// view.
|
||||
if(!$state.includes('projects')) {
|
||||
goTojobResults('scmUpdateStdout');
|
||||
goTojobResults('project_update');
|
||||
}
|
||||
}
|
||||
else if(_.has(data, 'inventory_update')) {
|
||||
@ -183,7 +183,7 @@ export default
|
||||
// page then we want to stay on that page. Otherwise go to the stdout
|
||||
// view.
|
||||
if(!$state.includes('inventories.edit')) {
|
||||
goTojobResults('inventorySyncStdout');
|
||||
goTojobResults('playbook');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ export default
|
||||
Wait('stop');
|
||||
$("#prompt-for-days-facts").dialog("close");
|
||||
$("#configure-dialog").dialog('close');
|
||||
$state.go('managementJobStdout', {id: data.system_job}, {reload:true});
|
||||
$state.go('jobz', { id: data.system_job, type: 'system' }, { reload: true });
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
let template_id = scope.job_template_id;
|
||||
@ -222,7 +222,7 @@ export default
|
||||
Wait('stop');
|
||||
$("#prompt-for-days").dialog("close");
|
||||
// $("#configure-dialog").dialog('close');
|
||||
$state.go('managementJobStdout', {id: data.system_job}, {reload:true});
|
||||
$state.go('jobz', { id: data.system_job, type: 'system' }, { reload: true });
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
let template_id = scope.job_template_id;
|
||||
|
||||
@ -234,7 +234,7 @@ export default ['$scope', '$rootScope', '$location',
|
||||
$scope.viewJob = function(url) {
|
||||
// Pull the id out of the URL
|
||||
var id = url.replace(/^\//, '').split('/')[3];
|
||||
$state.go('inventorySyncStdout', { id: id });
|
||||
$state.go('jobz', { id: id, type: 'inventory' });
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -187,7 +187,7 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert',
|
||||
// Grab the id from summary_fields
|
||||
var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id;
|
||||
|
||||
$state.go('scmUpdateStdout', { id: id });
|
||||
$state.go('jobz', { id: id, type: 'project' });
|
||||
|
||||
} else {
|
||||
Alert('No Updates Available', 'There is no SCM update information available for this project. An update has not yet been ' +
|
||||
|
||||
@ -146,7 +146,7 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert',
|
||||
// Grab the id from summary_fields
|
||||
var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id;
|
||||
|
||||
$state.go('scmUpdateStdout', { id: id });
|
||||
$state.go('jobz', { id: id, type: 'project'}, { reload: true });
|
||||
|
||||
} else {
|
||||
Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' +
|
||||
|
||||
@ -1,311 +1,507 @@
|
||||
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', 'SmartSearchService',
|
||||
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) {
|
||||
return {
|
||||
// kick off building a model for a specific endpoint
|
||||
// this is usually a list's basePath
|
||||
// unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields
|
||||
initFieldset(path, name) {
|
||||
let defer = $q.defer();
|
||||
defer.resolve(this.getCommonModelOptions(path, name));
|
||||
return defer.promise;
|
||||
},
|
||||
function searchWithoutKey (term, singleSearchParam = null) {
|
||||
if (singleSearchParam) {
|
||||
return { [singleSearchParam]: `search=${encodeURIComponent(term)}` };
|
||||
}
|
||||
return { search: encodeURIComponent(term) };
|
||||
}
|
||||
|
||||
getCommonModelOptions(path, name) {
|
||||
let resolve, base,
|
||||
defer = $q.defer();
|
||||
function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) {
|
||||
return {
|
||||
// kick off building a model for a specific endpoint
|
||||
// this is usually a list's basePath
|
||||
// unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields
|
||||
initFieldset(path, name) {
|
||||
let defer = $q.defer();
|
||||
defer.resolve(this.getCommonModelOptions(path, name));
|
||||
return defer.promise;
|
||||
},
|
||||
getCommonModelOptions(path, name) {
|
||||
let resolve, base,
|
||||
defer = $q.defer();
|
||||
|
||||
this.url = path;
|
||||
resolve = this.options(path)
|
||||
.then((res) => {
|
||||
base = res.data.actions.GET;
|
||||
let relatedSearchFields = res.data.related_search_fields;
|
||||
defer.resolve({
|
||||
models: {
|
||||
[name]: new DjangoSearchModel(name, base, relatedSearchFields)
|
||||
},
|
||||
options: res
|
||||
});
|
||||
this.url = path;
|
||||
resolve = this.options(path)
|
||||
.then((res) => {
|
||||
base = res.data.actions.GET;
|
||||
let relatedSearchFields = res.data.related_search_fields;
|
||||
defer.resolve({
|
||||
models: {
|
||||
[name]: new DjangoSearchModel(name, base, relatedSearchFields)
|
||||
},
|
||||
options: res
|
||||
});
|
||||
return defer.promise;
|
||||
},
|
||||
});
|
||||
return defer.promise;
|
||||
},
|
||||
replaceDefaultFlags (value) {
|
||||
value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains");
|
||||
value = value.toString().replace(/__search_DEFAULT/g, "__search");
|
||||
|
||||
replaceDefaultFlags (value) {
|
||||
value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains");
|
||||
value = value.toString().replace(/__search_DEFAULT/g, "__search");
|
||||
return value;
|
||||
},
|
||||
replaceEncodedTokens(value) {
|
||||
return decodeURIComponent(value).replace(/"|'/g, "");
|
||||
},
|
||||
encodeTerms (values, key) {
|
||||
key = this.replaceDefaultFlags(key);
|
||||
|
||||
return value;
|
||||
},
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values];
|
||||
}
|
||||
|
||||
replaceEncodedTokens(value) {
|
||||
return decodeURIComponent(value).replace(/"|'/g, "");
|
||||
},
|
||||
return values
|
||||
.map(value => {
|
||||
value = this.replaceDefaultFlags(value);
|
||||
value = this.replaceEncodedTokens(value);
|
||||
return [key, value];
|
||||
});
|
||||
|
||||
encodeTerms (values, key) {
|
||||
key = this.replaceDefaultFlags(key);
|
||||
},
|
||||
// encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL
|
||||
encodeQueryset(params) {
|
||||
if (typeof params !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = this.replaceEncodedTokens(values);
|
||||
|
||||
return `${key}=${values}`;
|
||||
return _.reduce(params, (result, value, key) => {
|
||||
if (result !== '?') {
|
||||
result += '&';
|
||||
}
|
||||
|
||||
return values
|
||||
.map(value => {
|
||||
value = this.replaceDefaultFlags(value);
|
||||
value = this.replaceEncodedTokens(value);
|
||||
|
||||
return `${key}=${value}`;
|
||||
})
|
||||
const encodedTermString = this.encodeTerms(value, key)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('&');
|
||||
},
|
||||
// encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL
|
||||
encodeQueryset(params) {
|
||||
if (typeof params !== 'object') {
|
||||
return '';
|
||||
|
||||
return result += encodedTermString;
|
||||
}, '?');
|
||||
},
|
||||
// like encodeQueryset, but return an actual unstringified API-consumable http param object
|
||||
encodeQuerysetObject(params) {
|
||||
return _.reduce(params, (obj, value, key) => {
|
||||
const encodedTerms = this.encodeTerms(value, key);
|
||||
|
||||
for (let encodedIndex in encodedTerms) {
|
||||
const [encodedKey, encodedValue] = encodedTerms[encodedIndex];
|
||||
obj[encodedKey] = obj[encodedKey] || [];
|
||||
obj[encodedKey].push(encodedValue);
|
||||
}
|
||||
|
||||
return _.reduce(params, (result, value, key) => {
|
||||
if (result !== '?') {
|
||||
result += '&';
|
||||
}
|
||||
|
||||
return result += this.encodeTerms(value, key);
|
||||
}, '?');
|
||||
},
|
||||
// encodes a ui smart-search param to a django-friendly param
|
||||
// operand:key:comparator:value => {operand__key__comparator: value}
|
||||
encodeParam(params){
|
||||
// Assumption here is that we have a key and a value so the length
|
||||
// of the paramParts array will be 2. [0] is the key and [1] the value
|
||||
let paramParts = SmartSearchService.splitTermIntoParts(params.term);
|
||||
let keySplit = paramParts[0].split('.');
|
||||
let exclude = false;
|
||||
let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false;
|
||||
if(keySplit[0].match(/^-/g)) {
|
||||
exclude = true;
|
||||
keySplit[0] = keySplit[0].replace(/^-/, '');
|
||||
}
|
||||
let paramString = exclude ? "not__" : "";
|
||||
let valueString = paramParts[1];
|
||||
if(keySplit.length === 1) {
|
||||
if(params.searchTerm && !lessThanGreaterThan) {
|
||||
if(params.singleSearchParam) {
|
||||
paramString += keySplit[0] + '__icontains';
|
||||
}
|
||||
else {
|
||||
paramString += keySplit[0] + '__icontains_DEFAULT';
|
||||
}
|
||||
}
|
||||
else if(params.relatedSearchTerm) {
|
||||
if(params.singleSearchParam) {
|
||||
paramString += keySplit[0];
|
||||
}
|
||||
else {
|
||||
paramString += keySplit[0] + '__search_DEFAULT';
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
},
|
||||
// encodes a ui smart-search param to a django-friendly param
|
||||
// operand:key:comparator:value => {operand__key__comparator: value}
|
||||
encodeParam({ term, relatedSearchTerm, searchTerm, singleSearchParam }){
|
||||
// Assumption here is that we have a key and a value so the length
|
||||
// of the paramParts array will be 2. [0] is the key and [1] the value
|
||||
let paramParts = SmartSearchService.splitTermIntoParts(term);
|
||||
let keySplit = paramParts[0].split('.');
|
||||
let exclude = false;
|
||||
let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false;
|
||||
if(keySplit[0].match(/^-/g)) {
|
||||
exclude = true;
|
||||
keySplit[0] = keySplit[0].replace(/^-/, '');
|
||||
}
|
||||
let paramString = exclude ? "not__" : "";
|
||||
let valueString = paramParts[1];
|
||||
if(keySplit.length === 1) {
|
||||
if(searchTerm && !lessThanGreaterThan) {
|
||||
if(singleSearchParam) {
|
||||
paramString += keySplit[0] + '__icontains';
|
||||
}
|
||||
else {
|
||||
paramString += keySplit[0] + '__icontains_DEFAULT';
|
||||
}
|
||||
}
|
||||
else if(relatedSearchTerm) {
|
||||
if(singleSearchParam) {
|
||||
paramString += keySplit[0];
|
||||
}
|
||||
}
|
||||
else {
|
||||
paramString += keySplit.join('__');
|
||||
}
|
||||
|
||||
if(lessThanGreaterThan) {
|
||||
if(paramParts[1].match(/^>=.*$/)) {
|
||||
paramString += '__gte';
|
||||
valueString = valueString.replace(/^(>=)/,"");
|
||||
}
|
||||
else if(paramParts[1].match(/^<=.*$/)) {
|
||||
paramString += '__lte';
|
||||
valueString = valueString.replace(/^(<=)/,"");
|
||||
}
|
||||
else if(paramParts[1].match(/^<.*$/)) {
|
||||
paramString += '__lt';
|
||||
valueString = valueString.replace(/^(<)/,"");
|
||||
}
|
||||
else if(paramParts[1].match(/^>.*$/)) {
|
||||
paramString += '__gt';
|
||||
valueString = valueString.replace(/^(>)/,"");
|
||||
}
|
||||
}
|
||||
|
||||
if(params.singleSearchParam) {
|
||||
return {[params.singleSearchParam]: paramString + "=" + valueString};
|
||||
}
|
||||
else {
|
||||
return {[paramString] : encodeURIComponent(valueString)};
|
||||
}
|
||||
},
|
||||
// decodes a django queryset param into a ui smart-search tag or set of tags
|
||||
decodeParam(value, key){
|
||||
|
||||
let decodeParamString = function(searchString) {
|
||||
if(key === 'search') {
|
||||
// Don't include 'search:' in the search tag
|
||||
return decodeURIComponent(`${searchString}`);
|
||||
}
|
||||
else {
|
||||
key = key.toString().replace(/__icontains_DEFAULT/g, "");
|
||||
key = key.toString().replace(/__search_DEFAULT/g, "");
|
||||
let split = key.split('__');
|
||||
let decodedParam = searchString;
|
||||
let exclude = false;
|
||||
if(key.startsWith('not__')) {
|
||||
exclude = true;
|
||||
split = split.splice(1, split.length);
|
||||
}
|
||||
if(key.endsWith('__gt')) {
|
||||
decodedParam = '>' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
else if(key.endsWith('__lt')) {
|
||||
decodedParam = '<' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
else if(key.endsWith('__gte')) {
|
||||
decodedParam = '>=' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
else if(key.endsWith('__lte')) {
|
||||
decodedParam = '<=' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
|
||||
let uriDecodedParam = decodeURIComponent(decodedParam);
|
||||
|
||||
return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`;
|
||||
paramString += keySplit[0] + '__search_DEFAULT';
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
paramString += keySplit[0];
|
||||
}
|
||||
}
|
||||
else {
|
||||
paramString += keySplit.join('__');
|
||||
}
|
||||
|
||||
if(lessThanGreaterThan) {
|
||||
if(paramParts[1].match(/^>=.*$/)) {
|
||||
paramString += '__gte';
|
||||
valueString = valueString.replace(/^(>=)/,"");
|
||||
}
|
||||
else if(paramParts[1].match(/^<=.*$/)) {
|
||||
paramString += '__lte';
|
||||
valueString = valueString.replace(/^(<=)/,"");
|
||||
}
|
||||
else if(paramParts[1].match(/^<.*$/)) {
|
||||
paramString += '__lt';
|
||||
valueString = valueString.replace(/^(<)/,"");
|
||||
}
|
||||
else if(paramParts[1].match(/^>.*$/)) {
|
||||
paramString += '__gt';
|
||||
valueString = valueString.replace(/^(>)/,"");
|
||||
}
|
||||
}
|
||||
|
||||
if(singleSearchParam) {
|
||||
return {[singleSearchParam]: paramString + "=" + valueString};
|
||||
}
|
||||
else {
|
||||
return {[paramString] : encodeURIComponent(valueString)};
|
||||
}
|
||||
},
|
||||
// decodes a django queryset param into a ui smart-search tag or set of tags
|
||||
decodeParam(value, key){
|
||||
|
||||
let decodeParamString = function(searchString) {
|
||||
if(key === 'search') {
|
||||
// Don't include 'search:' in the search tag
|
||||
return decodeURIComponent(`${searchString}`);
|
||||
}
|
||||
else {
|
||||
key = key.toString().replace(/__icontains_DEFAULT/g, "");
|
||||
key = key.toString().replace(/__search_DEFAULT/g, "");
|
||||
let split = key.split('__');
|
||||
let decodedParam = searchString;
|
||||
let exclude = false;
|
||||
if(key.startsWith('not__')) {
|
||||
exclude = true;
|
||||
split = split.splice(1, split.length);
|
||||
}
|
||||
if(key.endsWith('__gt')) {
|
||||
decodedParam = '>' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
else if(key.endsWith('__lt')) {
|
||||
decodedParam = '<' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
else if(key.endsWith('__gte')) {
|
||||
decodedParam = '>=' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
else if(key.endsWith('__lte')) {
|
||||
decodedParam = '<=' + decodedParam;
|
||||
split = split.splice(0, split.length-1);
|
||||
}
|
||||
|
||||
let uriDecodedParam = decodeURIComponent(decodedParam);
|
||||
|
||||
return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`;
|
||||
}
|
||||
};
|
||||
|
||||
if (Array.isArray(value)){
|
||||
value = _.uniq(_.flattenDeep(value));
|
||||
return _.map(value, (item) => {
|
||||
return decodeParamString(item);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return decodeParamString(value);
|
||||
}
|
||||
},
|
||||
// encodes a django queryset for ui-router's URLMatcherFactory
|
||||
// {operand__key__comparator: value, } => 'operand:key:comparator:value;...'
|
||||
// value.isArray expands to:
|
||||
// {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...'
|
||||
encodeArr(params) {
|
||||
let url;
|
||||
url = _.reduce(params, (result, value, key) => {
|
||||
return result.concat(encodeUrlString(value, key));
|
||||
}, []);
|
||||
|
||||
return url.join(';');
|
||||
|
||||
// {key:'value'} => 'key:value'
|
||||
// {key: [value1, value2, ...]} => ['key:value1', 'key:value2']
|
||||
function encodeUrlString(value, key){
|
||||
if (Array.isArray(value)){
|
||||
value = _.uniq(_.flattenDeep(value));
|
||||
return _.map(value, (item) => {
|
||||
return decodeParamString(item);
|
||||
return `${key}:${item}`;
|
||||
});
|
||||
}
|
||||
else {
|
||||
return decodeParamString(value);
|
||||
}
|
||||
},
|
||||
|
||||
// encodes a django queryset for ui-router's URLMatcherFactory
|
||||
// {operand__key__comparator: value, } => 'operand:key:comparator:value;...'
|
||||
// value.isArray expands to:
|
||||
// {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...'
|
||||
encodeArr(params) {
|
||||
let url;
|
||||
url = _.reduce(params, (result, value, key) => {
|
||||
return result.concat(encodeUrlString(value, key));
|
||||
}, []);
|
||||
|
||||
return url.join(';');
|
||||
|
||||
// {key:'value'} => 'key:value'
|
||||
// {key: [value1, value2, ...]} => ['key:value1', 'key:value2']
|
||||
function encodeUrlString(value, key){
|
||||
if (Array.isArray(value)){
|
||||
value = _.uniq(_.flattenDeep(value));
|
||||
return _.map(value, (item) => {
|
||||
return `${key}:${item}`;
|
||||
});
|
||||
}
|
||||
else {
|
||||
return `${key}:${value}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// decodes a django queryset for ui-router's URLMatcherFactory
|
||||
// 'operand:key:comparator:value,...' => {operand__key__comparator: value, }
|
||||
decodeArr(arr) {
|
||||
let params = {};
|
||||
_.forEach(arr.split(';'), (item) => {
|
||||
let key = item.split(':')[0],
|
||||
value = item.split(':')[1];
|
||||
if(!params[key]){
|
||||
params[key] = value;
|
||||
}
|
||||
else if (Array.isArray(params[key])){
|
||||
params[key] = _.uniq(_.flattenDeep(params[key]));
|
||||
params[key].push(value);
|
||||
}
|
||||
else {
|
||||
params[key] = [params[key], value];
|
||||
}
|
||||
});
|
||||
return params;
|
||||
},
|
||||
// REST utilities
|
||||
options(endpoint) {
|
||||
Rest.setUrl(endpoint);
|
||||
return Rest.options(endpoint);
|
||||
},
|
||||
search(endpoint, params) {
|
||||
Wait('start');
|
||||
this.url = `${endpoint}${this.encodeQueryset(params)}`;
|
||||
Rest.setUrl(this.url);
|
||||
|
||||
return Rest.get()
|
||||
.then(function(response) {
|
||||
Wait('stop');
|
||||
|
||||
if (response
|
||||
.headers('X-UI-Max-Events') !== null) {
|
||||
response.data.maxEvents = response.
|
||||
headers('X-UI-Max-Events');
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch(function(response) {
|
||||
Wait('stop');
|
||||
|
||||
this.error(response.data, response.status);
|
||||
|
||||
throw response;
|
||||
}.bind(this));
|
||||
},
|
||||
error(data, status) {
|
||||
if(data && data.detail){
|
||||
let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail);
|
||||
|
||||
if(_.isArray(error)){
|
||||
data.detail = error[0];
|
||||
}
|
||||
}
|
||||
ProcessErrors($rootScope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: `Invalid search term entered. GET returned: ${status}`
|
||||
});
|
||||
},
|
||||
// Removes state definition defaults and pagination terms
|
||||
stripDefaultParams(params, defaults) {
|
||||
if(defaults) {
|
||||
let stripped =_.pick(params, (value, key) => {
|
||||
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
|
||||
return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null;
|
||||
});
|
||||
let strippedCopy = _.cloneDeep(stripped);
|
||||
if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){
|
||||
for (var key in strippedCopy) {
|
||||
if (strippedCopy.hasOwnProperty(key)) {
|
||||
let value = strippedCopy[key];
|
||||
if(_.isArray(value)){
|
||||
let index = _.indexOf(value, defaults[key]);
|
||||
value = value.splice(index, 1)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
stripped = strippedCopy;
|
||||
}
|
||||
return _(strippedCopy).map(this.decodeParam).flatten().value();
|
||||
}
|
||||
else {
|
||||
return _(params).map(this.decodeParam).flatten().value();
|
||||
return `${key}:${value}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
// decodes a django queryset for ui-router's URLMatcherFactory
|
||||
// 'operand:key:comparator:value,...' => {operand__key__comparator: value, }
|
||||
decodeArr(arr) {
|
||||
let params = {};
|
||||
|
||||
if (!arr) {
|
||||
return params;
|
||||
}
|
||||
|
||||
_.forEach(arr.split(';'), (item) => {
|
||||
let key = item.split(':')[0],
|
||||
value = item.split(':')[1];
|
||||
if(!params[key]){
|
||||
params[key] = value;
|
||||
}
|
||||
else if (Array.isArray(params[key])){
|
||||
params[key] = _.uniq(_.flattenDeep(params[key]));
|
||||
params[key].push(value);
|
||||
}
|
||||
else {
|
||||
params[key] = [params[key], value];
|
||||
}
|
||||
});
|
||||
return params;
|
||||
},
|
||||
// REST utilities
|
||||
options(endpoint) {
|
||||
Rest.setUrl(endpoint);
|
||||
return Rest.options(endpoint);
|
||||
},
|
||||
search(endpoint, params) {
|
||||
Wait('start');
|
||||
this.url = `${endpoint}${this.encodeQueryset(params)}`;
|
||||
Rest.setUrl(this.url);
|
||||
|
||||
return Rest.get()
|
||||
.then(function(response) {
|
||||
Wait('stop');
|
||||
|
||||
if (response
|
||||
.headers('X-UI-Max-Events') !== null) {
|
||||
response.data.maxEvents = response.
|
||||
headers('X-UI-Max-Events');
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch(function(response) {
|
||||
Wait('stop');
|
||||
|
||||
this.error(response.data, response.status);
|
||||
|
||||
throw response;
|
||||
}.bind(this));
|
||||
},
|
||||
error(data, status) {
|
||||
if(data && data.detail){
|
||||
let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail);
|
||||
|
||||
if(_.isArray(error)){
|
||||
data.detail = error[0];
|
||||
}
|
||||
}
|
||||
ProcessErrors($rootScope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: `Invalid search term entered. GET returned: ${status}`
|
||||
});
|
||||
},
|
||||
// Removes state definition defaults and pagination terms
|
||||
stripDefaultParams(params, defaultParams) {
|
||||
if (!params) {
|
||||
return [];
|
||||
}
|
||||
if(defaultParams) {
|
||||
let stripped =_.pick(params, (value, key) => {
|
||||
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
|
||||
return defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null;
|
||||
});
|
||||
let strippedCopy = _.cloneDeep(stripped);
|
||||
if(_.keys(_.pick(defaultParams, _.keys(strippedCopy))).length > 0){
|
||||
for (var key in strippedCopy) {
|
||||
if (strippedCopy.hasOwnProperty(key)) {
|
||||
let value = strippedCopy[key];
|
||||
if(_.isArray(value)){
|
||||
let index = _.indexOf(value, defaultParams[key]);
|
||||
value = value.splice(index, 1)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
stripped = strippedCopy;
|
||||
}
|
||||
return _(strippedCopy).map(this.decodeParam).flatten().value();
|
||||
}
|
||||
else {
|
||||
return _(params).map(this.decodeParam).flatten().value();
|
||||
}
|
||||
},
|
||||
mergeQueryset (queryset, additional, singleSearchParam) {
|
||||
const space = '%20and%20';
|
||||
|
||||
const merged = _.merge({}, queryset, additional, (objectValue, sourceValue, key, object) => {
|
||||
if (!(object[key] && object[key] !== sourceValue)) {
|
||||
// // https://lodash.com/docs/3.10.1#each
|
||||
// If this returns undefined merging is handled by default _.merge algorithm
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (_.isArray(object[key])) {
|
||||
object[key].push(sourceValue);
|
||||
return object[key];
|
||||
}
|
||||
|
||||
if (singleSearchParam) {
|
||||
if (!object[key]) {
|
||||
return sourceValue;
|
||||
}
|
||||
|
||||
const singleSearchParamKeys = object[key].split(space);
|
||||
|
||||
if (_.includes(singleSearchParamKeys, sourceValue)) {
|
||||
return object[key];
|
||||
}
|
||||
|
||||
return `${object[key]}${space}${sourceValue}`;
|
||||
}
|
||||
|
||||
// Start the array of keys
|
||||
return [object[key], sourceValue];
|
||||
});
|
||||
|
||||
return merged;
|
||||
},
|
||||
getSearchInputQueryset (searchInput, isRelatedField = null, isAnsibleFactField = null, singleSearchParam = null) {
|
||||
// XXX Should find a better approach than passing in the two 'is...Field' callbacks XXX
|
||||
const space = '%20and%20';
|
||||
let params = {};
|
||||
|
||||
// Remove leading/trailing whitespace if there is any
|
||||
const terms = (searchInput) ? searchInput.trim() : '';
|
||||
|
||||
if (!(terms && terms !== '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let splitTerms;
|
||||
|
||||
if (singleSearchParam === 'host_filter') {
|
||||
splitTerms = SmartSearchService.splitFilterIntoTerms(terms);
|
||||
} else {
|
||||
splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
|
||||
}
|
||||
|
||||
const combineSameSearches = (a, b) => {
|
||||
if (!a) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (_.isArray(a)) {
|
||||
return a.concat(b);
|
||||
}
|
||||
|
||||
if (singleSearchParam) {
|
||||
return `${a}${space}${b}`;
|
||||
}
|
||||
|
||||
return [a, b];
|
||||
};
|
||||
|
||||
_.each(splitTerms, term => {
|
||||
const termParts = SmartSearchService.splitTermIntoParts(term);
|
||||
let termParams;
|
||||
|
||||
if (termParts.length === 1) {
|
||||
termParams = searchWithoutKey(term, singleSearchParam);
|
||||
} else if (isAnsibleFactField && isAnsibleFactField(termParts)) {
|
||||
termParams = this.encodeParam({ term, singleSearchParam });
|
||||
} else if (isRelatedField && isRelatedField(termParts)) {
|
||||
termParams = this.encodeParam({ term, singleSearchParam, related: true });
|
||||
} else {
|
||||
termParams = this.encodeParam({ term, singleSearchParam });
|
||||
}
|
||||
|
||||
params = _.merge(params, termParams, combineSameSearches);
|
||||
});
|
||||
|
||||
return params;
|
||||
},
|
||||
removeTermsFromQueryset(queryset, term, isRelatedField = null, singleSearchParam = null) {
|
||||
const modifiedQueryset = _.cloneDeep(queryset);
|
||||
|
||||
const removeSingleTermFromQueryset = (value, key) => {
|
||||
const space = '%20and%20';
|
||||
|
||||
if (Array.isArray(modifiedQueryset[key])) {
|
||||
modifiedQueryset[key] = modifiedQueryset[key].filter(item => item !== value);
|
||||
if (modifiedQueryset[key].length < 1) {
|
||||
delete modifiedQueryset[key];
|
||||
}
|
||||
} else if (singleSearchParam && _.get(modifiedQueryset, singleSearchParam, []).includes(space)) {
|
||||
const searchParamParts = modifiedQueryset[singleSearchParam].split(space);
|
||||
// The value side of each paramPart might have been encoded in
|
||||
// SmartSearch.splitFilterIntoTerms
|
||||
_.each(searchParamParts, (paramPart, paramPartIndex) => {
|
||||
searchParamParts[paramPartIndex] = decodeURIComponent(paramPart);
|
||||
});
|
||||
|
||||
const paramPartIndex = searchParamParts.indexOf(value);
|
||||
|
||||
if (paramPartIndex !== -1) {
|
||||
searchParamParts.splice(paramPartIndex, 1);
|
||||
}
|
||||
|
||||
modifiedQueryset[singleSearchParam] = searchParamParts.join(space);
|
||||
|
||||
} else {
|
||||
delete modifiedQueryset[key];
|
||||
}
|
||||
};
|
||||
|
||||
const termParts = SmartSearchService.splitTermIntoParts(term);
|
||||
|
||||
let removed;
|
||||
|
||||
if (termParts.length === 1) {
|
||||
removed = searchWithoutKey(term, singleSearchParam);
|
||||
} else if (isRelatedField && isRelatedField(termParts)) {
|
||||
removed = this.encodeParam({ term, singleSearchParam, related: true });
|
||||
} else {
|
||||
removed = this.encodeParam({ term, singleSearchParam });
|
||||
}
|
||||
|
||||
if (!removed) {
|
||||
removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam);
|
||||
}
|
||||
|
||||
_.each(removed, removeSingleTermFromQueryset);
|
||||
|
||||
return modifiedQueryset;
|
||||
},
|
||||
createSearchTagsFromQueryset(queryset, defaultParams = null, singleSearchParam = null) {
|
||||
const space = '%20and%20';
|
||||
const modifiedQueryset = angular.copy(queryset);
|
||||
|
||||
let searchTags = [];
|
||||
|
||||
if (singleSearchParam && modifiedQueryset[singleSearchParam]) {
|
||||
const searchParam = modifiedQueryset[singleSearchParam].split(space);
|
||||
delete modifiedQueryset[singleSearchParam];
|
||||
|
||||
$.each(searchParam, (index, param) => {
|
||||
const paramParts = decodeURIComponent(param).split(/=(.+)/);
|
||||
const reconstructedSearchString = this.decodeParam(paramParts[1], paramParts[0]);
|
||||
|
||||
searchTags.push(reconstructedSearchString);
|
||||
});
|
||||
}
|
||||
|
||||
return searchTags.concat(this.stripDefaultParams(modifiedQueryset, defaultParams));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
QuerysetService.$inject = [
|
||||
'$q',
|
||||
'Rest',
|
||||
'ProcessErrors',
|
||||
'$rootScope',
|
||||
'Wait',
|
||||
'DjangoSearchModel',
|
||||
'SmartSearchService',
|
||||
];
|
||||
|
||||
export default QuerysetService;
|
||||
|
||||
@ -1,422 +1,254 @@
|
||||
export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', 'ConfigService', '$transitions',
|
||||
function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n, configService, $transitions) {
|
||||
function SmartSearchController (
|
||||
$scope,
|
||||
$state,
|
||||
$stateParams,
|
||||
$transitions,
|
||||
configService,
|
||||
GetBasePath,
|
||||
i18n,
|
||||
qs
|
||||
) {
|
||||
const searchKey = `${$scope.iterator}_search`;
|
||||
const optionsKey = `${$scope.list.iterator}_options`;
|
||||
|
||||
let path,
|
||||
defaults,
|
||||
queryset,
|
||||
transitionSuccessListener;
|
||||
let path;
|
||||
let defaults;
|
||||
let queryset;
|
||||
let transitionSuccessListener;
|
||||
|
||||
configService.getConfig()
|
||||
.then(config => init(config));
|
||||
configService.getConfig()
|
||||
.then(config => init(config));
|
||||
|
||||
function init(config) {
|
||||
let version;
|
||||
function init (config) {
|
||||
let version;
|
||||
|
||||
try {
|
||||
version = config.version.split('-')[0];
|
||||
} catch (err) {
|
||||
version = 'latest';
|
||||
}
|
||||
try {
|
||||
[version] = config.version.split('-');
|
||||
} catch (err) {
|
||||
version = 'latest';
|
||||
}
|
||||
|
||||
$scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`;
|
||||
$scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`;
|
||||
$scope.searchPlaceholder = i18n._('Search');
|
||||
|
||||
if($scope.defaultParams) {
|
||||
defaults = $scope.defaultParams;
|
||||
}
|
||||
else {
|
||||
// steps through the current tree of $state configurations, grabs default search params
|
||||
defaults = _.find($state.$current.path, (step) => {
|
||||
if(step && step.params && step.params.hasOwnProperty(`${$scope.iterator}_search`)){
|
||||
return step.params.hasOwnProperty(`${$scope.iterator}_search`);
|
||||
}
|
||||
}).params[`${$scope.iterator}_search`].config.value;
|
||||
}
|
||||
if ($scope.defaultParams) {
|
||||
defaults = $scope.defaultParams;
|
||||
} else {
|
||||
// steps through the current tree of $state configurations, grabs default search params
|
||||
const stateConfig = _.find($state.$current.path, step => _.has(step, `params.${searchKey}`));
|
||||
defaults = stateConfig.params[searchKey].config.value;
|
||||
}
|
||||
|
||||
if($scope.querySet) {
|
||||
queryset = _.cloneDeep($scope.querySet);
|
||||
}
|
||||
else {
|
||||
queryset = $state.params[`${$scope.iterator}_search`];
|
||||
}
|
||||
if ($scope.querySet) {
|
||||
queryset = _.cloneDeep($scope.querySet);
|
||||
} else {
|
||||
queryset = $state.params[searchKey];
|
||||
}
|
||||
|
||||
path = GetBasePath($scope.basePath) || $scope.basePath;
|
||||
generateSearchTags();
|
||||
qs.initFieldset(path, $scope.djangoModel).then((data) => {
|
||||
path = GetBasePath($scope.basePath) || $scope.basePath;
|
||||
generateSearchTags();
|
||||
|
||||
qs.initFieldset(path, $scope.djangoModel)
|
||||
.then((data) => {
|
||||
$scope.models = data.models;
|
||||
$scope.options = data.options.data;
|
||||
if ($scope.list) {
|
||||
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
|
||||
}
|
||||
});
|
||||
$scope.searchPlaceholder = $scope.disableSearch ? i18n._('Cannot search running job') : i18n._('Search');
|
||||
|
||||
function compareParams(a, b) {
|
||||
for (let key in a) {
|
||||
if (!(key in b) || a[key].toString() !== b[key].toString()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (let key in b) {
|
||||
if (!(key in a)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if(transitionSuccessListener) {
|
||||
transitionSuccessListener();
|
||||
}
|
||||
|
||||
transitionSuccessListener = $transitions.onSuccess({}, function(trans) {
|
||||
// State has changed - check to see if this is a param change
|
||||
if(trans.from().name === trans.to().name) {
|
||||
if(!compareParams(trans.params('from')[`${$scope.iterator}_search`], trans.params('to')[`${$scope.iterator}_search`])) {
|
||||
// Params are not the same - we need to update the search. This should only happen when the user
|
||||
// hits the forward/back navigation buttons in their browser.
|
||||
queryset = trans.params('to')[`${$scope.iterator}_search`];
|
||||
qs.search(path, queryset).then((res) => {
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
$scope.$emit('updateDataset', res.data);
|
||||
});
|
||||
|
||||
$scope.searchTerm = null;
|
||||
generateSearchTags();
|
||||
}
|
||||
$scope.$emit(optionsKey, data.options);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', transitionSuccessListener);
|
||||
|
||||
$scope.$watch('disableSearch', function(disableSearch){
|
||||
if(disableSearch) {
|
||||
$scope.searchPlaceholder = i18n._('Cannot search running job');
|
||||
function compareParams (a, b) {
|
||||
for (let key in a) {
|
||||
if (!(key in b) || a[key].toString() !== b[key].toString()) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$scope.searchPlaceholder = i18n._('Search');
|
||||
}
|
||||
for (let key in b) {
|
||||
if (!(key in a)) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function generateSearchTags() {
|
||||
$scope.searchTags = [];
|
||||
|
||||
let querysetCopy = angular.copy(queryset);
|
||||
|
||||
if($scope.singleSearchParam && querysetCopy[$scope.singleSearchParam]) {
|
||||
let searchParam = querysetCopy[$scope.singleSearchParam].split('%20and%20');
|
||||
delete querysetCopy[$scope.singleSearchParam];
|
||||
|
||||
$.each(searchParam, function(index, param) {
|
||||
let paramParts = decodeURIComponent(param).split(/=(.+)/);
|
||||
let reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]);
|
||||
$scope.searchTags.push(reconstructedSearchString);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults));
|
||||
if (transitionSuccessListener) {
|
||||
transitionSuccessListener();
|
||||
}
|
||||
|
||||
function revertSearch(queryToBeRestored) {
|
||||
queryset = queryToBeRestored;
|
||||
// https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic
|
||||
// This transition will not reload controllers/resolves/views
|
||||
// but will register new $stateParams[$scope.iterator + '_search'] terms
|
||||
if(!$scope.querySet) {
|
||||
$state.go('.', {
|
||||
[$scope.iterator + '_search']: queryset });
|
||||
}
|
||||
qs.search(path, queryset).then((res) => {
|
||||
if($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
}
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
});
|
||||
|
||||
$scope.searchTerm = null;
|
||||
|
||||
generateSearchTags();
|
||||
}
|
||||
|
||||
function searchWithoutKey(term) {
|
||||
if($scope.singleSearchParam) {
|
||||
return {
|
||||
[$scope.singleSearchParam]: "search=" + encodeURIComponent(term)
|
||||
};
|
||||
}
|
||||
return {
|
||||
search: encodeURIComponent(term)
|
||||
};
|
||||
}
|
||||
|
||||
$scope.toggleKeyPane = function() {
|
||||
$scope.showKeyPane = !$scope.showKeyPane;
|
||||
};
|
||||
|
||||
// add a search tag, merge new queryset, $state.go()
|
||||
$scope.addTerm = function(terms) {
|
||||
let params = {},
|
||||
origQueryset = _.clone(queryset);
|
||||
|
||||
// Remove leading/trailing whitespace if there is any
|
||||
terms = (terms) ? terms.trim() : "";
|
||||
|
||||
if(terms && terms !== '') {
|
||||
let splitTerms;
|
||||
|
||||
if ($scope.singleSearchParam === 'host_filter') {
|
||||
splitTerms = SmartSearchService.splitFilterIntoTerms(terms);
|
||||
} else {
|
||||
splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
|
||||
}
|
||||
|
||||
_.forEach(splitTerms, (term) => {
|
||||
let termParts = SmartSearchService.splitTermIntoParts(term);
|
||||
|
||||
function combineSameSearches(a,b){
|
||||
if (_.isArray(a)) {
|
||||
return a.concat(b);
|
||||
}
|
||||
else {
|
||||
if(a) {
|
||||
if($scope.singleSearchParam) {
|
||||
return a + "%20and%20" + b;
|
||||
}
|
||||
else {
|
||||
return [a,b];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($scope.singleSearchParam) {
|
||||
if (termParts.length === 1) {
|
||||
params = _.merge(params, searchWithoutKey(term), combineSameSearches);
|
||||
}
|
||||
else {
|
||||
let root = termParts[0].split(".")[0].replace(/^-/, '');
|
||||
if(_.has($scope.models[$scope.list.name].base, root) || root === "ansible_facts") {
|
||||
if(_.has($scope.models[$scope.list.name].base[root], "type") && $scope.models[$scope.list.name].base[root].type === 'field'){
|
||||
// Intent is to land here for searching on the base model.
|
||||
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches);
|
||||
}
|
||||
else {
|
||||
// Intent is to land here when performing ansible_facts searches
|
||||
params = _.merge(params, qs.encodeParam({term: term, searchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches);
|
||||
}
|
||||
}
|
||||
else if(_.contains($scope.models[$scope.list.name].related, root)) {
|
||||
// Intent is to land here for related searches
|
||||
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches);
|
||||
}
|
||||
// Its not a search term or a related search term - treat it as a string
|
||||
else {
|
||||
params = _.merge(params, searchWithoutKey(term), combineSameSearches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
// if only a value is provided, search using default keys
|
||||
if (termParts.length === 1) {
|
||||
params = _.merge(params, searchWithoutKey(term), combineSameSearches);
|
||||
} else {
|
||||
// Figure out if this is a search term
|
||||
let root = termParts[0].split(".")[0].replace(/^-/, '');
|
||||
if(_.has($scope.models[$scope.list.name].base, root)) {
|
||||
if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') {
|
||||
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches);
|
||||
}
|
||||
else {
|
||||
params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches);
|
||||
}
|
||||
}
|
||||
// The related fields need to also be checked for related searches.
|
||||
// The related fields for the search are retrieved from the API
|
||||
// options endpoint, and are stored in the $scope.model. FYI, the
|
||||
// Django search model is what sets the related fields on the model.
|
||||
else if(_.contains($scope.models[$scope.list.name].related, root)) {
|
||||
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches);
|
||||
}
|
||||
// Its not a search term or a related search term - treat it as a string
|
||||
else {
|
||||
params = _.merge(params, searchWithoutKey(term), combineSameSearches);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
queryset = _.merge({}, queryset, params, (objectValue, sourceValue, key, object) => {
|
||||
if (object[key] && object[key] !== sourceValue){
|
||||
if(_.isArray(object[key])) {
|
||||
// Add the new value to the array and return
|
||||
object[key].push(sourceValue);
|
||||
return object[key];
|
||||
}
|
||||
else {
|
||||
if($scope.singleSearchParam) {
|
||||
if(!object[key]) {
|
||||
return sourceValue;
|
||||
}
|
||||
else {
|
||||
let singleSearchParamKeys = object[key].split("%20and%20");
|
||||
|
||||
if(_.includes(singleSearchParamKeys, sourceValue)) {
|
||||
return object[key];
|
||||
}
|
||||
else {
|
||||
return object[key] + "%20and%20" + sourceValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the array of keys
|
||||
return [object[key], sourceValue];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// // https://lodash.com/docs/3.10.1#merge
|
||||
// If customizer fn returns undefined merging is handled by default _.merge algorithm
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Go back to the first page after a new search
|
||||
delete queryset.page;
|
||||
|
||||
// https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic
|
||||
// This transition will not reload controllers/resolves/views
|
||||
// but will register new $stateParams[$scope.iterator + '_search'] terms
|
||||
if(!$scope.querySet) {
|
||||
$state.go('.', {[$scope.iterator + '_search']:queryset }).then(function(){
|
||||
// ISSUE: same as above in $scope.remove. For some reason deleting the page
|
||||
// from the queryset works for all lists except lists in modals.
|
||||
delete $stateParams[$scope.iterator + '_search'].page;
|
||||
});
|
||||
}
|
||||
qs.search(path, queryset).then((res) => {
|
||||
if($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
}
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
})
|
||||
.catch(function() {
|
||||
revertSearch(origQueryset);
|
||||
});
|
||||
|
||||
$scope.searchTerm = null;
|
||||
|
||||
generateSearchTags();
|
||||
}
|
||||
};
|
||||
|
||||
// remove tag, merge new queryset, $state.go
|
||||
$scope.removeTerm = function(index) {
|
||||
let tagToRemove = $scope.searchTags.splice(index, 1)[0],
|
||||
termParts = SmartSearchService.splitTermIntoParts(tagToRemove),
|
||||
removed;
|
||||
|
||||
let removeFromQuerySet = function(set) {
|
||||
_.each(removed, (value, key) => {
|
||||
if (Array.isArray(set[key])){
|
||||
_.remove(set[key], (item) => item === value);
|
||||
// If the array is now empty, remove that key
|
||||
if (set[key].length === 0) {
|
||||
delete set[key];
|
||||
}
|
||||
} else {
|
||||
if ($scope.singleSearchParam && set[$scope.singleSearchParam] && set[$scope.singleSearchParam].includes("%20and%20")) {
|
||||
let searchParamParts = set[$scope.singleSearchParam].split("%20and%20");
|
||||
// The value side of each paramPart might have been encoded in SmartSearch.splitFilterIntoTerms
|
||||
_.each(searchParamParts, (paramPart, paramPartIndex) => {
|
||||
searchParamParts[paramPartIndex] = decodeURIComponent(paramPart);
|
||||
});
|
||||
var index = searchParamParts.indexOf(value);
|
||||
if (index !== -1) {
|
||||
searchParamParts.splice(index, 1);
|
||||
}
|
||||
set[$scope.singleSearchParam] = searchParamParts.join("%20and%20");
|
||||
} else {
|
||||
delete set[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (termParts.length === 1) {
|
||||
removed = searchWithoutKey(tagToRemove);
|
||||
} else {
|
||||
let root = termParts[0].split(".")[0].replace(/^-/, '');
|
||||
let encodeParams = {
|
||||
term: tagToRemove,
|
||||
singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false
|
||||
};
|
||||
if($scope.models[$scope.list.name]) {
|
||||
if($scope.singleSearchParam) {
|
||||
removed = qs.encodeParam(encodeParams);
|
||||
}
|
||||
else if(_.has($scope.models[$scope.list.name].base, root)) {
|
||||
if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') {
|
||||
encodeParams.relatedSearchTerm = true;
|
||||
}
|
||||
else {
|
||||
encodeParams.searchTerm = true;
|
||||
}
|
||||
removed = qs.encodeParam(encodeParams);
|
||||
}
|
||||
else if(_.contains($scope.models[$scope.list.name].related, root)) {
|
||||
encodeParams.relatedSearchTerm = true;
|
||||
removed = qs.encodeParam(encodeParams);
|
||||
}
|
||||
else {
|
||||
removed = searchWithoutKey(termParts[termParts.length-1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
removed = searchWithoutKey(termParts[termParts.length-1]);
|
||||
}
|
||||
}
|
||||
removeFromQuerySet(queryset);
|
||||
if (!$scope.querySet) {
|
||||
$state.go('.', {
|
||||
[$scope.iterator + '_search']: queryset }).then(function(){
|
||||
// ISSUE: for some reason deleting a tag from a list in a modal does not
|
||||
// remove the param from $stateParams. Here we'll manually check to make sure
|
||||
// that that happened and remove it if it didn't.
|
||||
|
||||
removeFromQuerySet($stateParams[`${$scope.iterator}_search`]);
|
||||
transitionSuccessListener = $transitions.onSuccess({}, trans => {
|
||||
// State has changed - check to see if this is a param change
|
||||
if (trans.from().name === trans.to().name) {
|
||||
if (!compareParams(trans.params('from')[searchKey], trans.params('to')[searchKey])) {
|
||||
// Params are not the same - we need to update the search. This should only
|
||||
// happen when the user hits the forward/back browser navigation buttons.
|
||||
queryset = trans.params('to')[searchKey];
|
||||
qs.search(path, queryset).then((res) => {
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
$scope.$emit('updateDataset', res.data);
|
||||
});
|
||||
}
|
||||
qs.search(path, queryset).then((res) => {
|
||||
if($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
|
||||
$scope.searchTerm = null;
|
||||
generateSearchTags();
|
||||
}
|
||||
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
|
||||
generateSearchTags();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.clearAllTerms = function(){
|
||||
let cleared = _.cloneDeep(defaults);
|
||||
delete cleared.page;
|
||||
queryset = cleared;
|
||||
if(!$scope.querySet) {
|
||||
$state.go('.', {[$scope.iterator + '_search']: queryset});
|
||||
}
|
||||
qs.search(path, queryset).then((res) => {
|
||||
if($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
}
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
});
|
||||
$scope.searchTags = qs.stripDefaultParams(queryset, defaults);
|
||||
};
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', transitionSuccessListener);
|
||||
$scope.$watch('disableSearch', disableSearch => {
|
||||
if (disableSearch) {
|
||||
$scope.searchPlaceholder = i18n._('Cannot search running job');
|
||||
} else {
|
||||
$scope.searchPlaceholder = i18n._('Search');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateSearchTags () {
|
||||
const { singleSearchParam } = $scope;
|
||||
$scope.searchTags = qs.createSearchTagsFromQueryset(queryset, defaults, singleSearchParam);
|
||||
}
|
||||
|
||||
function revertSearch (queryToBeRestored) {
|
||||
queryset = queryToBeRestored;
|
||||
// https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic
|
||||
// This transition will not reload controllers/resolves/views
|
||||
// but will register new $stateParams[$scope.iterator + '_search'] terms
|
||||
if (!$scope.querySet) {
|
||||
$state.go('.', { [searchKey]: queryset });
|
||||
}
|
||||
qs.search(path, queryset).then((res) => {
|
||||
if ($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
}
|
||||
$scope.dataset = res.data;
|
||||
$scope.collection = res.data.results;
|
||||
});
|
||||
|
||||
$scope.searchTerm = null;
|
||||
|
||||
generateSearchTags();
|
||||
}
|
||||
|
||||
$scope.toggleKeyPane = () => {
|
||||
$scope.showKeyPane = !$scope.showKeyPane;
|
||||
};
|
||||
|
||||
function isAnsibleFactField (termParts) {
|
||||
const rootField = termParts[0].split('.')[0].replace(/^-/, '');
|
||||
return rootField === 'ansible_facts';
|
||||
}
|
||||
|
||||
function isRelatedField (termParts) {
|
||||
const rootField = termParts[0].split('.')[0].replace(/^-/, '');
|
||||
const listName = $scope.list.name;
|
||||
const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`;
|
||||
|
||||
const isRelatedSearchTermField = (_.contains($scope.models[listName].related, rootField));
|
||||
const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field');
|
||||
|
||||
return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField);
|
||||
}
|
||||
|
||||
$scope.addTerms = terms => {
|
||||
const { singleSearchParam } = $scope;
|
||||
const unmodifiedQueryset = _.clone(queryset);
|
||||
|
||||
const searchInputQueryset = qs.getSearchInputQueryset(terms, isRelatedField, isAnsibleFactField, singleSearchParam);
|
||||
const modifiedQueryset = qs.mergeQueryset(queryset, searchInputQueryset, singleSearchParam);
|
||||
|
||||
// Go back to the first page after a new search
|
||||
delete modifiedQueryset.page;
|
||||
|
||||
// https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic
|
||||
// This transition will not reload controllers/resolves/views but will register new
|
||||
// $stateParams[searchKey] terms.
|
||||
if (!$scope.querySet) {
|
||||
$state.go('.', { [searchKey]: modifiedQueryset })
|
||||
.then(() => {
|
||||
// same as above in $scope.remove. For some reason deleting the page
|
||||
// from the queryset works for all lists except lists in modals.
|
||||
delete $stateParams[searchKey].page;
|
||||
});
|
||||
}
|
||||
|
||||
qs.search(path, modifiedQueryset)
|
||||
.then(({ data }) => {
|
||||
if ($scope.querySet) {
|
||||
$scope.querySet = modifiedQueryset;
|
||||
}
|
||||
$scope.dataset = data;
|
||||
$scope.collection = data.results;
|
||||
})
|
||||
.catch(() => revertSearch(unmodifiedQueryset));
|
||||
|
||||
$scope.searchTerm = null;
|
||||
|
||||
generateSearchTags();
|
||||
};
|
||||
// remove tag, merge new queryset, $state.go
|
||||
$scope.removeTerm = index => {
|
||||
const { singleSearchParam } = $scope;
|
||||
const [term] = $scope.searchTags.splice(index, 1);
|
||||
|
||||
const modifiedQueryset = qs.removeTermsFromQueryset(queryset, term, isRelatedField, singleSearchParam);
|
||||
|
||||
if (!$scope.querySet) {
|
||||
$state.go('.', { [searchKey]: modifiedQueryset })
|
||||
.then(() => {
|
||||
// for some reason deleting a tag from a list in a modal does not
|
||||
// remove the param from $stateParams. Here we'll manually check to make sure
|
||||
// that that happened and remove it if it didn't.
|
||||
const clearedParams = qs.removeTermsFromQueryset($stateParams[searchKey], term, isRelatedField, singleSearchParam);
|
||||
$stateParams[searchKey] = clearedParams;
|
||||
});
|
||||
}
|
||||
|
||||
qs.search(path, queryset)
|
||||
.then(({ data }) => {
|
||||
if ($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
}
|
||||
$scope.dataset = data;
|
||||
$scope.collection = data.results;
|
||||
});
|
||||
|
||||
generateSearchTags();
|
||||
};
|
||||
|
||||
$scope.clearAllTerms = () => {
|
||||
const cleared = _.cloneDeep(defaults);
|
||||
|
||||
delete cleared.page;
|
||||
|
||||
queryset = cleared;
|
||||
|
||||
if (!$scope.querySet) {
|
||||
$state.go('.', { [searchKey]: queryset });
|
||||
}
|
||||
|
||||
qs.search(path, queryset)
|
||||
.then(({ data }) => {
|
||||
if ($scope.querySet) {
|
||||
$scope.querySet = queryset;
|
||||
}
|
||||
$scope.dataset = data;
|
||||
$scope.collection = data.results;
|
||||
});
|
||||
|
||||
$scope.searchTags = qs.stripDefaultParams(queryset, defaults);
|
||||
};
|
||||
}
|
||||
|
||||
SmartSearchController.$inject = [
|
||||
'$scope',
|
||||
'$state',
|
||||
'$stateParams',
|
||||
'$transitions',
|
||||
'ConfigService',
|
||||
'GetBasePath',
|
||||
'i18n',
|
||||
'QuerySet',
|
||||
];
|
||||
|
||||
export default SmartSearchController;
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
<div class="SmartSearch-bar" ng-class="{'SmartSearch-bar--fullWidth': searchBarFullWidth}">
|
||||
<div class="SmartSearch-searchTermContainer">
|
||||
<!-- string search input -->
|
||||
<form name="smartSearch" class="SmartSearch-form" aw-enter-key="addTerm(searchTerm)" novalidate>
|
||||
<form name="smartSearch" class="SmartSearch-form" aw-enter-key="addTerms(searchTerm)" novalidate>
|
||||
<input class="SmartSearch-input" ng-model="searchTerm" placeholder="{{searchPlaceholder}}"
|
||||
ng-disabled="disableSearch">
|
||||
</form>
|
||||
<div type="submit" class="SmartSearch-searchButton" ng-disabled="!searchTerm" ng-click="addTerm(searchTerm)">
|
||||
<div type="submit" class="SmartSearch-searchButton" ng-disabled="!searchTerm" ng-click="addTerms(searchTerm)">
|
||||
<i class="fa fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -90,12 +90,18 @@ export default
|
||||
// ex: 'ws-jobs-<jobId>'
|
||||
str = `ws-${data.group_name}-${data.job}`;
|
||||
}
|
||||
else if(data.group_name==="project_update_events"){
|
||||
str = `ws-${data.group_name}-${data.project_update}`;
|
||||
}
|
||||
else if(data.group_name==="ad_hoc_command_events"){
|
||||
// The naming scheme is "ws" then a
|
||||
// dash (-) and the group_name, then the job ID
|
||||
// ex: 'ws-jobs-<jobId>'
|
||||
str = `ws-${data.group_name}-${data.ad_hoc_command}`;
|
||||
}
|
||||
else if(data.group_name==="system_job_events"){
|
||||
str = `ws-${data.group_name}-${data.system_job}`;
|
||||
}
|
||||
else if(data.group_name==="inventory_update_events"){
|
||||
str = `ws-${data.group_name}-${data.inventory_update}`;
|
||||
}
|
||||
else if(data.group_name==="control"){
|
||||
// As of v. 3.1.0, there is only 1 "control"
|
||||
// message, which is for expiring the session if the
|
||||
@ -210,7 +216,7 @@ export default
|
||||
// socket-enabled AND socket-disabled, and whether the $state
|
||||
// requires a subscribe or an unsubscribe
|
||||
var self = this;
|
||||
socketPromise.promise.then(function(){
|
||||
return socketPromise.promise.then(function(){
|
||||
if(!state.data || !state.data.socket){
|
||||
_.merge(state.data, {socket: {groups: {}}});
|
||||
self.unsubscribe(state);
|
||||
|
||||
@ -27,7 +27,7 @@ export default ['$scope', '$filter',
|
||||
if (typeof $scope.templateType !== 'undefined' && $scope.templateType === 'workflow_job_template') {
|
||||
detailsBaseUrl = '/#/workflows/';
|
||||
} else {
|
||||
detailsBaseUrl = '/#/jobs/';
|
||||
detailsBaseUrl = '/#/jobz/playbook/';
|
||||
}
|
||||
|
||||
var sparkData =
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut-container" ng-class="{'fullscreen': stdoutFullScreen}">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText" translate>
|
||||
RESULTS
|
||||
</div>
|
||||
<div class="StandardOut-actions">
|
||||
<div>
|
||||
<at-relaunch job="job"></at-relaunch>
|
||||
</div>
|
||||
<button id="cancel-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-show="job.status === 'waiting' || job.status === 'running' || job.status === 'pending'" aw-tool-tip="Cancel" data-original-title="" title=""><i class="fa fa-minus-circle"></i> </button>
|
||||
<button id="delete-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-hide="job.status === 'waiting' || job.status === 'running' || job.status === 'pending' " aw-tool-tip="Delete" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-details">
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.module_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Name</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ job.module_name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>STATUS</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<i class="fa icon-job-{{ job.status }}"></i>
|
||||
<span class="StandardOut-statusText StandardOut--capitalize">{{ job.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.started">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>STARTED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.started | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>FINISHED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.finished | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>ELAPSED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.elapsed }} seconds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.module_args">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Module Args</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ job.module_args }}</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Inventory</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a href="{{ inventory_url }}"
|
||||
aw-tool-tip="{{'The inventory this command ran on.'|translate}}"
|
||||
data-placement="top">{{ inventory_name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="credential_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Credential</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a href="{{ credential_url }}"
|
||||
aw-tool-tip="{{'The credential used to run this command.'|translate}}"
|
||||
data-placement="top">{{ credential_name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="created_by">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Launched By</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a href="/#/users/{{ created_by.id }}"
|
||||
aw-tool-tip="{{'The user who ran this command.'|translate}}"
|
||||
data-placement="top">{{ created_by.username }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- since zero is a falsy value, you need ng-show such that
|
||||
the number is >= 0 -->
|
||||
<div class="StandardOut-detailsRow" ng-show="forks >= 0">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Forks</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ forks }}</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="limit">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Limit</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ limit }}</div>
|
||||
</div>
|
||||
|
||||
<!-- since zero is a falsy value, you need ng-show such that
|
||||
the number is >= 0 -->
|
||||
<div class="StandardOut-detailsRow" ng-show="verbosity >= 0">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Verbosity</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ verbosity }}</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.extra_vars">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">
|
||||
{{ 'Extra Variables' | translate }}
|
||||
<i class="StandardOut-extraVarsHelp fa fa-question-circle"
|
||||
aw-tool-tip="Read only view of extra variables added to the ad-hoc command."
|
||||
data-placement="top">
|
||||
</i>
|
||||
</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<textarea
|
||||
rows="6"
|
||||
ng-model="extra_vars"
|
||||
name="variables"
|
||||
class="StandardOut-extraVars"
|
||||
id="pre-formatted-variables">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-rightPanel">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText" translate>STANDARD OUT</div>
|
||||
<div class="StandardOut-panelHeaderActions">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}" data-tip-watch="toggleStdoutFullscreenTooltip" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}" ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
<a href="/api/v2/ad_hoc_commands/{{ job.id }}/stdout?format=txt_download">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{'Download Output'|translate}}" data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,36 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import { templateUrl } from '../../shared/template-url/template-url.factory';
|
||||
|
||||
export default {
|
||||
name: 'adHocJobStdout',
|
||||
route: '/ad_hoc_commands/:id',
|
||||
templateUrl: templateUrl('standard-out/adhoc/standard-out-adhoc'),
|
||||
controller: 'JobStdoutController',
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ job.module_name }}"
|
||||
},
|
||||
data: {
|
||||
jobType: 'ad_hoc_commands',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"ad_hoc_command_events": []
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) {
|
||||
Rest.setUrl(GetBasePath('base') + 'ad_hoc_commands/' + $stateParams.id + '/');
|
||||
return Rest.get()
|
||||
.then(({data}) => {
|
||||
return data;
|
||||
});
|
||||
}]
|
||||
}
|
||||
};
|
||||
@ -1,152 +0,0 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut-container" ng-class="{'fullscreen': stdoutFullScreen}">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText" translate>
|
||||
RESULTS
|
||||
</div>
|
||||
<div class="StandardOut-actions">
|
||||
<at-relaunch job="job"></at-relaunch>
|
||||
<button id="cancel-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-show="job.status === 'waiting' || job.status === 'running' || job.status === 'pending'" aw-tool-tip="Cancel" data-original-title="" title=""><i class="fa fa-minus-circle"></i> </button>
|
||||
<button id="delete-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-hide="job.status === 'waiting' || job.status === 'running' || job.status === 'pending' " aw-tool-tip="Delete" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-details">
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="inventory_source_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>NAME</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a href="{{inv_manage_group_link}}">
|
||||
{{ inventory_source_name }}
|
||||
</a>
|
||||
<a href="{{ workflow_result_link }}"
|
||||
aw-tool-tip="{{'View workflow results'|translate}}"
|
||||
data-placement="top"
|
||||
data-original-title="" title="">
|
||||
<i class="WorkflowBadge"
|
||||
ng-show="job.launch_type === 'workflow' ">
|
||||
W
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>STATUS</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<i class="fa icon-job-{{ job.status }}"></i>
|
||||
<span class="StandardOut-statusText StandardOut--capitalize">{{ job.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXPLANATION DETAIL -->
|
||||
<div class="StandardOut-detailsRow">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>EXPLANATION</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{task_detail | limitTo:explanationLimit}}
|
||||
<span ng-show="explanationLimit && task_detail.length > explanationLimit">
|
||||
<span>... </span>
|
||||
<span class="StandardOut-seeMoreLess" ng-click="explanationLimit=undefined">Show More</span>
|
||||
</span>
|
||||
<span ng-show="explanationLimit === undefined" class="StandardOut-seeMoreLess" ng-click="explanationLimit=150">Show Less</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="{{job.license_error !== null}}">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>LICENSE ERROR</div>
|
||||
<div class="StandardOut-detailsContent StandardOut--capitalize">
|
||||
{{ job.license_error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.started">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>STARTED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.started | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>FINISHED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.finished | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>ELAPSED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.elapsed }} seconds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.launch_type">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>LAUNCH TYPE</div>
|
||||
<div class="StandardOut-detailsContent StandardOut--capitalize">
|
||||
{{ job.launch_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="credential_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>CREDENTIAL</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a ui-sref="credentials.edit({credential_id: credential})">
|
||||
{{ credential_name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="source">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>SOURCE</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ source }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="source_regions">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>REGIONS</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ source_regions }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="{{ job.overwrite !== null }}">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>OVERWRITE</div>
|
||||
<div class="StandardOut-detailsContent StandardOut--capitalize">
|
||||
{{ job.overwrite }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="{{ job.overwrite_vars !== null }}">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>OVERWRITE VARS</div>
|
||||
<div class="StandardOut-detailsContent StandardOut--capitalize">
|
||||
{{ job.overwrite_vars }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-rightPanel">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText" translate>STANDARD OUT</div>
|
||||
<div class="StandardOut-panelHeaderActions">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}" data-tip-watch="toggleStdoutFullscreenTooltip" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
<a href="/api/v2/inventory_updates/{{ job.id }}/stdout?format=txt_download">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{'Download Output'|translate}}" data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,38 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||
|
||||
// TODO: figure out what this route should be - should it be inventory_sync?
|
||||
|
||||
export default {
|
||||
name: 'inventorySyncStdout',
|
||||
route: '/inventory_sync/:id',
|
||||
templateUrl: templateUrl('standard-out/inventory-sync/standard-out-inventory-sync'),
|
||||
controller: 'JobStdoutController',
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ inventory_source_name }}"
|
||||
},
|
||||
data: {
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"inventory_update_events": [],
|
||||
}
|
||||
},
|
||||
jobType: 'inventory_updates'
|
||||
},
|
||||
resolve: {
|
||||
jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) {
|
||||
Rest.setUrl(GetBasePath('base') + 'inventory_updates/' + $stateParams.id + '/');
|
||||
return Rest.get()
|
||||
.then(({data}) => {
|
||||
return data;
|
||||
});
|
||||
}]
|
||||
}
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import standardOutLog from './standard-out-log.directive';
|
||||
export default
|
||||
angular.module('standardOutLogDirective', [])
|
||||
.directive('standardOutLog', standardOutLog);
|
||||
@ -1,202 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'ProcessErrors', 'Rest', 'Wait',
|
||||
function ($log, $rootScope, $scope, $state, $stateParams, ProcessErrors, Rest, Wait) {
|
||||
|
||||
var api_complete = false,
|
||||
current_range,
|
||||
loaded_sections = [],
|
||||
event_queue = 0,
|
||||
auto_scroll_down=true, // programmatic scroll to bottom
|
||||
live_event_processing = true,
|
||||
page_size = 500,
|
||||
job_id = $stateParams.id;
|
||||
|
||||
$scope.should_apply_live_events = true;
|
||||
|
||||
// Open up a socket for events depending on the type of job
|
||||
function openSockets() {
|
||||
if ($state.current.name === 'jobResult') {
|
||||
$log.debug("socket watching on job_events-" + job_id);
|
||||
$scope.$on(`ws-job_events-${job_id}`, function() {
|
||||
$log.debug("socket fired on job_events-" + job_id);
|
||||
if (api_complete) {
|
||||
event_queue++;
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($state.current.name === 'adHocJobStdout') {
|
||||
$log.debug("socket watching on ad_hoc_command_events-" + job_id);
|
||||
$scope.$on(`ws-ad_hoc_command_events-${job_id}`, function() {
|
||||
$log.debug("socket fired on ad_hoc_command_events-" + job_id);
|
||||
if (api_complete) {
|
||||
event_queue++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openSockets();
|
||||
|
||||
// This is a trigger for loading up the standard out
|
||||
if ($scope.removeLoadStdout) {
|
||||
$scope.removeLoadStdout();
|
||||
}
|
||||
$scope.removeLoadStdout = $scope.$on('LoadStdout', function() {
|
||||
if (loaded_sections.length === 0) {
|
||||
loadStdout();
|
||||
}
|
||||
else if (live_event_processing) {
|
||||
getNextSection();
|
||||
}
|
||||
});
|
||||
|
||||
// This interval checks to see whether or not we've gotten a new
|
||||
// event via sockets. If so, go out and update the standard out
|
||||
// log.
|
||||
$rootScope.jobStdOutInterval = setInterval( function() {
|
||||
if (event_queue > 0) {
|
||||
// events happened since the last check
|
||||
$log.debug('checking for stdout...');
|
||||
if (loaded_sections.length === 0) { ////this if statement for refresh
|
||||
$log.debug('calling LoadStdout');
|
||||
loadStdout();
|
||||
}
|
||||
else if (live_event_processing) {
|
||||
$log.debug('calling getNextSection');
|
||||
getNextSection();
|
||||
}
|
||||
event_queue = 0;
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// stdoutEndpoint gets passed through in the directive declaration.
|
||||
// This watcher fires off loadStdout() when the endpoint becomes
|
||||
// available.
|
||||
$scope.$watch('stdoutEndpoint', function(newVal, oldVal) {
|
||||
if(newVal && newVal !== oldVal) {
|
||||
// Fire off the server call
|
||||
loadStdout();
|
||||
}
|
||||
});
|
||||
|
||||
// stdoutText optionall gets passed through in the directive declaration.
|
||||
$scope.$watch('stdoutText', function(newVal, oldVal) {
|
||||
if(newVal && newVal !== oldVal) {
|
||||
$('#pre-container-content').html(newVal);
|
||||
}
|
||||
});
|
||||
|
||||
function loadStdout() {
|
||||
if (!$scope.stdoutEndpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=0&end_line=' + page_size);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
Wait('stop');
|
||||
if (data.content) {
|
||||
api_complete = true;
|
||||
$('#pre-container-content').html(data.content);
|
||||
current_range = data.range;
|
||||
if (data.content !== "Waiting for results...") {
|
||||
loaded_sections.push({
|
||||
start: (data.range.start < 0) ? 0 : data.range.start,
|
||||
end: data.range.end
|
||||
});
|
||||
}
|
||||
|
||||
$('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
|
||||
}
|
||||
else {
|
||||
api_complete = true;
|
||||
}
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
|
||||
});
|
||||
}
|
||||
|
||||
function getNextSection() {
|
||||
if (!$scope.stdoutEndpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get the next range of data from the API
|
||||
var start = loaded_sections[loaded_sections.length - 1].end, url;
|
||||
url = $scope.stdoutEndpoint + '?format=json&start_line=' + start + '&end_line=' + (start + page_size);
|
||||
$('#stdoutMoreRowsBottom').fadeIn();
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
if ($('#pre-container-content').html() === "Waiting for results...") {
|
||||
$('#pre-container-content').html(data.content);
|
||||
} else {
|
||||
$('#pre-container-content').append(data.content);
|
||||
}
|
||||
loaded_sections.push({
|
||||
start: (data.range.start < 0) ? 0 : data.range.start,
|
||||
end: data.range.end
|
||||
});
|
||||
if ($scope.should_apply_live_events) {
|
||||
// if user has not disabled live event view by scrolling upward, then scroll down to the new content
|
||||
current_range = data.range;
|
||||
auto_scroll_down = true; // prevent auto load from happening
|
||||
$('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
|
||||
}
|
||||
$('#stdoutMoreRowsBottom').fadeOut(400);
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
|
||||
});
|
||||
}
|
||||
|
||||
// lrInfiniteScroll handler
|
||||
// grabs the next stdout section
|
||||
$scope.stdOutGetNextSection = function(){
|
||||
if (current_range.absolute_end > current_range.end){
|
||||
var url = $scope.stdoutEndpoint + '?format=json&start_line=' + current_range.end +
|
||||
'&end_line=' + (current_range.end + page_size);
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
$('#pre-container-content').append(data.content);
|
||||
current_range = data.range;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// We watch for job status changes here. If the job completes we want to clear out the
|
||||
// stdout interval and kill the live_event_processing flag.
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) {
|
||||
if (data.status === 'failed' || data.status === 'canceled' ||
|
||||
data.status === 'error' || data.status === 'successful') {
|
||||
if ($rootScope.jobStdOutInterval) {
|
||||
window.clearInterval($rootScope.jobStdOutInterval);
|
||||
}
|
||||
if (live_event_processing) {
|
||||
if (loaded_sections.length === 0) {
|
||||
loadStdout();
|
||||
}
|
||||
else {
|
||||
getNextSection();
|
||||
}
|
||||
}
|
||||
live_event_processing = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}];
|
||||
@ -1,47 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import standardOutLogController from './standard-out-log.controller';
|
||||
export default [ 'templateUrl',
|
||||
function(templateUrl) {
|
||||
return {
|
||||
scope: {
|
||||
stdoutEndpoint: '=',
|
||||
stdoutText: '=',
|
||||
jobId: '='
|
||||
},
|
||||
templateUrl: templateUrl('standard-out/log/standard-out-log'),
|
||||
restrict: 'E',
|
||||
controller: standardOutLogController,
|
||||
link: function(scope) {
|
||||
// All of our DOM related stuff will go in here
|
||||
|
||||
var lastScrollTop,
|
||||
direction;
|
||||
|
||||
function detectDirection() {
|
||||
var st = $('#pre-container').scrollTop();
|
||||
if (st > lastScrollTop) {
|
||||
direction = "down";
|
||||
} else {
|
||||
direction = "up";
|
||||
}
|
||||
lastScrollTop = st;
|
||||
return direction;
|
||||
}
|
||||
|
||||
$('#pre-container').bind('scroll', function() {
|
||||
if (detectDirection() === "up") {
|
||||
scope.should_apply_live_events = false;
|
||||
}
|
||||
|
||||
if ($(this).scrollTop() + $(this).height() === $(this).prop("scrollHeight")) {
|
||||
scope.should_apply_live_events = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -1,8 +0,0 @@
|
||||
<div class="StandardOut-consoleOutput" lr-infinite-scroll="stdOutGetNextSection" scroll-threshold="300" time-threshold="500">
|
||||
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer">
|
||||
<div id="pre-container-content" class="StandardOut-preContent"></div>
|
||||
</div>
|
||||
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
|
||||
<i class="fa fa-cog fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,22 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import stdoutAdhocRoute from './adhoc/standard-out-adhoc.route';
|
||||
import stdoutManagementJobsRoute from './management-jobs/standard-out-management-jobs.route';
|
||||
import stdoutInventorySyncRoute from './inventory-sync/standard-out-inventory-sync.route';
|
||||
import stdoutScmUpdateRoute from './scm-update/standard-out-scm-update.route';
|
||||
import {JobStdoutController} from './standard-out.controller';
|
||||
import StandardOutHelper from './standard-out-factories/main';
|
||||
import standardOutLogDirective from './log/main';
|
||||
|
||||
export default angular.module('standardOut', [StandardOutHelper.name, standardOutLogDirective.name])
|
||||
.controller('JobStdoutController', JobStdoutController)
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
$stateExtender.addState(stdoutAdhocRoute);
|
||||
$stateExtender.addState(stdoutManagementJobsRoute);
|
||||
$stateExtender.addState(stdoutInventorySyncRoute);
|
||||
$stateExtender.addState(stdoutScmUpdateRoute);
|
||||
}]);
|
||||
@ -1,84 +0,0 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut-container">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText">
|
||||
RESULTS
|
||||
</div>
|
||||
<div class="StandardOut-actions">
|
||||
<button id="cancel-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-show="job.status === 'waiting' || job.status === 'running' || job.status === 'pending'" aw-tool-tip="Cancel" data-original-title="" title=""><i class="fa fa-minus-circle"></i> </button>
|
||||
<button id="delete-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-hide="job.status === 'waiting' || job.status === 'running' || job.status === 'pending' " aw-tool-tip="Delete" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-details">
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">NAME</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ job.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">STATUS</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<i class="fa icon-job-{{ job.status }}"></i>
|
||||
<span class="StandardOut-statusText StandardOut--capitalize">{{ job.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.started">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">STARTED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.started | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">FINISHED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.finished | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">ELAPSED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.elapsed }} seconds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.launch_type">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4">LAUNCH TYPE</div>
|
||||
<div class="StandardOut-detailsContent StandardOut--capitalize">
|
||||
{{ job.launch_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow--extraVars" ng-show="job.extra_vars">
|
||||
<div class="StandardOut-detailsLabel">EXTRA VARIABLES</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-extraVarsContainer" ng-show="job.extra_vars">
|
||||
<textarea rows="6" ng-model="variables" name="variables" class="StandardOut-extraVars" id="pre-formatted-variables"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-rightPanel">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
|
||||
<div class="StandardOut-panelHeaderActions">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}" data-tip-watch="toggleStdoutFullscreenTooltip" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<standard-out-log stdout-text="job.result_stdout"></standard-out-log>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,36 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import { templateUrl } from '../../shared/template-url/template-url.factory';
|
||||
|
||||
export default {
|
||||
name: 'managementJobStdout',
|
||||
route: '/management_jobs/:id',
|
||||
templateUrl: templateUrl('standard-out/management-jobs/standard-out-management-jobs'),
|
||||
controller: 'JobStdoutController',
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ job.name }}"
|
||||
},
|
||||
data: {
|
||||
jobType: 'system_jobs',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"system_job_events": [],
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) {
|
||||
Rest.setUrl(GetBasePath('base') + 'system_jobs/' + $stateParams.id + '/');
|
||||
return Rest.get()
|
||||
.then(({data}) => {
|
||||
return data;
|
||||
});
|
||||
}]
|
||||
}
|
||||
};
|
||||
@ -1,113 +0,0 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut-container" ng-class="{'fullscreen': stdoutFullScreen}">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText" translate>
|
||||
RESULTS
|
||||
</div>
|
||||
<div class="StandardOut-actions">
|
||||
<at-relaunch job="job"></at-relaunch>
|
||||
<button id="cancel-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-show="job.status === 'waiting' || job.status === 'running' || job.status === 'pending'" aw-tool-tip="{{'Cancel'|translate}}" data-original-title="" title=""><i class="fa fa-minus-circle"></i> </button>
|
||||
<button id="delete-job-button" class="List-actionButton List-actionButton--delete jobResult-launchButton" data-placement="top" ng-click="deleteJob()" ng-hide="job.status === 'waiting' || job.status === 'running' || job.status === 'pending' " aw-tool-tip="{{'Delete'|translate}}" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-details">
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="project_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>NAME</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a ui-sref="projects.edit({project_id: job.project})">
|
||||
{{ project_name }}
|
||||
</a>
|
||||
<a href="{{ workflow_result_link }}"
|
||||
aw-tool-tip="{{'View workflow results'|translate}}"
|
||||
data-placement="top"
|
||||
data-original-title="" title="">
|
||||
<i class="WorkflowBadge"
|
||||
ng-show="job.launch_type === 'workflow' ">
|
||||
W
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>STATUS</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<i class="fa icon-job-{{ job.status }}"></i>
|
||||
<span class="StandardOut-statusText StandardOut--capitalize">{{ job.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.started">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>STARTED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.started | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>FINISHED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.finished | longDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.finished">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>ELAPSED</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
{{ job.elapsed }} seconds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.launch_type">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>LAUNCH TYPE</div>
|
||||
<div class="StandardOut-detailsContent StandardOut--capitalize">
|
||||
{{ job.launch_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="project_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>PROJECT</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a ui-sref="projects.edit({project_id: job.project})">
|
||||
{{ project_name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="credential_name">
|
||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>CREDENTIAL</div>
|
||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||
<a ui-sref="credentials.edit({credential_id: credential})">
|
||||
{{ credential_name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="StandardOut-rightPanel">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
<div class="StandardOut-panelHeaderText" translate>STANDARD OUT</div>
|
||||
<div class="StandardOut-panelHeaderActions">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}" data-tip-watch="toggleStdoutFullscreenTooltip" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
<a href="/api/v2/project_updates/{{ job.id }}/stdout?format=txt_download">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="{{'Download Output'|translate}}" data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,38 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import { templateUrl } from '../../shared/template-url/template-url.factory';
|
||||
|
||||
// TODO: figure out what this route should be - should it be scm_update?
|
||||
|
||||
export default {
|
||||
name: 'scmUpdateStdout',
|
||||
route: '/scm_update/:id',
|
||||
templateUrl: templateUrl('standard-out/scm-update/standard-out-scm-update'),
|
||||
controller: 'JobStdoutController',
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ project_name }}"
|
||||
},
|
||||
data: {
|
||||
jobType: 'project_updates',
|
||||
socket: {
|
||||
"groups": {
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"project_update_events": [],
|
||||
}
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) {
|
||||
Rest.setUrl(GetBasePath('base') + 'project_updates/' + $stateParams.id + '/');
|
||||
return Rest.get()
|
||||
.then(({data}) => {
|
||||
return data;
|
||||
});
|
||||
}]
|
||||
}
|
||||
};
|
||||
@ -1,145 +0,0 @@
|
||||
export default
|
||||
function DeleteJob($state, Find, Rest, Wait, ProcessErrors, Prompt, Alert,
|
||||
$filter, i18n) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
id = params.id,
|
||||
job = params.job,
|
||||
callback = params.callback,
|
||||
action, jobs, url, action_label, hdr;
|
||||
|
||||
if (!job) {
|
||||
if (scope.completed_jobs) {
|
||||
jobs = scope.completed_jobs;
|
||||
}
|
||||
else if (scope.running_jobs) {
|
||||
jobs = scope.running_jobs;
|
||||
}
|
||||
else if (scope.queued_jobs) {
|
||||
jobs = scope.queued_jobs;
|
||||
}
|
||||
else if (scope.all_jobs) {
|
||||
jobs = scope.all_jobs;
|
||||
}
|
||||
else if (scope.jobs) {
|
||||
jobs = scope.jobs;
|
||||
}
|
||||
job = Find({list: jobs, key: 'id', val: id });
|
||||
}
|
||||
|
||||
if (job.status === 'pending' || job.status === 'running' || job.status === 'waiting') {
|
||||
url = job.related.cancel;
|
||||
action_label = 'cancel';
|
||||
hdr = i18n._('Cancel');
|
||||
} else {
|
||||
url = job.url;
|
||||
action_label = 'delete';
|
||||
hdr = i18n._('Delete');
|
||||
}
|
||||
|
||||
action = function () {
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
if (action_label === 'cancel') {
|
||||
Rest.post()
|
||||
.then(() => {
|
||||
$('#prompt-modal').modal('hide');
|
||||
if (callback) {
|
||||
scope.$emit(callback, action_label);
|
||||
}
|
||||
else {
|
||||
$state.reload();
|
||||
Wait('stop');
|
||||
}
|
||||
})
|
||||
.catch(({obj, status}) => {
|
||||
Wait('stop');
|
||||
$('#prompt-modal').modal('hide');
|
||||
if (status === 403) {
|
||||
Alert('Error', obj.detail);
|
||||
}
|
||||
// Ignore the error. The job most likely already finished.
|
||||
// ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url +
|
||||
// ' failed. POST returned status: ' + status });
|
||||
});
|
||||
} else {
|
||||
Rest.destroy()
|
||||
.then(() => {
|
||||
$('#prompt-modal').modal('hide');
|
||||
if (callback) {
|
||||
scope.$emit(callback, action_label);
|
||||
}
|
||||
else {
|
||||
let reloadListStateParams = null;
|
||||
|
||||
if(scope.jobs.length === 1 && $state.params.job_search && !_.isEmpty($state.params.job_search.page) && $state.params.job_search.page !== '1') {
|
||||
reloadListStateParams = _.cloneDeep($state.params);
|
||||
reloadListStateParams.job_search.page = (parseInt(reloadListStateParams.job_search.page)-1).toString();
|
||||
}
|
||||
|
||||
$state.go('.', reloadListStateParams, {reload: true});
|
||||
Wait('stop');
|
||||
}
|
||||
})
|
||||
.catch(({obj, status}) => {
|
||||
Wait('stop');
|
||||
$('#prompt-modal').modal('hide');
|
||||
if (status === 403) {
|
||||
Alert('Error', obj.detail);
|
||||
}
|
||||
// Ignore the error. The job most likely already finished.
|
||||
//ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url +
|
||||
// ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (scope.removeCancelNotAllowed) {
|
||||
scope.removeCancelNotAllowed();
|
||||
}
|
||||
scope.removeCancelNotAllowed = scope.$on('CancelNotAllowed', function() {
|
||||
Wait('stop');
|
||||
Alert('Job Completed', 'The request to cancel the job could not be submitted. The job already completed.', 'alert-info');
|
||||
});
|
||||
|
||||
if (scope.removeCancelJob) {
|
||||
scope.removeCancelJob();
|
||||
}
|
||||
scope.removeCancelJob = scope.$on('CancelJob', function() {
|
||||
var cancelBody = "<div class=\"Prompt-bodyQuery\">" + i18n._("Are you sure you want to submit the request to cancel this job?") + "</div>";
|
||||
var deleteBody = "<div class=\"Prompt-bodyQuery\">" + i18n._("Are you sure you want to delete this job?") + "</div>";
|
||||
Prompt({
|
||||
hdr: hdr,
|
||||
resourceName: `#${job.id} ` + $filter('sanitize')(job.name),
|
||||
body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody,
|
||||
action: action,
|
||||
actionText: (action_label === 'cancel' || job.status === 'new') ? i18n._("OK") : i18n._("DELETE")
|
||||
});
|
||||
});
|
||||
|
||||
if (action_label === 'cancel') {
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
if (data.can_cancel) {
|
||||
scope.$emit('CancelJob');
|
||||
}
|
||||
else {
|
||||
scope.$emit('CancelNotAllowed');
|
||||
}
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url +
|
||||
' failed. GET returned: ' + status });
|
||||
});
|
||||
}
|
||||
else {
|
||||
scope.$emit('CancelJob');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
DeleteJob.$inject =
|
||||
[ '$state', 'Find', 'Rest', 'Wait',
|
||||
'ProcessErrors', 'Prompt', 'Alert', '$filter', 'i18n'
|
||||
];
|
||||
@ -1,36 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
['Rest', 'ProcessErrors', 'Empty', function(Rest, ProcessErrors, Empty) {
|
||||
return function(params) {
|
||||
var url = params.url,
|
||||
scope_var = params.scope_var,
|
||||
scope = params.scope,
|
||||
callback = params.callback;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
if (scope_var === 'inventory_source') {
|
||||
scope.inventory = data.inventory;
|
||||
}
|
||||
if (!Empty(data.name)) {
|
||||
scope[scope_var + '_name'] = data.name;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
scope.$emit(callback, data);
|
||||
}
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
if (status === 403 && params.ignore_403) {
|
||||
return;
|
||||
}
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
};
|
||||
}];
|
||||
@ -1,13 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import lookUpName from './lookup-name.factory';
|
||||
import DeleteJob from './delete-job.factory';
|
||||
|
||||
export default
|
||||
angular.module('StandardOutHelper', [])
|
||||
.factory('LookUpName', lookUpName)
|
||||
.factory('DeleteJob', DeleteJob);
|
||||
@ -1,266 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name controllers.function:JobStdout
|
||||
* @description This controller's for the standard out page that can be displayed when a job runs
|
||||
*/
|
||||
|
||||
export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
||||
GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName,
|
||||
ParseTypeChange, ParseVariableString, DeleteJob, Wait, i18n,
|
||||
fieldChoices, fieldLabels, Project, Alert, InventorySource,
|
||||
jobData) {
|
||||
|
||||
var job_id = $stateParams.id,
|
||||
jobType = $state.current.data.jobType;
|
||||
|
||||
// This scope variable controls whether or not the left panel is shown and the right panel
|
||||
// is expanded to take up the full screen
|
||||
$scope.stdoutFullScreen = false;
|
||||
$scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output");
|
||||
|
||||
$scope.explanationLimit = 150;
|
||||
|
||||
// Listen for job status updates that may come across via sockets. We need to check the payload
|
||||
// to see whethere the updated job is the one that we're currently looking at.
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) {
|
||||
$scope.job.status = data.status;
|
||||
}
|
||||
|
||||
if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') {
|
||||
// Go out and refresh the job details
|
||||
|
||||
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
updateJobObj(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.previousTaskFailed = false;
|
||||
|
||||
$scope.$watch('job.job_explanation', function(explanation) {
|
||||
if (explanation && explanation.split(":")[0] === "Previous Task Failed") {
|
||||
$scope.previousTaskFailed = true;
|
||||
|
||||
var taskObj = JSON.parse(explanation.substring(explanation.split(":")[0].length + 1));
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var fieldChoice = fieldChoices({
|
||||
$scope: $scope,
|
||||
url: GetBasePath('unified_jobs'),
|
||||
field: 'type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
fieldChoice.then(function (choices) {
|
||||
choices =
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
$scope.explanation_fail_type = choices[taskObj.job_type];
|
||||
$scope.explanation_fail_name = taskObj.job_name;
|
||||
$scope.explanation_fail_id = taskObj.job_id;
|
||||
$scope.task_detail = $scope.explanation_fail_type + " failed for " + $scope.explanation_fail_name + " with ID " + $scope.explanation_fail_id + ".";
|
||||
});
|
||||
} else {
|
||||
$scope.previousTaskFailed = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Set the parse type so that CodeMirror knows how to display extra params YAML/JSON
|
||||
$scope.parseType = 'yaml';
|
||||
|
||||
function updateJobObj(updatedJobData) {
|
||||
|
||||
// Go out and get the job details based on the job type. jobType gets defined
|
||||
// in the data block of the route declaration for each of the different types
|
||||
// of stdout jobs.
|
||||
|
||||
$scope.job = updatedJobData;
|
||||
$scope.job_template_name = updatedJobData.name;
|
||||
$scope.created_by = updatedJobData.summary_fields.created_by;
|
||||
$scope.project_name = (updatedJobData.summary_fields.project) ? updatedJobData.summary_fields.project.name : '';
|
||||
$scope.inventory_name = (updatedJobData.summary_fields.inventory) ? updatedJobData.summary_fields.inventory.name : '';
|
||||
$scope.job_template_url = '/#/templates/' + updatedJobData.unified_job_template;
|
||||
if($scope.inventory_name && updatedJobData.inventory && updatedJobData.summary_fields.inventory && updatedJobData.summary_fields.inventory.kind) {
|
||||
if(updatedJobData.summary_fields.inventory.kind === '') {
|
||||
$scope.inventory_url = '/#/inventories/inventory' + updatedJobData.inventory;
|
||||
}
|
||||
else if(updatedJobData.summary_fields.inventory.kind === 'smart') {
|
||||
$scope.inventory_url = '/#/inventories/smart_inventory' + updatedJobData.inventory;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$scope.inventory_url = '';
|
||||
}
|
||||
$scope.project_url = ($scope.project_name && updatedJobData.project) ? '/#/projects/' + updatedJobData.project : '';
|
||||
$scope.credential_name = (updatedJobData.summary_fields.credential) ? updatedJobData.summary_fields.credential.name : '';
|
||||
$scope.credential_url = (updatedJobData.credential) ? '/#/credentials/' + updatedJobData.credential : '';
|
||||
$scope.cloud_credential_url = (updatedJobData.cloud_credential) ? '/#/credentials/' + updatedJobData.cloud_credential : '';
|
||||
if(updatedJobData.summary_fields && updatedJobData.summary_fields.source_workflow_job &&
|
||||
updatedJobData.summary_fields.source_workflow_job.id){
|
||||
$scope.workflow_result_link = `/#/workflows/${updatedJobData.summary_fields.source_workflow_job.id}`;
|
||||
}
|
||||
$scope.playbook = updatedJobData.playbook;
|
||||
$scope.credential = updatedJobData.credential;
|
||||
$scope.cloud_credential = updatedJobData.cloud_credential;
|
||||
$scope.forks = updatedJobData.forks;
|
||||
$scope.limit = updatedJobData.limit;
|
||||
$scope.verbosity = updatedJobData.verbosity;
|
||||
$scope.job_tags = updatedJobData.job_tags;
|
||||
$scope.job.module_name = updatedJobData.module_name;
|
||||
if (updatedJobData.extra_vars) {
|
||||
$scope.variables = ParseVariableString(updatedJobData.extra_vars);
|
||||
}
|
||||
|
||||
$scope.$on('getInventorySource', function(e, d) {
|
||||
$scope.inv_manage_group_link = '/#/inventories/inventory/' + d.inventory + '/inventory_sources/edit/' + d.id;
|
||||
});
|
||||
|
||||
// If we have a source then we have to go get the source choices from the server
|
||||
if (!Empty(updatedJobData.source)) {
|
||||
if ($scope.removeChoicesReady) {
|
||||
$scope.removeChoicesReady();
|
||||
}
|
||||
$scope.removeChoicesReady = $scope.$on('ChoicesReady', function() {
|
||||
$scope.source_choices.every(function(e) {
|
||||
if (e.value === updatedJobData.source) {
|
||||
$scope.source = e.label;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
// GetChoices can be found in the helper: Utilities.js
|
||||
// It attaches the source choices to $scope.source_choices.
|
||||
// Then, when the callback is fired, $scope.source is bound
|
||||
// to the corresponding label.
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: GetBasePath('inventory_sources'),
|
||||
field: 'source',
|
||||
variable: 'source_choices',
|
||||
choice_name: 'choices',
|
||||
callback: 'ChoicesReady'
|
||||
});
|
||||
}
|
||||
|
||||
// LookUpName can be found in the lookup-name.factory
|
||||
// It attaches the name that it gets (based on the url)
|
||||
// to the $scope variable defined by the attribute scope_var.
|
||||
if (!Empty(updatedJobData.credential)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'credential',
|
||||
url: GetBasePath('credentials') + updatedJobData.credential + '/',
|
||||
ignore_403: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(updatedJobData.inventory)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'inventory',
|
||||
url: GetBasePath('inventory') + updatedJobData.inventory + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(updatedJobData.project)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'project',
|
||||
url: GetBasePath('projects') + updatedJobData.project + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(updatedJobData.cloud_credential)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'cloud_credential',
|
||||
url: GetBasePath('credentials') + updatedJobData.cloud_credential + '/',
|
||||
ignore_403: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(updatedJobData.inventory_source)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'inventory_source',
|
||||
url: GetBasePath('inventory_sources') + updatedJobData.inventory_source + '/',
|
||||
callback: 'getInventorySource'
|
||||
});
|
||||
}
|
||||
|
||||
if (updatedJobData.extra_vars) {
|
||||
ParseTypeChange({
|
||||
scope: $scope,
|
||||
field_id: 'pre-formatted-variables',
|
||||
readOnly: true
|
||||
});
|
||||
}
|
||||
|
||||
// If the job isn't running we want to clear out the interval that goes out and checks for stdout updates.
|
||||
// This interval is defined in the standard out log directive controller.
|
||||
if (updatedJobData.status === 'successful' || updatedJobData.status === 'failed' || updatedJobData.status === 'error' || updatedJobData.status === 'canceled') {
|
||||
if ($rootScope.jobStdOutInterval) {
|
||||
window.clearInterval($rootScope.jobStdOutInterval);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($scope.removeDeleteFinished) {
|
||||
$scope.removeDeleteFinished();
|
||||
}
|
||||
$scope.removeDeleteFinished = $scope.$on('DeleteFinished', function(e, action) {
|
||||
Wait('stop');
|
||||
if (action !== 'cancel') {
|
||||
Wait('stop');
|
||||
$state.go('jobs');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: this is currently not used but is necessary for cases where sockets
|
||||
// are not available and a manual refresh trigger is needed.
|
||||
$scope.refresh = function(){
|
||||
$scope.$emit('LoadStdout');
|
||||
};
|
||||
|
||||
// Click binding for the expand/collapse button on the standard out log
|
||||
$scope.toggleStdoutFullscreen = function() {
|
||||
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
|
||||
|
||||
if ($scope.stdoutFullScreen === true) {
|
||||
$scope.toggleStdoutFullscreenTooltip = i18n._("Collapse Output");
|
||||
} else if ($scope.stdoutFullScreen === false) {
|
||||
$scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output");
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteJob = function() {
|
||||
DeleteJob({
|
||||
scope: $scope,
|
||||
id: $scope.job.id,
|
||||
job: $scope.job,
|
||||
callback: 'DeleteFinished'
|
||||
});
|
||||
};
|
||||
|
||||
updateJobObj(jobData);
|
||||
|
||||
}
|
||||
|
||||
JobStdoutController.$inject = [ '$rootScope', '$scope', '$state',
|
||||
'$stateParams', 'GetBasePath', 'Rest', 'ProcessErrors',
|
||||
'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange',
|
||||
'ParseVariableString', 'DeleteJob', 'Wait', 'i18n',
|
||||
'fieldChoices', 'fieldLabels', 'ProjectModel', 'Alert', 'InventorySourceModel',
|
||||
'jobData'];
|
||||
@ -898,13 +898,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
|
||||
let goToJobResults = function(job_type) {
|
||||
if(job_type === 'job') {
|
||||
$state.go('jobResult', {id: d.job.id});
|
||||
$state.go('jobz', {id: d.job.id, type: 'playbook'});
|
||||
}
|
||||
else if(job_type === 'inventory_update') {
|
||||
$state.go('inventorySyncStdout', {id: d.job.id});
|
||||
$state.go('jobz', {id: d.job.id, type: 'inventory'});
|
||||
}
|
||||
else if(job_type === 'project_update') {
|
||||
$state.go('scmUpdateStdout', {id: d.job.id});
|
||||
$state.go('jobz', {id: d.job.id, type: 'project'});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -126,9 +126,8 @@ standard-out-log {
|
||||
|
||||
.StandardOut-panelHeaderActions {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
font-size: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.StandardOut-actions {
|
||||
@ -137,12 +136,11 @@ standard-out-log {
|
||||
|
||||
.StandardOut-actionButton {
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
height: 20px;
|
||||
min-width: 30px;
|
||||
color: @list-action-icon;
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.StandardOut-actionButton:hover {
|
||||
@ -171,4 +169,4 @@ standard-out-log {
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
@ -107,12 +107,14 @@
|
||||
"angular-sanitize": "~1.6.6",
|
||||
"angular-scheduler": "git+https://git@github.com/ansible/angular-scheduler#v0.3.2",
|
||||
"angular-tz-extensions": "git+https://git@github.com/ansible/angular-tz-extensions#v0.5.2",
|
||||
"ansi-to-html": "^0.6.3",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bootstrap": "^3.3.7",
|
||||
"bootstrap-datepicker": "^1.7.1",
|
||||
"codemirror": "^5.17.0",
|
||||
"components-font-awesome": "^4.6.1",
|
||||
"d3": "~3.3.13",
|
||||
"html-entities": "^1.2.1",
|
||||
"javascript-detect-element-resize": "^0.5.3",
|
||||
"jquery": "~2.2.4",
|
||||
"jquery-ui": "^1.12.1",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user