implement new style jobs list in ui

This commit is contained in:
John Mitchell 2018-03-22 18:56:22 -04:00
parent e7cfe1e0b6
commit 95f80ce512
No known key found for this signature in database
GPG Key ID: FE6A9B5BD4EB5C94
18 changed files with 412 additions and 69 deletions

View File

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

View File

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

View File

@ -0,0 +1,19 @@
<div class="tab-pane" id="jobs-page">
<at-panel ng-cloak id="htmlTemplate">
<div>
<div ng-hide="$state.is('jobs.schedules')">
<at-panel-heading hide-dismiss="true">
<translate>JOBS</translate>
</at-panel-heading>
<div ui-view="jobsList"></div>
</div>
<div ng-hide="!$state.is('jobs.schedules')">
<at-panel-heading hide-dismiss="true">
<translate>SCHEDULES</translate>
</at-panel-heading>
<div ui-view="schedulesList"></div>
</div>
</div>
</at-panel>
<div ng-include="'/static/partials/logviewer.html'"></div>
</div>

View File

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

View File

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

View File

@ -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 = `<div class="Prompt-bodyQuery">${strings.get('deleteResource.CONFIRM', 'job')}</div>`;
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;

View File

@ -0,0 +1,82 @@
<at-panel-body>
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="jobs"
base-path="unified_jobs"
iterator="job"
list="list"
dataset="job_dataset"
collection="collection"
search-tags="searchTags"
query-set="querySet">
</smart-search>
</div>
<at-list results="jobs">
<!-- TODO: implement resources are missing red indicator as present in mockup -->
<at-row ng-repeat="job in jobs" job-id="{{ job.id }}">
<div class="at-Row-items">
<!-- TODO: include workflow tab as well -->
<at-row-item
status="{{ job.status }}"
status-tip="Job {{job.status}}. Click for details."
header-value="{{ job.name }}"
header-link="{{ vm.getLink(job) }}"
header-tag="{{ vm.jobTypes[job.type] }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_STARTED') }}"
value="{{ job.started | longDate }}"
inline="true">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_FINISHED') }}"
value="{{ job.finished | longDate }}"
inline="true">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_LAUNCHED_BY') }}"
value="{{ job.summary_fields.created_by.username }}"
value-link="/#/users/{{ job.summary_fields.created_by.id }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_JOB_TEMPLATE') }}"
value="{{ job.summary_fields.job_template.name }}"
value-link="/#/templates/job_template/{{ job.summary_fields.job_template.id }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_INVENTORY') }}"
value="{{ job.summary_fields.inventory.name }}"
value-link="/#/inventories/{{ job.summary_fields.inventory.id }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_PROJECT') }}"
value="{{ job.summary_fields.project.name }}"
value-link="/#/projects/{{ job.summary_fields.project.id }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_CREDENTIALS') }}"
tag-values="job.summary_fields.credentials"
tags-are-creds="true">
</at-row-item>
<labels-list class="LabelList" show-delete="false" is-row-item="true">
</labels-list>
</div>
<div class="at-Row-actions">
<at-relaunch
ng-show="job.summary_fields.user_capabilities.start">
</at-relaunch>
<at-row-action icon="fa-trash" ng-click="vm.deleteJob(job)"
ng-show="job.summary_fields.user_capabilities.delete">
</at-row-action>
</div>
</at-row>
</at-list>
<paginate
collection="collection"
dataset="job_dataset"
iterator="job"
base-path="unified_jobs"
query-set="querySet">
</paginate>
</at-panel-body>

View File

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

View File

@ -7,10 +7,13 @@ function atRowItem () {
transclude: true,
templateUrl,
scope: {
inline: '@',
badge: '@',
headerValue: '@',
headerLink: '@',
headerTag: '@',
status: '@',
statusTip: '@',
labelValue: '@',
labelLink: '@',
labelState: '@',

View File

@ -1,5 +1,13 @@
<div class="at-RowItem" ng-class="{'at-RowItem--isHeader': headerValue}"
ng-show="headerValue || value || (smartStatus && smartStatus.summary_fields.recent_jobs.length) || (tagValues && tagValues.length)">
<div class="at-RowItem" ng-class="{'at-RowItem--isHeader': headerValue, 'at-RowItem--inline': inline}"
ng-show="status || headerValue || value || (smartStatus && smartStatus.summary_fields.recent_jobs.length) || (tagValues && tagValues.length)">
<div class="at-RowItem-status" ng-if="status">
<a ng-if="headerLink" ng-href="{{ headerLink }}"
aw-tool-tip="{{ statusTip }}" aw-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>
</a>
<i ng-if="!headerLink" class="fa icon-job-{{ status }}"></i>
</div>
<div class="at-RowItem-header" ng-if="headerValue && headerLink">
<a ng-href="{{ headerLink }}">{{ headerValue }}</a>
</div>
@ -41,4 +49,4 @@
{{ tag.name }}
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

@ -86,4 +86,4 @@ InstanceJobsController.$inject = [
'InstanceModel'
];
export default InstanceJobsController;
export default InstanceJobsController;

View File

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

View File

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

View File

@ -349,7 +349,7 @@ export default
}]
},
views: {
'list@jobs': {
'schedulesList@jobs': {
templateProvider: function(ScheduleList, generateList){
let html = generateList.build({
list: ScheduleList,

View File

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