diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js
index d653707847..5bf1aad89c 100644
--- a/awx/ui/build/webpack.watch.js
+++ b/awx/ui/build/webpack.watch.js
@@ -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
+ }]
}
};
diff --git a/awx/ui/client/features/_index.less b/awx/ui/client/features/_index.less
index e2339dc9e4..59e8e4630b 100644
--- a/awx/ui/client/features/_index.less
+++ b/awx/ui/client/features/_index.less
@@ -1,2 +1,3 @@
@import 'credentials/_index';
+@import 'output/_index';
@import 'users/tokens/_index';
diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js
index 763894c93c..0a6ec3864c 100644
--- a/awx/ui/client/features/index.js
+++ b/awx/ui/client/features/index.js
@@ -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;
diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js
index b78a1b77b6..ea57c4e9f5 100644
--- a/awx/ui/client/features/jobs/jobsList.controller.js
+++ b/awx/ui/client/features/jobs/jobsList.controller.js
@@ -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}`;
diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less
new file mode 100644
index 0000000000..71228d20e7
--- /dev/null
+++ b/awx/ui/client/features/output/_index.less
@@ -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;
+}
\ No newline at end of file
diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js
new file mode 100644
index 0000000000..3c0e677d9b
--- /dev/null
+++ b/awx/ui/client/features/output/details.directive.js
@@ -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 = `
-
-
-
diff --git a/awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html b/awx/ui/client/features/output/host-event/host-event-stderr.partial.html
similarity index 100%
rename from awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html
rename to awx/ui/client/features/output/host-event/host-event-stderr.partial.html
diff --git a/awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html b/awx/ui/client/features/output/host-event/host-event-stdout.partial.html
similarity index 100%
rename from awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html
rename to awx/ui/client/features/output/host-event/host-event-stdout.partial.html
diff --git a/awx/ui/client/features/output/host-event/host-event.controller.js b/awx/ui/client/features/output/host-event/host-event.controller.js
new file mode 100644
index 0000000000..67105ba7a0
--- /dev/null
+++ b/awx/ui/client/features/output/host-event/host-event.controller.js
@@ -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;
diff --git a/awx/ui/client/features/output/host-event/host-event.route.js b/awx/ui/client/features/output/host-event/host-event.route.js
new file mode 100644
index 0000000000..06f3eeac51
--- /dev/null
+++ b/awx/ui/client/features/output/host-event/host-event.route.js
@@ -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 };
diff --git a/awx/ui/client/features/output/host-event/host-event.service.js b/awx/ui/client/features/output/host-event/host-event.service.js
new file mode 100644
index 0000000000..a1e6952725
--- /dev/null
+++ b/awx/ui/client/features/output/host-event/host-event.service.js
@@ -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;
diff --git a/awx/ui/client/features/output/host-event/index.js b/awx/ui/client/features/output/host-event/index.js
new file mode 100644
index 0000000000..f00b0b36b4
--- /dev/null
+++ b/awx/ui/client/features/output/host-event/index.js
@@ -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;
diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js
new file mode 100644
index 0000000000..0fee736561
--- /dev/null
+++ b/awx/ui/client/features/output/index.controller.js
@@ -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;
diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js
new file mode 100644
index 0000000000..3aaadc2553
--- /dev/null
+++ b/awx/ui/client/features/output/index.js
@@ -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;
diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html
new file mode 100644
index 0000000000..4e0a791bc4
--- /dev/null
+++ b/awx/ui/client/features/output/index.view.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+ {{ vm.title }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/features/output/jobs.strings.js b/awx/ui/client/features/output/jobs.strings.js
new file mode 100644
index 0000000000..af52a98472
--- /dev/null
+++ b/awx/ui/client/features/output/jobs.strings.js
@@ -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;
diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js
new file mode 100644
index 0000000000..5f19fe921a
--- /dev/null
+++ b/awx/ui/client/features/output/page.service.js
@@ -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;
diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js
new file mode 100644
index 0000000000..12ca797b32
--- /dev/null
+++ b/awx/ui/client/features/output/render.service.js
@@ -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 = `
| `;
+ }
+
+ if (current.isHost) {
+ tdEvent = `
${content} | `;
+ }
+
+ if (current.time && current.line === ln) {
+ timestamp = `
${current.time}`;
+ }
+
+ if (current.parents) {
+ classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
+ }
+ }
+
+ if (!tdEvent) {
+ tdEvent = `
${content} | `;
+ }
+
+ if (!tdToggle) {
+ tdToggle = '
| ';
+ }
+
+ if (!ln) {
+ ln = '...';
+ }
+
+ return `
+
+ ${tdToggle}
+ | ${ln} |
+ ${tdEvent}
+ ${timestamp} |
+
`;
+ };
+
+ 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;
diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js
new file mode 100644
index 0000000000..a568813ddc
--- /dev/null
+++ b/awx/ui/client/features/output/scroll.service.js
@@ -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;
diff --git a/awx/ui/client/features/output/search.directive.js b/awx/ui/client/features/output/search.directive.js
new file mode 100644
index 0000000000..0a688f92bb
--- /dev/null
+++ b/awx/ui/client/features/output/search.directive.js
@@ -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;
diff --git a/awx/ui/client/features/output/search.partial.html b/awx/ui/client/features/output/search.partial.html
new file mode 100644
index 0000000000..d7acedc3d4
--- /dev/null
+++ b/awx/ui/client/features/output/search.partial.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
EXAMPLES:
+
{{ tag }}
+
+
+
+ FIELDS:
+ {{ field }},
+
+
+
ADDITIONAL INFORMATION:
+ For additional information on advanced search search syntax please see the Ansible Tower
+
documentation.
+
+
+
diff --git a/awx/ui/client/features/output/stats.directive.js b/awx/ui/client/features/output/stats.directive.js
new file mode 100644
index 0000000000..51fbd89afb
--- /dev/null
+++ b/awx/ui/client/features/output/stats.directive.js
@@ -0,0 +1,76 @@
+const templateUrl = require('~features/output/stats.partial.html');
+
+let status;
+let strings;
+
+function createStatsBarTooltip (key, count) {
+ const label = `
${key}`;
+ const badge = `
${count}`;
+
+ 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;
diff --git a/awx/ui/client/features/output/stats.partial.html b/awx/ui/client/features/output/stats.partial.html
new file mode 100644
index 0000000000..95668d4e2b
--- /dev/null
+++ b/awx/ui/client/features/output/stats.partial.html
@@ -0,0 +1,81 @@
+
+
+
plays
+
...
+
{{ vm.plays }}
+
+
tasks
+
...
+
{{ vm.tasks }}
+
+
hosts
+
...
+
{{ vm.hosts }}
+
+
elapsed
+
...
+
+ {{ vm.elapsed * 1000 | duration: "hh:mm:ss" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js
new file mode 100644
index 0000000000..a3d80dbc01
--- /dev/null
+++ b/awx/ui/client/features/output/status.service.js
@@ -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;
diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js
index d4c9403823..40dcf2907c 100644
--- a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js
+++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js
@@ -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({
diff --git a/awx/ui/client/lib/components/modal/modal.directive.js b/awx/ui/client/lib/components/modal/modal.directive.js
index 302ff92a03..f3def99885 100644
--- a/awx/ui/client/lib/components/modal/modal.directive.js
+++ b/awx/ui/client/lib/components/modal/modal.directive.js
@@ -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'
];
diff --git a/awx/ui/client/lib/components/panel/_index.less b/awx/ui/client/lib/components/panel/_index.less
index 7e50d8593a..b89faeb405 100644
--- a/awx/ui/client/lib/components/panel/_index.less
+++ b/awx/ui/client/lib/components/panel/_index.less
@@ -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;
+ }
+}
diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js
index 1d88abdb8b..3d7d6b09ff 100644
--- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js
+++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js
@@ -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, {
diff --git a/awx/ui/client/lib/models/AdHocCommand.js b/awx/ui/client/lib/models/AdHocCommand.js
index c398219531..7bea2677ac 100644
--- a/awx/ui/client/lib/models/AdHocCommand.js
+++ b/awx/ui/client/lib/models/AdHocCommand.js
@@ -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;
diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js
index a2c202aa79..912d9a984c 100644
--- a/awx/ui/client/lib/models/Base.js
+++ b/awx/ui/client/lib/models/Base.js
@@ -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`);
diff --git a/awx/ui/client/lib/models/InventoryUpdate.js b/awx/ui/client/lib/models/InventoryUpdate.js
new file mode 100644
index 0000000000..668a05459d
--- /dev/null
+++ b/awx/ui/client/lib/models/InventoryUpdate.js
@@ -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;
diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js
index 7d87f82330..7dd58482a5 100644
--- a/awx/ui/client/lib/models/Job.js
+++ b/awx/ui/client/lib/models/Job.js
@@ -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;
diff --git a/awx/ui/client/lib/models/JobEvent.js b/awx/ui/client/lib/models/JobEvent.js
new file mode 100644
index 0000000000..1c71ba9c54
--- /dev/null
+++ b/awx/ui/client/lib/models/JobEvent.js
@@ -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;
diff --git a/awx/ui/client/lib/models/ProjectUpdate.js b/awx/ui/client/lib/models/ProjectUpdate.js
new file mode 100644
index 0000000000..df038283cf
--- /dev/null
+++ b/awx/ui/client/lib/models/ProjectUpdate.js
@@ -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;
diff --git a/awx/ui/client/lib/models/SystemJob.js b/awx/ui/client/lib/models/SystemJob.js
new file mode 100644
index 0000000000..1f1f1c5ee3
--- /dev/null
+++ b/awx/ui/client/lib/models/SystemJob.js
@@ -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;
diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js
index fb902fb91c..d50c825e22 100644
--- a/awx/ui/client/lib/models/index.js
+++ b/awx/ui/client/lib/models/index.js
@@ -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;
diff --git a/awx/ui/client/lib/theme/_global.less b/awx/ui/client/lib/theme/_global.less
index 6995b224a5..2b9f67fdec 100644
--- a/awx/ui/client/lib/theme/_global.less
+++ b/awx/ui/client/lib/theme/_global.less
@@ -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%;
}
diff --git a/awx/ui/client/lib/theme/_utility.less b/awx/ui/client/lib/theme/_utility.less
index 1f47a481f3..b3663b74a3 100644
--- a/awx/ui/client/lib/theme/_utility.less
+++ b/awx/ui/client/lib/theme/_utility.less
@@ -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
+}
diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less
index 9a9f564840..a0f7738272 100644
--- a/awx/ui/client/lib/theme/index.less
+++ b/awx/ui/client/lib/theme/index.less
@@ -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';
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js
index ab64107955..3567df4ef5 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -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();
diff --git a/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js
index 4d8be63ccd..93a2147834 100644
--- a/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js
+++ b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js
@@ -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,
diff --git a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js
index 5aa5938714..e5e020fc6d 100644
--- a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js
+++ b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js
@@ -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, {
diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js
index 495c201646..7b2e28e8aa 100644
--- a/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js
+++ b/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js
@@ -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' });
};
}
diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js
index 661d122c88..725069044e 100644
--- a/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js
+++ b/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js
@@ -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' } );
};
}
diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js
index e7820f79c8..c889517506 100644
--- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js
+++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js
@@ -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!',
diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js
deleted file mode 100644
index c97861ac6b..0000000000
--- a/awx/ui/client/src/job-results/event-queue.service.js
+++ /dev/null
@@ -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;
-}];
diff --git a/awx/ui/client/src/job-results/host-event/host-event.controller.js b/awx/ui/client/src/job-results/host-event/host-event.controller.js
deleted file mode 100644
index 330e581189..0000000000
--- a/awx/ui/client/src/job-results/host-event/host-event.controller.js
+++ /dev/null
@@ -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();
- }];
diff --git a/awx/ui/client/src/job-results/host-event/host-event.route.js b/awx/ui/client/src/job-results/host-event/host-event.route.js
deleted file mode 100644
index e0a32ad7e4..0000000000
--- a/awx/ui/client/src/job-results/host-event/host-event.route.js
+++ /dev/null
@@ -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 };
diff --git a/awx/ui/client/src/job-results/host-event/main.js b/awx/ui/client/src/job-results/host-event/main.js
deleted file mode 100644
index 76832b45e5..0000000000
--- a/awx/ui/client/src/job-results/host-event/main.js
+++ /dev/null
@@ -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);
- }]);
diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less
deleted file mode 100644
index 5eb948eaad..0000000000
--- a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less
+++ /dev/null
@@ -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;
-
-}
diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js
deleted file mode 100644
index 5a8b5b3206..0000000000
--- a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js
+++ /dev/null
@@ -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`] = `
${key}${val[key]}`;
- }
- });
-
- // 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();
- });
- }
- };
-}];
diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html
deleted file mode 100644
index a8854b6d09..0000000000
--- a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
diff --git a/awx/ui/client/src/job-results/host-status-bar/main.js b/awx/ui/client/src/job-results/host-status-bar/main.js
deleted file mode 100644
index 2b17a2e414..0000000000
--- a/awx/ui/client/src/job-results/host-status-bar/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less
deleted file mode 100644
index d186677cd1..0000000000
--- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less
+++ /dev/null
@@ -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;
- }
-}
diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js
deleted file mode 100644
index 14a34a607a..0000000000
--- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js
+++ /dev/null
@@ -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');
- });
- }
- });
- }
- }
- };
- }
- };
-}];
diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html
deleted file mode 100644
index 63ae96d90c..0000000000
--- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
The standard output is too large to display. Please specify additional filters to narrow the standard out.
-
Too much previous output to display. Showing running standard output.
-
Job details are not available for this job. Please download to view standard out.
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/job-results/job-results-stdout/main.js b/awx/ui/client/src/job-results/job-results-stdout/main.js
deleted file mode 100644
index 5fc583b9b1..0000000000
--- a/awx/ui/client/src/job-results/job-results-stdout/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less
deleted file mode 100644
index 356aeb5a6a..0000000000
--- a/awx/ui/client/src/job-results/job-results.block.less
+++ /dev/null
@@ -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;
-}
diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js
deleted file mode 100644
index e7e8f2c716..0000000000
--- a/awx/ui/client/src/job-results/job-results.controller.js
+++ /dev/null
@@ -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('
');
- }
-
- // 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",
- "
"+mungedEvent.stdout+"
")
- .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 = '
';
-
- 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());
- });
-}];
diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html
deleted file mode 100644
index 9d8e1a119b..0000000000
--- a/awx/ui/client/src/job-results/job-results.partial.html
+++ /dev/null
@@ -1,566 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ status_label | translate }}
-
-
-
-
-
-
-
- {{ job.job_explanation }}
-
-
- {{task_detail | limitTo:explanationLimit}}
-
- ...
- Show More
-
- Show Less
-
-
-
-
-
-
-
- {{ job.started | longDate }}
-
-
-
-
-
-
-
- {{ (job.finished |
- longDate) || "Not Finished" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ type_label }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ job.playbook }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ job.forks }}
-
-
-
-
-
-
-
- {{ job.limit }}
-
-
-
-
-
-
-
- {{ verbosity_label }}
-
-
-
-
-
-
-
- {{ job.summary_fields.instance_group.name }}
-
- Isolated
-
-
-
-
-
-
-
-
- {{ job.job_tags }}
-
-
-
-
-
-
-
- {{ job.skip_tags }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ job.name }}
-
-
-
-
-
-
- Plays
-
-
- {{ playCount || 0}}
-
-
-
-
- Tasks
-
-
- {{ taskCount || 0}}
-
-
-
-
- Hosts
-
-
- {{ hostCount || 0}}
-
-
-
-
-
-
-
-
- Elapsed
-
-
- {{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js
deleted file mode 100644
index 60c06de7cd..0000000000
--- a/awx/ui/client/src/job-results/job-results.route.js
+++ /dev/null
@@ -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'
-};
diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js
deleted file mode 100644
index 6b77575fff..0000000000
--- a/awx/ui/client/src/job-results/job-results.service.js
+++ /dev/null
@@ -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: `
- ${i18n._("Are you sure you want to delete this job?")}
-
`,
- 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: `
- ${i18n._("Are you sure you want to cancel this job?")}
-
`,
- 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;
-}];
diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js
deleted file mode 100644
index f0aedc7c43..0000000000
--- a/awx/ui/client/src/job-results/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js
deleted file mode 100644
index 66c969b9c4..0000000000
--- a/awx/ui/client/src/job-results/parse-stdout.service.js
+++ /dev/null
@@ -1,293 +0,0 @@
-/*************************************************
-* Copyright (c) 2016 Ansible, Inc.
-*
-* All Rights Reserved
-*************************************************/
-
-export default ['$log', 'moment', 'i18n', function($log, moment, i18n){
- var val = {
- // parses stdout string from api and formats various codes to the
- // correct dom structure
- prettify: function(line, unstyled){
- line = line
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-
- // 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, '
');
- line = line.replace(/(|)\[0;30m/g, '');
- line = line.replace(/(|)\[1;30m/g, '');
- line = line.replace(/(|)\[[0,1];31m/g, '');
- line = line.replace(/(|)\[0;32m(=|)/g, '');
- line = line.replace(/(|)\[0;32m1/g, '');
- line = line.replace(/(|)\[0;33m/g, '');
- line = line.replace(/(|)\[0;34m/g, '');
- line = line.replace(/(|)\[[0,1];35m/g, '');
- line = line.replace(/(|)\[0;36m/g, '');
- line = line.replace(/()\s/g, '$1');
-
- //end span
- line = line.replace(/(|)\[0m/g, '');
- /* 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(/()\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}
${i18n._("Status")}: ${event.event_display}
${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 `${time}
`;
- }
- else if(event.event_name === "playbook_on_stats" && line.indexOf("PLAY") > -1){
- time = moment(event.created).format('HH:mm:ss');
- return `${time}
`;
- }
- else {
- return emptySpan;
- }
-
- },
- // used to add expand/collapse icon next to line numbers of headers
- getCollapseIcon: function(event, line) {
- var clickClass,
- expanderizerSpecifier;
-
- var emptySpan = `
-`;
-
- 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 = `
-
-
-
-`;
- 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 `
-
-
${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}
-
{
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}) => {
diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js
index 37fd3292b3..2e80cdc369 100644
--- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js
+++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js
@@ -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');
}
}
}
diff --git a/awx/ui/client/src/management-jobs/card/card.controller.js b/awx/ui/client/src/management-jobs/card/card.controller.js
index a66f5d7ec5..12dceb22b9 100644
--- a/awx/ui/client/src/management-jobs/card/card.controller.js
+++ b/awx/ui/client/src/management-jobs/card/card.controller.js
@@ -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;
diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js
index 0e74eb5132..b26fffb264 100644
--- a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js
+++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js
@@ -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' });
};
diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js
index 39c322e183..853534f142 100644
--- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js
+++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js
@@ -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 ' +
diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js
index ec21e0009c..1bcd7db2d6 100644
--- a/awx/ui/client/src/projects/list/projects-list.controller.js
+++ b/awx/ui/client/src/projects/list/projects-list.controller.js
@@ -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 ' +
diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js
index 943f63818b..7d79ac1ec1 100644
--- a/awx/ui/client/src/shared/smart-search/queryset.service.js
+++ b/awx/ui/client/src/shared/smart-search/queryset.service.js
@@ -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;
diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js
index e7dd435307..7c1e7eaca8 100644
--- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js
+++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js
@@ -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;
diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html
index 1f31adcf9e..ce52fea759 100644
--- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html
+++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html
@@ -3,11 +3,11 @@
-
-
diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js
index 7c390d0cba..0e50b0e671 100644
--- a/awx/ui/client/src/shared/socket/socket.service.js
+++ b/awx/ui/client/src/shared/socket/socket.service.js
@@ -90,12 +90,18 @@ export default
// ex: 'ws-jobs-
'
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-'
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);
diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js
index 8026a4d10c..1966e0b68c 100644
--- a/awx/ui/client/src/smart-status/smart-status.controller.js
+++ b/awx/ui/client/src/smart-status/smart-status.controller.js
@@ -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 =
diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html
deleted file mode 100644
index 84afa03728..0000000000
--- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html
+++ /dev/null
@@ -1,147 +0,0 @@
-
-
-
-
-
-
-
-
-
-
Name
-
{{ job.module_name }}
-
-
-
-
STATUS
-
-
- {{ job.status }}
-
-
-
-
-
STARTED
-
- {{ job.started | longDate }}
-
-
-
-
-
FINISHED
-
- {{ job.finished | longDate }}
-
-
-
-
-
ELAPSED
-
- {{ job.elapsed }} seconds
-
-
-
-
-
Module Args
-
{{ job.module_args }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Verbosity
-
{{ verbosity }}
-
-
-
-
- {{ 'Extra Variables' | translate }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js
deleted file mode 100644
index 899a98e9f7..0000000000
--- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js
+++ /dev/null
@@ -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;
- });
- }]
- }
-};
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html
deleted file mode 100644
index 48f2d65b7e..0000000000
--- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html
+++ /dev/null
@@ -1,152 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
STATUS
-
-
- {{ job.status }}
-
-
-
-
-
-
EXPLANATION
-
- {{task_detail | limitTo:explanationLimit}}
-
- ...
- Show More
-
- Show Less
-
-
-
-
-
LICENSE ERROR
-
- {{ job.license_error }}
-
-
-
-
-
STARTED
-
- {{ job.started | longDate }}
-
-
-
-
-
FINISHED
-
- {{ job.finished | longDate }}
-
-
-
-
-
ELAPSED
-
- {{ job.elapsed }} seconds
-
-
-
-
-
LAUNCH TYPE
-
- {{ job.launch_type }}
-
-
-
-
-
-
-
SOURCE
-
- {{ source }}
-
-
-
-
-
REGIONS
-
- {{ source_regions }}
-
-
-
-
-
OVERWRITE
-
- {{ job.overwrite }}
-
-
-
-
-
OVERWRITE VARS
-
- {{ job.overwrite_vars }}
-
-
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js
deleted file mode 100644
index bdd1a9a2b1..0000000000
--- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js
+++ /dev/null
@@ -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;
- });
- }]
- }
-};
diff --git a/awx/ui/client/src/standard-out/log/main.js b/awx/ui/client/src/standard-out/log/main.js
deleted file mode 100644
index bb97a737be..0000000000
--- a/awx/ui/client/src/standard-out/log/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js b/awx/ui/client/src/standard-out/log/standard-out-log.controller.js
deleted file mode 100644
index 728f8faedf..0000000000
--- a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js
+++ /dev/null
@@ -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;
- }
- }
- });
-
-}];
diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.directive.js b/awx/ui/client/src/standard-out/log/standard-out-log.directive.js
deleted file mode 100644
index d7d0656441..0000000000
--- a/awx/ui/client/src/standard-out/log/standard-out-log.directive.js
+++ /dev/null
@@ -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;
- }
- });
- }
- };
-}];
diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.partial.html b/awx/ui/client/src/standard-out/log/standard-out-log.partial.html
deleted file mode 100644
index 45d9f70cbb..0000000000
--- a/awx/ui/client/src/standard-out/log/standard-out-log.partial.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/awx/ui/client/src/standard-out/main.js b/awx/ui/client/src/standard-out/main.js
deleted file mode 100644
index 1e0f451014..0000000000
--- a/awx/ui/client/src/standard-out/main.js
+++ /dev/null
@@ -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);
- }]);
diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html
deleted file mode 100644
index 8c9740a5eb..0000000000
--- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
-
-
-
-
NAME
-
{{ job.name }}
-
-
-
-
STATUS
-
-
- {{ job.status }}
-
-
-
-
-
STARTED
-
- {{ job.started | longDate }}
-
-
-
-
-
FINISHED
-
- {{ job.finished | longDate }}
-
-
-
-
-
ELAPSED
-
- {{ job.elapsed }} seconds
-
-
-
-
-
LAUNCH TYPE
-
- {{ job.launch_type }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
STANDARD OUT
-
-
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js
deleted file mode 100644
index e3a59e884d..0000000000
--- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js
+++ /dev/null
@@ -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;
- });
- }]
- }
-};
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html
deleted file mode 100644
index f9828d4c02..0000000000
--- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
STATUS
-
-
- {{ job.status }}
-
-
-
-
-
STARTED
-
- {{ job.started | longDate }}
-
-
-
-
-
FINISHED
-
- {{ job.finished | longDate }}
-
-
-
-
-
ELAPSED
-
- {{ job.elapsed }} seconds
-
-
-
-
-
LAUNCH TYPE
-
- {{ job.launch_type }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js
deleted file mode 100644
index 818509ccc7..0000000000
--- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js
+++ /dev/null
@@ -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;
- });
- }]
- }
-};
diff --git a/awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js b/awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js
deleted file mode 100644
index 6e28362f3a..0000000000
--- a/awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js
+++ /dev/null
@@ -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 = "" + i18n._("Are you sure you want to submit the request to cancel this job?") + "
";
- var deleteBody = "" + i18n._("Are you sure you want to delete this job?") + "
";
- 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'
-];
diff --git a/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js b/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js
deleted file mode 100644
index 43b39da58c..0000000000
--- a/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js
+++ /dev/null
@@ -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 });
- });
- };
- }];
diff --git a/awx/ui/client/src/standard-out/standard-out-factories/main.js b/awx/ui/client/src/standard-out/standard-out-factories/main.js
deleted file mode 100644
index 935c8dca37..0000000000
--- a/awx/ui/client/src/standard-out/standard-out-factories/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js
deleted file mode 100644
index 1a707aa706..0000000000
--- a/awx/ui/client/src/standard-out/standard-out.controller.js
+++ /dev/null
@@ -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'];
diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js
index 70bab1727b..7cfc754d86 100644
--- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js
+++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js
@@ -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'});
}
};
diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/workflow-results/standard-out.block.less
similarity index 97%
rename from awx/ui/client/src/standard-out/standard-out.block.less
rename to awx/ui/client/src/workflow-results/standard-out.block.less
index fe7e0d2757..980f401c75 100644
--- a/awx/ui/client/src/standard-out/standard-out.block.less
+++ b/awx/ui/client/src/workflow-results/standard-out.block.less
@@ -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;
-}
+}
\ No newline at end of file
diff --git a/awx/ui/package.json b/awx/ui/package.json
index 2d9f32f488..24a42aa207 100644
--- a/awx/ui/package.json
+++ b/awx/ui/package.json
@@ -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",
diff --git a/awx/ui/test/e2e/tests/smoke.js b/awx/ui/test/e2e/tests/smoke.js
index 5ef4a4ebfd..931ef9f6d5 100644
--- a/awx/ui/test/e2e/tests/smoke.js
+++ b/awx/ui/test/e2e/tests/smoke.js
@@ -296,7 +296,7 @@ module.exports = {
client.waitForElementVisible('div.spinny');
client.waitForElementNotVisible('div.spinny');
- client.waitForElementVisible('.JobResults-detailsPanel');
+ client.waitForElementVisible('at-job-details');
client.waitForElementNotPresent(running, 60000);
client.waitForElementVisible(success, 60000);
diff --git a/awx/ui/test/e2e/tests/test-search-tag-add-remove.js b/awx/ui/test/e2e/tests/test-search-tag-add-remove.js
new file mode 100644
index 0000000000..294f7aa582
--- /dev/null
+++ b/awx/ui/test/e2e/tests/test-search-tag-add-remove.js
@@ -0,0 +1,124 @@
+import { range } from 'lodash';
+
+import { getAdminMachineCredential } from '../fixtures';
+
+const spinny = 'div.spinny';
+const searchInput = 'smart-search input';
+const searchSubmit = 'smart-search i[class*="search"]';
+const searchTags = 'smart-search .SmartSearch-tagContainer';
+const searchClearAll = 'smart-search .SmartSearch-clearAll';
+const searchTagDelete = 'i[class*="fa-times"]';
+
+const createTagSelector = n => `${searchTags}:nth-of-type(${n})`;
+const createTagDeleteSelector = n => `${searchTags}:nth-of-type(${n}) ${searchTagDelete}`;
+
+const checkTags = (client, tags) => {
+ const strategy = 'css selector';
+
+ const countReached = createTagSelector(tags.length);
+ const countExceeded = createTagSelector(tags.length + 1);
+
+ if (tags.length > 0) {
+ client.waitForElementVisible(countReached);
+ client.waitForElementNotPresent(countExceeded);
+ }
+
+ client.elements(strategy, searchTags, tagElements => {
+ client.assert.equal(tagElements.value.length, tags.length);
+
+ let n = -1;
+ tagElements.value.map(o => o.ELEMENT).forEach(id => {
+ client.elementIdText(id, ({ value }) => {
+ client.assert.equal(value, tags[++n]);
+ });
+ });
+ });
+};
+
+module.exports = {
+ before: (client, done) => {
+ const resources = range(25).map(n => getAdminMachineCredential(`test-search-${n}`));
+
+ Promise.all(resources).then(done);
+ },
+ 'add and remove search tags': client => {
+ const credentials = client.page.credentials();
+
+ client.login();
+ client.waitForAngular();
+
+ credentials.section.navigation.waitForElementVisible('@credentials');
+ credentials.section.navigation.click('@credentials');
+
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ client.waitForElementVisible(searchInput);
+ client.waitForElementVisible(searchSubmit);
+
+ client.expect.element(searchInput).enabled;
+ client.expect.element(searchSubmit).enabled;
+
+ checkTags(client, []);
+
+ client.setValue(searchInput, 'foo');
+ client.click(searchSubmit);
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, ['foo']);
+
+ client.setValue(searchInput, 'bar e2e');
+ client.click(searchSubmit);
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, ['foo', 'bar', 'e2e']);
+
+ client.click(searchClearAll);
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, []);
+
+ client.setValue(searchInput, 'fiz name:foo');
+ client.click(searchSubmit);
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, ['fiz', 'name:foo']);
+
+ client.click(searchClearAll);
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, []);
+
+ client.setValue(searchInput, 'hello name:world fiz');
+ client.click(searchSubmit);
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, ['hello', 'fiz', 'name:world']);
+
+ client.click(createTagDeleteSelector(2));
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, ['hello', 'name:world']);
+
+ client.click(createTagDeleteSelector(1));
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, ['name:world']);
+
+ client.click(createTagDeleteSelector(1));
+ client.waitForElementVisible(spinny);
+ client.waitForElementNotVisible(spinny);
+
+ checkTags(client, []);
+
+ client.end();
+ },
+};
diff --git a/awx/ui/test/spec/job-results/job-results.controller-test.js b/awx/ui/test/spec/job-results/job-results.controller-test.js
deleted file mode 100644
index c05f2329f7..0000000000
--- a/awx/ui/test/spec/job-results/job-results.controller-test.js
+++ /dev/null
@@ -1,701 +0,0 @@
-'use strict';
-import moment from 'moment';
-
-describe('Controller: jobResultsController', () => {
- // Setup
- let jobResultsController;
-
- let jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, eventResolve, populateResolve, $rScope, q, $log, Dataset, Rest, $state, QuerySet, i18n,fieldChoices, fieldLabels, $interval, workflowResultsService, statusSocket, jobExtraCredentials;
-
- statusSocket = function() {
- var fn = function() {};
- return fn;
- };
- jobData = {
- related: {},
- summary_fields: {
- inventory: {
- id: null,
- kind: ''
- }
- }
- };
- jobDataOptions = {
- actions: {
- get: {}
- }
- };
- jobLabels = {};
- jobFinished = true;
- count = {
- val: {},
- countFinished: false
- };
- eventResolve = {
- results: []
- };
- populateResolve = {};
-
- Dataset = {
- data: {foo: "bar"}
- };
-
- let provideVals = () => {
- angular.mock.module('jobResults', ($provide) => {
- ParseTypeChange = jasmine.createSpy('ParseTypeChange');
- ParseVariableString = jasmine.createSpy('ParseVariableString');
- jobResultsService = jasmine.createSpyObj('jobResultsService', [
- 'deleteJob',
- 'cancelJob',
- 'relaunchJob',
- 'getEvents',
- 'getJobData',
- ]);
- eventQueue = jasmine.createSpyObj('eventQueue', [
- 'populate',
- 'markProcessed',
- 'initialize'
- ]);
-
- Rest = jasmine.createSpyObj('Rest', [
- 'setUrl',
- 'get'
- ]);
-
- $state = jasmine.createSpyObj('$state', [
- 'reload'
- ]);
-
- QuerySet = jasmine.createSpyObj('QuerySet', [
- 'encodeQueryset'
- ]);
-
- i18n = {
- _: function(txt) {
- return txt;
- }
- };
-
- $provide.service('workflowResultsService', () => {
- return jasmine.createSpyObj('workflowResultsService', ['createOneSecondTimer', 'destroyTimer']);
- });
-
- $provide.value('statusSocket', statusSocket);
-
- $provide.value('jobData', jobData);
- $provide.value('jobDataOptions', jobDataOptions);
- $provide.value('jobLabels', jobLabels);
- $provide.value('jobFinished', jobFinished);
- $provide.value('count', count);
- $provide.value('ParseTypeChange', ParseTypeChange);
- $provide.value('ParseVariableString', ParseVariableString);
- $provide.value('jobResultsService', jobResultsService);
- $provide.value('eventQueue', eventQueue);
- $provide.value('Dataset', Dataset);
- $provide.value('Rest', Rest);
- $provide.value('$state', $state);
- $provide.value('QuerySet', QuerySet);
- $provide.value('i18n', i18n);
- $provide.value('fieldChoices', fieldChoices);
- $provide.value('fieldLabels', fieldLabels);
- $provide.value('jobExtraCredentials', jobExtraCredentials);
- });
- };
-
- let injectVals = () => {
- angular.mock.inject((_jobData_, _jobDataOptions_, _jobLabels_, _jobFinished_, _count_, _ParseTypeChange_, _ParseVariableString_, _jobResultsService_, _eventQueue_, _$compile_, $rootScope, $controller, $q, $httpBackend, _$log_, _Dataset_, _Rest_, _$state_, _QuerySet_, _$interval_, _workflowResultsService_, _statusSocket_) => {
- // when you call $scope.$apply() (which you need to do to
- // to get inside of .then blocks to test), something is
- // causing a request for all static files.
- //
- // this is a hack to just pass those requests through
- //
- // from googling this is probably due to angular-router
- // weirdness
- $httpBackend.when("GET", (url) => (url
- .indexOf("/static/") !== -1))
- .respond('');
-
- $httpBackend
- .whenGET('/api')
- .respond(200, '');
-
- $scope = $rootScope.$new();
- $rScope = $rootScope;
- q = $q;
- jobData = _jobData_;
- jobDataOptions = _jobDataOptions_;
- jobLabels = _jobLabels_;
- jobFinished = _jobFinished_;
- count = _count_;
- ParseTypeChange = _ParseTypeChange_;
- ParseVariableString = _ParseVariableString_;
- ParseVariableString.and.returnValue(jobData.extra_vars);
- jobResultsService = _jobResultsService_;
- eventQueue = _eventQueue_;
- $log = _$log_;
- Dataset = _Dataset_;
- Rest = _Rest_;
- $state = _$state_;
- QuerySet = _QuerySet_;
- $interval = _$interval_;
- workflowResultsService = _workflowResultsService_;
- statusSocket = _statusSocket_;
-
- jobResultsService.getEvents.and
- .returnValue(eventResolve);
- eventQueue.populate.and
- .returnValue(populateResolve);
-
- jobResultsService.getJobData = function() {
- var deferred = $q.defer();
- deferred.resolve({});
- return deferred.promise;
- };
-
- $compile = _$compile_;
-
- jobResultsController = $controller('jobResultsController', {
- $scope: $scope,
- jobData: jobData,
- jobDataOptions: jobDataOptions,
- jobLabels: jobLabels,
- jobFinished: jobFinished,
- count: count,
- ParseTypeChange: ParseTypeChange,
- jobResultsService: jobResultsService,
- eventQueue: eventQueue,
- $compile: $compile,
- $log: $log,
- $q: q,
- Dataset: Dataset,
- Rest: Rest,
- $state: $state,
- QuerySet: QuerySet,
- statusSocket: statusSocket
- });
- });
- };
-
- beforeEach(angular.mock.module('shared'));
-
- let bootstrapTest = () => {
- provideVals();
- injectVals();
- };
-
- describe('bootstrap resolve values on scope', () => {
- beforeEach(() => {
- bootstrapTest();
- });
-
- it('should set values to scope based on resolve', () => {
- expect($scope.job).toBe(jobData);
- expect($scope.jobOptions).toBe(jobDataOptions.actions.GET);
- expect($scope.labels).toBe(jobLabels);
- });
- });
-
- describe('getLinks()', () => {
- beforeEach(() => {
- jobData.related = {
- "created_by": "api/v2/users/12",
- "inventory": "api/v2/inventories/12",
- "project": "api/v2/projects/12",
- "credential": "api/v2/credentials/12",
- "cloud_credential": "api/v2/credentials/13",
- "network_credential": "api/v2/credentials/14",
- };
-
- jobData.summary_fields.inventory = {
- id: 12,
- kind: ''
- };
-
- bootstrapTest();
- });
-
- it('should transform related links and set to scope var', () => {
- expect($scope.created_by_link).toBe('/#/users/12');
- expect($scope.inventory_link).toBe('/#/inventories/inventory/12');
- expect($scope.project_link).toBe('/#/projects/12');
- expect($scope.machine_credential_link).toBe('/#/credentials/12');
- expect($scope.cloud_credential_link).toBe('/#/credentials/13');
- expect($scope.network_credential_link).toBe('/#/credentials/14');
- });
- });
-
- describe('getLabels()', () => {
- beforeEach(() => {
- jobDataOptions.actions.GET = {
- status: {
- choices: [
- ["new",
- "New"]
- ]
- },
- job_type: {
- choices: [
- ["job",
- "Playbook Run"]
- ]
- },
- verbosity: {
- choices: [
- [0,
- "0 (Normal)"]
- ]
- }
- };
- jobData.status = "new";
- jobData.job_type = "job";
- jobData.verbosity = 0;
-
- bootstrapTest();
- });
-
- it('should set scope variables based on options', () => {
- $scope.job_status = jobData.status;
-
- $scope.$apply();
- expect($scope.status_label).toBe("New");
- expect($scope.type_label).toBe("Playbook Run");
- expect($scope.verbosity_label).toBe("0 (Normal)");
- });
- });
-
- describe('elapsed timer', () => {
- describe('job running', () => {
- beforeEach(() => {
- jobData.started = moment();
- jobData.status = 'running';
-
- bootstrapTest();
- });
-
- it('should start timer', () => {
- expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled();
- });
- });
-
- describe('job waiting', () => {
- beforeEach(() => {
- jobData.started = null;
- jobData.status = 'waiting';
-
- bootstrapTest();
- });
-
- it('should not start timer', () => {
- expect(workflowResultsService.createOneSecondTimer).not.toHaveBeenCalled();
- });
- });
-
- describe('job transitions to running', () => {
- beforeEach(() => {
- jobData.started = null;
- jobData.status = 'waiting';
- jobData.id = 13;
-
- bootstrapTest();
-
- $rScope.$broadcast('ws-jobs', { unified_job_id: jobData.id, status: 'running' });
- });
-
- it('should start timer', () => {
- expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled();
- });
-
- describe('job transitions from running to finished', () => {
- it('should cleanup timer', () => {
- $rScope.$broadcast('ws-jobs', { unified_job_id: jobData.id, status: 'successful' });
- expect(workflowResultsService.destroyTimer).toHaveBeenCalled();
- });
- });
- });
- });
-
- describe('extra vars stuff', () => {
- let extraVars = "foo";
-
- beforeEach(() => {
- jobData.extra_vars = extraVars;
-
- bootstrapTest();
- });
-
- it('should have extra vars on scope', () => {
- expect($scope.job.extra_vars).toBe(extraVars);
- });
-
- it('should call ParseVariableString and set to scope', () => {
- expect(ParseVariableString)
- .toHaveBeenCalledWith(extraVars);
- expect($scope.variables).toBe(extraVars);
- });
-
- it('should set the parse type to yaml', () => {
- expect($scope.parseType).toBe('yaml');
- });
-
- it('should call ParseTypeChange with proper params', () => {
- let params = {
- scope: $scope,
- field_id: 'pre-formatted-variables',
- readOnly: true
- };
-
- expect(ParseTypeChange)
- .toHaveBeenCalledWith(params);
- });
- });
-
- describe('$scope.toggleStdoutFullscreen', () => {
- beforeEach(() => {
- bootstrapTest();
- });
-
- it('should toggle $scope.stdoutFullScreen', () => {
- // essentially set to false
- expect($scope.stdoutFullScreen).toBe(false);
-
- // toggle once to true
- $scope.toggleStdoutFullscreen();
- expect($scope.stdoutFullScreen).toBe(true);
-
- // toggle again to false
- $scope.toggleStdoutFullscreen();
- expect($scope.stdoutFullScreen).toBe(false);
- });
- });
-
- describe('$scope.deleteJob', () => {
- beforeEach(() => {
- bootstrapTest();
- });
-
- it('should delete the job', () => {
- let job = $scope.job;
- $scope.deleteJob();
- expect(jobResultsService.deleteJob).toHaveBeenCalledWith(job);
- });
- });
-
- describe('$scope.cancelJob', () => {
- beforeEach(() => {
- bootstrapTest();
- });
-
- it('should cancel the job', () => {
- let job = $scope.job;
- $scope.cancelJob();
- expect(jobResultsService.cancelJob).toHaveBeenCalledWith(job);
- });
- });
-
- describe('count stuff', () => {
- beforeEach(() => {
- count = {
- val: {
- ok: 1,
- skipped: 2,
- unreachable: 3,
- failures: 4,
- changed: 5
- },
- countFinished: true
- };
-
- bootstrapTest();
- });
-
- it('should set count values to scope', () => {
- expect($scope.count).toBe(count.val);
- expect($scope.countFinished).toBe(true);
- });
-
- it('should find the hostCount based on the count', () => {
- expect($scope.hostCount).toBe(15);
- });
- });
-
- describe('follow stuff - incomplete', () => {
- beforeEach(() => {
- jobFinished = false;
-
- bootstrapTest();
- });
-
- it('should set followEngaged based on jobFinished incomplete', () => {
- expect($scope.followEngaged).toBe(true);
- });
-
- it('should set followTooltip based on jobFinished incomplete', () => {
- expect($scope.followTooltip).toBe("Currently following standard out as it comes in. Click to unfollow.");
- });
- });
-
- describe('follow stuff - finished', () => {
- beforeEach(() => {
- jobFinished = true;
-
- bootstrapTest();
- });
-
- it('should set followEngaged based on jobFinished', () => {
- expect($scope.followEngaged).toBe(false);
- });
-
- it('should set followTooltip based on jobFinished', () => {
- expect($scope.followTooltip).toBe("Jump to last line of standard out.");
- });
- });
-
- describe('event stuff', () => {
- beforeEach(() => {
- jobData.id = 1;
- jobData.related.job_events = "url";
-
- bootstrapTest();
- });
-
- xit('should make a rest call to get already completed events', () => {
- expect(jobResultsService.getEvents).toHaveBeenCalledWith("url");
- });
-
- xit('should call processEvent when receiving message', () => {
- let eventPayload = {"foo": "bar"};
- $rScope.$broadcast('ws-job_events-1', eventPayload);
- expect(eventQueue.populate).toHaveBeenCalledWith(eventPayload);
- });
-
- it('should set the job status on scope when receiving message', () => {
- let eventPayload = {
- unified_job_id: 1,
- status: 'finished'
- };
- $rScope.$broadcast('ws-jobs', eventPayload);
- expect($scope.job_status).toBe(eventPayload.status);
- });
- });
-
- describe('getEvents and populate stuff', () => {
- describe('getEvents', () => {
- let event1 = {
- event: 'foo'
- };
-
- let event2 = {
- event_name: 'bar'
- };
-
- let event1Processed = {
- event_name: 'foo'
- };
-
- beforeEach(() => {
- eventResolve = {
- results: [
- event1,
- event2
- ]
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('should change the event name to event_name', () => {
- expect(eventQueue.populate)
- .toHaveBeenCalledWith(event1Processed);
- });
-
- xit('should pass through the event with event_name', () => {
- expect(eventQueue.populate)
- .toHaveBeenCalledWith(event2);
- });
-
- xit('should have called populate twice', () => {
- expect(eventQueue.populate.calls.count()).toEqual(2);
- });
-
- // TODO: can't figure out how to a test of events.next...
- // if you set events.next to true it causes the tests to
- // stop running
- });
-
- describe('populate - start time', () => {
- beforeEach(() => {
- jobData.start = "";
-
- populateResolve = {
- startTime: 'foo',
- changes: ['startTime']
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('sets start time when passed as a change', () => {
- expect($scope.job.start).toBe('foo');
- });
- });
-
- describe('populate - start time already set', () => {
- beforeEach(() => {
- jobData.start = "bar";
-
- populateResolve = {
- startTime: 'foo',
- changes: ['startTime']
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('does not set start time because already set', () => {
- expect($scope.job.start).toBe('bar');
- });
- });
-
- describe('populate - count already received', () => {
- let receiveCount = {
- ok: 2,
- skipped: 2,
- unreachable: 2,
- failures: 2,
- changed: 2
- };
-
- let alreadyCount = {
- ok: 3,
- skipped: 3,
- unreachable: 3,
- failures: 3,
- changed: 3
- };
-
- beforeEach(() => {
- count.countFinished = true;
- count.val = alreadyCount;
-
- populateResolve = {
- count: receiveCount,
- changes: ['count']
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('count does not change', () => {
- expect($scope.count).toBe(alreadyCount);
- expect($scope.hostCount).toBe(15);
- });
- });
-
- describe('populate - playCount, taskCount and countFinished', () => {
- beforeEach(() => {
-
- populateResolve = {
- playCount: 12,
- taskCount: 13,
- changes: ['playCount', 'taskCount', 'countFinished']
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('sets playCount', () => {
- expect($scope.playCount).toBe(12);
- });
-
- xit('sets taskCount', () => {
- expect($scope.taskCount).toBe(13);
- });
-
- xit('sets countFinished', () => {
- expect($scope.countFinished).toBe(true);
- });
- });
-
- describe('populate - finishedTime', () => {
- beforeEach(() => {
- jobData.finished = "";
-
- populateResolve = {
- finishedTime: "finished_time",
- changes: ['finishedTime']
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('sets finished time and changes follow tooltip', () => {
- expect($scope.job.finished).toBe('finished_time');
- expect($scope.jobFinished).toBe(true);
- expect($scope.followTooltip)
- .toBe("Jump to last line of standard out.");
- });
- });
-
- describe('populate - finishedTime when already finished', () => {
- beforeEach(() => {
- jobData.finished = "already_set";
-
- populateResolve = {
- finishedTime: "finished_time",
- changes: ['finishedTime']
- };
-
- bootstrapTest();
-
- $scope.$apply();
- });
-
- xit('does not set finished time because already set', () => {
- expect($scope.job.finished).toBe('already_set');
- expect($scope.jobFinished).toBe(true);
- expect($scope.followTooltip)
- .toBe("Jump to last line of standard out.");
- });
- });
-
- describe('populate - stdout', () => {
- beforeEach(() => {
-
- populateResolve = {
- counter: 12,
- stdout: "line",
- changes: ['stdout']
- };
-
- bootstrapTest();
-
- spyOn($log, 'error');
-
- $scope.followEngaged = true;
-
- $scope.$apply();
- });
-
- xit('creates new child scope for the event', () => {
- expect($scope.events[12].event).toBe(populateResolve);
-
- // in unit test, followScroll should not be defined as
- // directive has not been instantiated
- expect($log.error).toHaveBeenCalledWith("follow scroll undefined, standard out directive not loaded yet?");
- });
- });
- });
-});
diff --git a/awx/ui/test/spec/job-results/job-results.service-test.js b/awx/ui/test/spec/job-results/job-results.service-test.js
deleted file mode 100644
index 1219f2e450..0000000000
--- a/awx/ui/test/spec/job-results/job-results.service-test.js
+++ /dev/null
@@ -1,50 +0,0 @@
-'use strict';
-
-describe('jobResultsService', () => {
- let jobResultsService;
-
- beforeEach(angular.mock.module('awApp'));
-
- beforeEach(angular.mock.inject(( _jobResultsService_) => {
- jobResultsService = _jobResultsService_;
- }));
-
- describe('getCountsFromStatsEvent()', () => {
- it('properly counts hosts based on task state', () => {
- let event_data = {
- "skipped": {
- "skipped-host": 5 // this host skipped all 5 tasks
- },
- "ok": {
- "ok-host": 5, // this host was ok on all 5 tasks
- "changed-host": 4 // this host had 4 ok tasks, had 1 changed task
- },
- "changed": {
- "changed-host": 1
- },
- "failures": {
- "failed-host": 1 // this host had a failed task
- },
- "dark": {
- "unreachable-host": 1 // this host was unreachable
- },
- "processed": {
- "ok-host": 1,
- "changed-host": 1,
- "skipped-host": 1,
- "failed-host": 1,
- "unreachable-host": 1
- },
- "playbook_uuid": "c23d8872-c92a-4e96-9f78-abe6fef38f33",
- "playbook": "some_playbook.yml",
- };
- expect(jobResultsService.getCountsFromStatsEvent(event_data)).toEqual({
- 'ok': 1,
- 'skipped': 1,
- 'unreachable': 1,
- 'failures': 1,
- 'changed': 1
- });
- });
- });
-});
diff --git a/awx/ui/test/spec/job-results/parse-stdout.service-test.js b/awx/ui/test/spec/job-results/parse-stdout.service-test.js
deleted file mode 100644
index 86441c1ffa..0000000000
--- a/awx/ui/test/spec/job-results/parse-stdout.service-test.js
+++ /dev/null
@@ -1,212 +0,0 @@
-'use strict';
-
-describe('parseStdoutService', () => {
- let parseStdoutService,
- log;
-
- beforeEach(angular.mock.module('awApp'));
-
- beforeEach(angular.mock.module('jobResults',($provide) => {
- log = jasmine.createSpyObj('$log', [
- 'error'
- ]);
-
- $provide.value('$log', log);
- }));
-
- beforeEach(angular.mock.inject((_$log_, _parseStdoutService_) => {
- parseStdoutService = _parseStdoutService_;
- }));
-
- describe('prettify()', () => {
- it('returns lines of stdout with styling classes', () => {
- let line = "[0;32mok: [host-00][0m",
- styledLine = 'ok: [host-00]';
- expect(parseStdoutService.prettify(line)).toBe(styledLine);
- });
-
- it('can return lines of stdout without styling classes', () => {
- let line = "[0;32mok: [host-00][0m",
- unstyled = "unstyled",
- unstyledLine = 'ok: [host-00]';
- expect(parseStdoutService.prettify(line, unstyled)).toBe(unstyledLine);
- });
-
- it('can return empty strings', () => {
- expect(parseStdoutService.prettify("")).toBe("");
- });
- });
-
- describe('getLineClasses()', () => {
- it('creates a string that is used as a class', () => {
- let headerEvent = {
- event_name: 'playbook_on_task_start',
- event_data: {
- play_uuid:"0f667a23-d9ab-4128-a735-80566bcdbca0",
- task_uuid: "80dd087c-268b-45e8-9aab-1083bcfd9364"
- }
- };
- let lineNum = 3;
- let line = "TASK [setup] *******************************************************************";
- let styledLine = " header_task header_task_80dd087c-268b-45e8-9aab-1083bcfd9364 actual_header play_0f667a23-d9ab-4128-a735-80566bcdbca0 line_num_3";
- expect(parseStdoutService.getLineClasses(headerEvent, line, lineNum)).toBe(styledLine);
- });
- });
-
- describe('getStartTime()', () => {
- // TODO: the problem is that the date here calls moment, and thus
- // the date will be timezone'd in the string (this could be
- // different based on where you are)
- xit('creates returns a badge with the start time of the event', () => {
- let headerEvent = {
- event_name: 'playbook_on_play_start',
- created: "2016-11-22T21:15:54.736Z"
- };
-
- let line = "PLAY [add hosts to inventory] **************************************************";
- let badgeDiv = '13:15:54
';
- expect(parseStdoutService.getStartTimeBadge(headerEvent, line)).toBe(badgeDiv);
- });
- });
-
- describe('getCollapseIcon()', () => {
- let emptySpan = `
-`;
-
- it('returns empty expander for non-header event', () => {
- let nonHeaderEvent = {
- event_name: 'not_header',
- start_line: 0,
- end_line: 1,
- stdout:"line1"
- };
- expect(parseStdoutService.getCollapseIcon(nonHeaderEvent))
- .toBe(emptySpan);
- });
-
- it('returns collapse/decollapse icons for header events', () => {
- let headerEvent = {
- event_name: 'playbook_on_task_start',
- start_line: 0,
- end_line: 1,
- stdout:"line1",
- event_data: {
- task_uuid: '1da9012d-18e6-4562-85cd-83cf10a97f86'
- }
- };
- let line = "TASK [setup] *******************************************************************";
- let expandSpan = `
-
-
-
-`;
-
- expect(parseStdoutService.getCollapseIcon(headerEvent, line))
- .toBe(expandSpan);
- });
- });
-
- describe('getLineArr()', () => {
- it('returns stdout in array format', () => {
- let mockEvent = {
- start_line: 12,
- end_line: 14,
- stdout: "line1\r\nline2\r\n"
- };
- let expectedReturn = [[13, "line1"],[14, "line2"]];
-
- let returnedEvent = parseStdoutService.getLineArr(mockEvent);
-
- expect(returnedEvent).toEqual(expectedReturn);
- });
-
- it('deals correctly with capped lines', () => {
- let mockEvent = {
- start_line: 7,
- end_line: 11,
- stdout: "a\r\nb\r\nc..."
- };
- let expectedReturn = [[8, "a"],[9, "b"], [10,"c..."]];
-
- let returnedEvent = parseStdoutService.getLineArr(mockEvent);
-
- expect(returnedEvent).toEqual(expectedReturn);
- });
- });
-
- describe('parseStdout()', () => {
- let mockEvent = {"foo": "bar"};
-
- it('calls functions', function() {
- spyOn(parseStdoutService, 'getLineArr').and
- .returnValue([[13, 'line1'], [14, 'line2']]);
- spyOn(parseStdoutService, 'getLineClasses').and
- .returnValue("");
- spyOn(parseStdoutService, 'getCollapseIcon').and
- .returnValue("");
- spyOn(parseStdoutService, 'getAnchorTags').and
- .returnValue("");
- spyOn(parseStdoutService, 'prettify').and
- .returnValue("prettified_line");
- spyOn(parseStdoutService, 'getStartTimeBadge').and
- .returnValue("");
-
- parseStdoutService.parseStdout(mockEvent);
-
- expect(parseStdoutService.getLineArr)
- .toHaveBeenCalledWith(mockEvent);
- expect(parseStdoutService.getLineClasses)
- .toHaveBeenCalledWith(mockEvent, 'line1', 13);
- expect(parseStdoutService.getCollapseIcon)
- .toHaveBeenCalledWith(mockEvent, 'line1');
- expect(parseStdoutService.getAnchorTags)
- .toHaveBeenCalledWith(mockEvent);
- expect(parseStdoutService.prettify)
- .toHaveBeenCalledWith('line1');
- expect(parseStdoutService.getStartTimeBadge)
- .toHaveBeenCalledWith(mockEvent, 'line1');
-
- // get line arr should be called once for the event
- expect(parseStdoutService.getLineArr.calls.count())
- .toBe(1);
-
- // other functions should be called twice (once for each
- // line)
- expect(parseStdoutService.getLineClasses.calls.count())
- .toBe(2);
- expect(parseStdoutService.getCollapseIcon.calls.count())
- .toBe(2);
- expect(parseStdoutService.getAnchorTags.calls.count())
- .toBe(2);
- expect(parseStdoutService.prettify.calls.count())
- .toBe(2);
- });
-
- it('returns dom-ified lines', function() {
- spyOn(parseStdoutService, 'getLineArr').and
- .returnValue([[13, 'line1']]);
- spyOn(parseStdoutService, 'getLineClasses').and
- .returnValue("line_classes");
- spyOn(parseStdoutService, 'getCollapseIcon').and
- .returnValue("collapse_icon_dom");
- spyOn(parseStdoutService, 'getAnchorTags').and
- .returnValue(`" anchor_tag_dom`);
- spyOn(parseStdoutService, 'prettify').and
- .returnValue("prettified_line");
- spyOn(parseStdoutService, 'getStartTimeBadge').and
- .returnValue("");
-
- var returnedString = parseStdoutService.parseStdout(mockEvent);
-
- var expectedString = `
-
-
collapse_icon_dom13
-
prettified_line
-
`;
- expect(returnedString).toBe(expectedString);
- });
- });
-});
diff --git a/awx/ui/test/spec/karma.spec.js b/awx/ui/test/spec/karma.spec.js
index abff3172f5..ae2dc8899d 100644
--- a/awx/ui/test/spec/karma.spec.js
+++ b/awx/ui/test/spec/karma.spec.js
@@ -13,6 +13,7 @@ module.exports = config => {
frameworks: ['jasmine'],
reporters: ['progress', 'junit'],
files:[
+ './polyfills.js',
path.join(SRC_PATH, '**/*.html'),
path.join(SRC_PATH, 'vendor.js'),
path.join(NODE_MODULES, 'angular-mocks/angular-mocks.js'),
diff --git a/awx/ui/test/spec/polyfills.js b/awx/ui/test/spec/polyfills.js
new file mode 100644
index 0000000000..8b7342e5fd
--- /dev/null
+++ b/awx/ui/test/spec/polyfills.js
@@ -0,0 +1,31 @@
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+
+if (typeof Object.assign != 'function') {
+ // Must be writable: true, enumerable: false, configurable: true
+ Object.defineProperty(Object, "assign", {
+ value: function assign(target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ },
+ writable: true,
+ configurable: true
+ });
+}
diff --git a/awx/ui/test/unit/karma.unit.js b/awx/ui/test/unit/karma.unit.js
index 5ccd1fe151..d839aff5f2 100644
--- a/awx/ui/test/unit/karma.unit.js
+++ b/awx/ui/test/unit/karma.unit.js
@@ -14,6 +14,7 @@ module.exports = config => {
browsers: ['PhantomJS'],
reporters: ['progress', 'junit'],
files: [
+ './polyfills.js',
path.join(SRC_PATH, 'vendor.js'),
path.join(SRC_PATH, 'app.js'),
path.join(SRC_PATH, '**/*.html'),
diff --git a/awx/ui/test/unit/polyfills.js b/awx/ui/test/unit/polyfills.js
new file mode 100644
index 0000000000..25b18055bd
--- /dev/null
+++ b/awx/ui/test/unit/polyfills.js
@@ -0,0 +1,33 @@
+/* eslint-disable */
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+
+if (typeof Object.assign != 'function') {
+ // Must be writable: true, enumerable: false, configurable: true
+ Object.defineProperty(Object, "assign", {
+ value: function assign(target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ },
+ writable: true,
+ configurable: true
+ });
+}