1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

Merge pull request #6784 from marshmalien/rampartsViews

Implementation of Instance Groups read-only views in Tower Settings
This commit is contained in:
Marliana Lara 2017-06-30 14:43:19 -04:00 committed by GitHub
commit 193f3fa10e
37 changed files with 1005 additions and 67 deletions

View File

@ -60,7 +60,7 @@ table, tbody {
height: 40px;
font-size: 14px;
color: @list-item;
border-bottom: 1px solid @default-white-button-bord;
border-bottom: 1px solid @default-border;
}
.List-tableRow:last-of-type {
@ -176,6 +176,27 @@ table, tbody {
text-transform: uppercase;
}
.List-exitHolder {
justify-content: flex-end;
display:flex;
}
.List-exit {
cursor:pointer;
padding:0px;
border: none;
height:20px;
font-size: 20px;
background-color:@default-bg;
color:@d7grey;
transition: color 0.2s;
line-height:1;
}
.List-exit:hover{
color:@default-icon;
}
.List-actionHolder {
justify-content: flex-end;
display: flex;

View File

@ -34,6 +34,13 @@
.BreadCrumb-menuLink:hover {
color: @bc-link-icon-focus;
}
.BreadCrumb-menuLink {
.BreadCrumb-menuLinkImage.fa-refresh {
&:hover {
color: @default-link;
}
}
}
.BreadCrumb-menuLinkImage {
font-size: 18px;
color: @bc-link-icon;

View File

@ -12,6 +12,7 @@ export default
scope.showActivityStreamButton = false;
scope.showRefreshButton = false;
scope.alwaysShowRefreshButton = false;
scope.loadingLicense = true;
scope.$on("$stateChangeSuccess", function updateActivityStreamButton(event, toState, toParams, fromState, fromParams) {
@ -48,6 +49,7 @@ export default
}
scope.showRefreshButton = (streamConfig && streamConfig.refreshButton) ? true : false;
scope.alwaysShowRefreshButton = (streamConfig && streamConfig.alwaysShowRefreshButton) ? true: false;
});
// scope.$on('featuresLoaded', function(){

View File

@ -8,7 +8,7 @@
data-trigger="hover"
data-container="body"
ng-hide= "loadingLicense || licenseMissing"
ng-if="socketStatus === 'error' && showRefreshButton"
ng-if="(socketStatus === 'error' && showRefreshButton) || alwaysShowRefreshButton"
ng-click="refresh()">
<i class="BreadCrumb-menuLinkImage fa fa-refresh"
alt="Refresh the page">

View File

@ -0,0 +1,28 @@
@import "../../shared/branding/colors.default.less";
capacity-bar {
width: 50%;
margin-right: 10px;
min-width: 100px;
.CapacityBar {
background-color: @default-bg;
display: flex;
flex: 0 0 auto;
height: 10px;
border: 1px solid @default-link;
width: 100%;
border-radius: 100vw;
overflow: hidden;
}
.CapacityBar-remaining {
background-color: @default-link;
flex: 0 0 auto;
}
.CapacityBar-consumed {
flex: 0 0 auto;
}
}

View File

@ -0,0 +1,18 @@
export default ['templateUrl',
function (templateUrl) {
return {
scope: {
capacity: '='
},
templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'),
restrict: 'E',
link: function(scope) {
scope.$watch('capacity', function() {
scope.PercentRemainingStyle = {
'flex-grow': scope.capacity * 0.01
};
}, true);
}
};
}
];

View File

@ -0,0 +1,4 @@
<div class="CapacityBar">
<div class="CapacityBar-remaining" ng-style="PercentRemainingStyle"></div>
<div class="CapacityBar-consumed"></div>
</div>

View File

@ -0,0 +1,5 @@
import capacityBar from './capacity-bar.directive';
export default
angular.module('capacityBarDirective', [])
.directive('capacityBar', capacityBar);

View File

@ -0,0 +1,56 @@
@import "../shared/branding/colors.default.less";
.InstanceGroups {
.BreadCrumb-menuLinkImage:hover {
color: @default-link;
}
.List-details {
align-self: flex-end;
color: @default-interface-txt;
display: flex;
flex: 0 0 auto;
font-size: 12px;
margin-right:20px;
text-transform: uppercase;
}
.Capacity-details {
display: flex;
margin-right: 20px;
align-items: center;
.Capacity-details--label {
color: @default-interface-txt;
margin: 0 10px 0 0;
}
.Capacity-details--percentage {
color: @default-data-txt;
}
}
.RunningJobs-details {
align-items: center;
display: flex;
.RunningJobs-details--label {
margin: 0 10px 0 0;
}
}
.List-tableCell--capacityRemainingColumn {
display: flex;
height: 40px;
align-items: center;
}
.List-noItems {
margin-top: 20px;
}
.List-tableRow .List-titleBadge {
margin: 0 0 0 5px;
}
}

View File

@ -0,0 +1,34 @@
<div class="Panel">
<div class="row Form-tabRow">
<div class="col-xs-12">
<div class="List-header">
<div class="List-title">
<div class="List-titleText">{{ instanceGroupName }}</div>
</div>
<div class="List-details">
<div class="Capacity-details">
<p class="Capacity-details--label" translate>Capacity</p>
<capacity-bar capacity="instanceGroupCapacity"></capacity-bar>
<span class="Capacity-details--percentage">{{ instanceGroupCapacity }}%</span>
</div>
<div class="RunningJobs-details">
<p class="RunningJobs-details--label" translate>Running Jobs</p>
<span class="badge List-titleBadge">
{{ instanceGroupJobsRunning }}
</span>
</div>
</div>
<div class="List-exitHolder">
<button class="List-exit" ng-click="$state.go('instanceGroups')">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
<div class="Form-tabHolder">
<div class="Form-tab Form-tab--notitle" ng-click="$state.go('instanceGroups.instances.list', {instance_group_id: $stateParams.instance_group_id})" ng-class="{'is-selected': $state.includes('instanceGroups.instances.list')}" translate>INSTANCES</div>
<div class="Form-tab Form-tab--notitle" ng-click="$state.go('instanceGroups.instances.jobs', {instance_group_id: $stateParams.instance_group_id})" ng-class="{'is-selected': $state.includes('instanceGroups.instances.jobs')}" translate>JOBS</div>
</div>
</div>
</div>
<div ui-view="list"></div>
</div>

View File

@ -1,4 +0,0 @@
#InstanceGroups {
display: flex;
padding: 0 12px;
}

View File

@ -14,8 +14,10 @@ export default ['i18n', function(i18n) {
label: i18n._('Name'),
columnClass: 'col-md-3 col-sm-9 col-xs-9',
modalColumnClass: 'col-md-8',
uiSref: 'instanceGroups.instances.list({instance_group_id: instance_group.id})',
ngClass: "{'isActive' : isActive()}"
},
capacity: {
percent_capacity_remaining: {
label: i18n._('Capacity'),
nosort: true,
},

View File

@ -0,0 +1,11 @@
<div class="tab-pane InstanceGroups" id="instance-groups-panel">
<aw-limit-panels max-panels="2" panel-container="instance-groups-panel"></aw-limit-panels>
<div ui-view="instanceJobs"></div>
<div ui-view="instances"></div>
<div ng-cloak id="htmlTemplate" class="Panel">
<div ui-view="list"></div>
</div>
</div>

View File

@ -0,0 +1,41 @@
import {templateUrl} from '../shared/template-url/template-url.factory';
import { N_ } from '../i18n';
export default {
name: 'instanceGroups',
url: '/instance_groups',
searchPrefix: 'instance_group',
ncyBreadcrumb: {
parent: 'setup',
label: N_('INSTANCE GROUPS')
},
params: {
instance_group_search: {
value: {
page_size: '10',
order_by: 'name'
}
}
},
data: {
alwaysShowRefreshButton: true,
},
views: {
'@': {
templateUrl: templateUrl('./instance-groups/instance-groups'),
},
'list@instanceGroups': {
templateUrl: templateUrl('./instance-groups/list/instance-groups-list'),
controller: 'InstanceGroupsList'
}
},
resolve: {
Dataset: ['InstanceGroupList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
return qs.search(path, $stateParams[`${list.iterator}_search`]);
}
]
}
};

View File

@ -0,0 +1,40 @@
import { N_ } from '../../../i18n';
export default {
name: 'instanceGroups.instances.list.job.list',
url: '/jobs',
searchPrefix: 'instance_job',
ncyBreadcrumb: {
parent: 'instanceGroups.instances.list',
label: N_('{{ breadcrumb.instance_name }}')
},
params: {
instance_job_search: {
value: {
page_size: '10',
order_by: '-finished',
not__launch_type: 'sync'
}
}
},
views: {
'list@instanceGroups.instances.list.job': {
templateProvider: function(InstanceJobsList, generateList) {
let html = generateList.build({
list: InstanceJobsList
});
return html;
},
controller: 'InstanceJobsController'
}
},
resolve: {
Dataset: ['InstanceJobsList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = `${GetBasePath('instances')}${$stateParams.instance_id}/jobs`;
return qs.search(path, $stateParams[`${list.iterator}_search`]);
}
],
}
};

View File

@ -0,0 +1,82 @@
export default ['$scope','InstanceJobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, InstanceJobsList, GetBasePath, Rest, Dataset, Find, $state, $q) {
let list = InstanceJobsList;
init();
function init(){
$scope.optionsDefer = $q.defer();
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
$scope.$on(`${list.iterator}_options`, function(event, data){
$scope.options = data.data.actions.GET;
optionsRequestDataProcessing();
});
// iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched
function optionsRequestDataProcessing(){
if($scope[list.name] && $scope[list.name].length > 0) {
$scope[list.name].forEach(function(item, item_idx) {
var itm = $scope[list.name][item_idx];
if(item.summary_fields && item.summary_fields.source_workflow_job &&
item.summary_fields.source_workflow_job.id){
item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`;
}
// Set the item type label
if (list.fields.type && $scope.options &&
$scope.options.hasOwnProperty('type')) {
$scope.options.type.choices.forEach(function(choice) {
if (choice[0] === item.type) {
itm.type_label = choice[1];
}
});
}
buildTooltips(itm);
});
}
}
function buildTooltips(job) {
job.status_tip = 'Job ' + job.status + ". Click for details.";
}
$scope.viewjobResults = function(job) {
var goTojobResults = function(state) {
$state.go(state, { id: job.id }, { reload: true });
};
switch (job.type) {
case 'job':
goTojobResults('jobResult');
break;
case 'ad_hoc_command':
goTojobResults('adHocJobStdout');
break;
case 'system_job':
goTojobResults('managementJobStdout');
break;
case 'project_update':
goTojobResults('scmUpdateStdout');
break;
case 'inventory_update':
goTojobResults('inventorySyncStdout');
break;
case 'workflow_job':
goTojobResults('workflowResults');
break;
}
};
$scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing();
}
);
}
];

View File

@ -0,0 +1,78 @@
export default ['i18n', function(i18n) {
return {
name: 'instance_jobs',
iterator: 'instance_job',
index: false,
hover: false,
well: false,
emptyListText: i18n._('No jobs have yet run.'),
title: false,
basePath: 'api/v2/instances/{{$stateParams.instance_id}}/jobs',
fields: {
status: {
label: '',
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus',
dataTipWatch: 'instance_job.status_tip',
awToolTip: "{{ instance_job.status_tip }}",
awTipPlacement: "right",
dataTitle: "{{ instance_job.status_popover_title }}",
icon: 'icon-job-{{ instance_job.status }}',
iconOnly: true,
ngClick:"viewjobResults(instance_job)",
nosort: true
},
id: {
label: i18n._('ID'),
ngClick:"viewjobResults(instance_job)",
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent',
awToolTip: "{{ instance_job.status_tip }}",
dataPlacement: 'top',
noLink: true
},
name: {
label: i18n._('Name'),
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6',
ngClick: "viewjobResults(instance_job)",
nosort: true,
badgePlacement: 'right',
badgeCustom: true,
badgeIcon: `<a href="{{ instance_job.workflow_result_link }}"
aw-tool-tip="{{'View workflow results'|translate}}"
data-placement="top"
data-original-title="" title="">
<i class="WorkflowBadge"
ng-show="instance_job.launch_type === 'workflow' ">
W
</i>
</a>`
},
type: {
label: i18n._('Type'),
ngBind: 'instance_job.type_label',
link: false,
columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs",
nosort: true
},
finished: {
label: i18n._('Finished'),
noLink: true,
filter: "longDate",
columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs",
key: true,
desc: true,
nosort: true
},
labels: {
label: i18n._('Labels'),
type: 'labels',
nosort: true,
showDelete: false,
columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs',
sourceModel: 'labels',
sourceField: 'name',
},
}
};
}];

View File

@ -0,0 +1,33 @@
<div class="Panel">
<div class="row Form-tabRow">
<div class="col-xs-12">
<div class="List-header">
<div class="List-title">
<div class="List-titleText">{{ instanceName }}</div>
</div>
<div class="List-details">
<div class="Capacity-details">
<p class="Capacity-details--label" translate>Capacity</p>
<capacity-bar capacity="instanceCapacity"></capacity-bar>
<span class="Capacity-details--percentage">{{ instanceCapacity }}%</span>
</div>
<div class="RunningJobs-details">
<p class="RunningJobs-details--label" translate>Running Jobs</p>
<span class="badge List-titleBadge">
{{ instanceJobsRunning }}
</span>
</div>
</div>
<div class="List-exitHolder">
<button class="List-exit" ng-click="$state.go('instanceGroups.instances.list')">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
<div class="Form-tabHolder">
<div class="Form-tab Form-tab--notitle is-selected" translate>JOBS</div>
</div>
</div>
</div>
<div class="instance-jobs-list" ui-view="list"></div>
</div>

View File

@ -0,0 +1,37 @@
import { templateUrl } from '../../../shared/template-url/template-url.factory';
export default {
name: 'instanceGroups.instances.list.job',
url: '/:instance_id',
abstract: true,
ncyBreadcrumb: {
skip: true
},
views: {
'instanceJobs@instanceGroups': {
templateUrl: templateUrl('./instance-groups/instances/instance-jobs/instance-jobs'),
controller: function($scope, $rootScope, instance) {
$scope.instanceName = instance.hostname;
$scope.instanceCapacity = instance.percent_capacity_remaining;
$scope.instanceJobsRunning = instance.jobs_running;
$rootScope.breadcrumb.instance_name = instance.hostname;
}
}
},
resolve: {
instance: ['GetBasePath', 'Rest', 'ProcessErrors', '$stateParams', function(GetBasePath, Rest, ProcessErrors, $stateParams) {
let url = GetBasePath('instances') + $stateParams.instance_id;
Rest.setUrl(url);
return Rest.get()
.then(({data}) => {
return data;
})
.catch(({data, status}) => {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get instance groups info. GET returned status: ' + status
});
});
}]
}
};

View File

@ -0,0 +1,43 @@
<div class="instances-list">
<smart-search django-model="instances" base-path="instances" iterator="instance" dataset="instance_dataset"
list="list" collection="instances" search-tags="searchTags">
</smart-search>
<div class="List-noItems ng-hide" ng-show="instances.length === 0 &amp;&amp; (searchTags | isEmpty)" translate>PLEASE ADD ITEMS TO THIS LIST</div>
<div class="list-table-container" ng-show="instances.length > 0">
<table id="instances_table" class="List-table" is-extended="false">
<thead>
<tr class="List-tableHeaderRow">
<th id="instance-hostname-header" class="List-tableHeader list-header col-md-5 col-sm-5 col-xs-5" ng-click="columnNoSort !== 'true' &amp;&amp; toggleColumnOrderBy()"
ng-class="{'list-header-noSort' : columnNoSort === 'true'}" base-path="instances" collection="instances"
dataset="instance_dataset" column-sort="" column-field="hostname" column-iterator="instance" column-no-sort="undefined"
column-label="Name" column-custom-class="col-md-5 col-sm-5 col-xs-5" query-set="instance_queryset">
"{{'Name' | translate}}"
<i ng-hide="columnNoSort === 'true'" class="fa columnSortIcon fa-sort-up" ng-class="orderByIcon()"></i>
</th>
<th id="instance-percent_capacity_remaining-header" class="List-tableHeader list-header list-header-noSort" translate>
Capacity
</th>
<th id="instance-jobs_running-header" class="List-tableHeader list-header list-header-noSort" translate>
Running Jobs
</th>
</tr>
</thead>
<tbody>
<!-- ngRepeat: instance in instances -->
<tr ng-class="{isActive: isActive(instance.id)}" id="instance.id" class="List-tableRow instance_class ng-scope" ng-repeat="instance in instances">
<td class="List-tableCell hostname-column col-md-5 col-sm-5 col-xs-5">
<a ui-sref="instanceGroups.instances.list.job.list({instance_id: instance.id})" class="ng-binding">{{ instance.hostname }}</a></td>
<td class="List-tableCell List-tableCell--capacityRemainingColumn ng-binding">
<capacity-bar capacity="instance.percent_capacity_remaining"></capacity-bar><span>{{ instance.percent_capacity_remaining }}%</span>
</td>
<td class="List-tableCell jobs_running-column ng-binding">
<a ui-sref="instanceGroups.instances.jobs({instance_group_id: $stateParams.instance_group_id})">
{{ instance.jobs_running }}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,34 @@
import {templateUrl} from '../../shared/template-url/template-url.factory';
import { N_ } from '../../i18n';
export default {
name: 'instanceGroups.instances.list',
url: '/instances',
searchPrefix: 'instance',
ncyBreadcrumb: {
parent: 'instanceGroups',
label: N_('{{breadcrumb.instance_group_name}}')
},
params: {
instance_search: {
value: {
page_size: '10',
order_by: 'hostname'
}
}
},
views: {
'list@instanceGroups.instances': {
templateUrl: templateUrl('./instance-groups/instances/instances-list'),
controller: 'InstanceListController'
}
},
resolve: {
Dataset: ['InstanceList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/instances`;
return qs.search(path, $stateParams[`${list.iterator}_search`]);
}
]
}
};

View File

@ -0,0 +1,20 @@
export default ['$scope', 'InstanceList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, InstanceList, GetBasePath, Rest, Dataset, Find, $state, $q) {
let list = InstanceList;
init();
function init(){
$scope.optionsDefer = $q.defer();
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
$scope.isActive = function(id) {
let selected = parseInt($state.params.instance_id);
return id === selected;
};
}
];

View File

@ -0,0 +1,29 @@
export default ['i18n', function(i18n) {
return {
name: 'instances' ,
iterator: 'instance',
listTitle: false,
index: false,
hover: false,
tabs: true,
well: true,
fields: {
hostname: {
key: true,
label: i18n._('Name'),
columnClass: 'col-md-3 col-sm-9 col-xs-9',
modalColumnClass: 'col-md-8',
uiSref: 'instanceGroups.instances.list.job({instance_id: instance.id})'
},
percent_capacity_remaining: {
label: i18n._('Capacity'),
nosort: true,
},
jobs_running: {
label: i18n._('Running Jobs'),
nosort: true,
},
}
};
}];

View File

@ -0,0 +1,34 @@
import {templateUrl} from '../../shared/template-url/template-url.factory';
export default {
name: 'instanceGroups.instances',
url: '/:instance_group_id',
abstract: true,
views: {
'instances@instanceGroups': {
templateUrl: templateUrl('./instance-groups/instance-group'),
controller: function($scope, $rootScope, instanceGroup) {
$scope.instanceGroupName = instanceGroup.name;
$scope.instanceGroupCapacity = instanceGroup.percent_capacity_remaining;
$scope.instanceGroupJobsRunning = instanceGroup.jobs_running;
$rootScope.breadcrumb.instance_group_name = instanceGroup.name;
}
}
},
resolve: {
instanceGroup: ['GetBasePath', 'Rest', 'ProcessErrors', '$stateParams', function(GetBasePath, Rest, ProcessErrors, $stateParams) {
let url = GetBasePath('instance_groups') + $stateParams.instance_group_id;
Rest.setUrl(url);
return Rest.get()
.then(({data}) => {
return data;
})
.catch(({data, status}) => {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get instance groups info. GET returned status: ' + status
});
});
}]
}
};

View File

@ -0,0 +1,40 @@
import { N_ } from '../../i18n';
export default {
name: 'instanceGroups.instances.jobs',
url: '/jobs',
searchPrefix: 'job',
ncyBreadcrumb: {
parent: 'instanceGroups.instances.list',
label: N_('JOBS')
},
params: {
job_search: {
value: {
page_size: '10',
order_by: '-finished',
not__launch_type: 'sync'
}
},
instance_group_id: null
},
views: {
'list@instanceGroups.instances': {
templateProvider: function(JobsList, generateList) {
let html = generateList.build({
list: JobsList
});
return html;
},
controller: 'JobsListController'
}
},
resolve: {
Dataset: ['JobsList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/jobs`;
return qs.search(path, $stateParams[`${list.iterator}_search`]);
}
]
}
};

View File

@ -0,0 +1,82 @@
export default ['$scope','JobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, JobsList, GetBasePath, Rest, Dataset, Find, $state, $q) {
let list = JobsList;
init();
function init(){
$scope.optionsDefer = $q.defer();
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
$scope.$on(`${list.iterator}_options`, function(event, data){
$scope.options = data.data.actions.GET;
optionsRequestDataProcessing();
});
// iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched
function optionsRequestDataProcessing(){
if($scope[list.name] && $scope[list.name].length > 0) {
$scope[list.name].forEach(function(item, item_idx) {
var itm = $scope[list.name][item_idx];
if(item.summary_fields && item.summary_fields.source_workflow_job &&
item.summary_fields.source_workflow_job.id){
item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`;
}
// Set the item type label
if (list.fields.type && $scope.options &&
$scope.options.hasOwnProperty('type')) {
$scope.options.type.choices.forEach(function(choice) {
if (choice[0] === item.type) {
itm.type_label = choice[1];
}
});
}
buildTooltips(itm);
});
}
}
function buildTooltips(job) {
job.status_tip = 'Job ' + job.status + ". Click for details.";
}
$scope.viewjobResults = function(job) {
var goTojobResults = function(state) {
$state.go(state, { id: job.id }, { reload: true });
};
switch (job.type) {
case 'job':
goTojobResults('jobResult');
break;
case 'ad_hoc_command':
goTojobResults('adHocJobStdout');
break;
case 'system_job':
goTojobResults('managementJobStdout');
break;
case 'project_update':
goTojobResults('scmUpdateStdout');
break;
case 'inventory_update':
goTojobResults('inventorySyncStdout');
break;
case 'workflow_job':
goTojobResults('workflowResults');
break;
}
};
$scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing();
}
);
}
];

View File

@ -0,0 +1,76 @@
export default ['i18n', function (i18n) {
return {
name: 'jobs',
iterator: 'job',
basePath: 'api/v2/instance_groups/{{$stateParams.instance_group_id}}/jobs/',
index: false,
hover: false,
well: true,
emptyListText: i18n._('No jobs have yet run.'),
listTitle: false,
fields: {
status: {
label: '',
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus',
dataTipWatch: 'job.status_tip',
awToolTip: "{{ job.status_tip }}",
awTipPlacement: "right",
dataTitle: "{{ job.status_popover_title }}",
icon: 'icon-job-{{ job.status }}',
iconOnly: true,
ngClick: "viewjobResults(job)",
nosort: true
},
id: {
label: i18n._('ID'),
ngClick: "viewjobResults(job)",
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent',
awToolTip: "{{ job.status_tip }}",
dataPlacement: 'top',
noLink: true
},
name: {
label: i18n._('Name'),
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6',
ngClick: "viewjobResults(job)",
badgePlacement: 'right',
badgeCustom: true,
nosort: true,
badgeIcon: `<a href="{{ job.workflow_result_link }}"
aw-tool-tip="{{'View workflow results'|translate}}"
data-placement="top"
data-original-title="" title="">
<i class="WorkflowBadge"
ng-show="job.launch_type === 'workflow' ">
W
</i>
</a>`
},
type: {
label: i18n._('Type'),
ngBind: 'job.type_label',
columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs",
nosort: true
},
finished: {
label: i18n._('Finished'),
noLink: true,
filter: "longDate",
columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs",
key: true,
desc: true,
nosort: true
},
labels: {
label: i18n._('Labels'),
type: 'labels',
nosort: true,
showDelete: false,
columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs',
sourceModel: 'labels',
sourceField: 'name'
},
}
};
}];

View File

@ -1,42 +1,19 @@
export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state, $q) {
export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state',
function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state) {
let list = InstanceGroupList;
init();
function init(){
$scope.optionsDefer = $q.defer();
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
$scope.instanceGroupCount = Dataset.data.count;
}
// iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched
function optionsRequestDataProcessing(){
$scope.optionsDefer.promise.then(function(options) {
if($scope.list.name === 'instance_groups'){
if ($scope[list.name] !== undefined) {
$scope[list.name].forEach(function(item, item_idx) {
var itm = $scope[list.name][item_idx];
// Set the item type label
if (list.fields.kind && options && options.actions && options.actions.GET && options.actions.GET.kind) {
options.actions.GET.kind.choices.forEach(function(choice) {
if (choice[0] === item.kind) {
itm.kind_label = choice[1];
}
});
}
});
}
}
});
}
$scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing();
}
);
$scope.isActive = function(id) {
let selected = parseInt($state.params.instance_group_id);
return id === selected;
};
}
];

View File

@ -0,0 +1,63 @@
<div class="List-header">
<div class="List-title">
<div class="List-titleText" translate>
INSTANCE GROUPS
</div>
<span class="badge List-titleBadge">
{{ instanceGroupCount }}
</span>
</div>
</div>
<smart-search django-model="instance_groups" base-path="instance_groups" iterator="instance_group" list="list" dataset="instance_group_dataset"
collection="instance_groups" search-tags="searchTags">
</smart-search>
<div class="List-noItems" ng-show="instance_groups.length < 1" translate>PLEASE ADD ITEMS TO THIS LIST</div>
<div class="list-table-container" ng-show="instance_groups.length > 0">
<table id="instance_groups_table" class="List-table" is-extended="false">
<thead>
<tr class="List-tableHeaderRow">
<th id="instance_group-name-header" class="List-tableHeader list-header col-md-5 col-sm-5 col-xs-5" ng-click="columnNoSort !== 'true' &amp;&amp; toggleColumnOrderBy()"
ng-class="{'list-header-noSort' : columnNoSort === 'true'}" base-path="instance_groups" collection="instance_groups"
dataset="instance_group_dataset" column-sort="" column-field="name" column-iterator="instance_group" column-no-sort="undefined"
column-label="Name" column-custom-class="col-md-5 col-sm-5 col-xs-5" query-set="instance_group_queryset">
"{{'Name' | translate}}"
<i ng-hide="columnNoSort === 'true'" class="fa columnSortIcon fa-sort-up" ng-class="orderByIcon()"></i>
</th>
<th id="instance_group-percent_capacity_remaining-header" class="List-tableHeader list-header list-header-noSort" translate>
Capacity
</th>
<th id="instance_group-jobs_running-header" class="List-tableHeader list-header list-header-noSort" translate>
Running Jobs
</th>
</tr>
</thead>
<tbody>
<!-- ngRepeat: instance_group in instance_groups -->
<tr ng-class="{isActive: isActive(instance_group.id)}" id="instance_group.id" class="List-tableRow instance_group_class ng-scope" ng-repeat="instance_group in instance_groups">
<td class="List-tableCell name-column col-md-5 col-sm-5 col-xs-5">
<a ui-sref="instanceGroups.instances.list({instance_group_id: instance_group.id})" class="ng-binding" >{{ instance_group.name }}</a>
<span class="badge List-titleBadge">{{ instance_group.instances }}</span>
</td>
<td class="List-tableCell List-tableCell--capacityRemainingColumn ng-binding">
<capacity-bar capacity="instance_group.percent_capacity_remaining"></capacity-bar><span>{{ instance_group.percent_capacity_remaining }}%</span>
</td>
<td class="List-tableCell jobs_running-column ng-binding">
<a ui-sref="instanceGroups.instances.jobs({instance_group_id: instance_group.id})">
{{ instance_group.jobs_running }}
</a>
</td>
</tr>
</tbody>
</table>
</div>
<paginate
base-path="instance_groups"
iterator="instance_group"
dataset="instance_group_dataset"
collection="instance_groups"
query-set="instance_group_queryset">
</paginate>

View File

@ -1,35 +1,58 @@
import InstanceGroupsList from './list/instance-groups-list.controller';
import instanceGroupsMultiselect from './instance-groups-multiselect/instance-groups.directive';
import instanceGroupsModal from './instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive';
import instanceGroupsMultiselect from '../shared/instance-groups-multiselect/instance-groups.directive';
import instanceGroupsModal from '../shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive';
import instanceGroupsRoute from './instance-groups.route';
import instancesListRoute from './instances/instances-list.route';
import JobsList from './jobs/jobs.list';
import jobsListRoute from './jobs/jobs-list.route';
import JobsListController from './jobs/jobs.controller';
import InstanceList from './instances/instances.list';
import instancesRoute from './instances/instances.route';
import InstanceListController from './instances/instances.controller';
import InstanceJobsList from './instances/instance-jobs/instance-jobs.list';
import instanceJobsRoute from './instances/instance-jobs/instance-jobs.route';
import instanceJobsListRoute from './instances/instance-jobs/instance-jobs-list.route';
import InstanceJobsController from './instances/instance-jobs/instance-jobs.controller';
import CapacityBar from './capacity-bar/main';
import list from './instance-groups.list';
import service from './instance-groups.service';
import { N_ } from '../i18n';
export default
angular.module('instanceGroups', [])
angular.module('instanceGroups', [CapacityBar.name])
.service('InstanceGroupsService', service)
.factory('InstanceGroupList', list)
.factory('JobsList', JobsList)
.factory('InstanceList', InstanceList)
.factory('InstanceJobsList', InstanceJobsList)
.controller('InstanceGroupsList', InstanceGroupsList)
.controller('JobsListController', JobsListController)
.controller('InstanceListController', InstanceListController)
.controller('InstanceJobsController', InstanceJobsController)
.directive('instanceGroupsMultiselect', instanceGroupsMultiselect)
.directive('instanceGroupsModal', instanceGroupsModal)
.config(['$stateProvider', 'stateDefinitionsProvider',
function($stateProvider, stateDefinitionsProvider) {
let stateDefinitions = stateDefinitionsProvider.$get();
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
let stateExtender = $stateExtenderProvider.$get();
function generateInstanceGroupsStates() {
return new Promise((resolve) => {
resolve({
states: [
stateExtender.buildDefinition(instanceGroupsRoute),
stateExtender.buildDefinition(instancesRoute),
stateExtender.buildDefinition(instancesListRoute),
stateExtender.buildDefinition(jobsListRoute),
stateExtender.buildDefinition(instanceJobsRoute),
stateExtender.buildDefinition(instanceJobsListRoute)
]
});
});
}
$stateProvider.state({
name: 'instanceGroups',
url: '/instance_groups',
lazyLoad: () => stateDefinitions.generateTree({
parent: 'instanceGroups',
list: 'InstanceGroupList',
controllers: {
list: 'InstanceGroupsList'
},
ncyBreadcrumb: {
parent: 'setup',
label: N_('INSTANCE GROUPS')
}
})
lazyLoad: () => generateInstanceGroupsStates()
});
}
]);
}]);

View File

@ -382,6 +382,8 @@ angular.module('GeneratorHelpers', [systemStatus.name])
html += "<a href=\"\" " + Attr(field, 'ngClick') + " ";
} else if (field.ngHref) {
html += "<a ng-href=\"" + field.ngHref + "\" ";
} else if (field.uiSref) {
html += "<a ui-sref=\"" + field.uiSref + "\" ";
} else if (field.link || (field.key && (field.link === undefined || field.link))) {
html += "<a href=\"#/" + base + "/{{" + list.iterator + ".id }}\" ";
} else {

View File

@ -4,7 +4,7 @@ export default ['templateUrl', function(templateUrl) {
scope: {
instanceGroups: '='
},
templateUrl: templateUrl('instance-groups/instance-groups-multiselect/instance-groups-modal/instance-groups-modal'),
templateUrl: templateUrl('shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal'),
link: function(scope, element) {
@ -47,7 +47,7 @@ export default ['templateUrl', function(templateUrl) {
instanceGroupList.well = false;
instanceGroupList.multiSelect = true;
instanceGroupList.multiSelectExtended = true;
delete instanceGroupList.fields.capacity;
delete instanceGroupList.fields.percent_capacity_remaining;
delete instanceGroupList.fields.jobs_running;
let html = `${GenerateList.build({

View File

@ -2,8 +2,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header Form-header">
<div class="Form-title Form-title--uppercase">INSTANCE GROUPS</div>
<!-- optional: transclude header fields -->
<div class="Form-title Form-title--uppercase" translate>Select Instance Groups</div>
<div class="Form-header--fields"></div>
<div class="Form-exitHolder">
<button type="button" class="Form-exit" ng-click="cancelForm()">
@ -15,10 +14,9 @@
<div id="instance-groups-modal-body"> {{ instance_group }} </div>
</div>
<div class="modal-footer">
<button type="button" ng-click="cancelForm()" class="Lookup-cancel btn btn-default">CANCEL</button>
<button type="button" ng-click="saveForm()" ng-disabled="!instance_groups || instance_groups.length === 0" class="Lookup-save btn btn-primary">SAVE</button>
<button type="button" ng-click="cancelForm()" class="Lookup-cancel btn btn-default" translate>CANCEL</button>
<button type="button" ng-click="saveForm()" ng-disabled="!instance_groups || instance_groups.length === 0" class="Lookup-save btn btn-primary" translate>SAVE</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
@import "../../shared/branding/colors.default.less";
#InstanceGroups {
display: flex;
padding: 0 12px;
}
#instance-groups-panel {
table {
overflow: hidden;
}
.List-header {
margin-bottom: 20px;
}
.isActive {
border-left: 10px solid @list-row-select-bord;
}
.instances-list,
.instance-jobs-list {
margin-top: 20px;
}
}

View File

@ -6,7 +6,7 @@ export default ['templateUrl', '$compile',
instanceGroups: '='
},
restrict: 'E',
templateUrl: templateUrl('instance-groups/instance-groups-multiselect/instance-groups'),
templateUrl: templateUrl('shared/instance-groups-multiselect/instance-groups'),
controller: instanceGroupsMultiselectController,
link: function(scope) {
scope.openInstanceGroupsModal = function() {

View File

@ -1,6 +1,6 @@
<div class="input-group Form-mixedInputGroup">
<span class="input-group-btn Form-variableHeightButtonGroup">
<button type="button" class="Form-lookupButton Form-lookupButton--variableHeight btn btn-default" ng-click="openInstanceGroupsModal()">
<span class="input-group-btn">
<button type="button" class="Form-lookupButton btn btn-default" ng-click="openInstanceGroupsModal()">
<i class="fa fa-search"></i>
</button>
</span>