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
54 changed files with 1759 additions and 852 deletions

View File

@@ -71,6 +71,7 @@ function ComponentsStrings (BaseString) {
INVENTORY_SCRIPTS: t.s('Inventory Scripts'), INVENTORY_SCRIPTS: t.s('Inventory Scripts'),
NOTIFICATIONS: t.s('Notifications'), NOTIFICATIONS: t.s('Notifications'),
MANAGEMENT_JOBS: t.s('Management Jobs'), MANAGEMENT_JOBS: t.s('Management Jobs'),
INSTANCES: t.s('Instances'),
INSTANCE_GROUPS: t.s('Instance Groups'), INSTANCE_GROUPS: t.s('Instance Groups'),
SETTINGS: t.s('Settings'), SETTINGS: t.s('Settings'),
FOOTER_ABOUT: t.s('About'), FOOTER_ABOUT: t.s('About'),
@@ -78,7 +79,8 @@ function ComponentsStrings (BaseString) {
}; };
ns.capacityBar = { 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 = { ns.relaunch = {

View File

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

View File

@@ -163,7 +163,7 @@
} }
.at-InputMessage--rejected { .at-InputMessage--rejected {
font-size: @at-font-size-help-text; font-size: @at-font-size-help-text;
color: @at-color-error; color: @at-color-error;
margin: @at-margin-input-message 0 0 0; margin: @at-margin-input-message 0 0 0;
padding: 0; padding: 0;
@@ -182,7 +182,7 @@
& > i { & > i {
font-size: @at-font-size-button; font-size: @at-font-size-button;
position: absolute; position: absolute;
z-index: 3; z-index: 3;
pointer-events: none; pointer-events: none;
top: @at-height-input / 3; top: @at-height-input / 3;
@@ -218,3 +218,37 @@
min-height: @at-height-textarea; min-height: @at-height-textarea;
padding: 6px @at-padding-input 0 @at-padding-input; 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.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 = [ AtInputLookupController.$inject = [

View File

@@ -1,27 +1,45 @@
<div class="col-sm-{{::col}} at-InputContainer"> <div class="col-sm-{{::col}} at-InputContainer">
<div class="form-group at-u-flat"> <div class="form-group at-u-flat">
<at-input-label></at-input-label> <at-input-label></at-input-label>
<div class="input-group"> <div class="input-group">
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn at-ButtonHollow--default at-Input-button" <button class="btn at-ButtonHollow--default at-Input-button"
ng-disabled="state._disabled || form.disabled" ng-disabled="state._disabled || form.disabled"
ng-click="vm.lookup()"> ng-click="vm.lookup()">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
</span> </span>
<input type="text"
class="form-control at-Input" <!-- Refactor / Add conditional -->
ng-class="{ 'at-Input--rejected': state._rejected }"
ng-model="state._displayValue" <input type="text"
ng-attr-tabindex="{{ tab || undefined }}" class="form-control at-Input"
ng-attr-placeholder="{{::state._placeholder || undefined }}" ng-class="{ 'at-Input--rejected': state._rejected }"
ng-change="vm.searchOnInput()" ng-model="state._displayValue"
ng-disabled="state._disabled || form.disabled" /> 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> </div>
<at-input-message></at-input-message> <div ui-view="{{ state._resource }}"></div>
</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; 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 { .at-Row-actions {
display: flex; display: flex;
} }
.at-Row-items { .at-Row-items {
align-self: flex-start; align-self: flex-start;
flex: 1;
} }
.at-RowItem { .at-RowItem {
@@ -101,6 +115,7 @@
} }
.at-RowItem--isHeader { .at-RowItem--isHeader {
color: @at-color-body-text;
margin-bottom: @at-margin-bottom-list-header; margin-bottom: @at-margin-bottom-list-header;
line-height: @at-line-height-list-row-item-header; line-height: @at-line-height-list-row-item-header;
} }
@@ -146,8 +161,26 @@
.at-RowItem-label { .at-RowItem-label {
text-transform: uppercase; text-transform: uppercase;
width: auto;
width: @at-width-list-row-item-label; width: @at-width-list-row-item-label;
color: @at-color-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 { .at-RowAction {
@@ -180,6 +213,11 @@
background-color: @at-color-list-row-action-hover-danger; 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) { @media screen and (max-width: @at-breakpoint-compact-list) {
.at-Row-actions { .at-Row-actions {
flex-direction: column; flex-direction: column;

View File

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

View File

@@ -9,13 +9,19 @@
<div class="at-RowItem-tag at-RowItem-tag--header" ng-if="headerTag"> <div class="at-RowItem-tag at-RowItem-tag--header" ng-if="headerTag">
{{ headerTag }} {{ headerTag }}
</div> </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 }} {{ labelValue }}
</div> </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"> <div class="at-RowItem-value" ng-if="value && valueLink">
<a ng-href="{{ valueLink }}">{{ value }}</a> <a ng-href="{{ valueLink }}">{{ value }}</a>
</div> </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"> ng-bind-html="value">
</div> </div>
<aw-smart-status jobs="smartStatus.summary_fields.recent_jobs" <aw-smart-status jobs="smartStatus.summary_fields.recent_jobs"
@@ -35,4 +41,4 @@
{{ tag.name }} {{ tag.name }}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -129,6 +129,10 @@ function httpPost (config = {}) {
data: config.data data: config.data
}; };
if (config.url) {
req.url = `${this.path}${config.url}`;
}
return $http(req) return $http(req)
.then(res => { .then(res => {
this.model.GET = res.data; this.model.GET = res.data;
@@ -323,7 +327,7 @@ function has (method, keys) {
return value !== undefined && value !== null; return value !== undefined && value !== null;
} }
function extend (method, related) { function extend (method, related, config = {}) {
if (!related) { if (!related) {
related = method; related = method;
method = 'GET'; method = 'GET';
@@ -337,6 +341,8 @@ function extend (method, related) {
url: this.get(`related.${related}`) url: this.get(`related.${related}`)
}; };
Object.assign(req, config);
return $http(req) return $http(req)
.then(({ data }) => { .then(({ data }) => {
this.set(method, `related.${related}`, 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 Project from '~models/Project';
import JobTemplate from '~models/JobTemplate'; import JobTemplate from '~models/JobTemplate';
import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode'; import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode';
import Instance from '~models/Instance';
import InstanceGroup from '~models/InstanceGroup';
import InventorySource from '~models/InventorySource'; import InventorySource from '~models/InventorySource';
import Inventory from '~models/Inventory'; import Inventory from '~models/Inventory';
import InventoryScript from '~models/InventoryScript'; import InventoryScript from '~models/InventoryScript';
@@ -32,6 +34,8 @@ angular
.service('ProjectModel', Project) .service('ProjectModel', Project)
.service('JobTemplateModel', JobTemplate) .service('JobTemplateModel', JobTemplate)
.service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode)
.service('InstanceModel', Instance)
.service('InstanceGroupModel', InstanceGroup)
.service('InventorySourceModel', InventorySource) .service('InventorySourceModel', InventorySource)
.service('InventoryModel', Inventory) .service('InventoryModel', Inventory)
.service('InventoryScriptModel', InventoryScript) .service('InventoryScriptModel', InventoryScript)

View File

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

View File

@@ -21,6 +21,7 @@
} }
.at-mixin-Button () { .at-mixin-Button () {
border-radius: @at-border-radius;
height: @at-height-input; height: @at-height-input;
padding: @at-padding-button-vertical @at-padding-button-horizontal; padding: @at-padding-button-vertical @at-padding-button-horizontal;
font-size: @at-font-size-body; font-size: @at-font-size-body;
@@ -102,3 +103,21 @@
.at-mixin-FontFixedWidth () { .at-mixin-FontFixedWidth () {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 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-icon: @at-gray-b7;
@at-color-input-placeholder: @at-gray-848992; @at-color-input-placeholder: @at-gray-848992;
@at-color-input-text: @at-gray-161b1f; @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-dismiss: @at-gray-d7;
@at-color-icon-popover: @at-gray-848992; @at-color-icon-popover: @at-gray-848992;

View File

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

View File

@@ -97,7 +97,6 @@ angular
users.name, users.name,
projects.name, projects.name,
scheduler.name, scheduler.name,
instanceGroups.name,
'Utilities', 'Utilities',
'templates', 'templates',
@@ -105,6 +104,7 @@ angular
'AWDirectives', 'AWDirectives',
'features', 'features',
instanceGroups,
atFeatures, atFeatures,
atLibComponents, atLibComponents,
atLibModels, atLibModels,
@@ -316,6 +316,21 @@ angular
activateTab(); 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) { $transitions.onSuccess({}, function(trans) {
if(trans.to() === trans.from()) { 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 { capacity-bar {
width: 50%;
margin-right: 25px;
min-width: 100px;
display: flex;
align-items: center; align-items: center;
color: @at-gray-70;
display: flex;
font-size: @at-font-size;
min-width: 100px;
white-space: nowrap;
.CapacityBar { .CapacityBar {
background-color: @default-bg; background-color: @default-bg;
display: flex;
flex: 0 0 auto;
height: 10px;
border: 1px solid @default-link;
width: 100%;
border-radius: 100vw; border-radius: 100vw;
border: 1px solid @default-link;
display: flex;
flex: 1;
height: 10px;
margin-right: @at-space-2x;
min-width: 100px;
overflow: hidden; overflow: hidden;
margin-right: 10px; width: 100%;
} }
.CapacityBar-remaining { .CapacityBar-remaining {
@@ -28,14 +29,21 @@ capacity-bar {
} }
.CapacityBar--offline { .CapacityBar--offline {
border-color: @d7grey; color: @at-red;
border-color: @at-gray-a9;
.CapacityBar-remaining { .CapacityBar-remaining {
background-color: @d7grey; background-color: @at-gray-b7;
} }
} }
.Capacity-details--percentage { .Capacity-details--label {
color: @default-data-txt; 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', export default ['templateUrl', 'ComponentsStrings',
function (templateUrl, strings) { function (templateUrl, strings) {
return { return {
scope: { scope: {
capacity: '=', capacity: '=',
totalCapacity: '=' totalCapacity: '=',
}, labelValue: '@',
templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'), badge: '='
restrict: 'E', },
link: function(scope) { templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'),
scope.isOffline = false; restrict: 'E',
link: function(scope) {
scope.isOffline = false;
scope.$watch('totalCapacity', function(val) { scope.$watch('totalCapacity', function(val) {
if (val === 0) { if (val === 0) {
scope.isOffline = true; scope.isOffline = true;
scope.offlineTip = strings.get(`capacityBar.IS_OFFLINE`); scope.labelValue = strings.get(`capacityBar.IS_OFFLINE_LABEL`);
} else { scope.offlineTip = strings.get(`capacityBar.IS_OFFLINE`);
scope.isOffline = false; } else {
scope.offlineTip = null; scope.isOffline = false;
} scope.offlineTip = null;
}, true); }
}, true);
scope.$watch('capacity', function() { scope.$watch('capacity', function() {
if (scope.totalCapacity !== 0) { if (scope.totalCapacity !== 0) {
var percentageCapacity = Math var percentageCapacity = Math
.round(scope.capacity / scope.totalCapacity * 1000) / 10; .round(scope.capacity / scope.totalCapacity * 1000) / 10;
scope.CapacityStyle = { scope.CapacityStyle = {
'flex-grow': percentageCapacity * 0.01 'flex-grow': percentageCapacity * 0.01
}; };
scope.consumedCapacity = `${percentageCapacity}%`; scope.consumedCapacity = `${percentageCapacity}%`;
} else { } else {
scope.CapacityStyle = { scope.CapacityStyle = {
'flex-grow': 1 'flex-grow': 1
}; };
scope.consumedCapacity = null; scope.consumedCapacity = null;
} }
}, true); }, true);
} }
}; };
} }
]; ];

View File

@@ -1,11 +1,20 @@
<span class="Capacity-details--label" ng-class="{'CapacityBar--offline': isOffline}">
{{labelValue}}
</span>
<div class="CapacityBar" <div class="CapacityBar"
ng-class="{'CapacityBar--offline': isOffline}" ng-class="{'CapacityBar--offline': isOffline}"
ng-if="!badge"
aw-tool-tip="{{ offlineTip }}" aw-tool-tip="{{ offlineTip }}"
data-tip-watch="offlineTip" data-tip-watch="offlineTip"
data-placement="top" data-placement="top"
data-trigger="hover" data-trigger="hover"
data-container="body"> data-container="body">
<div class="CapacityBar-remaining" ng-style="CapacityStyle"></div> <div class="CapacityBar-remaining" ng-style="CapacityStyle"></div>
<div class="CapacityBar-consumed"></div> <div class="CapacityBar-consumed"></div>
</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"> <div class="tab-pane InstanceGroups" id="instance-groups-panel">
<aw-limit-panels max-panels="2" panel-container="instance-groups-panel"></aw-limit-panels> <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="instanceJobs"></div>
<div ui-view="instances"></div> <div ui-view="instances"></div>
<div ng-cloak id="htmlTemplate" class="Panel"> <div ui-view="jobs"></div>
<div ui-view="list"></div>
</div> <div ui-view="list"></div>
</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(){ function init(){
$scope.optionsDefer = $q.defer(); vm.strings = strings;
$scope.list = list; vm.jobStrings = jobStrings;
$scope[`${list.iterator}_dataset`] = Dataset.data; vm.queryset = { page_size: '10', order_by: '-finished'};
$scope[list.name] = $scope[`${list.iterator}_dataset`].results; 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){ vm.tab = {
$scope.options = data.data.actions.GET; details: {_hide: true},
optionsRequestDataProcessing(); instances: {_hide: true},
}); jobs: {_hide: true}
// 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();
}
);
} }
];
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"> <at-panel>
<smart-search django-model="instances" base-path="instances" iterator="instance" dataset="instance_dataset" <at-panel-heading>
list="list" collection="instances" search-tags="searchTags"> {{ vm.panelTitle }}
</smart-search> </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> <at-tab-group>
<div class="list-table-container" ng-show="instances.length > 0"> <at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
<table id="instances_table" class="List-table" is-extended="false"> <at-tab state="vm.tab.instances">{{:: vm.strings.get('tab.INSTANCES') }}</at-tab>
<thead> <at-tab state="vm.tab.jobs" ng-hide="$state.current.name === 'instanceGroups.add'">{{:: vm.strings.get('tab.JOBS') }}</at-tab>
<tr class="List-tableHeaderRow"> </at-tab-group>
<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" <at-panel-body
dataset="instance_dataset" column-sort="" column-field="hostname" column-iterator="instance" column-no-sort="undefined" style="margin-top: 20px">
column-label="Name" column-custom-class="col-md-5 col-sm-5 col-xs-5" query-set="instance_queryset"> <div class="at-List-toolbar">
"{{'Name' | translate}}" <smart-search
<i ng-if="columnNoSort !== 'true'" class="fa columnSortIcon fa-sort-up" ng-class="orderByIcon()"></i> class="at-List-search"
</th> django-model="instances"
<th id="instance-jobs_running-header" class="List-tableHeader list-header list-header-noSort" translate> base-path="instances"
Running Jobs iterator="instance"
</th> list="list"
<th id="instance-consumed_capacity-header" class="List-tableHeader list-header list-header-noSort" translate> dataset="vm.instances"
Used Capacity collection="collection"
</th> search-tags="searchTags">
</tr> </smart-search>
</thead> <!-- TODO: only show when permission -->
<tbody> <div class="at-List-toolbarAction">
<!-- ngRepeat: instance in instances --> <button
<tr ng-class="{isActive: isActive(instance.id)}" id="instance.id" class="List-tableRow instance_class ng-scope" ng-repeat="instance in instances"> type="button"
<td class="List-tableCell hostname-column col-md-5 col-sm-5 col-xs-5"> ng-click="$state.go('instanceGroups.instances.modal.add')"
<a ui-sref="instanceGroups.instances.list.job.list({instance_id: instance.id})" class="ng-binding">{{ instance.hostname }}</a> class="at-Button--add"
</td> aria-haspopup="true"
<td class="List-tableCell jobs_running-column ng-binding"> aria-expanded="false">
<a ui-sref="instanceGroups.instances.jobs({instance_group_id: $stateParams.instance_group_id})"> </button>
{{ instance.jobs_running }} <div ui-view="modal"></div>
</a> </div>
</td> </div>
<td class="List-tableCell List-tableCell--capacityColumn ng-binding"> <at-list results='vm.instances'>
<capacity-bar capacity="instance.consumed_capacity" total-capacity="instance.capacity"> <at-row ng-repeat="instance in vm.instances"
</td> ng-class="{'at-Row--active': (instance.id === vm.activeId)}">
</tr>
</tbody> <div class="at-Row-items">
</table> <at-row-item
</div> header-value="{{ instance.hostname }}"
</div> 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 InstancesController ($scope, $state, models, strings, Dataset) {
function($scope, InstanceList, GetBasePath, Rest, Dataset, Find, $state, $q) { const { instanceGroup } = models;
let list = InstanceList; 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(){ function init() {
$scope.optionsDefer = $q.defer(); $scope.list = {
$scope.list = list; iterator: 'instance',
$scope[`${list.iterator}_dataset`] = Dataset.data; name: 'instances'
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
$scope.isActive = function(id) {
let selected = parseInt($state.params.instance_id);
return id === selected;
}; };
$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(){ function init(){
$scope.optionsDefer = $q.defer(); let instance_group_id = instanceGroup.get('id');
$scope.list = list; vm.strings = strings;
$scope[`${list.iterator}_dataset`] = Dataset.data; vm.jobStrings = jobStrings;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results; 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){ vm.tab = {
$scope.options = data.data.actions.GET; details: {
optionsRequestDataProcessing(); _go: 'instanceGroups.edit',
}); _params: { instance_group_id },
_label: strings.get('tab.DETAILS')
// iterate over the list and add fields like type label, after the },
// OPTIONS request returns, or the list is sorted/paginated/searched instances: {
function optionsRequestDataProcessing(){ _go: 'instanceGroups.instances',
_params: { instance_group_id },
if($scope[list.name] && $scope[list.name].length > 0) { _label: strings.get('tab.INSTANCES')
$scope[list.name].forEach(function(item, item_idx) { },
var itm = $scope[list.name][item_idx]; jobs: {
if(item.summary_fields && item.summary_fields.source_workflow_job && _active: true,
item.summary_fields.source_workflow_job.id){ _label: strings.get('tab.JOBS')
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();
}
);
} }
];
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) { // export default ['i18n', function (i18n) {
return { // return {
name: 'jobs', // name: 'jobs',
iterator: 'job', // iterator: 'job',
basePath: 'api/v2/instance_groups/{{$stateParams.instance_group_id}}/jobs/', // basePath: 'api/v2/instance_groups/{{$stateParams.instance_group_id}}/jobs/',
index: false, // index: false,
hover: false, // hover: false,
well: true, // well: true,
emptyListText: i18n._('No jobs have yet run.'), // emptyListText: i18n._('No jobs have yet run.'),
listTitle: false, // listTitle: false,
fields: { // fields: {
status: { // status: {
label: '', // label: '',
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', // columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus',
dataTipWatch: 'job.status_tip', // dataTipWatch: 'job.status_tip',
awToolTip: "{{ job.status_tip }}", // awToolTip: "{{ job.status_tip }}",
awTipPlacement: "right", // awTipPlacement: "right",
dataTitle: "{{ job.status_popover_title }}", // dataTitle: "{{ job.status_popover_title }}",
icon: 'icon-job-{{ job.status }}', // icon: 'icon-job-{{ job.status }}',
iconOnly: true, // iconOnly: true,
ngClick: "viewjobResults(job)", // ngClick: "viewjobResults(job)",
nosort: true // nosort: true
}, // },
id: { // id: {
label: i18n._('ID'), // label: i18n._('ID'),
ngClick: "viewjobResults(job)", // ngClick: "viewjobResults(job)",
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', // columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent',
awToolTip: "{{ job.status_tip }}", // awToolTip: "{{ job.status_tip }}",
dataPlacement: 'top', // dataPlacement: 'top',
noLink: true // noLink: true
}, // },
name: { // name: {
label: i18n._('Name'), // label: i18n._('Name'),
columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', // columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6',
ngClick: "viewjobResults(job)", // ngClick: "viewjobResults(job)",
badgePlacement: 'right', // badgePlacement: 'right',
badgeCustom: true, // badgeCustom: true,
nosort: true, // nosort: true,
badgeIcon: `<a href="{{ job.workflow_result_link }}" // badgeIcon: `<a href="{{ job.workflow_result_link }}"
aw-tool-tip="{{'View workflow results'|translate}}" // aw-tool-tip="{{'View workflow results'|translate}}"
data-placement="top" // data-placement="top"
data-original-title="" title=""> // data-original-title="" title="">
<i class="WorkflowBadge" // <i class="WorkflowBadge"
ng-show="job.launch_type === 'workflow' "> // ng-show="job.launch_type === 'workflow' ">
W // W
</i> // </i>
</a>` // </a>`
}, // },
type: { // type: {
label: i18n._('Type'), // label: i18n._('Type'),
ngBind: 'job.type_label', // ngBind: 'job.type_label',
columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs", // columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs",
nosort: true // nosort: true
}, // },
finished: { // finished: {
label: i18n._('Finished'), // label: i18n._('Finished'),
noLink: true, // noLink: true,
filter: "longDate", // filter: "longDate",
columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs", // columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs",
key: true, // key: true,
desc: true, // desc: true,
nosort: true // nosort: true
}, // },
labels: { // labels: {
label: i18n._('Labels'), // label: i18n._('Labels'),
type: 'labels', // type: 'labels',
nosort: true, // nosort: true,
showDelete: false, // showDelete: false,
columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs', // columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs',
sourceModel: 'labels', // sourceModel: 'labels',
sourceField: 'name' // 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', export default ['$scope', 'InstanceGroupList', 'resolvedModels', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', 'ComponentsStrings',
function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state) { function($scope, InstanceGroupList, resolvedModels, GetBasePath, Rest, Dataset, Find, $state, $q, strings) {
let list = InstanceGroupList; let list = InstanceGroupList;
const vm = this;
const { instanceGroup } = resolvedModels;
init(); init();
function init(){ function init(){
vm.panelTitle = strings.get('layout.INSTANCE_GROUPS');
$scope.list = list; $scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data; $scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results; $scope[list.name] = $scope[`${list.iterator}_dataset`].results;
$scope.instanceGroupCount = Dataset.data.count; $scope.instanceGroupCount = Dataset.data.count;
} }
$scope.isActive = function(id) { $scope.selection = {};
let selected = parseInt($state.params.instance_group_id);
return id === selected; $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"> <at-panel>
<div class="List-title"> <at-panel-heading hide-dismiss="true">
<div class="List-titleText" translate> {{ vm.panelTitle }}
INSTANCE GROUPS
</div>
<span class="badge List-titleBadge"> <span class="badge List-titleBadge">
{{ instanceGroupCount }} {{ instanceGroupCount }}
</span> </span>
</div> </at-panel-heading>
</div>
<smart-search django-model="instance_groups" base-path="instance_groups" iterator="instance_group" list="list" dataset="instance_group_dataset" <at-panel-body>
collection="instance_groups" search-tags="searchTags"> <div class="at-List-toolbar">
</smart-search> <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"> <input type="checkbox"
<table id="instance_groups_table" class="List-table" is-extended="false"> class="at-Row-checkbox"
<thead> ng-model="selection[instance_group.id]"/>
<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()" <div class="at-Row-items">
ng-class="{'list-header-noSort' : columnNoSort === 'true'}" base-path="instance_groups" collection="instance_groups" <at-row-item
dataset="instance_group_dataset" column-sort="" column-field="name" column-iterator="instance_group" column-no-sort="undefined" header-value="{{ instance_group.name }}"
column-label="Name" column-custom-class="col-md-5 col-sm-5 col-xs-5" query-set="instance_group_queryset"> header-link="/#/instance_groups/{{ instance_group.id }}">
"{{'Name' | translate}}" </at-row-item>
<i ng-if="columnNoSort !== 'true'" class="fa columnSortIcon fa-sort-up" ng-class="orderByIcon()"></i>
</th> <div class="at-Row--rowLayout">
<th id="instance_group-jobs_running-header" class="List-tableHeader list-header list-header-noSort" translate> <at-row-item
Running Jobs label-value="Instances"
</th> label-link="/#/instance_groups/{{ instance_group.id }}/instances"
<th id="instance_group-consumed_capacity-header" class="List-tableHeader list-header list-header-noSort" translate> value="{{ instance_group.instances }}"
Used Capacity badge="true">
</th> </at-row-item>
</tr>
</thead> <at-row-item
<tbody> label-value="Running Jobs"
<!-- ngRepeat: instance_group in instance_groups --> label-link="/#/instance_groups/{{ instance_group.id }}/jobs"
<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"> value="{{ instance_group.jobs_running }}"
<td class="List-tableCell name-column col-md-5 col-sm-5 col-xs-5"> badge="true">
<a ui-sref="instanceGroups.instances.list({instance_group_id: instance_group.id})" class="ng-binding" >{{ instance_group.name }}</a> </at-row-item>
<span class="badge List-titleBadge">{{ instance_group.instances }}</span> </div>
</td>
<td class="List-tableCell jobs_running-column ng-binding"> </div>
<a ui-sref="instanceGroups.instances.jobs({instance_group_id: instance_group.id})">
{{ instance_group.jobs_running }} <div class="at-Row-actions">
</a> <capacity-bar label-value="Used Capacity" capacity="instance_group.consumed_capacity" total-capacity="instance_group.capacity"></capacity-bar>
</td> </div>
<td class="List-tableCell List-tableCell--capacityColumn ng-binding"> </at-row>
<capacity-bar capacity="instance_group.consumed_capacity" total-capacity="instance_group.capacity"></capacity-bar> </at-list>
</td> </at-panel-body>
</tr> <paginate
</tbody> base-path="instance_groups"
</table> iterator="instance_group"
</div> 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 InstanceGroupsList from './list/instance-groups-list.controller';
import instanceGroupsMultiselect from '../shared/instance-groups-multiselect/instance-groups.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 instanceGroupsModal from '../shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive';
import instanceGroupsRoute from './instance-groups.route'; import InstanceGroupJobsListController from './jobs/jobs.controller';
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 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 InstanceJobsController from './instances/instance-jobs/instance-jobs.controller';
import CapacityBar from './capacity-bar/main'; import CapacityBar from './capacity-bar/main';
import list from './instance-groups.list'; import list from './instance-groups.list';
import service from './instance-groups.service'; import service from './instance-groups.service';
export default import { templateUrl } from '../shared/template-url/template-url.factory';
angular.module('instanceGroups', [CapacityBar.name])
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) .service('InstanceGroupsService', service)
.factory('InstanceGroupList', list) .factory('InstanceGroupList', list)
.factory('JobsList', JobsList)
.factory('InstanceList', InstanceList)
.factory('InstanceJobsList', InstanceJobsList)
.controller('InstanceGroupsList', InstanceGroupsList) .controller('InstanceGroupsList', InstanceGroupsList)
.controller('JobsListController', JobsListController) .controller('InstanceGroupJobsListController', InstanceGroupJobsListController)
.controller('InstanceListController', InstanceListController) .controller('InstanceListController', InstanceListController)
.controller('InstanceJobsController', InstanceJobsController) .controller('InstanceJobsController', InstanceJobsController)
.directive('instanceGroupsMultiselect', instanceGroupsMultiselect) .directive('instanceGroupsMultiselect', instanceGroupsMultiselect)
.directive('instanceGroupsModal', instanceGroupsModal) .directive('instanceGroupsModal', instanceGroupsModal)
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', .service('InstanceGroupsStrings', InstanceGroupsStrings)
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { .service('JobStrings', JobStrings)
let stateExtender = $stateExtenderProvider.$get(); .run(InstanceGroupsRun);
export default MODULE_NAME;
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()
});
}]);

View File

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