diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js
index 93f5ab1416..ddc765533b 100644
--- a/awx/ui/client/lib/components/components.strings.js
+++ b/awx/ui/client/lib/components/components.strings.js
@@ -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 = {
diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js
index 6f15cae762..d33e79329a 100644
--- a/awx/ui/client/lib/components/index.js
+++ b/awx/ui/client/lib/components/index.js
@@ -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)
diff --git a/awx/ui/client/lib/components/input/_index.less b/awx/ui/client/lib/components/input/_index.less
index b92692c34e..03589273eb 100644
--- a/awx/ui/client/lib/components/input/_index.less
+++ b/awx/ui/client/lib/components/input/_index.less
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/awx/ui/client/lib/components/input/lookup.directive.js b/awx/ui/client/lib/components/input/lookup.directive.js
index 1e6cc13588..fcee7ad72c 100644
--- a/awx/ui/client/lib/components/input/lookup.directive.js
+++ b/awx/ui/client/lib/components/input/lookup.directive.js
@@ -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 = [
diff --git a/awx/ui/client/lib/components/input/lookup.partial.html b/awx/ui/client/lib/components/input/lookup.partial.html
index 21ebf03b5d..271c24212f 100644
--- a/awx/ui/client/lib/components/input/lookup.partial.html
+++ b/awx/ui/client/lib/components/input/lookup.partial.html
@@ -1,27 +1,45 @@
\ No newline at end of file
diff --git a/awx/ui/client/lib/components/input/slider.directive.js b/awx/ui/client/lib/components/input/slider.directive.js
new file mode 100644
index 0000000000..a2e1b8c28e
--- /dev/null
+++ b/awx/ui/client/lib/components/input/slider.directive.js
@@ -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;
diff --git a/awx/ui/client/lib/components/input/slider.partial.html b/awx/ui/client/lib/components/input/slider.partial.html
new file mode 100644
index 0000000000..e4649149f8
--- /dev/null
+++ b/awx/ui/client/lib/components/input/slider.partial.html
@@ -0,0 +1,13 @@
+
diff --git a/awx/ui/client/lib/components/list/_index.less b/awx/ui/client/lib/components/list/_index.less
index cd1fa9a023..77997bbbcc 100644
--- a/awx/ui/client/lib/components/list/_index.less
+++ b/awx/ui/client/lib/components/list/_index.less
@@ -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;
diff --git a/awx/ui/client/lib/components/list/row-item.directive.js b/awx/ui/client/lib/components/list/row-item.directive.js
index 972008f7a2..e07820468e 100644
--- a/awx/ui/client/lib/components/list/row-item.directive.js
+++ b/awx/ui/client/lib/components/list/row-item.directive.js
@@ -7,10 +7,13 @@ function atRowItem () {
transclude: true,
templateUrl,
scope: {
+ badge: '@',
headerValue: '@',
headerLink: '@',
headerTag: '@',
labelValue: '@',
+ labelLink: '@',
+ labelState: '@',
value: '@',
valueLink: '@',
smartStatus: '=?',
diff --git a/awx/ui/client/lib/components/list/row-item.partial.html b/awx/ui/client/lib/components/list/row-item.partial.html
index a9b81ae20c..ca58947b79 100644
--- a/awx/ui/client/lib/components/list/row-item.partial.html
+++ b/awx/ui/client/lib/components/list/row-item.partial.html
@@ -9,13 +9,19 @@
-
-
+
\ No newline at end of file
diff --git a/awx/ui/client/lib/components/tabs/tab.partial.html b/awx/ui/client/lib/components/tabs/tab.partial.html
index 263a5d1d96..747e470571 100644
--- a/awx/ui/client/lib/components/tabs/tab.partial.html
+++ b/awx/ui/client/lib/components/tabs/tab.partial.html
@@ -1,6 +1,7 @@
diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js
index 8845c24f82..7fafb05c75 100644
--- a/awx/ui/client/lib/models/Base.js
+++ b/awx/ui/client/lib/models/Base.js
@@ -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);
diff --git a/awx/ui/client/lib/models/Instance.js b/awx/ui/client/lib/models/Instance.js
new file mode 100644
index 0000000000..09b7df0547
--- /dev/null
+++ b/awx/ui/client/lib/models/Instance.js
@@ -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;
diff --git a/awx/ui/client/lib/models/InstanceGroup.js b/awx/ui/client/lib/models/InstanceGroup.js
new file mode 100644
index 0000000000..cc82432c42
--- /dev/null
+++ b/awx/ui/client/lib/models/InstanceGroup.js
@@ -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;
diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js
new file mode 100644
index 0000000000..9be420b2f9
--- /dev/null
+++ b/awx/ui/client/lib/models/Job.js
@@ -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;
diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js
index 6dbea4c954..3c43af7915 100644
--- a/awx/ui/client/lib/models/index.js
+++ b/awx/ui/client/lib/models/index.js
@@ -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)
diff --git a/awx/ui/client/lib/theme/_global.less b/awx/ui/client/lib/theme/_global.less
index b1e46927ef..b62f501c33 100644
--- a/awx/ui/client/lib/theme/_global.less
+++ b/awx/ui/client/lib/theme/_global.less
@@ -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%;
}
diff --git a/awx/ui/client/lib/theme/_mixins.less b/awx/ui/client/lib/theme/_mixins.less
index 6dc36a7b24..d40373c836 100644
--- a/awx/ui/client/lib/theme/_mixins.less
+++ b/awx/ui/client/lib/theme/_mixins.less
@@ -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;
+}
\ No newline at end of file
diff --git a/awx/ui/client/lib/theme/_variables.less b/awx/ui/client/lib/theme/_variables.less
index be5cde41ee..cd3a8a6675 100644
--- a/awx/ui/client/lib/theme/_variables.less
+++ b/awx/ui/client/lib/theme/_variables.less
@@ -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;
diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less
index 1f0d3dd254..6b3f241ffd 100644
--- a/awx/ui/client/lib/theme/index.less
+++ b/awx/ui/client/lib/theme/index.less
@@ -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';
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js
index e1667e6fb8..069ee2dd80 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -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()) {
diff --git a/awx/ui/client/src/instance-groups/add-edit/add-edit-instance-groups.view.html b/awx/ui/client/src/instance-groups/add-edit/add-edit-instance-groups.view.html
new file mode 100644
index 0000000000..a980d74ec8
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/add-edit/add-edit-instance-groups.view.html
@@ -0,0 +1,34 @@
+
+
+ {{ vm.panelTitle }}
+
+
+
+ {{:: vm.strings.get('tab.DETAILS') }}
+ {{:: vm.strings.get('tab.INSTANCES') }}
+ {{:: vm.strings.get('tab.JOBS') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/add-edit/add-instance-group.controller.js b/awx/ui/client/src/instance-groups/add-edit/add-instance-group.controller.js
new file mode 100644
index 0000000000..9fc838115b
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/add-edit/add-instance-group.controller.js
@@ -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;
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/add-edit/add-instance-list-policy.controller.js b/awx/ui/client/src/instance-groups/add-edit/add-instance-list-policy.controller.js
new file mode 100644
index 0000000000..c7819ba797
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/add-edit/add-instance-list-policy.controller.js
@@ -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;
diff --git a/awx/ui/client/src/instance-groups/add-edit/add-instance-list-policy.partial.html b/awx/ui/client/src/instance-groups/add-edit/add-instance-list-policy.partial.html
new file mode 100644
index 0000000000..29493add34
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/add-edit/add-instance-list-policy.partial.html
@@ -0,0 +1,53 @@
+
+
+
+ {{ vm.panelTitle }} | {{ vm.instanceGroupName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{:: vm.strings.get('CANCEL') }}
+
+
+ {{:: vm.strings.get('SAVE') }}
+
+
+
+
+
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/add-edit/edit-instance-group.controller.js b/awx/ui/client/src/instance-groups/add-edit/edit-instance-group.controller.js
new file mode 100644
index 0000000000..49b197c6ec
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/add-edit/edit-instance-group.controller.js
@@ -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;
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less
index 1caba245f0..5668eef8ad 100644
--- a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less
+++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js
index 5ea07d2dd3..7301d6e898 100644
--- a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js
+++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js
@@ -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);
+ }
+ };
+}
+];
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html
index d80ff84bc0..6c1fadb823 100644
--- a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html
+++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html
@@ -1,11 +1,20 @@
+
+ {{labelValue}}
+
+
-{{ consumedCapacity }}
+
+
+ {{ consumedCapacity }}
+
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/instance-groups.partial.html b/awx/ui/client/src/instance-groups/instance-groups.partial.html
index baeaf59f00..217efd98d0 100644
--- a/awx/ui/client/src/instance-groups/instance-groups.partial.html
+++ b/awx/ui/client/src/instance-groups/instance-groups.partial.html
@@ -1,11 +1,13 @@
diff --git a/awx/ui/client/src/instance-groups/instance-groups.route.js b/awx/ui/client/src/instance-groups/instance-groups.route.js
deleted file mode 100644
index c265b35cd9..0000000000
--- a/awx/ui/client/src/instance-groups/instance-groups.route.js
+++ /dev/null
@@ -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`]);
- }
- ]
- }
-};
diff --git a/awx/ui/client/src/instance-groups/instance-groups.strings.js b/awx/ui/client/src/instance-groups/instance-groups.strings.js
new file mode 100644
index 0000000000..a351fe0ca8
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/instance-groups.strings.js
@@ -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;
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js
deleted file mode 100644
index 1d82ca854e..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js
+++ /dev/null
@@ -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`]);
- }
- ],
- }
-};
diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js
index a7d50764f5..5fbce21d4b 100644
--- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js
+++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js
@@ -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();
- }
- );
}
-];
\ No newline at end of file
+
+ 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;
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js
deleted file mode 100644
index 58476f0054..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js
+++ /dev/null
@@ -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: `
-
- W
-
- `
- },
- 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',
- },
- }
- };
-}];
diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html
deleted file mode 100644
index 9c40fe931f..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js
deleted file mode 100644
index 7e9be9a9de..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js
+++ /dev/null
@@ -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
- });
- });
- }]
- }
-};
diff --git a/awx/ui/client/src/instance-groups/instances/instance-modal.block.less b/awx/ui/client/src/instance-groups/instances/instance-modal.block.less
new file mode 100644
index 0000000000..49ea9e10e7
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/instances/instance-modal.block.less
@@ -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;
+ }
+}
diff --git a/awx/ui/client/src/instance-groups/instances/instance-modal.controller.js b/awx/ui/client/src/instance-groups/instances/instance-modal.controller.js
new file mode 100644
index 0000000000..dcb2c436d6
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/instances/instance-modal.controller.js
@@ -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;
diff --git a/awx/ui/client/src/instance-groups/instances/instance-modal.partial.html b/awx/ui/client/src/instance-groups/instances/instance-modal.partial.html
new file mode 100644
index 0000000000..0eb9187064
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/instances/instance-modal.partial.html
@@ -0,0 +1,55 @@
+
+
+
+ {{ vm.panelTitle }} | {{ vm.instanceGroupName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{:: vm.strings.get('CANCEL') }}
+
+
+ {{:: vm.strings.get('SAVE') }}
+
+
+
+
+
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.partial.html b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html
index da8f052423..814588a101 100644
--- a/awx/ui/client/src/instance-groups/instances/instances-list.partial.html
+++ b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html
@@ -1,44 +1,62 @@
-
-
-
+
+
+ {{ vm.panelTitle }}
+
- PLEASE ADD ITEMS TO THIS LIST
-
-
+
+ {{:: vm.strings.get('tab.DETAILS') }}
+ {{:: vm.strings.get('tab.INSTANCES') }}
+ {{:: vm.strings.get('tab.JOBS') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.route.js b/awx/ui/client/src/instance-groups/instances/instances-list.route.js
deleted file mode 100644
index 16549d9d1e..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instances-list.route.js
+++ /dev/null
@@ -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`]);
- }
- ]
- }
-};
diff --git a/awx/ui/client/src/instance-groups/instances/instances.controller.js b/awx/ui/client/src/instance-groups/instances/instances.controller.js
index 0481d84263..6e8de76864 100644
--- a/awx/ui/client/src/instance-groups/instances/instances.controller.js
+++ b/awx/ui/client/src/instance-groups/instances/instances.controller.js
@@ -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;
}
-];
\ No newline at end of file
+
+ 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;
diff --git a/awx/ui/client/src/instance-groups/instances/instances.list.js b/awx/ui/client/src/instance-groups/instances/instances.list.js
deleted file mode 100644
index 048279d6c8..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instances.list.js
+++ /dev/null
@@ -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,
- },
- }
- };
-}];
diff --git a/awx/ui/client/src/instance-groups/instances/instances.route.js b/awx/ui/client/src/instance-groups/instances/instances.route.js
deleted file mode 100644
index 8890171b58..0000000000
--- a/awx/ui/client/src/instance-groups/instances/instances.route.js
+++ /dev/null
@@ -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
- });
- });
- }]
- }
-};
diff --git a/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js
deleted file mode 100644
index 03854eca20..0000000000
--- a/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js
+++ /dev/null
@@ -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`]);
- }
- ]
- }
-};
diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.controller.js b/awx/ui/client/src/instance-groups/jobs/jobs.controller.js
index cfe2f73327..a6054805a0 100644
--- a/awx/ui/client/src/instance-groups/jobs/jobs.controller.js
+++ b/awx/ui/client/src/instance-groups/jobs/jobs.controller.js
@@ -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();
- }
- );
}
-];
\ No newline at end of file
+
+ 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;
\ No newline at end of file
diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.list.js b/awx/ui/client/src/instance-groups/jobs/jobs.list.js
index 59e14ba19b..061674054d 100644
--- a/awx/ui/client/src/instance-groups/jobs/jobs.list.js
+++ b/awx/ui/client/src/instance-groups/jobs/jobs.list.js
@@ -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: `
-
- W
-
- `
- },
- 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: `
+//
+// W
+//
+// `
+// },
+// 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'
+// },
+// }
+// };
+// }];
diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.strings.js b/awx/ui/client/src/instance-groups/jobs/jobs.strings.js
new file mode 100644
index 0000000000..099f7a61b9
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/jobs/jobs.strings.js
@@ -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;
diff --git a/awx/ui/client/src/instance-groups/jobs/list.view.html b/awx/ui/client/src/instance-groups/jobs/list.view.html
new file mode 100644
index 0000000000..bd36b97548
--- /dev/null
+++ b/awx/ui/client/src/instance-groups/jobs/list.view.html
@@ -0,0 +1,103 @@
+
+
+ {{ vm.panelTitle }}
+
+
+ {{:: vm.strings.get('tab.DETAILS') }}
+ {{:: vm.strings.get('tab.INSTANCES') }}
+ {{:: vm.strings.get('tab.JOBS') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js b/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js
index 381e2419bf..4f1145ee1c 100644
--- a/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js
+++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js
@@ -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');
};
}
-];
\ No newline at end of file
+];
diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html
index f3d470afd9..ae2f5df32d 100644
--- a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html
+++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html
@@ -1,63 +1,84 @@
-
+
-
-
+
+
-PLEASE ADD ITEMS TO THIS LIST
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
diff --git a/awx/ui/client/src/instance-groups/main.js b/awx/ui/client/src/instance-groups/main.js
index 4f9410f0e0..8489cf83f5 100644
--- a/awx/ui/client/src/instance-groups/main.js
+++ b/awx/ui/client/src/instance-groups/main.js
@@ -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: `
+
`,
+ }
+ }
+ });
+
+ $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: `
+
`,
+ }
+ }
+ });
+
+ $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: `
+
`,
+ }
+ }
+ });
+
+ $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;
diff --git a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html
index ba96157381..48eb41b1b5 100644
--- a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html
+++ b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html
@@ -11,6 +11,7 @@
{{selectedRow.name}}
+ {{selectedRow.hostname}}