Merge pull request #1163 from ansible/job-results

job results / job event output
This commit is contained in:
Jake McDermott 2018-04-05 03:12:29 -04:00 committed by GitHub
commit dd0e7e2751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 5194 additions and 6778 deletions

View File

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

View File

@ -1,2 +1,3 @@
@import 'credentials/_index';
@import 'output/_index';
@import 'users/tokens/_index';

View File

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

View File

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

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

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

View 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>

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

View File

@ -15,6 +15,7 @@
}
.HostEvent .CodeMirror{
overflow-x: hidden;
max-height: none!important;
}
.HostEvent-close:hover{

View File

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

View File

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

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

View File

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

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

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

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

View 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">&nbsp;</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>

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

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

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

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

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

View 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>

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

View 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>

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

View File

@ -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({

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
// 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;
}];

View File

@ -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}) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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