Add Instance and InstanceGroup models

This commit is contained in:
Marliana Lara 2018-01-10 12:59:53 -05:00 committed by Matthew Jones
parent 70bf78e29f
commit 368101812c
No known key found for this signature in database
GPG Key ID: 76A4C17A97590C1C
54 changed files with 1759 additions and 852 deletions

View File

@ -71,6 +71,7 @@ function ComponentsStrings (BaseString) {
INVENTORY_SCRIPTS: t.s('Inventory Scripts'),
NOTIFICATIONS: t.s('Notifications'),
MANAGEMENT_JOBS: t.s('Management Jobs'),
INSTANCES: t.s('Instances'),
INSTANCE_GROUPS: t.s('Instance Groups'),
SETTINGS: t.s('Settings'),
FOOTER_ABOUT: t.s('About'),
@ -78,7 +79,8 @@ function ComponentsStrings (BaseString) {
};
ns.capacityBar = {
IS_OFFLINE: t.s('Unavailable to run jobs.')
IS_OFFLINE: t.s('Unavailable to run jobs.'),
IS_OFFLINE_LABEL: t.s('Unavailable')
};
ns.relaunch = {

View File

@ -12,6 +12,7 @@ import inputLookup from '~components/input/lookup.directive';
import inputMessage from '~components/input/message.directive';
import inputSecret from '~components/input/secret.directive';
import inputSelect from '~components/input/select.directive';
import inputSlider from '~components/input/slider.directive';
import inputText from '~components/input/text.directive';
import inputTextarea from '~components/input/textarea.directive';
import inputTextareaSecret from '~components/input/textarea-secret.directive';
@ -54,6 +55,7 @@ angular
.directive('atInputMessage', inputMessage)
.directive('atInputSecret', inputSecret)
.directive('atInputSelect', inputSelect)
.directive('atInputSlider', inputSlider)
.directive('atInputText', inputText)
.directive('atInputTextarea', inputTextarea)
.directive('atInputTextareaSecret', inputTextareaSecret)

View File

@ -163,7 +163,7 @@
}
.at-InputMessage--rejected {
font-size: @at-font-size-help-text;
font-size: @at-font-size-help-text;
color: @at-color-error;
margin: @at-margin-input-message 0 0 0;
padding: 0;
@ -182,7 +182,7 @@
& > i {
font-size: @at-font-size-button;
position: absolute;
position: absolute;
z-index: 3;
pointer-events: none;
top: @at-height-input / 3;
@ -218,3 +218,37 @@
min-height: @at-height-textarea;
padding: 6px @at-padding-input 0 @at-padding-input;
}
.at-InputSlider {
display: flex;
padding: 5px 0;
p {
color: @at-color-form-label;
font-size: @at-font-size-help-text;
font-weight: @at-font-weight-body;
margin: 0 0 0 10px;
padding: 0;
width: 50px;
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
height: 20px;
border-right: 1px solid @at-color-input-slider-track;
border-left: 1px solid @at-color-input-slider-track;
&:focus {
outline: none;
}
&::-webkit-slider-runnable-track {
.at-mixin-sliderTrack();
}
&::-webkit-slider-thumb {
.at-mixin-sliderThumb();
}
}
}

View File

@ -118,6 +118,16 @@ function AtInputLookupController (baseInputController, $q, $state) {
vm.searchAfterDebounce();
};
vm.removeTag = (i) => {
let list;
if (!i.id) {
list = _.remove(scope.state._value, i);
} else {
list = _.remove(scope.state._value, i.id);
}
scope.state._value = list;
};
}
AtInputLookupController.$inject = [

View File

@ -1,27 +1,45 @@
<div class="col-sm-{{::col}} at-InputContainer">
<div class="form-group at-u-flat">
<at-input-label></at-input-label>
<div class="form-group at-u-flat">
<at-input-label></at-input-label>
<div class="input-group">
<span class="input-group-btn">
<button class="btn at-ButtonHollow--default at-Input-button"
ng-disabled="state._disabled || form.disabled"
ng-click="vm.lookup()">
<i class="fa fa-search"></i>
</button>
</span>
<input type="text"
class="form-control at-Input"
ng-class="{ 'at-Input--rejected': state._rejected }"
ng-model="state._displayValue"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}"
ng-change="vm.searchOnInput()"
ng-disabled="state._disabled || form.disabled" />
<div class="input-group">
<span class="input-group-btn">
<button class="btn at-ButtonHollow--default at-Input-button"
ng-disabled="state._disabled || form.disabled"
ng-click="vm.lookup()">
<i class="fa fa-search"></i>
</button>
</span>
<!-- Refactor / Add conditional -->
<input type="text"
class="form-control at-Input"
ng-class="{ 'at-Input--rejected': state._rejected }"
ng-model="state._displayValue"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}"
ng-change="vm.searchOnInput()"
ng-hide="state._lookupTags"
ng-disabled="state._disabled || form.disabled">
</input>
<span class="form-control Form-textInput Form-textInput--variableHeight input-medium lookup LabelList-lookupTags"
ng-if="state._lookupTags">
<div class="LabelList-tagContainer" ng-repeat="tag in state._value">
<div class="LabelList-deleteContainer" ng-click="vm.removeTag(tag)">
<i class="fa fa-times LabelList-tagDelete"></i>
</div>
<div class="LabelList-tag LabelList-tag--deletable">
<span ng-if="tag.hostname" class="LabelList-name">{{ tag.hostname }}</span>
<span ng-if="!tag.hostname" class="LabelList-name">{{ tag }}</span>
</div>
</div>
</span>
</div>
<at-input-message></at-input-message>
</div>
<at-input-message></at-input-message>
</div>
<div ui-view="{{ state._resource }}"></div>
</div>
<div ui-view="{{ state._resource }}"></div>
</div>

View File

@ -0,0 +1,38 @@
const templateUrl = require('~components/input/slider.partial.html');
function atInputSliderLink (scope, element, attrs, controllers) {
const [formController, inputController] = controllers;
inputController.init(scope, element, formController);
}
function atInputSliderController (baseInputController) {
const vm = this || {};
vm.init = (_scope_, _element_, form) => {
baseInputController.call(vm, 'input', _scope_, _element_, form);
vm.check();
};
}
atInputSliderController.$inject = ['BaseInputController'];
function atInputSlider () {
return {
restrict: 'E',
require: ['^^atForm', 'atInputSlider'],
replace: true,
templateUrl,
controller: atInputSliderController,
controllerAs: 'vm',
link: atInputSliderLink,
scope: {
state: '=?',
col: '@',
tab: '@'
}
};
}
export default atInputSlider;

View File

@ -0,0 +1,13 @@
<div class="col-sm-{{::col}} at-InputContainer">
<div class="form-group at-u-flat">
<at-input-label></at-input-label>
<div class="at-InputSlider">
<input type="range"
ng-model="state._value"
min="0"
max="100"
ng-change="vm.slide(state._value)"/>
<p>{{ state._value }}%</p>
</div>
</div>
</div>

View File

@ -86,12 +86,26 @@
border-top: @at-border-default-width solid @at-color-list-border;
}
.at-Row--rowLayout {
display: flex;
flex-direction: row;
.at-RowItem {
margin-right: @at-space-4x;
&-label {
width: auto;
}
}
}
.at-Row-actions {
display: flex;
}
.at-Row-items {
align-self: flex-start;
flex: 1;
}
.at-RowItem {
@ -101,6 +115,7 @@
}
.at-RowItem--isHeader {
color: @at-color-body-text;
margin-bottom: @at-margin-bottom-list-header;
line-height: @at-line-height-list-row-item-header;
}
@ -146,8 +161,26 @@
.at-RowItem-label {
text-transform: uppercase;
width: auto;
width: @at-width-list-row-item-label;
color: @at-color-list-row-item-label;
font-size: @at-font-size;
}
.at-RowItem-value {
font-size: @at-font-size-3x;
}
.at-RowItem-badge {
background-color: @at-gray-848992;
border-radius: @at-border-radius;
color: @at-white;
font-size: 11px;
font-weight: normal;
height: 14px;
line-height: 10px;
margin: 0 10px;
padding: 2px 10px;
}
.at-RowAction {
@ -180,6 +213,11 @@
background-color: @at-color-list-row-action-hover-danger;
}
.at-Row .at-Row-checkbox {
align-self: start;
margin: 2px 20px 0 0;
}
@media screen and (max-width: @at-breakpoint-compact-list) {
.at-Row-actions {
flex-direction: column;

View File

@ -7,10 +7,13 @@ function atRowItem () {
transclude: true,
templateUrl,
scope: {
badge: '@',
headerValue: '@',
headerLink: '@',
headerTag: '@',
labelValue: '@',
labelLink: '@',
labelState: '@',
value: '@',
valueLink: '@',
smartStatus: '=?',

View File

@ -9,13 +9,19 @@
<div class="at-RowItem-tag at-RowItem-tag--header" ng-if="headerTag">
{{ headerTag }}
</div>
<div class="at-RowItem-label" ng-if="labelValue">
<div class="at-RowItem-label" ng-if="labelValue && labelLink">
<a ng-href="{{ labelLink }}">{{ labelValue }}</a>
</div>
<div class="at-RowItem-label" ng-if="labelValue && !labelLink && !labelState">
{{ labelValue }}
</div>
<div class="at-RowItem-label" ng-if="labelValue && labelState">
<a ui-sref="{{ labelState }}" ui-sref-opts="{reload: true, notify: true}">{{ labelValue }}</a>
</div>
<div class="at-RowItem-value" ng-if="value && valueLink">
<a ng-href="{{ valueLink }}">{{ value }}</a>
</div>
<div class="at-RowItem-value" ng-if="value && !valueLink"
<div class="at-RowItem-value" ng-class="{'at-RowItem-badge': badge}" ng-if="value && !valueLink"
ng-bind-html="value">
</div>
<aw-smart-status jobs="smartStatus.summary_fields.recent_jobs"
@ -35,4 +41,4 @@
{{ tag.name }}
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
<button class="btn at-ButtonHollow--default at-Tab"
ng-attr-disabled="{{ state._disabled || undefined }}"
ng-class="{ 'at-Tab--active': state._active, 'at-Tab--disabled': state._disabled }"
ng-hide="{{ state._hide }}"
ng-click="vm.go()">
<ng-transclude></ng-transclude>
</button>

View File

@ -129,6 +129,10 @@ function httpPost (config = {}) {
data: config.data
};
if (config.url) {
req.url = `${this.path}${config.url}`;
}
return $http(req)
.then(res => {
this.model.GET = res.data;
@ -323,7 +327,7 @@ function has (method, keys) {
return value !== undefined && value !== null;
}
function extend (method, related) {
function extend (method, related, config = {}) {
if (!related) {
related = method;
method = 'GET';
@ -337,6 +341,8 @@ function extend (method, related) {
url: this.get(`related.${related}`)
};
Object.assign(req, config);
return $http(req)
.then(({ data }) => {
this.set(method, `related.${related}`, data);

View File

@ -0,0 +1,47 @@
let Base;
function createFormSchema (method, config) {
if (!config) {
config = method;
method = 'GET';
}
const schema = Object.assign({}, this.options(`actions.${method.toUpperCase()}`));
if (config && config.omit) {
config.omit.forEach(key => delete schema[key]);
}
Object.keys(schema).forEach(key => {
schema[key].id = key;
if (this.has(key)) {
schema[key]._value = this.get(key);
}
});
return schema;
}
function InstanceModel (method, resource, config) {
// Base takes two args: resource and settings
// resource is the string endpoint
Base.call(this, 'instances');
this.Constructor = InstanceModel;
this.createFormSchema = createFormSchema.bind(this);
return this.create(method, resource, config);
}
function InstanceModelLoader (BaseModel) {
Base = BaseModel;
return InstanceModel;
}
InstanceModelLoader.$inject = [
'BaseModel'
];
export default InstanceModelLoader;

View File

@ -0,0 +1,47 @@
let Base;
function createFormSchema (method, config) {
if (!config) {
config = method;
method = 'GET';
}
const schema = Object.assign({}, this.options(`actions.${method.toUpperCase()}`));
if (config && config.omit) {
config.omit.forEach(key => delete schema[key]);
}
Object.keys(schema).forEach(key => {
schema[key].id = key;
if (this.has(key)) {
schema[key]._value = this.get(key);
}
});
return schema;
}
function InstanceGroupModel (method, resource, config) {
// Base takes two args: resource and settings
// resource is the string endpoint
Base.call(this, 'instance_groups');
this.Constructor = InstanceGroupModel;
this.createFormSchema = createFormSchema.bind(this);
return this.create(method, resource, config);
}
function InstanceGroupModelLoader (BaseModel) {
Base = BaseModel;
return InstanceGroupModel;
}
InstanceGroupModelLoader.$inject = [
'BaseModel'
];
export default InstanceGroupModelLoader;

View File

@ -0,0 +1,21 @@
let Base;
function JobModel (method, resource, config) {
Base.call(this, 'jobs');
this.Constructor = JobModel;
return this.create(method, resource, config);
}
function JobModelLoader (BaseModel) {
Base = BaseModel;
return JobModel;
}
JobModelLoader.$inject = [
'BaseModel'
];
export default JobModelLoader;

View File

@ -9,6 +9,8 @@ import Organization from '~models/Organization';
import Project from '~models/Project';
import JobTemplate from '~models/JobTemplate';
import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode';
import Instance from '~models/Instance';
import InstanceGroup from '~models/InstanceGroup';
import InventorySource from '~models/InventorySource';
import Inventory from '~models/Inventory';
import InventoryScript from '~models/InventoryScript';
@ -32,6 +34,8 @@ angular
.service('ProjectModel', Project)
.service('JobTemplateModel', JobTemplate)
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode)
.service('InstanceModel', Instance)
.service('InstanceGroupModel', InstanceGroup)
.service('InventorySourceModel', InventorySource)
.service('InventoryModel', Inventory)
.service('InventoryScriptModel', InventoryScript)

View File

@ -15,7 +15,16 @@
background: @at-color-disabled;
}
}
.at-Button--add {
&:extend(.at-Button--success all);
&:before {
content: "+";
font-size: 20px;
}
border-color: transparent;
}
.at-Button--info {
.at-mixin-Button();
.at-mixin-ButtonColor('at-color-info', 'at-color-default');
@ -26,7 +35,7 @@
.at-mixin-ButtonColor('at-color-error', 'at-color-default');
}
.at-ButtonHollow--default {
.at-ButtonHollow--default {
.at-mixin-Button();
.at-mixin-ButtonHollow(
'at-color-default',
@ -41,5 +50,5 @@
}
.at-Button--expand {
width: 100%;
width: 100%;
}

View File

@ -21,6 +21,7 @@
}
.at-mixin-Button () {
border-radius: @at-border-radius;
height: @at-height-input;
padding: @at-padding-button-vertical @at-padding-button-horizontal;
font-size: @at-font-size-body;
@ -102,3 +103,21 @@
.at-mixin-FontFixedWidth () {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
.at-mixin-sliderTrack() {
background: @at-color-input-slider-track;
cursor: pointer;
height: 1px;
width: 100%;
}
.at-mixin-sliderThumb() {
-webkit-appearance: none;
background: @at-color-input-slider-thumb;
border-radius: 50%;
border: none;
cursor: pointer;
height: 16px;
margin-top: -7px;
width: 16px;
}

View File

@ -147,6 +147,8 @@
@at-color-input-icon: @at-gray-b7;
@at-color-input-placeholder: @at-gray-848992;
@at-color-input-text: @at-gray-161b1f;
@at-color-input-slider-thumb: @at-blue;
@at-color-input-slider-track: @at-gray-b7;
@at-color-icon-dismiss: @at-gray-d7;
@at-color-icon-popover: @at-gray-848992;

View File

@ -73,6 +73,7 @@
@import '../../src/home/dashboard/dashboard.block.less';
@import '../../src/instance-groups/capacity-bar/capacity-bar.block.less';
@import '../../src/instance-groups/instance-group.block.less';
@import '../../src/instance-groups/instances/instance-modal.block.less';
@import '../../src/inventories-hosts/inventories/insights/insights.block.less';
@import '../../src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.block.less';
@import '../../src/inventories-hosts/inventories/related/hosts/related-groups-labels/relatedGroupsLabelsList.block.less';

View File

@ -97,7 +97,6 @@ angular
users.name,
projects.name,
scheduler.name,
instanceGroups.name,
'Utilities',
'templates',
@ -105,6 +104,7 @@ angular
'AWDirectives',
'features',
instanceGroups,
atFeatures,
atLibComponents,
atLibModels,
@ -316,6 +316,21 @@ angular
activateTab();
});
$transitions.onCreate({}, function(trans) {
console.log('$onCreate ' +trans.to().name);
});
$transitions.onBefore({}, function(trans) {
console.log('$onBefore ' +trans.to().name);
});
$transitions.onError({}, function(trans) {
console.log('$onError ' +trans.to().name);
});
$transitions.onExit({}, function(trans) {
console.log('$onExit ' +trans.to().name);
});
$transitions.onSuccess({}, function(trans) {
if(trans.to() === trans.from()) {

View File

@ -0,0 +1,34 @@
<at-panel>
<at-panel-heading>
{{ vm.panelTitle }}
</at-panel-heading>
<at-tab-group>
<at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
<at-tab state="vm.tab.instances" ng-hide="$state.includes('instanceGroups.add')">{{:: vm.strings.get('tab.INSTANCES') }}</at-tab>
<at-tab state="vm.tab.jobs" ng-hide="$state.includes('instanceGroups.add')">{{:: vm.strings.get('tab.JOBS') }}</at-tab>
</at-tab-group>
<at-panel-body>
<at-form state="vm.form" autocomplete="off">
<!-- Name -->
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
<!-- Minimum Instances -->
<at-input-text col="4" tab="3" state="vm.form.policy_instance_minimum"></at-input-text>
<!-- Range Slider -->
<at-input-slider col="4" tab="3" state="vm.form.policy_instance_percentage"></at-input-slider>
<!-- Instances List May need to turn into lookup tags... -->
<at-input-lookup col="4" tab="4" state="vm.form.policy_instance_list"></at-input-lookup>
<div ui-view="modal"></div>
<at-action-group col="12" pos="right">
<at-form-action type="cancel" to="instanceGroups"></at-form-action>
<at-form-action type="save"></at-form-action>
</at-action-group>
</at-form>
</at-panel-body>
</at-panel>

View File

@ -0,0 +1,44 @@
function AddController ($scope, $state, models, strings) {
const vm = this || {};
const { instanceGroup, instance } = models;
vm.mode = 'add';
vm.strings = strings;
vm.panelTitle = "New Instance Group";
vm.tab = {
details: { _active: true },
instances: {_disabled: true },
jobs: {_disabled: true }
};
vm.form = instanceGroup.createFormSchema('post');
vm.form.policy_instance_percentage._value = 0;
vm.form.policy_instance_list._lookupTags = true;
vm.form.policy_instance_list._model = instance;
vm.form.policy_instance_list._placeholder = "Policy Instance List";
vm.form.policy_instance_list._resource = 'instances';
vm.form.policy_instance_list._route = 'instanceGroups.add.modal.instances';
vm.form.policy_instance_list._value = [];
vm.form.save = data => {
data.policy_instance_list = data.policy_instance_list.map(instance => instance.hostname);
return instanceGroup.request('post', { data });
};
vm.form.onSaveSuccess = res => {
$state.go('instanceGroups.edit', { instance_group_id: res.data.id }, { reload: true });
};
}
AddController.$inject = [
'$scope',
'$state',
'resolvedModels',
'InstanceGroupsStrings'
];
export default AddController;

View File

@ -0,0 +1,40 @@
function InstanceModalController ($scope, $state, $http, $q, models, strings) {
const { instance } = models;
const vm = this || {};
vm.setInstances = () => {
vm.instances = instance.get('results').map(instance => {
instance.isSelected = false;
return instance;
});
}
init();
function init() {
vm.strings = strings;
vm.panelTitle = strings.get('instance.PANEL_TITLE');
vm.setInstances();
};
$scope.$watch('vm.instances', function() {
vm.selectedRows = _.filter(vm.instances, 'isSelected')
vm.deselectedRows = _.filter(vm.instances, 'isSelected', false);
}, true);
vm.submit = () => {
$scope.$parent.$parent.$parent.state.policy_instance_list._value = vm.selectedRows;
$state.go("^.^");
};
}
InstanceModalController.$inject = [
'$scope',
'$state',
'$http',
'$q',
'resolvedModels',
'InstanceGroupsStrings'
];
export default InstanceModalController;

View File

@ -0,0 +1,53 @@
<div id="instance-modal" class="modal-dialog">
<at-panel on-dismiss="^.^">
<at-panel-heading>
{{ vm.panelTitle }} | {{ vm.instanceGroupName}}
</at-panel-heading>
<multi-select-preview selected-rows='vm.selectedRows' available-rows='vm.instances'></multi-select-preview>
<at-panel-body
style="margin: 20px 0">
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="instances"
base-path="instances"
iterator="instance"
list="list"
dataset="vm.instances"
collection="collection"
search-tags="searchTags">
</smart-search>
</div>
<at-list results='vm.instances'>
<at-row ng-repeat="instance in vm.instances"
ng-class="{'at-Row--active': (instance.id === vm.activeId)}">
<input type="checkbox"
style="margin-right: 20px"
ng-class="{ 'at-Input--rejected': state.rejected }"
ng-model="instance.isSelected"
ng-checked="instance.isSelected"
ng-attr-tabindex="{{ tab || undefined }}"
ng-disabled="state._disabled" />
<div class="at-Row-items" style="flex: 1">
<at-row-item
header-value="{{ instance.hostname }}">
</at-row-item>
</div>
</at-row>
</at-list>
<div class="pull-right" style="margin-top:20px">
<button class="btn at-ButtonHollow--default"
ng-click="$state.go('^.^')">
{{:: vm.strings.get('CANCEL') }}
</button>
<button class="btn at-Button--success"
ng-click="vm.submit()">
{{:: vm.strings.get('SAVE') }}
</button>
</div>
</at-panel-body>
</at-panel>
</div>

View File

@ -0,0 +1,55 @@
function EditController ($rootScope, $state, models, strings) {
const vm = this || {};
const { instanceGroup, instance } = models;
$rootScope.breadcrumb.instance_group_name = instanceGroup.get('name');
vm.mode = 'edit';
vm.strings = strings;
vm.panelTitle = instanceGroup.get('name');
vm.tab = {
details: {
_active: true,
_go: 'instanceGroups.edit',
_params: { instance_group_id: instanceGroup.get('id') }
},
instances: {
_go: 'instanceGroups.instances',
_params: { instance_group_id: instanceGroup.get('id') }
},
jobs: {
_go: 'instanceGroups.jobs',
_params: { instance_group_id: instanceGroup.get('id') }
}
};
vm.form = instanceGroup.createFormSchema('put');
vm.form.policy_instance_list._lookupTags = true;
vm.form.policy_instance_list._model = instance;
vm.form.policy_instance_list._placeholder = "Policy Instance List";
vm.form.policy_instance_list._resource = 'instances';
vm.form.policy_instance_list._route = 'instanceGroups.edit.modal.instances';
vm.form.policy_instance_list._value = instanceGroup.get('policy_instance_list');
vm.form.save = data => {
instanceGroup.unset('policy_instance_list');
data.policy_instance_list = data.policy_instance_list.map(instance => instance.hostname);
return instanceGroup.request('put', { data });
};
vm.form.onSaveSuccess = res => {
$state.go('instanceGroups.edit', { instance_group_id: res.data.id }, { reload: true });
};
}
EditController.$inject = [
'$rootScope',
'$state',
'resolvedModels',
'InstanceGroupsStrings'
];
export default EditController;

View File

@ -1,21 +1,22 @@
capacity-bar {
width: 50%;
margin-right: 25px;
min-width: 100px;
display: flex;
align-items: center;
color: @at-gray-70;
display: flex;
font-size: @at-font-size;
min-width: 100px;
white-space: nowrap;
.CapacityBar {
background-color: @default-bg;
display: flex;
flex: 0 0 auto;
height: 10px;
border: 1px solid @default-link;
width: 100%;
border-radius: 100vw;
border: 1px solid @default-link;
display: flex;
flex: 1;
height: 10px;
margin-right: @at-space-2x;
min-width: 100px;
overflow: hidden;
margin-right: 10px;
width: 100%;
}
.CapacityBar-remaining {
@ -28,14 +29,21 @@ capacity-bar {
}
.CapacityBar--offline {
border-color: @d7grey;
color: @at-red;
border-color: @at-gray-a9;
.CapacityBar-remaining {
background-color: @d7grey;
background-color: @at-gray-b7;
}
}
.Capacity-details--percentage {
color: @default-data-txt;
.Capacity-details--label {
margin-right: @at-space-2x;
text-align: right;
text-transform: uppercase;
}
}
.Capacity-details--percentage {
width: 40px;
}
}

View File

@ -1,44 +1,47 @@
export default ['templateUrl', 'ComponentsStrings',
function (templateUrl, strings) {
return {
scope: {
capacity: '=',
totalCapacity: '='
},
templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'),
restrict: 'E',
link: function(scope) {
scope.isOffline = false;
function (templateUrl, strings) {
return {
scope: {
capacity: '=',
totalCapacity: '=',
labelValue: '@',
badge: '='
},
templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'),
restrict: 'E',
link: function(scope) {
scope.isOffline = false;
scope.$watch('totalCapacity', function(val) {
if (val === 0) {
scope.isOffline = true;
scope.offlineTip = strings.get(`capacityBar.IS_OFFLINE`);
} else {
scope.isOffline = false;
scope.offlineTip = null;
}
}, true);
scope.$watch('totalCapacity', function(val) {
if (val === 0) {
scope.isOffline = true;
scope.labelValue = strings.get(`capacityBar.IS_OFFLINE_LABEL`);
scope.offlineTip = strings.get(`capacityBar.IS_OFFLINE`);
} else {
scope.isOffline = false;
scope.offlineTip = null;
}
}, true);
scope.$watch('capacity', function() {
if (scope.totalCapacity !== 0) {
var percentageCapacity = Math
.round(scope.capacity / scope.totalCapacity * 1000) / 10;
scope.$watch('capacity', function() {
if (scope.totalCapacity !== 0) {
var percentageCapacity = Math
.round(scope.capacity / scope.totalCapacity * 1000) / 10;
scope.CapacityStyle = {
'flex-grow': percentageCapacity * 0.01
};
scope.CapacityStyle = {
'flex-grow': percentageCapacity * 0.01
};
scope.consumedCapacity = `${percentageCapacity}%`;
} else {
scope.CapacityStyle = {
'flex-grow': 1
};
scope.consumedCapacity = `${percentageCapacity}%`;
} else {
scope.CapacityStyle = {
'flex-grow': 1
};
scope.consumedCapacity = null;
}
}, true);
}
};
}
];
scope.consumedCapacity = null;
}
}, true);
}
};
}
];

View File

@ -1,11 +1,20 @@
<span class="Capacity-details--label" ng-class="{'CapacityBar--offline': isOffline}">
{{labelValue}}
</span>
<div class="CapacityBar"
ng-class="{'CapacityBar--offline': isOffline}"
ng-if="!badge"
aw-tool-tip="{{ offlineTip }}"
data-tip-watch="offlineTip"
data-placement="top"
data-trigger="hover"
data-container="body">
<div class="CapacityBar-remaining" ng-style="CapacityStyle"></div>
<div class="CapacityBar-consumed"></div>
<div class="CapacityBar-remaining" ng-style="CapacityStyle"></div>
<div class="CapacityBar-consumed"></div>
</div>
<span class="Capacity-details--percentage" ng-show="consumedCapacity">{{ consumedCapacity }}</span>
<span class="Capacity-details--percentage"
ng-class="{'badge List-titleBadge': badge}">
{{ consumedCapacity }}
</span>

View File

@ -1,11 +1,13 @@
<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="add"></div>
<div ui-view="edit"></div>
<div ui-view="instanceJobs"></div>
<div ui-view="instances"></div>
<div ng-cloak id="htmlTemplate" class="Panel">
<div ui-view="list"></div>
</div>
<div ui-view="jobs"></div>
<div ui-view="list"></div>
</div>

View File

@ -1,41 +0,0 @@
import {templateUrl} from '../shared/template-url/template-url.factory';
import { N_ } from '../i18n';
export default {
name: 'instanceGroups',
url: '/instance_groups',
searchPrefix: 'instance_group',
ncyBreadcrumb: {
label: N_('INSTANCE GROUPS')
},
params: {
instance_group_search: {
value: {
page_size: '20',
order_by: 'name'
},
dynamic: true
}
},
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,25 @@
function InstanceGroupsStrings (BaseString) {
BaseString.call(this, 'instanceGroups');
const { t } = this;
const ns = this.instanceGroups;
ns.state = {
ADD_BREADCRUMB_LABEL: t.s('CREATE INSTANCE GROUP'),
EDIT_BREADCRUMB_LABEL: t.s('EDIT INSTANCE GROUP')
};
ns.tab = {
DETAILS: t.s('DETAILS'),
INSTANCES: t.s('INSTANCES'),
JOBS: t.s('JOBS')
};
ns.instance = {
PANEL_TITLE: t.s('SELECT INSTANCE')
}
}
InstanceGroupsStrings.$inject = ['BaseStringService'];
export default InstanceGroupsStrings;

View File

@ -1,41 +0,0 @@
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: '20',
order_by: '-finished',
not__launch_type: 'sync'
},
dynamic: true
}
},
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

@ -1,82 +1,81 @@
export default ['$scope','InstanceJobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, InstanceJobsList, GetBasePath, Rest, Dataset, Find, $state, $q) {
let list = InstanceJobsList;
function InstanceJobsController ($scope, GetBasePath, Rest, Dataset, Find, $filter, $state, $q, model, strings, jobStrings) {
const vm = this || {};
const { instance } = model;
init();
init();
function init(){
$scope.optionsDefer = $q.defer();
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
function init(){
vm.strings = strings;
vm.jobStrings = jobStrings;
vm.queryset = { page_size: '10', order_by: '-finished'};
vm.jobs = instance.get('related.jobs.results');
vm.dataset = instance.get('related.jobs');
vm.count = instance.get('related.jobs.count');
vm.panelTitle = `${jobStrings.get('list.PANEL_TITLE')} | ${instance.get('hostname')}`
$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;
}
vm.tab = {
details: {_hide: true},
instances: {_hide: true},
jobs: {_hide: true}
};
$scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing();
}
);
}
];
vm.getTime = function(time) {
let val = "";
if (time) {
val += $filter('longDate')(time);
}
if (val === "") {
val = undefined;
}
return val;
};
$scope.isSuccessful = function (status) {
return (status === "successful");
};
$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;
}
};
}
InstanceJobsController.$inject = [
'$scope',
'GetBasePath',
'Rest',
'Dataset',
'Find',
'$filter',
'$state',
'$q',
'resolvedModels',
'InstanceGroupsStrings',
'JobStrings'
];
export default InstanceJobsController;

View File

@ -1,78 +0,0 @@
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

@ -1,32 +0,0 @@
<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>Used Capacity</p>
<capacity-bar capacity="instanceCapacity" total-capacity="instanceTotalCapacity"></capacity-bar>
</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

@ -1,38 +0,0 @@
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.consumed_capacity;
$scope.instanceTotalCapacity = instance.capacity;
$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,24 @@
.Modal-backdrop {
position: fixed;
top: 0px;
left: 0px;
height:100%;
width:100%;
background: #000;
z-index: 2;
opacity: 0.5;
}
.Modal-holder {
position: fixed;
top: 1;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
z-index: 3;
.modal-dialog {
padding-top: 100px;
}
}

View File

@ -0,0 +1,74 @@
function InstanceModalController ($scope, $state, $http, $q, models, strings) {
const { instance, instanceGroup } = models;
const vm = this || {};
vm.setInstances = () => {
vm.instances = instance.get('results').map(instance => {
instance.isSelected = false;
return instance;
});
}
vm.setRelatedInstances = () => {
vm.instanceGroupName = instanceGroup.get('name');
vm.relatedInstances = instanceGroup.get('related.instances.results');
vm.relatedInstanceIds = vm.relatedInstances.map(instance => instance.id);
vm.instances = instance.get('results').map(instance => {
instance.isSelected = vm.relatedInstanceIds.includes(instance.id);
return instance;
});
}
init();
function init() {
vm.strings = strings;
vm.panelTitle = strings.get('instance.PANEL_TITLE');
vm.instanceGroupId = instanceGroup.get('id');
if (vm.instanceGroupId === undefined) {
vm.setInstances();
} else {
vm.setRelatedInstances();
}
};
$scope.$watch('vm.instances', function() {
vm.selectedRows = _.filter(vm.instances, 'isSelected')
vm.deselectedRows = _.filter(vm.instances, 'isSelected', false);
}, true);
vm.submit = () => {
let associate = vm.selectedRows
.map(instance => ({id: instance.id}));
let disassociate = vm.deselectedRows
.map(instance => ({id: instance.id, disassociate: true}));
let all = associate.concat(disassociate);
let defers = all.map((data) => {
let config = {
url: `${vm.instanceGroupId}/instances/`,
data: data
}
return instanceGroup.http.post(config);
});
Promise.all(defers)
.then(vm.onSaveSuccess);
};
vm.onSaveSuccess = () => {
$state.go('instanceGroups.instances', {}, {reload: 'instanceGroups.instances'});
};
}
InstanceModalController.$inject = [
'$scope',
'$state',
'$http',
'$q',
'resolvedModels',
'InstanceGroupsStrings'
];
export default InstanceModalController;

View File

@ -0,0 +1,55 @@
<div id="instance-modal" class="modal-dialog">
<at-panel on-dismiss="instanceGroups.instances">
<at-panel-heading>
{{ vm.panelTitle }} | {{ vm.instanceGroupName}}
</at-panel-heading>
<multi-select-preview selected-rows='vm.selectedRows' available-rows='vm.instances'></multi-select-preview>
<at-panel-body
style="margin: 20px 0">
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="instances"
base-path="instances"
iterator="instance"
list="list"
dataset="vm.instances"
collection="collection"
search-tags="searchTags">
</smart-search>
</div>
<at-list results='vm.instances'>
<at-row ng-repeat="instance in vm.instances"
ng-class="{'at-Row--active': (instance.id === vm.activeId)}">
<input type="checkbox"
style="margin-right: 20px"
ng-class="{ 'at-Input--rejected': state.rejected }"
ng-model="instance.isSelected"
ng-checked="instance.isSelected"
ng-attr-tabindex="{{ tab || undefined }}"
ng-disabled="state._disabled" />
<div class="at-Row-items" style="flex: 1">
<at-row-item
header-value="{{ instance.hostname }}"
header-tag="{{ vm.instanceTypes[instance.type] }}"></at-row-item>
</at-row-item>
</div>
</at-row>
</at-list>
<div class="pull-right" style="margin-top:20px">
<button class="btn at-ButtonHollow--default"
style="margin-right:10px"
ng-click="$state.go('instanceGroups.instances')">
{{:: vm.strings.get('CANCEL') }}
</button>
<button class="btn at-Button--success"
ng-click="vm.submit()">
{{:: vm.strings.get('SAVE') }}
</button>
</div>
</at-panel-body>
</at-panel>
</div>

View File

@ -1,44 +1,62 @@
<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>
<at-panel>
<at-panel-heading>
{{ vm.panelTitle }}
</at-panel-heading>
<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-if="columnNoSort !== 'true'" class="fa columnSortIcon fa-sort-up" ng-class="orderByIcon()"></i>
</th>
<th id="instance-jobs_running-header" class="List-tableHeader list-header list-header-noSort" translate>
Running Jobs
</th>
<th id="instance-consumed_capacity-header" class="List-tableHeader list-header list-header-noSort" translate>
Used Capacity
</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 jobs_running-column ng-binding">
<a ui-sref="instanceGroups.instances.jobs({instance_group_id: $stateParams.instance_group_id})">
{{ instance.jobs_running }}
</a>
</td>
<td class="List-tableCell List-tableCell--capacityColumn ng-binding">
<capacity-bar capacity="instance.consumed_capacity" total-capacity="instance.capacity">
</td>
</tr>
</tbody>
</table>
</div>
</div>
<at-tab-group>
<at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
<at-tab state="vm.tab.instances">{{:: vm.strings.get('tab.INSTANCES') }}</at-tab>
<at-tab state="vm.tab.jobs" ng-hide="$state.current.name === 'instanceGroups.add'">{{:: vm.strings.get('tab.JOBS') }}</at-tab>
</at-tab-group>
<at-panel-body
style="margin-top: 20px">
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="instances"
base-path="instances"
iterator="instance"
list="list"
dataset="vm.instances"
collection="collection"
search-tags="searchTags">
</smart-search>
<!-- TODO: only show when permission -->
<div class="at-List-toolbarAction">
<button
type="button"
ng-click="$state.go('instanceGroups.instances.modal.add')"
class="at-Button--add"
aria-haspopup="true"
aria-expanded="false">
</button>
<div ui-view="modal"></div>
</div>
</div>
<at-list results='vm.instances'>
<at-row ng-repeat="instance in vm.instances"
ng-class="{'at-Row--active': (instance.id === vm.activeId)}">
<div class="at-Row-items">
<at-row-item
header-value="{{ instance.hostname }}"
header-tag="{{ vm.instanceTypes[instance.type] }}"></at-row-item>
</at-row-item>
<div class="at-Row--rowLayout">
<at-row-item
label-value="Running Jobs"
label-state="instanceGroups.instanceJobs({instance_group_id: {{vm.instance_group_id}}, instance_id: {{instance.id}}})"
value="{{ instance.jobs_running }}"
badge="true">
</at-row-item>
</div>
</div>
<div class="at-Row-actions">
<capacity-bar label-value="Used Capacity" capacity="instance.consumed_capacity" total-capacity="instance.capacity"></capacity-bar>
</div>
</at-row>
</at-list>
</at-panel-body>
</at-panel>

View File

@ -1,35 +0,0 @@
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: '20',
order_by: 'hostname'
},
dynamic: true
}
},
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

@ -1,20 +1,55 @@
export default ['$scope', 'InstanceList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, InstanceList, GetBasePath, Rest, Dataset, Find, $state, $q) {
let list = InstanceList;
function InstancesController ($scope, $state, models, strings, Dataset) {
const { instanceGroup } = models;
const vm = this || {};
vm.strings = strings;
vm.panelTitle = instanceGroup.get('name');
vm.instances = instanceGroup.get('related.instances.results');
vm.instance_group_id = instanceGroup.get('id');
init();
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;
function init() {
$scope.list = {
iterator: 'instance',
name: 'instances'
};
$scope.collection = {
basepath: 'instances',
iterator: 'instance'
};
$scope[`${$scope.list.iterator}_dataset`] = Dataset.data;
$scope[$scope.list.name] = $scope[`${$scope.list.iterator}_dataset`].results;
}
];
vm.tab = {
details: {
_go: 'instanceGroups.edit',
_params: { instance_group_id: vm.instance_group_id }
},
instances: {
_active: true,
_go: 'instanceGroups.instances',
_params: { instance_group_id: vm.instance_group_id }
},
jobs: {
_go: 'instanceGroups.jobs',
_params: { instance_group_id: vm.instance_group_id }
}
};
$scope.isActive = function(id) {
let selected = parseInt($state.params.instance_id);
return id === selected;
};
}
InstancesController.$inject = [
'$scope',
'$state',
'resolvedModels',
'InstanceGroupsStrings',
'Dataset'
];
export default InstancesController;

View File

@ -1,29 +0,0 @@
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})'
},
consumed_capacity: {
label: i18n._('Capacity'),
nosort: true,
},
jobs_running: {
label: i18n._('Running Jobs'),
nosort: true,
},
}
};
}];

View File

@ -1,35 +0,0 @@
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.consumed_capacity;
$scope.instanceGroupTotalCapacity = instanceGroup.capacity;
$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

@ -1,41 +0,0 @@
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: '20',
order_by: '-finished',
not__launch_type: 'sync'
},
dynamic: true
},
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

@ -1,82 +1,93 @@
export default ['$scope','JobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q',
function($scope, JobsList, GetBasePath, Rest, Dataset, Find, $state, $q) {
let list = JobsList;
function InstanceGroupJobsController ($scope, GetBasePath, Rest, Dataset, Find, $filter, $state, $q, model, strings, jobStrings) {
const vm = this || {};
const { instanceGroup } = model;
init();
init();
function init(){
$scope.optionsDefer = $q.defer();
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
function init(){
let instance_group_id = instanceGroup.get('id');
vm.strings = strings;
vm.jobStrings = jobStrings;
vm.queryset = { page_size: '10', order_by: '-finished', instance_group_id: instance_group_id };
vm.jobs = instanceGroup.get('related.jobs.results');
vm.dataset = instanceGroup.get('related.jobs');
vm.count = instanceGroup.get('related.jobs.count');
vm.panelTitle = instanceGroup.get('name');
$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);
});
vm.tab = {
details: {
_go: 'instanceGroups.edit',
_params: { instance_group_id },
_label: strings.get('tab.DETAILS')
},
instances: {
_go: 'instanceGroups.instances',
_params: { instance_group_id },
_label: strings.get('tab.INSTANCES')
},
jobs: {
_active: true,
_label: strings.get('tab.JOBS')
}
}
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();
}
);
}
];
vm.getTime = function(time) {
let val = "";
if (time) {
val += $filter('longDate')(time);
}
if (val === "") {
val = undefined;
}
return val;
};
$scope.isSuccessful = function (status) {
return (status === "successful");
};
$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;
}
};
}
InstanceGroupJobsController.$inject = [
'$scope',
'GetBasePath',
'Rest',
'Dataset',
'Find',
'$filter',
'$state',
'$q',
'resolvedModels',
'InstanceGroupsStrings',
'JobStrings'
];
export default InstanceGroupJobsController;

View File

@ -1,76 +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,
// 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'
},
}
};
}];
// 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

@ -0,0 +1,30 @@
function JobStrings (BaseString) {
BaseString.call(this, 'jobs');
const { t } = this;
const ns = this.jobs;
ns.state = {
LIST_BREADCRUMB_LABEL: t.s('JOBS')
}
ns.list = {
PANEL_TITLE: t.s('JOBS'),
ADD_BUTTON_LABEL: t.s('ADD'),
ADD_DD_JT_LABEL: t.s('Job Template'),
ADD_DD_WF_LABEL: t.s('Workflow Template'),
ROW_ITEM_LABEL_ACTIVITY: t.s('Activity'),
ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'),
ROW_ITEM_LABEL_PROJECT: t.s('Project'),
ROW_ITEM_LABEL_TEMPLATE: t.s('Template'),
ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'),
ROW_ITEM_LABEL_MODIFIED: t.s('Last Modified'),
ROW_ITEM_LABEL_RAN: t.s('Last Ran'),
ROW_ITEM_LABEL_STARTED: t.s('Started'),
ROW_ITEM_LABEL_FINISHED: t.s('Finished')
}
}
JobStrings.$inject = ['BaseStringService'];
export default JobStrings;

View File

@ -0,0 +1,103 @@
<at-panel>
<at-panel-heading>
{{ vm.panelTitle }}
</at-panel-heading>
<at-tab-group>
<at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
<at-tab state="vm.tab.instances">{{:: vm.strings.get('tab.INSTANCES') }}</at-tab>
<at-tab state="vm.tab.jobs">{{:: vm.strings.get('tab.JOBS') }}</at-tab>
</at-tab-group>
<at-panel-body style="margin-top: 20px">
<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="vm.jobs">
<!-- TODO: implement resources are missing red indicator as present in mockup -->
<at-row ng-repeat="job in vm.jobs"
ng-class="{'at-Row--active': (job.id === vm.activeId)}"
job-id="{{ job.id }}">
<div style="align-self:flex-start;margin:0 10px 0 0">
<a href="{{ job.detailsUrl }}" ng-if="isSuccessful(job.status)" aw-tool-tip="Job successful. Click for details." aw-tip-placement="right">
<i class="fa DashboardList-status DashboardList-status--success icon-job-successful"
style="margin:0"></i>
</a>
<a href="{{ job.detailsUrl }}" ng-if="!isSuccessful(job.status)" aw-tool-tip="Job failed. Click for details." aw-tip-placement="right">
<i class="fa DashboardList-status DashboardList-status--failed icon-job-failed"
style="margin:0"></i>
</a>
</div>
<div class="at-Row-items">
<at-row-item
header-value="{{ job.name }}"
header-link="/#/jobs/{{ job.id }}"
header-tag="{{ vm.jobTypes[job.type] }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.jobStrings.get('list.ROW_ITEM_LABEL_STARTED') }}"
value="{{ vm.getTime(job.started) }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.jobStrings.get('list.ROW_ITEM_LABEL_FINISHED') }}"
value="{{ vm.getTime(job.finished) }}">
</at-row-item>
<at-row-item
header-value="{{ job.name }}"
header-link="/#/jobs/workflow_job/{{ job.id }}"
header-tag="{{ vm.jobTypes[job.type] }}"
ng-if="job.type === 'workflow_job_job'">
</at-row-item>
<at-row-item
label-value="{{:: vm.jobStrings.get('list.ROW_ITEM_LABEL_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.jobStrings.get('list.ROW_ITEM_LABEL_INVENTORY') }}"
value="{{ job.summary_fields.inventory.name }}"
value-link="/#/inventories/inventory/{{ job.summary_fields.inventory.id }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.jobStrings.get('list.ROW_ITEM_LABEL_PROJECT') }}"
value="{{ job.summary_fields.project.name }}"
value-link="/#/projects/{{ job.summary_fields.project.id }}">
</at-row-item>
<!-- TODO: add see more for creds -->
<at-row-item
label-value="{{:: vm.jobStrings.get('list.ROW_ITEM_LABEL_CREDENTIALS') }}"
tag-values="job.summary_fields.credentials"
tags-are-creds="true">
</at-row-item>
</div>
<div class="at-Row-actions">
<at-row-action icon="icon-launch" ng-click="vm.submitJob(job)"
ng-show="job.summary_fields.user_capabilities.start">
</at-row-action>
<at-row-action icon="fa-copy" ng-click="vm.copyjob(job)"
ng-show="job.summary_fields.user_capabilities.copy">
</at-row-action>
<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="vm.jobs"
dataset="vm.dataset"
iterator="job"
base-path="unified_jobs"
query-set="vm.queryset">
</paginate>
</at-panel-body>
</at-panel>

View File

@ -1,19 +1,43 @@
export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state',
function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state) {
export default ['$scope', 'InstanceGroupList', 'resolvedModels', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', 'ComponentsStrings',
function($scope, InstanceGroupList, resolvedModels, GetBasePath, Rest, Dataset, Find, $state, $q, strings) {
let list = InstanceGroupList;
const vm = this;
const { instanceGroup } = resolvedModels;
init();
function init(){
vm.panelTitle = strings.get('layout.INSTANCE_GROUPS');
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
$scope.instanceGroupCount = Dataset.data.count;
}
$scope.isActive = function(id) {
let selected = parseInt($state.params.instance_group_id);
return id === selected;
$scope.selection = {};
$scope.$watch('$state.params.instance_group_id', () => {
vm.activeId = parseInt($state.params.instance_group_id);
});
vm.delete = () => {
let deletables = $scope.selection;
deletables = Object.keys(deletables).filter((n) => deletables[n]);
//refactor
deletables.forEach((data) => {
let promise = instanceGroup.http.delete({resource: data})
Promise.resolve(promise).then(vm.onSaveSuccess);
});
}
vm.onSaveSuccess = () => {
$state.transitionTo($state.current, $state.params, {
reload: true, location: true, inherit: false, notify: true
});
}
$scope.createInstanceGroup = () => {
$state.go('instanceGroups.add');
};
}
];
];

View File

@ -1,63 +1,84 @@
<div class="List-header">
<div class="List-title">
<div class="List-titleText" translate>
INSTANCE GROUPS
</div>
<at-panel>
<at-panel-heading hide-dismiss="true">
{{ vm.panelTitle }}
<span class="badge List-titleBadge">
{{ instanceGroupCount }}
</span>
</div>
</div>
</at-panel-heading>
<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>
<at-panel-body>
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="instance_groups"
base-path="instance_groups"
iterator="instance_group"
list="list"
dataset="instance_group_dataset"
collection="collection"
search-tags="searchTags">
</smart-search>
<!-- TODO: only show when permission -->
<div class="at-List-toolbarAction">
<div ng-click="vm.delete()"
class="at-RowAction at-RowAction--danger"
style="margin-right:10px">
<i class="fa fa-trash"></i>
</div>
<button
type="button"
ui-sref="instanceGroups.add"
class="at-Button--add"
aria-haspopup="true"
aria-expanded="false">
</button>
</div>
</div>
<div class="List-noItems" ng-show="instance_groups.length < 1" translate>PLEASE ADD ITEMS TO THIS LIST</div>
<at-list>
<at-row ng-repeat="instance_group in instance_groups"
ng-class="{'at-Row--active': (instance_group.id === vm.activeId)}">
<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-if="columnNoSort !== 'true'" class="fa columnSortIcon fa-sort-up" ng-class="orderByIcon()"></i>
</th>
<th id="instance_group-jobs_running-header" class="List-tableHeader list-header list-header-noSort" translate>
Running Jobs
</th>
<th id="instance_group-consumed_capacity-header" class="List-tableHeader list-header list-header-noSort" translate>
Used Capacity
</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 jobs_running-column ng-binding">
<a ui-sref="instanceGroups.instances.jobs({instance_group_id: instance_group.id})">
{{ instance_group.jobs_running }}
</a>
</td>
<td class="List-tableCell List-tableCell--capacityColumn ng-binding">
<capacity-bar capacity="instance_group.consumed_capacity" total-capacity="instance_group.capacity"></capacity-bar>
</td>
</tr>
</tbody>
</table>
</div>
<input type="checkbox"
class="at-Row-checkbox"
ng-model="selection[instance_group.id]"/>
<div class="at-Row-items">
<at-row-item
header-value="{{ instance_group.name }}"
header-link="/#/instance_groups/{{ instance_group.id }}">
</at-row-item>
<div class="at-Row--rowLayout">
<at-row-item
label-value="Instances"
label-link="/#/instance_groups/{{ instance_group.id }}/instances"
value="{{ instance_group.instances }}"
badge="true">
</at-row-item>
<at-row-item
label-value="Running Jobs"
label-link="/#/instance_groups/{{ instance_group.id }}/jobs"
value="{{ instance_group.jobs_running }}"
badge="true">
</at-row-item>
</div>
</div>
<div class="at-Row-actions">
<capacity-bar label-value="Used Capacity" capacity="instance_group.consumed_capacity" total-capacity="instance_group.capacity"></capacity-bar>
</div>
</at-row>
</at-list>
</at-panel-body>
<paginate
base-path="instance_groups"
iterator="instance_group"
dataset="instance_group_dataset"
collection="instance_groups"
query-set="instance_group_queryset">
</paginate>
</at-panel>
<paginate
base-path="instance_groups"
iterator="instance_group"
dataset="instance_group_dataset"
collection="instance_groups"
query-set="instance_group_queryset">
</paginate>

View File

@ -1,58 +1,331 @@
import InstanceGroupsList from './list/instance-groups-list.controller';
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 InstanceGroupJobsListController from './jobs/jobs.controller';
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';
export default
angular.module('instanceGroups', [CapacityBar.name])
import { templateUrl } from '../shared/template-url/template-url.factory';
import addEditTemplate from './add-edit/add-edit-instance-groups.view.html';
import addInstanceModalTemplate from './add-edit/add-instance-list-policy.partial.html';
import addInstanceModalController from './add-edit/add-instance-list-policy.controller.js';
import instancesTemplate from './instances/instances-list.partial.html';
import instanceModalTemplate from './instances/instance-modal.partial.html';
import instanceModalController from './instances/instance-modal.controller.js';
import AddInstanceGroupController from './add-edit/add-instance-group.controller';
import EditInstanceGroupController from './add-edit/edit-instance-group.controller';
import InstanceGroupsStrings from './instance-groups.strings';
import JobStrings from './jobs/jobs.strings';
import jobsTemplate from './jobs/list.view.html';
const MODULE_NAME = 'instanceGroups';
function InstanceGroupsResolve ($q, $stateParams, InstanceGroup, Instance, Job) {
const instanceGroupId = $stateParams.instance_group_id;
const instanceId = $stateParams.instance_id;
let promises = {};
if (!instanceGroupId && !instanceId) {
promises.instanceGroup = new InstanceGroup(['get', 'options'])
promises.instance = new Instance(['get', 'options']);
return $q.all(promises);
}
if (instanceGroupId && instanceId) {
promises.instance = new Instance(['get', 'options'], [instanceId, instanceId])
.then((instance) => instance.extend('get', 'jobs', {params: {page_size: "10", order_by: "-finished"}}))
return $q.all(promises);
}
promises.instanceGroup = new InstanceGroup(['get', 'options'], [instanceGroupId, instanceGroupId])
.then((instanceGroup) => instanceGroup.extend('get', 'jobs', {params: {page_size: "10", order_by: "-finished"}}))
.then((instanceGroup) => instanceGroup.extend('get', 'instances'))
promises.instance = new Instance('get');
return $q.all(promises)
.then(models => models);
}
InstanceGroupsResolve.$inject = [
'$q',
'$stateParams',
'InstanceGroupModel',
'InstanceModel',
'JobModel'
];
function InstanceGroupsRun ($stateExtender, strings, ComponentsStrings) {
$stateExtender.addState({
name: 'instanceGroups',
url: '/instance_groups',
searchPrefix: 'instance_group',
ncyBreadcrumb: {
label: ComponentsStrings.get('layout.INSTANCE_GROUPS')
},
params: {
instance_group_search: {
value: {
page_size: '10',
order_by: 'name'
},
dynamic: true
}
},
data: {
alwaysShowRefreshButton: true,
},
views: {
'@': {
templateUrl: templateUrl('./instance-groups/instance-groups'),
},
'list@instanceGroups': {
templateUrl: templateUrl('./instance-groups/list/instance-groups-list'),
controller: 'InstanceGroupsList',
controllerAs: 'vm'
}
},
resolve: {
resolvedModels: InstanceGroupsResolve,
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`]);
}
]
}
});
$stateExtender.addState({
name: 'instanceGroups.add',
url: '/add',
ncyBreadcrumb: {
label: strings.get('state.ADD_BREADCRUMB_LABEL')
},
views: {
'add@instanceGroups': {
templateUrl: addEditTemplate,
controller: AddInstanceGroupController,
controllerAs: 'vm'
}
},
resolve: {
resolvedModels: InstanceGroupsResolve
}
});
$stateExtender.addState({
name: 'instanceGroups.add.modal',
abstract: true,
ncyBreadcrumb: {
skip: true,
},
views: {
"modal": {
template: `<div class="Modal-backdrop"></div>
<div class="Modal-holder" ui-view="modal" autoscroll="false"></div>`,
}
}
});
$stateExtender.addState({
name: 'instanceGroups.add.modal.instances',
ncyBreadcrumb: {
skip: true,
},
views: {
"modal": {
templateUrl: addInstanceModalTemplate,
controller: addInstanceModalController,
controllerAs: 'vm'
}
},
resolvedModels: InstanceGroupsResolve
});
$stateExtender.addState({
name: 'instanceGroups.edit',
route: '/:instance_group_id',
ncyBreadcrumb: {
label: strings.get('state.EDIT_BREADCRUMB_LABEL')
},
views: {
'edit@instanceGroups': {
templateUrl: addEditTemplate,
controller: EditInstanceGroupController,
controllerAs: 'vm'
}
},
resolve: {
resolvedModels: InstanceGroupsResolve
}
});
$stateExtender.addState({
name: 'instanceGroups.edit.modal',
abstract: true,
ncyBreadcrumb: {
skip: true,
},
views: {
"modal": {
template: `<div class="Modal-backdrop"></div>
<div class="Modal-holder" ui-view="modal" autoscroll="false"></div>`,
}
}
});
$stateExtender.addState({
name: 'instanceGroups.edit.modal.instances',
ncyBreadcrumb: {
skip: true,
},
views: {
"modal": {
templateUrl: addInstanceModalTemplate,
controller: addInstanceModalController,
controllerAs: 'vm'
}
},
resolvedModels: InstanceGroupsResolve
});
$stateExtender.addState({
name: 'instanceGroups.instances',
url: '/:instance_group_id/instances',
ncyBreadcrumb: {
parent: 'instanceGroups.edit',
label: ComponentsStrings.get('layout.INSTANCES')
},
params: {
instance_search: {
value: {
page_size: '10',
order_by: 'hostname'
},
dynamic: true
}
},
views: {
'instances@instanceGroups': {
templateUrl: instancesTemplate,
controller: 'InstanceListController',
controllerAs: 'vm'
}
},
resolve: {
resolvedModels: InstanceGroupsResolve
}
});
$stateExtender.addState({
name: 'instanceGroups.instances.modal',
abstract: true,
ncyBreadcrumb: {
skip: true,
},
views: {
"modal": {
template: `<div class="Modal-backdrop"></div>
<div class="Modal-holder" ui-view="modal" autoscroll="false"></div>`,
}
}
});
$stateExtender.addState({
name: 'instanceGroups.instances.modal.add',
ncyBreadcrumb: {
skip: true,
},
views: {
"modal": {
templateUrl: instanceModalTemplate,
controller: instanceModalController,
controllerAs: 'vm'
}
},
resolvedModels: InstanceGroupsResolve
});
$stateExtender.addState({
name: 'instanceGroups.instanceJobs',
url: '/:instance_group_id/instances/:instance_id/jobs',
ncyBreadcrumb: {
parent: 'instanceGroups.instances',
label: ComponentsStrings.get('layout.JOBS')
},
views: {
'instanceJobs@instanceGroups': {
templateUrl: jobsTemplate,
controller: 'InstanceJobsController',
controllerAs: 'vm'
},
},
params: {
job_search: {
value: {
page_size: '10',
order_by: '-finished'
},
dynamic: true
},
},
resolvedModels: InstanceGroupsResolve
});
$stateExtender.addState({
name: 'instanceGroups.jobs',
url: '/:instance_group_id/jobs',
ncyBreadcrumb: {
parent: 'instanceGroups.edit',
label: ComponentsStrings.get('layout.JOBS')
},
params: {
job_search: {
value: {
page_size: '10',
order_by: '-finished'
},
dynamic: true
}
},
views: {
'jobs@instanceGroups': {
templateUrl: jobsTemplate,
controller: 'InstanceGroupJobsListController',
controllerAs: 'vm'
},
},
resolve: {
resolvedModels: InstanceGroupsResolve
}
})
}
InstanceGroupsRun.$inject = [
'$stateExtender',
'InstanceGroupsStrings',
'ComponentsStrings'
];
angular.module(MODULE_NAME, [CapacityBar.name])
.service('InstanceGroupsService', service)
.factory('InstanceGroupList', list)
.factory('JobsList', JobsList)
.factory('InstanceList', InstanceList)
.factory('InstanceJobsList', InstanceJobsList)
.controller('InstanceGroupsList', InstanceGroupsList)
.controller('JobsListController', JobsListController)
.controller('InstanceGroupJobsListController', InstanceGroupJobsListController)
.controller('InstanceListController', InstanceListController)
.controller('InstanceJobsController', InstanceJobsController)
.directive('instanceGroupsMultiselect', instanceGroupsMultiselect)
.directive('instanceGroupsModal', instanceGroupsModal)
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
let stateExtender = $stateExtenderProvider.$get();
.service('InstanceGroupsStrings', InstanceGroupsStrings)
.service('JobStrings', JobStrings)
.run(InstanceGroupsRun);
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: () => generateInstanceGroupsStates()
});
}]);
export default MODULE_NAME;

View File

@ -11,6 +11,7 @@
</div>
<div class="MultiSelectPreview-previewTag MultiSelectPreview-previewTag--deletable">
<span>{{selectedRow.name}}</span>
<span ng-if="selectedRow.hostname">{{selectedRow.hostname}}</span>
</div>
</div>
</div>