Nodepool objects, cluster edit

This commit is contained in:
Vincent Fiduccia 2018-02-19 17:07:55 -07:00
parent 583bafd6cf
commit 2cfe986d22
No known key found for this signature in database
GPG Key ID: 2B29AD6BB2BB2582
16 changed files with 550 additions and 333 deletions

View File

@ -0,0 +1,29 @@
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { hash/* , all */ } from 'rsvp';
export default Route.extend({
access: service(),
globalStore: service(),
model(params) {
let globalStore = this.get('globalStore');
return hash({
cluster: globalStore.find('cluster', params.cluster_id),
nodeTemplates: globalStore.findAll('nodeTemplate'),
nodeDrivers: globalStore.findAll('nodeDriver'),
psps: globalStore.findAll('podSecurityPolicyTemplate'),
roleTemplates: globalStore.findAll('roleTemplate'),
users: globalStore.findAll('user'),
clusterRoleTemplateBinding: globalStore.findAll('clusterRoleTemplateBinding'),
me: get(this, 'access.me'),
});
},
setupController(controller/*, model*/) {
this._super(...arguments);
set(controller, 'step', 1);
}
});

View File

@ -0,0 +1,6 @@
{{cru-cluster
model=model
initialProvider=model.cluster.provider
mode="edit"
close=(action "close")
}}

View File

@ -17,13 +17,14 @@ export default Resource.extend(ResourceUsage, {
namespaces: hasMany('id', 'namespace', 'clusterId'),
projects: hasMany('id', 'project', 'clusterId'),
nodes: hasMany('id', 'node', 'clusterId'),
nodePools: hasMany('id', 'nodePool', 'clusterId'),
machines: alias('nodes'),
clusterRoleTemplateBindings: hasMany('id', 'clusterRoleTemplateBinding', 'clusterId'),
roleTemplateBindings: alias('clusterRoleTemplateBindings'),
actions: {
edit() {
this.get('router').transitionTo('global-admin.clusters.detail.edit', this.get('id'));
this.get('router').transitionTo('authenticated.cluster.edit', this.get('id'));
},
scaleDownPool(uuid) {
@ -55,6 +56,31 @@ export default Resource.extend(ResourceUsage, {
return null;
}),
provider: computed('configName','nodePools.@each.nodeTemplateId', function() {
const intl = get(this, 'intl');
const pools = get(this,'nodePools')||[];
const firstTemplate = get(pools,'firstObject.nodeTemplate');
switch ( get(this,'configName') ) {
case 'azureKubernetesServiceConfig':
return 'azureaks';
case 'googleKubernetesEngineConfig':
return 'googlegke';
case 'rancherKubernetesEngineConfig':
if ( !!pools ) {
if ( firstTemplate ) {
return get(firstTemplate, 'driver');
} else {
return null;
}
} else {
return 'custom';
}
default:
return 'import';
}
}),
displayProvider: computed('configName','nodePools.@each.nodeTemplateId', function() {
const intl = get(this, 'intl');
const pools = get(this,'nodePools')||[];

View File

@ -75,7 +75,7 @@
justify-content: center;
& > .nav-box-item {
min-height : 150px;
min-height : 120px;
flex : 0 1 auto;
position : relative;
outline : 0;

View File

@ -1,104 +1,15 @@
import Controller from '@ember/controller';
import { get, set, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import ChildHook from 'shared/mixins/child-hook';
import { alias } from '@ember/object/computed';
const MEMBER_CONFIG = {
type: 'clusterRoleTemplateBinding',
};
export default Controller.extend(ViewNewEdit, ChildHook, {
globalStore: service(),
intl: service(),
export default Controller.extend({
cluster: alias('model.cluster'),
primaryResource: alias('model.cluster'),
step: 1,
provider: 'googlegke',
memberConfig: MEMBER_CONFIG,
queryParams: ['provider'],
actions: {
clickNext() {
this.$('BUTTON[type="submit"]').click();
},
close() {
this.send('goToPrevious', 'global-admin.clusters.index');
},
},
providerChoices: computed('nodeDrivers.[]','intl.locale', function() {
const intl = get(this, 'intl');
const out = [
{name: 'googlegke', driver: 'googlegke'},
// {name: 'amazoneks', driver: 'amazoneks'},
{name: 'azureaks', driver: 'azureaks'},
];
get(this, 'model.nodeDrivers').filterBy('active',true).sortBy('name').forEach((driver) => {
const name = get(driver, 'name');
const hasUi = get(driver, 'hasUi');
out.push({
name: name,
driver: 'rke',
nodeComponent: hasUi ? name : 'generic',
nodeWhich: name,
});
}),
out.push({name: 'custom', driver: 'rke', nodeWhich: 'custom'});
out.push({name: 'import', driver: 'import'});
out.forEach((driver) => {
const key = `clusterNew.${driver.name}.label`;
if ( !get(driver,'displayName') && intl.exists(key) ) {
set(driver, 'displayName', intl.t(key));
}
});
return out;
}),
driverInfo: computed('provider', function() {
const name = get(this, 'provider');
const choices = get(this, 'providerChoices');
const entry = choices.findBy('name', name);
if ( entry ) {
return {
name: entry.name,
driverComponent: `cluster-driver/driver-${entry.driver}`,
nodeComponent: `node-driver/driver-${entry.nodeComponent}`,
nodeWhich: entry.nodeWhich,
};
}
}),
willSave() {
const cluster = get(this, 'cluster');
const field = get(this, 'configField');
cluster.clearProvidersExcept(field);
return this._super(...arguments);
},
didSave() {
const originalCluster = get(this, 'cluster');
return originalCluster.waitForCondition('BackingNamespaceCreated').then(() => {
return this.applyHooks().then(() => {
const clone = originalCluster.clone();
set(this, 'cluster', clone);
return clone;
});
});
},
doneSaving() {
set(this, 'step', 2);
},
});

View File

@ -1,68 +1,6 @@
<section class="header">
<h1>{{t (if (eq step 1) 'clustersPage.newCluster' 'clustersPage.newClusterName') name=cluster.displayName}}</h1>
</section>
{{#if (eq step 1)}}
<form onsubmit={{action "clickNext"}}>
<div class="row">
{{form-name-description
model=cluster
nameRequired=true
nameLabel='clusterNew.name.label'
namePlaceholder='clusterNew.name.placeholder'
descriptionPlaceholder='clusterNew.description.placeholder'
}}
</div>
{{#accordion-list showExpandAll=false as |al expandFn|}}
{{#accordion-list-item
title=(t 'clusterNew.members.label')
detail=(t 'clusterNew.members.detail')
showExpand=false
expandOnInit=true
expandAll=al.expandAll
expand=(action expandFn)
}}
{{form-members
editing=true
memberConfig=memberConfig
primaryResource=cluster
creator=model.me
roles=model.roleTemplates
users=model.users
type="cluster"
registerHook=(action "registerHook")
}}
{{/accordion-list-item}}
{{#accordion-list-item
title=(t 'clusterNew.config.label')
detail=(t 'clusterNew.config.detail')
showExpand=false
expandOnInit=true
expandAll=al.expandAll
expand=(action expandFn)
}}
<div class="row nav nav-boxes checked-active">
{{#each providerChoices as |choice|}}
{{#link-to (query-params provider=choice.name) class=(concat "col span-2 nav-box-item driver machine-driver " choice.name)}}
<p class="sr-only">{{choice.displayName}}</p>
{{/link-to}}
{{/each}}
</div>
{{/accordion-list-item}}
{{/accordion-list}}
{{top-errors errors=errors}}
{{save-cancel createLabel='saveCancel.next' save='save' cancel='close'}}
</form>
{{else}}
{{component driverInfo.driverComponent
editing=false
nodeComponent=driverInfo.nodeComponent
nodeWhich=driverInfo.nodeWhich
model=model
close=(action 'close')
}}
{{/if}}
{{cru-cluster
model=model
provider=provider
mode="new"
close=(action "close")
}}

View File

@ -136,5 +136,5 @@
{{/accordion-list}}
{{top-errors errors=errors}}
{{save-cancel save="save" cancel=close}}
{{save-cancel save="driverSave" cancel=close}}
{{/if}}

View File

@ -1,5 +1,6 @@
import Component from '@ember/component'
import ClusterDriver from 'global-admin/mixins/cluster-driver';
import { resolve } from 'rsvp';
import { equal } from '@ember/object/computed';
import { get, set, computed, observer } from '@ember/object';
import { satisfies } from 'shared/utils/parse-version';
@ -62,6 +63,8 @@ export default Component.extend(ClusterDriver, {
configField: 'rancherKubernetesEngineConfig',
headers,
model: null,
initialVersion: null,
networkChoices: [
@ -131,31 +134,18 @@ export default Component.extend(ClusterDriver, {
this.set('labels', out);
},
addPool() {
let nodePools = get(this, 'cluster.nodePools');
if ( !nodePools ) {
nodePools = [];
set(this, 'cluster.nodePools', nodePools);
}
driverSave(cb) {
cb = cb || function() {};
let templateId = null;
const lastNode = nodePools[nodePools.length-1];
if ( lastNode ) {
templateId = get(lastNode, 'nodeTemplateId');
}
resolve(this.willSave()).then((ok) => {
if ( !ok ) {
// Validation or something else said not to save
cb(false);
return;
}
nodePools.pushObject(get(this, 'globalStore').createRecord({
type: 'nodePool',
nodeTemplateId: templateId
}));
},
addNodeTemplate(node) {
get(this,'modalService').toggleModal('modal-edit-node-template', {nodeTemplate: null, driver: get(this, 'nodeWhich'), onAdd: onAdd});
function onAdd(nodeTemplate) {
set(node, 'nodeTemplateId', get(nodeTemplate, 'id'));
}
this.sendAction('save', cb);
});
},
},
@ -177,7 +167,7 @@ export default Component.extend(ClusterDriver, {
const intl = get(this, 'intl');
this._super(...arguments);
let errors = this.get('errors')||[];
const errors = get(this,'errors')||[];
if ( !get(this, 'isCustom') ) {
if ( !get(this, 'etcdOk') ) {
@ -197,7 +187,6 @@ export default Component.extend(ClusterDriver, {
return errors.length === 0;
},
doneSaving() {
if ( get(this, 'isCustom') ){
const cluster = get(this,'cluster');
@ -210,41 +199,6 @@ export default Component.extend(ClusterDriver, {
}
},
filteredNodeTemplates: computed('nodeWhich','model.nodeTemplates.@each.{state,driver}', function() {
const driver = get(this, 'nodeWhich');
let templates = get(this, 'model.nodeTemplates').filterBy('state','active').filterBy('driver', driver);
return templates;
}),
_nodeCountFor(role) {
let count = 0;
(get(this, 'cluster.nodePools')||[]).filterBy(role,true).forEach((pool) => {
let more = get(pool, 'quantity');
if ( more ) {
more = parseInt(more, 10);
}
count += more;
});
return count;
},
etcdOk: computed('cluster.nodePools.@each.{quantity,etcd}', function() {
let count = this._nodeCountFor('etcd');
return count === 1 || count === 3 || count === 5
}),
controlPlaneOk: computed('cluster.nodePools.@each.{quantity,controlPlane}', function() {
let count = this._nodeCountFor('controlPlane');
return count >= 1;
}),
workerOk: computed('cluster.nodePools.@each.{quantity,worker}', function() {
let count = this._nodeCountFor('worker');
return count >= 1;
}),
versionChanged: observer('config.kubernetesVersion','versionChoices.[]', function() {
const versions = get(this, 'versionChoices')||[];
const current = get(this, 'config.kubernetesVersion');

View File

@ -1,100 +1,12 @@
{{#if (eq step 1)}}
{{#accordion-list showExpandAll=false as |al expandFn|}}
{{#unless isCustom}}
{{#accordion-list-item
title=(t 'clusterNew.rke.nodes.title')
detail=(t 'clusterNew.rke.nodes.detail')
showExpand=false
expandOnInit=true
expandAll=al.expandAll
expand=(action expandFn)
{{cru-node-pools
cluster=cluster
driver=nodeWhich
nodeTemplates=model.nodeTemplates
registerHook=(action "registerHook")
}}
{{#sortable-table
classNames="grid sortable-table"
body=cluster.nodePools
suffix=true
search=false
bulkActions=false
rowActions=false
pagingLabel="pagination.node"
headers=headers
as |sortable kind node dt|
}}
{{#if (eq kind "row")}}
<tr class="main-row">
<td data-title="{{dt.hostnamePrefix}}">
<div class="mr-20">
{{input class="input-sm" value=node.hostnamePrefix}}
</div>
</td>
<td data-title="{{dt.quantity}}">
<div class="input-group mr-20">
{{input class="input-sm" type="number" min="1" value=node.quantity}}
<span class="input-group-addon bg-default">x</span>
</div>
</td>
<td data-title="{{dt.nodeTemplate}}">
{{#if filteredNodeTemplates.length}}
<div class="input-group input-sm">
{{new-select
class="input-sm"
content=filteredNodeTemplates
prompt="clusterNew.rke.nodes.templatePrompt"
localizedPrompt=true
optionLabelPath="displayName"
optionValuePath="id"
value=node.nodeTemplateId
}}
<div class="input-group-btn bg-primary">
<button type="button" class="btn btn-sm bg-primary" {{action "addNodeTemplate" node}}><i class="icon icon-plus"></i></button>
</div>
</div>
{{else}}
<button class="btn bg-primary" {{action "addNodeTemplate" node}}>{{t 'clusterNew.rke.nodes.addTemplate'}}</button>
{{/if}}
</td>
<td data-title="{{dt.etcd}}" class="text-center">
{{input type="checkbox" checked=node.etcd}}
</td>
<td data-title="{{dt.controlplane}}" class="text-center">
{{input type="checkbox" checked=node.controlPlane}}
</td>
<td data-title="{{dt.worker}}" class="text-center">
{{input type="checkbox" checked=node.worker}}
</td>
<td data-title="{{dt.remove}}" class="text-center">
<button class="btn bg-primary btn-sm" {{action "removeNode" node}}><i class="icon icon-minus"/></button>
</td>
</tr>
{{else if (eq kind "norows")}}
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted pt-40 pb-40">{{t 'nodesPage.table.noData'}}</td></tr>
{{else if (eq kind "suffix")}}
<tr class="banner bg-info suffix">
<td colspan="3" class="pl-20 text-bold">{{t 'clusterNew.rke.role.requirements.label'}}</td>
<td class="text-center {{if etcdOk "text-success" "text-error text-bold"}}">
<i class="icon {{if etcdOk "icon-success" "icon-x-circle"}}"></i>
{{t 'clusterNew.rke.role.requirements.etcd'}}
</td>
<td class="text-center {{if controlPlaneOk "text-success" "text-error text-bold"}}">
<i class="icon {{if controlPlaneOk "icon-success" "icon-x-circle"}}"></i>
{{t 'clusterNew.rke.role.requirements.controlplane'}}
</td>
<td class="text-center {{if workerOk "text-success" "text-error text-bold"}}">
<i class="icon {{if workerOk "icon-success" "icon-x-circle"}}"></i>
{{t 'clusterNew.rke.role.requirements.worker'}}
</td>
<td></td>
</tr>
{{/if}}
{{/sortable-table}}
<div class="mt-20">
<button class="btn bg-primary icon-btn p-0" {{action "addPool"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span>
<span>{{t 'clusterNew.rke.nodes.add'}}</span>
</button>
</div>
{{/accordion-list-item}}
{{/unless}}
{{#accordion-list-item
@ -185,7 +97,7 @@
{{/accordion-list}}
{{top-errors errors=errors}}
{{save-cancel createLabel=(if isCustom 'saveCancel.next' 'saveCancel.create') save="save" cancel=cancel}}
{{save-cancel createLabel=(if isCustom 'saveCancel.next' 'saveCancel.create') save=(action 'driverSave') cancel=cancel}}
{{else}}
<div class="mt-20">

View File

@ -0,0 +1,124 @@
import Component from '@ember/component';
import { get, set, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import ChildHook from 'shared/mixins/child-hook';
import { alias } from '@ember/object/computed';
const MEMBER_CONFIG = {
type: 'clusterRoleTemplateBinding',
};
export default Component.extend(ViewNewEdit, ChildHook, {
globalStore: service(),
intl: service(),
access: service(),
cluster: alias('model.cluster'),
primaryResource: alias('model.cluster'),
step: 1,
initialProvider: null,
memberConfig: MEMBER_CONFIG,
init() {
this._super(...arguments);
// On edit pass in initialProvider, for create just set provider directly
const initialProvider = get(this, 'initialProvider');
if ( initialProvider ) {
set(this, 'provider', initialProvider);
}
if ( get(this, 'cluster.id') && initialProvider ){
set(this,'step', 2);
}
},
actions: {
clickNext() {
this.$('BUTTON[type="submit"]').click();
},
close() {
this.send('goToPrevious', 'global-admin.clusters.index');
},
maybeSave(cb) {
const info = get(this, 'driverInfo');
if ( info.preSave ) {
this.send('save',cb);
} else {
this.doneSaving();
}
},
},
providerChoices: computed('nodeDrivers.[]','intl.locale', function() {
const intl = get(this, 'intl');
const out = [
{name: 'googlegke', driver: 'googlegke'},
// {name: 'amazoneks', driver: 'amazoneks'},
{name: 'azureaks', driver: 'azureaks'},
];
get(this, 'model.nodeDrivers').filterBy('active',true).sortBy('name').forEach((driver) => {
const name = get(driver, 'name');
const hasUi = get(driver, 'hasUi');
out.push({
name: name,
driver: 'rke',
nodeComponent: hasUi ? name : 'generic',
nodeWhich: name,
});
}),
out.push({name: 'custom', driver: 'rke', nodeWhich: 'custom', preSave: true});
out.push({name: 'import', driver: 'import', preSave: true});
out.forEach((driver) => {
const key = `clusterNew.${driver.name}.label`;
if ( !get(driver,'displayName') && intl.exists(key) ) {
set(driver, 'displayName', intl.t(key));
}
});
return out;
}),
driverInfo: computed('provider', function() {
const name = get(this, 'provider');
const choices = get(this, 'providerChoices');
const entry = choices.findBy('name', name);
if ( entry ) {
return {
name: entry.name,
driverComponent: `cluster-driver/driver-${entry.driver}`,
nodeComponent: `node-driver/driver-${entry.nodeComponent}`,
nodeWhich: entry.nodeWhich,
preSave: !!entry.preSave,
};
}
}),
didSave() {
const originalCluster = get(this, 'cluster');
return originalCluster.waitForCondition('BackingNamespaceCreated').then(() => {
return this.applyHooks().then(() => {
const clone = originalCluster.clone();
set(this, 'cluster', clone);
return clone;
});
});
},
doneSaving() {
if ( get(this, 'driverInfo.preSave') ) {
set(this, 'step', 2);
} else {
this.sendAction('close');
}
},
});

View File

@ -0,0 +1,71 @@
<section class="header">
<h1>
{{#if cluster.id}}
{{t 'clustersPage.editClusterName' name=cluster.displayName}}
{{else if (eq step 1)}}
{{t 'clustersPage.newCluster'}}
{{else}}
{{t 'clustersPage.newClusterName' name=cluster.displayName}}
{{/if}}
</h1>
</section>
<div class="row nav nav-boxes checked-active">
{{#each providerChoices as |choice|}}
{{#link-to (query-params provider=choice.name) class=(concat "col span-2 nav-box-item driver machine-driver " choice.name)}}
<p class="sr-only">{{choice.displayName}}</p>
{{/link-to}}
{{/each}}
</div>
{{#if (or isEdit (eq step 1))}}
<form onsubmit={{action "clickNext"}}>
<div class="row">
{{form-name-description
model=model.cluster
nameRequired=true
nameLabel='clusterNew.name.label'
namePlaceholder='clusterNew.name.placeholder'
descriptionPlaceholder='clusterNew.description.placeholder'
}}
</div>
{{#accordion-list showExpandAll=false as |al expandFn|}}
{{#accordion-list-item
title=(t 'clusterNew.members.label')
detail=(t 'clusterNew.members.detail')
expandAll=al.expandAll
everExpanded=true
expand=(action expandFn)
}}
{{form-members
editing=notView
memberConfig=memberConfig
primaryResource=cluster
creator=model.me
roles=model.roleTemplates
users=model.users
type="cluster"
registerHook=(action "registerHook")
}}
{{/accordion-list-item}}
{{/accordion-list}}
{{top-errors errors=errors}}
{{#if (and isNew driverInfo.preSave)}}
{{save-cancel createLabel='saveCancel.next' save='maybeSave' cancel='close'}}
{{/if}}
</form>
{{/if}}
{{#unless (and isNew driverInfo.preSave)}}
{{component driverInfo.driverComponent
mode=mode
nodeComponent=driverInfo.nodeComponent
nodeWhich=driverInfo.nodeWhich
model=model
save=(action 'save')
close=(action 'close')
registerHook=(action "registerHook")
}}
{{/unless}}

View File

@ -0,0 +1,117 @@
import Component from '@ember/component';
import layout from './template';
import { computed, get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import { all as PromiseAll } from 'rsvp';
export default Component.extend({
layout,
globalStore: service(),
cluster: null,
nodeTemplates: null,
driver: null, // docker-machine driver
originalPools: null,
nodePools: null,
init() {
this._super(...arguments);
const originalPools = (get(this,'cluster.nodePools')||[]).slice();
set(this, 'originalPools', originalPools);
set(this, 'nodePools', originalPools.slice());
this.sendAction('registerHook', this.savePools.bind(this), 'savePools');
},
actions: {
addPool() {
let nodePools = get(this, 'nodePools');
let templateId = null;
const lastNode = nodePools[nodePools.length-1];
if ( lastNode ) {
templateId = get(lastNode, 'nodeTemplateId');
}
nodePools.pushObject(get(this, 'globalStore').createRecord({
type: 'nodePool',
nodeTemplateId: templateId
}));
},
removePool(pool) {
get(this, 'nodePools').removeObject(pool);
},
addNodeTemplate(node) {
get(this,'modalService').toggleModal('modal-edit-node-template', {
nodeTemplate: null,
driver: get(this, 'driver'),
onAdd: function(nodeTemplate) {
set(node, 'nodeTemplateId', get(nodeTemplate, 'id'));
},
});
},
},
savePools: function() {
const nodePools = get(this, 'nodePools');
const original = get(this, 'originalPools');
const remove = [];
original.forEach((pool) => {
if ( !nodePools.includes(pool) ) {
// Remove
remove.push(pool);
}
});
const clusterId = get(this, 'cluster.id');
nodePools.forEach((pool) => {
set(pool, 'clusterId', clusterId);
});
return PromiseAll(nodePools.map(x => x.save())).then(() => {
return PromiseAll(remove.map(x => x.delete())).then(() => {
return get(this, 'cluster');
});
});
},
filteredNodeTemplates: computed('driver','nodeTemplates.@each.{state,driver}', function() {
const driver = get(this, 'driver');
let templates = get(this, 'nodeTemplates').filterBy('state','active').filterBy('driver', driver);
return templates;
}),
_nodeCountFor(role) {
let count = 0;
(get(this, 'cluster.nodePools')||[]).filterBy(role,true).forEach((pool) => {
let more = get(pool, 'quantity');
if ( more ) {
more = parseInt(more, 10);
}
count += more;
});
return count;
},
etcdOk: computed('cluster.nodePools.@each.{quantity,etcd}', function() {
let count = this._nodeCountFor('etcd');
return count === 1 || count === 3 || count === 5
}),
controlPlaneOk: computed('cluster.nodePools.@each.{quantity,controlPlane}', function() {
let count = this._nodeCountFor('controlPlane');
return count >= 1;
}),
workerOk: computed('cluster.nodePools.@each.{quantity,worker}', function() {
let count = this._nodeCountFor('worker');
return count >= 1;
}),
});

View File

@ -0,0 +1,94 @@
{{#accordion-list-item
title=(t 'clusterNew.rke.nodes.title')
detail=(t 'clusterNew.rke.nodes.detail')
showExpand=false
expandOnInit=true
expandAll=al.expandAll
expand=(action expandFn)
}}
{{#sortable-table
classNames="grid sortable-table"
body=model
suffix=true
search=false
bulkActions=false
rowActions=false
pagingLabel="pagination.nodePool"
headers=headers
as |sortable kind pool dt|
}}
{{#if (eq kind "row")}}
<tr class="main-row">
<td data-title="{{dt.hostnamePrefix}}">
<div class="mr-20">
{{input class="input-sm" value=pool.hostnamePrefix}}
</div>
</td>
<td data-title="{{dt.quantity}}">
<div class="input-group mr-20">
{{input class="input-sm" type="number" min="1" value=pool.quantity}}
<span class="input-group-addon bg-default">x</span>
</div>
</td>
<td data-title="{{dt.nodeTemplate}}">
{{#if filteredNodeTemplates.length}}
<div class="input-group input-sm">
{{new-select
class="input-sm"
content=filteredNodeTemplates
prompt="clusterNew.rke.nodes.templatePrompt"
localizedPrompt=true
optionLabelPath="displayName"
optionValuePath="id"
value=pool.nodeTemplateId
}}
<div class="input-group-btn bg-primary">
<button type="button" class="btn btn-sm bg-primary" {{action "addNodeTemplate" pool}}><i class="icon icon-plus"></i></button>
</div>
</div>
{{else}}
<button class="btn bg-primary" {{action "addNodeTemplate" pool}}>{{t 'clusterNew.rke.nodes.addTemplate'}}</button>
{{/if}}
</td>
<td data-title="{{dt.etcd}}" class="text-center">
{{input type="checkbox" checked=pool.etcd}}
</td>
<td data-title="{{dt.controlplane}}" class="text-center">
{{input type="checkbox" checked=pool.controlPlane}}
</td>
<td data-title="{{dt.worker}}" class="text-center">
{{input type="checkbox" checked=pool.worker}}
</td>
<td data-title="{{dt.remove}}" class="text-center">
<button class="btn bg-primary btn-sm" {{action "removePool" pool}}><i class="icon icon-minus"/></button>
</td>
</tr>
{{else if (eq kind "norows")}}
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted pt-40 pb-40">{{t 'nodesPage.table.noData'}}</td></tr>
{{else if (eq kind "suffix")}}
<tr class="banner bg-info suffix">
<td colspan="3" class="pl-20 text-bold">{{t 'clusterNew.rke.role.requirements.label'}}</td>
<td class="text-center {{if etcdOk "text-success" "text-error text-bold"}}">
<i class="icon {{if etcdOk "icon-success" "icon-x-circle"}}"></i>
{{t 'clusterNew.rke.role.requirements.etcd'}}
</td>
<td class="text-center {{if controlPlaneOk "text-success" "text-error text-bold"}}">
<i class="icon {{if controlPlaneOk "icon-success" "icon-x-circle"}}"></i>
{{t 'clusterNew.rke.role.requirements.controlplane'}}
</td>
<td class="text-center {{if workerOk "text-success" "text-error text-bold"}}">
<i class="icon {{if workerOk "icon-success" "icon-x-circle"}}"></i>
{{t 'clusterNew.rke.role.requirements.worker'}}
</td>
<td></td>
</tr>
{{/if}}
{{/sortable-table}}
<div class="mt-20">
<button class="btn bg-primary icon-btn p-0" {{action "addPool"}}>
<span class="darken"><i class="icon icon-plus text-small"/></span>
<span>{{t 'clusterNew.rke.nodes.add'}}</span>
</button>
</div>
{{/accordion-list-item}}

View File

@ -1,37 +1,62 @@
import Mixin from '@ember/object/mixin';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import { get, computed } from '@ember/object';
import { get, set, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { resolve } from 'rsvp';
import { inject as service } from '@ember/service';
export default Mixin.create(ViewNewEdit, {
export default Mixin.create({
configField: '<override me>',
close: null, // action to finish adding
mode: null,
save: null, // Action to save
close: null, // Action on complete
registerHook: null,
globalStore: service(),
router: service(),
cluster: alias('model.cluster'),
primaryResource: alias('model.cluster'),
errors: null,
init() {
this._super(...arguments);
},
actions: {
close() {
this.sendAction('close');
}
},
config: computed('configField', function() {
const field = 'cluster.' + get(this, 'configField');
return get(this, field);
}),
doneSaving() {
this.get('router').transitionTo('global-admin.clusters.index');
actions: {
driverSave(cb) {
resolve(this.willSave()).then((ok) => {
if ( !ok ) {
// Validation or something else said not to save
cb(false);
return;
}
this.sendAction('save', cb);
});
},
registerHook() {
const args = [].slice.call(arguments);
args.unshift('registerHook');
this.sendAction.apply(this, args);
}
},
willSave() {
const cluster = get(this, 'cluster');
const field = get(this, 'configField');
cluster.clearProvidersExcept(field);
set(this, 'errors',null);
const ok = this.validate();
return ok;
},
validate() {
const model = get(this,'cluster');
const errors = model.validationErrors();
set(this, 'errors', errors);
return errors.length === 0;
},
});

View File

@ -140,6 +140,10 @@ export default Component.extend({
return PromiseAll(add.map(x => x.save())).then(() => {
return PromiseAll(update.map(x => x.save())).then(() => {
return PromiseAll(remove.map(x => x.delete())).then(() => {
if ( this.isDestroyed || this.isDestroying ) {
return;
}
set(this, 'memberArray', []);
return get(this, 'primaryResource');
});

View File

@ -645,6 +645,7 @@ clustersPage:
header: Clusters
newCluster: Add Cluster
newClusterName: "Add Cluster: {name}"
editClusterName: "Edit Cluster: {name}"
cluster:
label: Cluster Name
provider:
@ -3141,7 +3142,7 @@ clusterNew:
placeholder: e.g. Cluster for dev and test workloads
members:
label: Member Roles
detail: Control who as access to the cluster and what permission they have to change it
detail: Control who has access to the cluster and what permission they have to change it.
config:
label: Provider
detail: Choose where the nodes for the cluster will come from
@ -4165,6 +4166,11 @@ pagination:
=0 {No Nodes}
=1 {{count} {count, plural, =1 {Node} other {Nodes}}}
other {{from} - {to} of {count} Nodes}}
nodePool: |
{pages, plural,
=0 {No Node Pools}
=1 {{count} {count, plural, =1 {Node Pool} other {Node Pools}}}
other {{from} - {to} of {count} Node Pools}}
nodeTemplate: |
{pages, plural,
=0 {No Node Templates}