diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js
index 01216e575f..763894c93c 100644
--- a/awx/ui/client/features/index.js
+++ b/awx/ui/client/features/index.js
@@ -6,6 +6,7 @@ import atFeaturesApplications from '~features/applications';
import atFeaturesCredentials from '~features/credentials';
import atFeaturesTemplates from '~features/templates';
import atFeaturesUsers from '~features/users';
+import atFeaturesJobs from '~features/jobs';
const MODULE_NAME = 'at.features';
@@ -16,7 +17,8 @@ angular.module(MODULE_NAME, [
atFeaturesApplications,
atFeaturesCredentials,
atFeaturesTemplates,
- atFeaturesUsers
+ atFeaturesUsers,
+ atFeaturesJobs
]);
export default MODULE_NAME;
diff --git a/awx/ui/client/features/jobs/index.js b/awx/ui/client/features/jobs/index.js
new file mode 100644
index 0000000000..7212944ddc
--- /dev/null
+++ b/awx/ui/client/features/jobs/index.js
@@ -0,0 +1,13 @@
+import JobsStrings from './jobs.strings';
+import jobsRoute from './jobs.route';
+
+const MODULE_NAME = 'at.features.jobs';
+
+angular
+ .module(MODULE_NAME, [])
+ .service('JobsStrings', JobsStrings)
+ .run(['$stateExtender', ($stateExtender) => {
+ $stateExtender.addState(jobsRoute);
+ }]);
+
+export default MODULE_NAME;
diff --git a/awx/ui/client/features/jobs/index.view.html b/awx/ui/client/features/jobs/index.view.html
new file mode 100644
index 0000000000..054e26c2ba
--- /dev/null
+++ b/awx/ui/client/features/jobs/index.view.html
@@ -0,0 +1,19 @@
+
diff --git a/awx/ui/client/features/jobs/jobs.route.js b/awx/ui/client/features/jobs/jobs.route.js
new file mode 100644
index 0000000000..4aed40d11d
--- /dev/null
+++ b/awx/ui/client/features/jobs/jobs.route.js
@@ -0,0 +1,67 @@
+import { N_ } from '../../src/i18n';
+import jobsListController from './jobsList.controller';
+
+const indexTemplate = require('~features/jobs/index.view.html');
+const jobsListTemplate = require('~features/jobs/jobsList.view.html');
+
+export default {
+ searchPrefix: 'job',
+ name: 'jobs',
+ url: '/jobs',
+ ncyBreadcrumb: {
+ label: N_('JOBS')
+ },
+ params: {
+ job_search: {
+ value: {
+ not__launch_type: 'sync',
+ order_by: '-finished'
+ },
+ dynamic: true,
+ squash: false
+ }
+ },
+ data: {
+ socket: {
+ groups: {
+ jobs: ['status_changed'],
+ schedules: ['changed']
+ }
+ }
+ },
+ resolve: {
+ resolvedModels: [
+ 'UnifiedJobModel',
+ (UnifiedJob) => {
+ const models = [
+ new UnifiedJob(['options']),
+ ];
+ return Promise.all(models);
+ },
+ ],
+ Dataset: [
+ '$stateParams',
+ 'Wait',
+ 'GetBasePath',
+ 'QuerySet',
+ ($stateParams, Wait, GetBasePath, qs) => {
+ const searchParam = $stateParams.job_search;
+ const searchPath = GetBasePath('unified_jobs');
+
+ Wait('start');
+ return qs.search(searchPath, searchParam)
+ .finally(() => Wait('stop'));
+ }
+ ],
+ },
+ views: {
+ '@': {
+ templateUrl: indexTemplate
+ },
+ 'jobsList@jobs': {
+ templateUrl: jobsListTemplate,
+ controller: jobsListController,
+ controllerAs: 'vm'
+ }
+ }
+};
diff --git a/awx/ui/client/features/jobs/jobs.strings.js b/awx/ui/client/features/jobs/jobs.strings.js
new file mode 100644
index 0000000000..a21c2b62e9
--- /dev/null
+++ b/awx/ui/client/features/jobs/jobs.strings.js
@@ -0,0 +1,20 @@
+function JobsStrings (BaseString) {
+ BaseString.call(this, 'jobs');
+
+ const { t } = this;
+ const ns = this.jobs;
+
+ ns.list = {
+ ROW_ITEM_LABEL_STARTED: t.s('Started'),
+ ROW_ITEM_LABEL_FINISHED: t.s('Finished'),
+ ROW_ITEM_LABEL_LAUNCHED_BY: t.s('Launched By'),
+ ROW_ITEM_LABEL_JOB_TEMPLATE: t.s('Job Template'),
+ ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'),
+ ROW_ITEM_LABEL_PROJECT: t.s('Project'),
+ ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'),
+ };
+}
+
+JobsStrings.$inject = ['BaseStringService'];
+
+export default JobsStrings;
diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js
new file mode 100644
index 0000000000..b78a1b77b6
--- /dev/null
+++ b/awx/ui/client/features/jobs/jobsList.controller.js
@@ -0,0 +1,137 @@
+/** ***********************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ ************************************************ */
+const mapChoices = choices => Object
+ .assign(...choices.map(([k, v]) => ({ [k]: v })));
+
+function ListJobsController (
+ $scope,
+ $state,
+ Dataset,
+ resolvedModels,
+ strings,
+ qs,
+ Prompt,
+ $filter,
+ ProcessErrors,
+ Wait,
+ Rest
+) {
+ const vm = this || {};
+ const [unifiedJob] = resolvedModels;
+
+ vm.strings = strings;
+
+ // smart-search
+ const name = 'jobs';
+ const iterator = 'job';
+ const key = 'job_dataset';
+
+ $scope.list = { iterator, name };
+ $scope.collection = { iterator, basePath: 'unified_jobs' };
+ $scope[key] = Dataset.data;
+ $scope[name] = Dataset.data.results;
+ $scope.$on('updateDataset', (e, dataset) => {
+ $scope[key] = dataset;
+ $scope[name] = dataset.results;
+ });
+ $scope.$on('ws-jobs', () => {
+ qs.search(unifiedJob.path, $state.params.job_search)
+ .then(({ data }) => {
+ $scope.$emit('updateDataset', data);
+ });
+ });
+
+ vm.jobTypes = mapChoices(unifiedJob
+ .options('actions.GET.type.choices'));
+
+ vm.getLink = ({ type, id }) => {
+ let link;
+
+ switch (type) {
+ case 'job':
+ link = `/#/jobs/${id}`;
+ break;
+ case 'ad_hoc_command':
+ link = `/#/ad_hoc_commands/${id}`;
+ break;
+ case 'system_job':
+ link = `/#/management_jobs/${id}`;
+ break;
+ case 'project_update':
+ link = `/#/scm_update/${id}`;
+ break;
+ case 'inventory_update':
+ link = `/#/inventory_sync/${id}`;
+ break;
+ case 'workflow_job':
+ link = `/#/workflows/${id}`;
+ break;
+ default:
+ link = '';
+ break;
+ }
+
+ return link;
+ };
+
+ vm.deleteJob = (job) => {
+ const action = () => {
+ $('#prompt-modal').modal('hide');
+ Wait('start');
+ Rest.setUrl(job.url);
+ Rest.destroy()
+ .then(() => {
+ let reloadListStateParams = null;
+
+ if ($scope.jobs.length === 1 && $state.params.job_search &&
+ !_.isEmpty($state.params.job_search.page) &&
+ $state.params.job_search.page !== '1') {
+ const page = `${(parseInt(reloadListStateParams
+ .job_search.page, 10) - 1)}`;
+ reloadListStateParams = _.cloneDeep($state.params);
+ reloadListStateParams.job_search.page = page;
+ }
+
+ $state.go('.', reloadListStateParams, { reload: true });
+ })
+ .catch(({ data, status }) => {
+ ProcessErrors($scope, data, status, null, {
+ hdr: strings.get('error.HEADER'),
+ msg: strings.get('error.CALL', { path: `${job.url}`, status })
+ });
+ })
+ .finally(() => {
+ Wait('stop');
+ });
+ };
+
+ const deleteModalBody = `${strings.get('deleteResource.CONFIRM', 'job')}
`;
+
+ Prompt({
+ hdr: strings.get('deleteResource.HEADER'),
+ resourceName: $filter('sanitize')(job.name),
+ body: deleteModalBody,
+ action,
+ actionText: 'DELETE'
+ });
+ };
+}
+
+ListJobsController.$inject = [
+ '$scope',
+ '$state',
+ 'Dataset',
+ 'resolvedModels',
+ 'JobsStrings',
+ 'QuerySet',
+ 'Prompt',
+ '$filter',
+ 'ProcessErrors',
+ 'Wait',
+ 'Rest'
+];
+
+export default ListJobsController;
diff --git a/awx/ui/client/features/jobs/jobsList.view.html b/awx/ui/client/features/jobs/jobsList.view.html
new file mode 100644
index 0000000000..85f54bbfdc
--- /dev/null
+++ b/awx/ui/client/features/jobs/jobsList.view.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/lib/components/list/_index.less b/awx/ui/client/lib/components/list/_index.less
index b4eac97d30..d053764fe0 100644
--- a/awx/ui/client/lib/components/list/_index.less
+++ b/awx/ui/client/lib/components/list/_index.less
@@ -151,6 +151,10 @@
line-height: @at-height-list-row-item;
}
+.at-RowItem-status {
+ margin-right: @at-margin-right-list-row-item-status;
+}
+
.at-RowItem--isHeader {
color: @at-color-body-text;
margin-bottom: @at-margin-bottom-list-header;
@@ -263,6 +267,16 @@
margin: 2px 20px 0 0;
}
+.at-RowItem--inline {
+ display: inline-flex;
+ margin-right: @at-margin-right-list-row-item-inline;
+
+ .at-RowItem-label {
+ width: auto;
+ margin-right: @at-margin-right-list-row-item-inline-label;
+ }
+}
+
@media screen and (max-width: @at-breakpoint-compact-list) {
.at-Row-actions {
flex-direction: column;
@@ -271,4 +285,14 @@
.at-RowAction {
margin: @at-margin-list-row-action-mobile;
}
+
+ .at-RowItem--inline {
+ display: flex;
+ margin-right: inherit;
+
+ .at-RowItem-label {
+ width: @at-width-list-row-item-label;
+ margin-right: inherit;
+ }
+ }
}
diff --git a/awx/ui/client/lib/components/list/row-item.directive.js b/awx/ui/client/lib/components/list/row-item.directive.js
index e07820468e..296aa28249 100644
--- a/awx/ui/client/lib/components/list/row-item.directive.js
+++ b/awx/ui/client/lib/components/list/row-item.directive.js
@@ -7,10 +7,13 @@ function atRowItem () {
transclude: true,
templateUrl,
scope: {
+ inline: '@',
badge: '@',
headerValue: '@',
headerLink: '@',
headerTag: '@',
+ status: '@',
+ statusTip: '@',
labelValue: '@',
labelLink: '@',
labelState: '@',
diff --git a/awx/ui/client/lib/components/list/row-item.partial.html b/awx/ui/client/lib/components/list/row-item.partial.html
index ca58947b79..d504f0f928 100644
--- a/awx/ui/client/lib/components/list/row-item.partial.html
+++ b/awx/ui/client/lib/components/list/row-item.partial.html
@@ -1,5 +1,13 @@
-
+
+
@@ -41,4 +49,4 @@
{{ tag.name }}
-
\ No newline at end of file
+
diff --git a/awx/ui/client/lib/models/UnifiedJob.js b/awx/ui/client/lib/models/UnifiedJob.js
new file mode 100644
index 0000000000..13078f8fa2
--- /dev/null
+++ b/awx/ui/client/lib/models/UnifiedJob.js
@@ -0,0 +1,21 @@
+let Base;
+
+function UnifiedJobModel (method, resource, config) {
+ Base.call(this, 'unified_jobs');
+
+ this.Constructor = UnifiedJobModel;
+
+ return this.create(method, resource, config);
+}
+
+function UnifiedJobModelLoader (BaseModel) {
+ Base = BaseModel;
+
+ return UnifiedJobModel;
+}
+
+UnifiedJobModelLoader.$inject = [
+ 'BaseModel'
+];
+
+export default UnifiedJobModelLoader;
diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js
index 3efc8d5299..fb902fb91c 100644
--- a/awx/ui/client/lib/models/index.js
+++ b/awx/ui/client/lib/models/index.js
@@ -23,6 +23,7 @@ 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';
const MODULE_NAME = 'at.lib.models';
@@ -49,6 +50,7 @@ angular
.service('OrganizationModel', Organization)
.service('ProjectModel', Project)
.service('ScheduleModel', Schedule)
+ .service('UnifiedJobModel', UnifiedJob)
.service('UnifiedJobTemplateModel', UnifiedJobTemplate)
.service('WorkflowJobModel', WorkflowJob)
.service('WorkflowJobTemplateModel', WorkflowJobTemplate)
diff --git a/awx/ui/client/lib/theme/_variables.less b/awx/ui/client/lib/theme/_variables.less
index 2d640a0ccd..918f67342c 100644
--- a/awx/ui/client/lib/theme/_variables.less
+++ b/awx/ui/client/lib/theme/_variables.less
@@ -262,6 +262,9 @@
@at-margin-right-list-row-item-tag-icon: 8px;
@at-margin-left-list-row-item-tag-container: -10px;
@at-margin-list-row-action-mobile: 10px;
+@at-margin-right-list-row-item-status: @at-space-2x;
+@at-margin-right-list-row-item-inline: @at-space-4x;
+@at-margin-right-list-row-item-inline-label: @at-space-2x;
@at-height-divider: @at-margin-panel;
@at-height-input: 30px;
diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js
index 492c256e1d..bdabf14436 100644
--- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js
+++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js
@@ -86,4 +86,4 @@ InstanceJobsController.$inject = [
'InstanceModel'
];
-export default InstanceJobsController;
\ No newline at end of file
+export default InstanceJobsController;
diff --git a/awx/ui/client/src/jobs/jobs.route.js b/awx/ui/client/src/jobs/jobs.route.js
deleted file mode 100644
index 27d6631153..0000000000
--- a/awx/ui/client/src/jobs/jobs.route.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*************************************************
- * Copyright (c) 2016 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
- import { N_ } from '../i18n';
- import {templateUrl} from '../shared/template-url/template-url.factory';
-
-export default {
- searchPrefix: 'job',
- name: 'jobs',
- url: '/jobs',
- ncyBreadcrumb: {
- label: N_("JOBS")
- },
- params: {
- job_search: {
- value: {
- not__launch_type: 'sync',
- order_by: '-finished'
- },
- dynamic: true,
- squash: false
- }
- },
- data: {
- socket: {
- "groups": {
- "jobs": ["status_changed"],
- "schedules": ["changed"]
- }
- }
- },
- resolve: {
- Dataset: ['AllJobsList', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => {
- let path = GetBasePath(list.basePath) || GetBasePath(list.name);
- return qs.search(path, $stateParams[`${list.iterator}_search`]);
- }],
- ListDefinition: ['AllJobsList', (list) => {
- return list;
- }]
- },
- views: {
- '@': {
- templateUrl: templateUrl('jobs/jobs')
- },
- 'list@jobs': {
- templateProvider: function(AllJobsList, generateList) {
- let html = generateList.build({
- list: AllJobsList,
- mode: 'edit'
- });
- return html;
- },
- controller: 'JobsList'
- }
- }
-};
diff --git a/awx/ui/client/src/jobs/main.js b/awx/ui/client/src/jobs/main.js
index 7aaf97035c..2bb8f0a7a5 100644
--- a/awx/ui/client/src/jobs/main.js
+++ b/awx/ui/client/src/jobs/main.js
@@ -5,15 +5,11 @@
*************************************************/
import jobsList from './jobs-list.controller';
-import jobsRoute from './jobs.route';
import DeleteJob from './factories/delete-job.factory';
import AllJobsList from './all-jobs.list';
export default
angular.module('JobsModule', [])
- .run(['$stateExtender', function($stateExtender) {
- $stateExtender.addState(jobsRoute);
- }])
.controller('JobsList', jobsList)
.factory('DeleteJob', DeleteJob)
.factory('AllJobsList', AllJobsList);
diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js
index 7671614054..93357de409 100644
--- a/awx/ui/client/src/scheduler/main.js
+++ b/awx/ui/client/src/scheduler/main.js
@@ -349,7 +349,7 @@ export default
}]
},
views: {
- 'list@jobs': {
+ 'schedulesList@jobs': {
templateProvider: function(ScheduleList, generateList){
let html = generateList.build({
list: ScheduleList,
diff --git a/awx/ui/client/src/templates/labels/labelsList.directive.js b/awx/ui/client/src/templates/labels/labelsList.directive.js
index 208c111b80..8402a14dd2 100644
--- a/awx/ui/client/src/templates/labels/labelsList.directive.js
+++ b/awx/ui/client/src/templates/labels/labelsList.directive.js
@@ -95,6 +95,11 @@ export default
if (scope.$parent.$parent.template) {
scope.labels = scope.$parent.$parent.template.summary_fields.labels.results.slice(0, 5);
scope.count = scope.$parent.$parent.template.summary_fields.labels.count;
+ } else if (scope.$parent.$parent.job) {
+ if (_.has(scope, '$parent.$parent.job.summary_fields.labels.results')) {
+ scope.labels = scope.$parent.$parent.job.summary_fields.labels.results.slice(0, 5);
+ scope.count = scope.$parent.$parent.job.summary_fields.labels.count;
+ }
} else {
scope.$watchCollection(scope.$parent.list.iterator, function() {
// To keep the array of labels fresh, we need to set up a watcher - otherwise, the