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 @@ +
+ +
+
+ + JOBS + +
+
+
+ + SCHEDULES + +
+
+
+
+
+
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