diff --git a/app/components/labels-section/component.js b/app/components/labels-section/component.js
index 53b699833..6758d741d 100644
--- a/app/components/labels-section/component.js
+++ b/app/components/labels-section/component.js
@@ -1,4 +1,5 @@
import Ember from 'ember';
+import C from 'ui/utils/constants';
export default Ember.Component.extend({
model: null,
@@ -8,7 +9,7 @@ export default Ember.Component.extend({
var obj = this.get('model')||{};
var keys = Ember.keys(obj);
keys.forEach(function(key) {
- var isUser = key.indexOf('io.rancher') !== 0;
+ var isUser = key.indexOf(C.LABEL.SYSTEM_PREFIX) !== 0;
out.push(Ember.Object.create({
key: key,
value: obj[key],
diff --git a/app/components/radio-button/component.js b/app/components/radio-button/component.js
index a319174c0..c3c45316d 100644
--- a/app/components/radio-button/component.js
+++ b/app/components/radio-button/component.js
@@ -4,10 +4,10 @@ export default Ember.Component.extend({
tagName: 'input',
type: 'radio',
disabled: false,
- attributeBindings: ['name', 'type', 'value', 'checked:checked', 'disabled:disabled'],
+ attributeBindings: ['name', 'type', 'checked:checked', 'disabled:disabled'],
click : function() {
- this.set('selection', this.$().val());
+ this.set('selection', this.get('value'));
},
checked : function() {
diff --git a/app/components/scheduling-rule-row/component.js b/app/components/scheduling-rule-row/component.js
new file mode 100644
index 000000000..7244cb222
--- /dev/null
+++ b/app/components/scheduling-rule-row/component.js
@@ -0,0 +1,252 @@
+import Ember from 'ember';
+import C from 'ui/utils/constants';
+
+function splitEquals(str) {
+ var idx = str.indexOf('=');
+ if ( idx === -1 )
+ {
+ return null;
+ }
+
+ return [ str.substr(0,idx) , str.substr(idx+1) ];
+}
+
+export default Ember.Component.extend({
+ rule: null,
+ instance: null,
+ allHosts: null,
+
+ tagName: 'TR',
+
+ kind: null,
+ suffix: null,
+ userKey: null,
+ userValue: null,
+
+ actions: {
+ setKey: function(key) {
+ this.set('userKey', key);
+ },
+
+ setValue: function(value) {
+ this.set('userValue', value);
+ },
+
+ remove: function() {
+ this.sendAction('remove', this.get('rule'));
+ }
+ },
+
+ init: function() {
+ this._super();
+
+ var key = this.get('rule.key')||'';
+ var value = this.get('rule.value')||'';
+ var splitValue = splitEquals(value);
+
+ var match = key.match(/((_soft)?(_ne)?)$/);
+ if ( match )
+ {
+ this.set('suffix', match[1]);
+ key = key.substr(0, key.length - match[1].length);
+ }
+ else
+ {
+ this.set('suffix','');
+ }
+
+ // Convert from an existing key into the 4 fields
+ switch ( key )
+ {
+ case C.LABEL.SCHED_CONTAINER:
+ this.setProperties({
+ kind: 'container_name',
+ userKey: '',
+ userValue: value,
+ });
+ break;
+ case C.LABEL.SCHED_CONTAINER_LABEL:
+ if ( splitValue[0] === C.LABEL.SERVICE_NAME )
+ {
+ this.setProperties({
+ kind: 'service_name',
+ userKey: '',
+ userValue: splitValue[1],
+ });
+ }
+ else
+ {
+ this.setProperties({
+ kind: 'container_label',
+ userKey: splitValue[0],
+ userValue: splitValue[1],
+ });
+ }
+ break;
+ case C.LABEL.SCHED_HOST_LABEL:
+ this.setProperties({
+ kind: 'host_label',
+ userKey: splitValue[0],
+ userValue: splitValue[1],
+ });
+ break;
+ }
+ },
+
+ valuesChanged: function() {
+ var key = '';
+ var value = null;
+
+ var userKey = this.get('userKey')||'';
+ var userValue = this.get('userValue')||'';
+
+ switch ( this.get('kind') )
+ {
+ case 'host_label':
+ key = C.LABEL.SCHED_HOST_LABEL;
+ if ( userKey && userValue )
+ {
+ value = encodeURIComponent(userKey) + '=' + encodeURIComponent(userValue);
+ }
+ break;
+ case 'container_label':
+ key = C.LABEL.SCHED_CONTAINER_LABEL;
+ if ( userKey && userValue )
+ {
+ value = encodeURIComponent(userKey) + '=' + encodeURIComponent(userValue);
+ }
+ break;
+ case 'container_name':
+ key = C.LABEL.SCHED_CONTAINER;
+ if ( userValue )
+ {
+ value = encodeURIComponent(userValue);
+ }
+ break;
+ case 'service_name':
+ key = C.LABEL.SCHED_CONTAINER_LABEL;
+ if ( userValue )
+ {
+ value = encodeURIComponent(C.LABEL.SERVICE_NAME) + '=' + encodeURIComponent(userValue);
+ }
+ break;
+ }
+
+ key += this.get('suffix');
+
+ Ember.setProperties(this.get('rule'),{
+ key: key,
+ value: value
+ });
+ }.observes('kind','suffix','userKey','userValue'),
+
+ schedulingRuleSuffixChoices: [
+ {label: 'must', value: ''},
+ {label: 'should', value: '_soft'},
+ {label: 'should not', value: '_soft_ne'},
+ {label: 'must not', value: '_ne'},
+ ],
+
+ schedulingRuleKindChoices: [
+ {label: 'host label', value: 'host_label'},
+ {label: 'container with label', value: 'container_label'},
+ {label: 'service with the name', value: 'service_name'},
+ {label: 'container with the name', value: 'container_name'},
+ ],
+
+ hostLabelKeyChoices: function() {
+ var out = [];
+ this.get('allHosts').forEach((host) => {
+ var keys = Ember.keys(host.get('labels')||{}).filter((key) => {
+ return key.indexOf(C.LABEL.SYSTEM_PREFIX) !== 0;
+ });
+ out.pushObjects(keys);
+ });
+
+ return out.sort().uniq();
+ }.property('allHosts.@each.labels'),
+
+ hostLabelValueChoices: function() {
+ var key = this.get('userKey');
+
+ var out = [];
+ this.get('allHosts').forEach((host) => {
+ var label = (host.get('labels')||{})[key];
+ if ( label )
+ {
+ var parts = label.split(/\s*,\s*/);
+ out.pushObjects(parts);
+ }
+ });
+
+ return out.sort().uniq();
+ }.property('userKey','allHosts.@each.labels'),
+
+ allContainers: function() {
+ var out = [];
+ this.get('allHosts').map((host) => {
+ var containers = (host.get('instances')||[]).filter(function(instance) {
+ return instance.get('kind') === 'container' &&
+ !instance.get('systemContainer');
+ });
+
+ out.pushObjects(containers);
+ });
+
+ return out.sortBy('name','id').uniq();
+ }.property('allHosts.@each.instancesUpdated'),
+
+ containerLabelKeyChoices: function() {
+ var out = [];
+ this.get('allContainers').forEach((container) => {
+ var keys = Ember.keys(container.get('labels')||{}).filter((key) => {
+ return key.indexOf(C.LABEL.SYSTEM_PREFIX) !== 0;
+ });
+ out.pushObjects(keys);
+ });
+
+ return out.sort().uniq();
+ }.property('allContainers.@each.labels'),
+
+ containerLabelValueChoices: function() {
+ var key = this.get('userKey');
+ var out = [];
+ this.get('allContainers').forEach((container) => {
+ var label = (container.get('labels')||{})[key];
+ if ( label )
+ {
+ var parts = label.split(/\s*,\s*/);
+ out.pushObjects(parts);
+ }
+ });
+
+ return out.sort().uniq();
+ }.property('userKey','allContainers.@each.labels'),
+
+ containerValueChoices: function() {
+ var out = [];
+ this.get('allContainers').forEach((container) => {
+ var name = container.get('name');
+ if ( name )
+ {
+ out.push(name);
+ }
+ });
+
+ return out.sort().uniq();
+ }.property('allContainers.@each.name'),
+
+ serviceValueChoices: function() {
+ var out = [];
+ this.get('allContainers').forEach((container) => {
+ var label = (container.get('labels')||{})[C.LABEL.SERVICE_NAME];
+ if ( label )
+ {
+ var parts = label.split(/\s*,\s*/);
+ out.pushObjects(parts);
+ }
+ });
+
+ return out.sort().uniq();
+ }.property('allContainers.@each.labels'),
+});
diff --git a/app/components/scheduling-rule-row/template.hbs b/app/components/scheduling-rule-row/template.hbs
new file mode 100644
index 000000000..e020da7b9
--- /dev/null
+++ b/app/components/scheduling-rule-row/template.hbs
@@ -0,0 +1,125 @@
+
The host |
+
+ {{view "select"
+ class="form-control input-sm"
+ content=schedulingRuleSuffixChoices
+ value=suffix
+ optionValuePath="content.value"
+ optionLabelPath="content.label"
+ }}
+ |
+have a |
+
+ {{view "select"
+ class="form-control input-sm"
+ content=schedulingRuleKindChoices
+ value=kind
+ optionValuePath="content.value"
+ optionLabelPath="content.label"
+ }}
+ |
+
+ {{#if (not (or (eq kind "service_name") (eq kind "container_name")))}}
+ of
+ {{/if}}
+ |
+
+ {{#if (eq kind "host_label")}}
+
+ {{/if}}
+ {{#if (eq kind "container_label")}}
+
+ {{/if}}
+ |
+
+ {{#if (not (or (eq kind "service_name") (eq kind "container_name")))}}
+ =
+ {{/if}}
+ |
+
+ {{#if (eq kind "host_label")}}
+
+ {{/if}}
+ {{#if (eq kind "container_label")}}
+
+ {{/if}}
+ {{#if (eq kind "service_name")}}
+
+ {{/if}}
+ {{#if (eq kind "container_name")}}
+
+ {{/if}}
+ |
+
+
+
+
+ |
diff --git a/app/containers/new/view.js b/app/containers/new/view.js
index 453eb841b..07b09096d 100644
--- a/app/containers/new/view.js
+++ b/app/containers/new/view.js
@@ -12,6 +12,7 @@ export default Ember.View.extend({
addDnsSearch: addAction('addDnsSearch', '.dns-search-value'),
addDevice: addAction('addDevice', '.device-host'),
addLabel: addAction('addLabel', '.label-key'),
+ addSchedulingRule: addAction('addSchedulingRule', '.schedule-rule'),
selectTab: function(name) {
this.set('context.tab',name);
diff --git a/app/mixins/edit-container.js b/app/mixins/edit-container.js
index 3f36e5566..60ef35203 100644
--- a/app/mixins/edit-container.js
+++ b/app/mixins/edit-container.js
@@ -131,7 +131,15 @@ export default Ember.Mixin.create(Cattle.NewOrEditMixin, EditHealthCheck, EditLa
ary.removeObject(item);
}
});
- }
+ },
+
+ addSchedulingRule: function() {
+ this.send('addSystemLabel');
+ },
+
+ removeSchedulingRule: function(obj) {
+ this.send('removeLabel', obj);
+ },
},
// ----------------------------------
@@ -161,6 +169,7 @@ export default Ember.Mixin.create(Cattle.NewOrEditMixin, EditHealthCheck, EditLa
this.initMemory();
this.initLabels();
this.initHealthCheck();
+ this.initScheduling();
}
},
@@ -835,6 +844,36 @@ export default Ember.Mixin.create(Cattle.NewOrEditMixin, EditHealthCheck, EditLa
}.observes('strEntryPoint'),
// ----------------------------------
+ // Scheduling
+ // ----------------------------------
+ initScheduling: function() {
+ if ( this.get('instance.requestedHostId') )
+ {
+ this.set('isRequestedHost',true);
+ }
+ else
+ {
+ this.set('isRequestedHost',false);
+ }
+
+ // @TODO import existing for clone
+ },
+
+ isRequestedHost: null,
+ isRequestedHostDidChange: function() {
+ if ( this.get('isRequestedHost') )
+ {
+ if ( !this.get('instance.requestedHostId') )
+ {
+ this.set('instance.requestedHostId', this.get('hostChoices.firstObject.id'));
+ }
+ }
+ else
+ {
+ this.set('instance.requestedHostId', null);
+ }
+ }.observes('isRequestedHost'),
+
// ----------------------------------
// Save
// ----------------------------------
diff --git a/app/mixins/edit-labels.js b/app/mixins/edit-labels.js
index 44f37b4ac..1e9933622 100644
--- a/app/mixins/edit-labels.js
+++ b/app/mixins/edit-labels.js
@@ -1,4 +1,5 @@
import Ember from 'ember';
+import C from 'ui/utils/constants';
export default Ember.Mixin.create({
labelResource: Ember.computed.alias('primaryResource'),
@@ -82,6 +83,10 @@ export default Ember.Mixin.create({
return (this.get('labelArray')||[]).filterProperty('isUser',true);
}.property('labelArray.@each.isUser'),
+ systemLabelArray: function() {
+ return (this.get('labelArray')||[]).filterProperty('isUser',false);
+ }.property('labelArray.@each.isUser'),
+
initFields: function() {
this._super();
this.initLabels();
@@ -95,7 +100,7 @@ export default Ember.Mixin.create({
out.push(Ember.Object.create({
key: key,
value: obj[key],
- isUser: key.indexOf('io.rancher') !== 0,
+ isUser: key.indexOf(C.LABEL.SYSTEM_PREFIX) !== 0,
}));
});
@@ -108,7 +113,11 @@ export default Ember.Mixin.create({
this.get('labelArray').forEach(function(row) {
if ( row.key )
{
- out[row.key] = row.value;
+ // System labels have to have a value before they're added, users ones can be just key.
+ if ( row.isUser || row.value )
+ {
+ out[row.key] = row.value;
+ }
}
});
this.set('labelResource.labels', out);
diff --git a/app/mixins/edit-service.js b/app/mixins/edit-service.js
index 3962aaad8..2e2d48e4f 100644
--- a/app/mixins/edit-service.js
+++ b/app/mixins/edit-service.js
@@ -77,18 +77,22 @@ export default Ember.Mixin.create(EditLabels, {
// ----------------------------------
// Scheduling
// ----------------------------------
- isScalePlural: Ember.computed.gt('service.scale', 1),
- isGlobalStr: null,
- isGlobal: Ember.computed.equal('isGlobalStr','yes'),
+ isGlobal: null,
initScheduling: function() {
var existing = this.getLabel(C.LABEL.SCHED_GLOBAL);
- this.set('isGlobalStr', (!!existing ? 'yes' : 'no'));
+ this.set('isGlobal', !!existing);
+ this._super();
+ if ( this.get('isRequestedHost') )
+ {
+ this.set('isGlobal', false);
+ }
},
globalDidChange: function() {
if ( this.get('isGlobal') )
{
this.setLabel(C.LABEL.SCHED_GLOBAL,'true');
+ this.set('isRequestedHost', false);
}
else
{
@@ -96,6 +100,13 @@ export default Ember.Mixin.create(EditLabels, {
}
}.observes('isGlobal'),
+ isRequestedHostDidChangeGlobal: function() {
+ if ( this.get('isRequestedHost') )
+ {
+ this.set('isGlobal', false);
+ }
+ }.observes('isRequestedHost'),
+
// ----------------------------------
// Save
// ----------------------------------
diff --git a/app/mixins/read-labels.js b/app/mixins/read-labels.js
index 0608894b2..e0f3c763a 100644
--- a/app/mixins/read-labels.js
+++ b/app/mixins/read-labels.js
@@ -1,4 +1,5 @@
import Ember from 'ember';
+import C from 'ui/utils/constants';
export default Ember.Mixin.create({
labelResource: null,
@@ -8,7 +9,7 @@ export default Ember.Mixin.create({
var obj = this.get('labelResource.labels')||{};
var keys = Ember.keys(obj).sort();
keys.forEach(function(key) {
- var isUser = key.indexOf('io.rancher') !== 0;
+ var isUser = key.indexOf(C.LABEL.SYSTEM_PREFIX) !== 0;
out.push(Ember.Object.create({
key: key,
value: obj[key],
diff --git a/app/service/model.js b/app/service/model.js
index 25e88ddc4..106b67ad9 100644
--- a/app/service/model.js
+++ b/app/service/model.js
@@ -1,4 +1,5 @@
import Cattle from 'ui/utils/cattle';
+import C from 'ui/utils/constants';
var Service = Cattle.TransitioningResource.extend({
type: 'service',
@@ -9,9 +10,12 @@ var Service = Cattle.TransitioningResource.extend({
}.observes('consumedservices.@each.{id,name,state}'),
healthState: function() {
+ var isGlobal = Object.keys(this.get('labels')||{}).indexOf(C.LABEL.SCHED_GLOBAL) >= 0;
+ var instances = this.get('instances')||[];
+
// Get the state of each instance
var healthy = 0;
- (this.get('instances')||[]).forEach((instance) => {
+ instances.forEach((instance) => {
var resource = instance.get('state');
var health = instance.get('healthState');
@@ -21,7 +25,7 @@ var Service = Cattle.TransitioningResource.extend({
}
});
- if ( healthy >= this.get('scale') )
+ if ( (isGlobal && healthy >= instances.get('length')) || (!isGlobal && healthy >= this.get('scale')) )
{
return 'healthy';
}
@@ -34,6 +38,7 @@ var Service = Cattle.TransitioningResource.extend({
combinedState: function() {
var service = this.get('state');
var health = this.get('healthState');
+
if ( ['active','updating-active'].indexOf(service) === -1 )
{
// If the service isn't active, return its state
diff --git a/app/service/new/template.hbs b/app/service/new/template.hbs
index 7a11d7bd9..f483735f5 100644
--- a/app/service/new/template.hbs
+++ b/app/service/new/template.hbs
@@ -10,7 +10,7 @@
-
+
@@ -20,7 +20,7 @@
-
+
diff --git a/app/service/new/view.js b/app/service/new/view.js
index ac6262034..d1013e926 100644
--- a/app/service/new/view.js
+++ b/app/service/new/view.js
@@ -5,5 +5,6 @@ export default NewContainerView.extend({
actions: {
addVolumeFromService: addAction('addVolumeFromService', '.volumefromservice-container'),
addServiceLink: addAction('addServiceLink', '.service-link'),
+ addSchedulingRule: addAction('addSchedulingRule', '.schedule-rule'),
},
});
diff --git a/app/styles/bootstrap-tweak.scss b/app/styles/bootstrap-tweak.scss
index a0d1033d5..eba489d46 100644
--- a/app/styles/bootstrap-tweak.scss
+++ b/app/styles/bootstrap-tweak.scss
@@ -174,6 +174,8 @@ fieldset[disabled] .btn {
}
.table {
+ margin-bottom: 0;
+
& > THEAD > TR > TH,
& > THEAD > TR > TD,
& > TBODY > TR > TH,
@@ -418,3 +420,8 @@ FORM LABEL,
.help-block {
color: #444;
}
+
+.form-group {
+ padding-bottom: 15px;
+ margin-bottom: 0;
+}
diff --git a/app/styles/layout.scss b/app/styles/layout.scss
index 4e1268264..5d7b8486a 100644
--- a/app/styles/layout.scss
+++ b/app/styles/layout.scss
@@ -631,7 +631,7 @@ ASIDE {
}
HR {
- margin: 0 0 15px 0;
+ margin: 8px 0 8px 0;
}
}
diff --git a/app/templates/container/edit-name.hbs b/app/templates/container/edit-name.hbs
index 8a55c52e3..20336dba7 100644
--- a/app/templates/container/edit-name.hbs
+++ b/app/templates/container/edit-name.hbs
@@ -7,7 +7,7 @@
-