diff --git a/app/authenticated/cluster/edit/route.js b/app/authenticated/cluster/edit/route.js new file mode 100644 index 000000000..03f7e1523 --- /dev/null +++ b/app/authenticated/cluster/edit/route.js @@ -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); + } +}); diff --git a/app/authenticated/cluster/edit/template.hbs b/app/authenticated/cluster/edit/template.hbs new file mode 100644 index 000000000..5bb4e4e30 --- /dev/null +++ b/app/authenticated/cluster/edit/template.hbs @@ -0,0 +1,6 @@ +{{cru-cluster + model=model + initialProvider=model.cluster.provider + mode="edit" + close=(action "close") +}} diff --git a/app/models/cluster.js b/app/models/cluster.js index 4cc3dba9f..69b39a3e4 100644 --- a/app/models/cluster.js +++ b/app/models/cluster.js @@ -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')||[]; diff --git a/app/styles/pages/_host.scss b/app/styles/pages/_host.scss index 63f57aefb..a2cb62856 100644 --- a/app/styles/pages/_host.scss +++ b/app/styles/pages/_host.scss @@ -75,7 +75,7 @@ justify-content: center; & > .nav-box-item { - min-height : 150px; + min-height : 120px; flex : 0 1 auto; position : relative; outline : 0; diff --git a/lib/global-admin/addon/clusters/new/controller.js b/lib/global-admin/addon/clusters/new/controller.js index 08fc902b7..1258d7d37 100644 --- a/lib/global-admin/addon/clusters/new/controller.js +++ b/lib/global-admin/addon/clusters/new/controller.js @@ -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); - }, }); diff --git a/lib/global-admin/addon/clusters/new/template.hbs b/lib/global-admin/addon/clusters/new/template.hbs index 2ac7acfce..67596e810 100644 --- a/lib/global-admin/addon/clusters/new/template.hbs +++ b/lib/global-admin/addon/clusters/new/template.hbs @@ -1,68 +1,6 @@ -
-

{{t (if (eq step 1) 'clustersPage.newCluster' 'clustersPage.newClusterName') name=cluster.displayName}}

-
- -{{#if (eq step 1)}} -
-
- {{form-name-description - model=cluster - nameRequired=true - nameLabel='clusterNew.name.label' - namePlaceholder='clusterNew.name.placeholder' - descriptionPlaceholder='clusterNew.description.placeholder' - }} -
- - {{#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) - }} - - {{/accordion-list-item}} - {{/accordion-list}} - - {{top-errors errors=errors}} - {{save-cancel createLabel='saveCancel.next' save='save' cancel='close'}} -
-{{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") +}} diff --git a/lib/global-admin/addon/components/cluster-driver/driver-googlegke/template.hbs b/lib/global-admin/addon/components/cluster-driver/driver-googlegke/template.hbs index d16dc8777..f536a4293 100644 --- a/lib/global-admin/addon/components/cluster-driver/driver-googlegke/template.hbs +++ b/lib/global-admin/addon/components/cluster-driver/driver-googlegke/template.hbs @@ -136,5 +136,5 @@ {{/accordion-list}} {{top-errors errors=errors}} - {{save-cancel save="save" cancel=close}} + {{save-cancel save="driverSave" cancel=close}} {{/if}} diff --git a/lib/global-admin/addon/components/cluster-driver/driver-rke/component.js b/lib/global-admin/addon/components/cluster-driver/driver-rke/component.js index 6afb012f0..8e4c9727a 100644 --- a/lib/global-admin/addon/components/cluster-driver/driver-rke/component.js +++ b/lib/global-admin/addon/components/cluster-driver/driver-rke/component.js @@ -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'); diff --git a/lib/global-admin/addon/components/cluster-driver/driver-rke/template.hbs b/lib/global-admin/addon/components/cluster-driver/driver-rke/template.hbs index 742c166bc..96900f64d 100644 --- a/lib/global-admin/addon/components/cluster-driver/driver-rke/template.hbs +++ b/lib/global-admin/addon/components/cluster-driver/driver-rke/template.hbs @@ -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")}} - - -
- {{input class="input-sm" value=node.hostnamePrefix}} -
- - -
- {{input class="input-sm" type="number" min="1" value=node.quantity}} - x -
- - - {{#if filteredNodeTemplates.length}} -
- {{new-select - class="input-sm" - content=filteredNodeTemplates - prompt="clusterNew.rke.nodes.templatePrompt" - localizedPrompt=true - optionLabelPath="displayName" - optionValuePath="id" - value=node.nodeTemplateId - }} -
- -
-
- {{else}} - - {{/if}} - - - {{input type="checkbox" checked=node.etcd}} - - - {{input type="checkbox" checked=node.controlPlane}} - - - {{input type="checkbox" checked=node.worker}} - - - - - - {{else if (eq kind "norows")}} - {{t 'nodesPage.table.noData'}} - {{else if (eq kind "suffix")}} - - {{t 'clusterNew.rke.role.requirements.label'}} - - - {{t 'clusterNew.rke.role.requirements.etcd'}} - - - - {{t 'clusterNew.rke.role.requirements.controlplane'}} - - - - {{t 'clusterNew.rke.role.requirements.worker'}} - - - - {{/if}} - {{/sortable-table}} - -
- -
- {{/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}}
diff --git a/lib/global-admin/addon/components/cru-cluster/component.js b/lib/global-admin/addon/components/cru-cluster/component.js new file mode 100644 index 000000000..e232ac9bf --- /dev/null +++ b/lib/global-admin/addon/components/cru-cluster/component.js @@ -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'); + } + }, +}); diff --git a/lib/global-admin/addon/components/cru-cluster/template.hbs b/lib/global-admin/addon/components/cru-cluster/template.hbs new file mode 100644 index 000000000..5c84ea8d9 --- /dev/null +++ b/lib/global-admin/addon/components/cru-cluster/template.hbs @@ -0,0 +1,71 @@ +
+

+ {{#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}} +

+
+ + + +{{#if (or isEdit (eq step 1))}} +
+
+ {{form-name-description + model=model.cluster + nameRequired=true + nameLabel='clusterNew.name.label' + namePlaceholder='clusterNew.name.placeholder' + descriptionPlaceholder='clusterNew.description.placeholder' + }} +
+ + {{#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}} +
+{{/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}} diff --git a/lib/global-admin/addon/components/cru-node-pools/component.js b/lib/global-admin/addon/components/cru-node-pools/component.js new file mode 100644 index 000000000..af3dc07ee --- /dev/null +++ b/lib/global-admin/addon/components/cru-node-pools/component.js @@ -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; + }), + +}); diff --git a/lib/global-admin/addon/components/cru-node-pools/template.hbs b/lib/global-admin/addon/components/cru-node-pools/template.hbs new file mode 100644 index 000000000..cdabd90f2 --- /dev/null +++ b/lib/global-admin/addon/components/cru-node-pools/template.hbs @@ -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")}} + + +
+ {{input class="input-sm" value=pool.hostnamePrefix}} +
+ + +
+ {{input class="input-sm" type="number" min="1" value=pool.quantity}} + x +
+ + + {{#if filteredNodeTemplates.length}} +
+ {{new-select + class="input-sm" + content=filteredNodeTemplates + prompt="clusterNew.rke.nodes.templatePrompt" + localizedPrompt=true + optionLabelPath="displayName" + optionValuePath="id" + value=pool.nodeTemplateId + }} +
+ +
+
+ {{else}} + + {{/if}} + + + {{input type="checkbox" checked=pool.etcd}} + + + {{input type="checkbox" checked=pool.controlPlane}} + + + {{input type="checkbox" checked=pool.worker}} + + + + + + {{else if (eq kind "norows")}} + {{t 'nodesPage.table.noData'}} + {{else if (eq kind "suffix")}} + + {{t 'clusterNew.rke.role.requirements.label'}} + + + {{t 'clusterNew.rke.role.requirements.etcd'}} + + + + {{t 'clusterNew.rke.role.requirements.controlplane'}} + + + + {{t 'clusterNew.rke.role.requirements.worker'}} + + + + {{/if}} + {{/sortable-table}} + +
+ +
+{{/accordion-list-item}} diff --git a/lib/global-admin/addon/mixins/cluster-driver.js b/lib/global-admin/addon/mixins/cluster-driver.js index e77a72571..5fe93a348 100644 --- a/lib/global-admin/addon/mixins/cluster-driver.js +++ b/lib/global-admin/addon/mixins/cluster-driver.js @@ -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: '', - 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; }, }); diff --git a/lib/shared/addon/components/form-members/component.js b/lib/shared/addon/components/form-members/component.js index ed4948f1c..5b5207b8e 100644 --- a/lib/shared/addon/components/form-members/component.js +++ b/lib/shared/addon/components/form-members/component.js @@ -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'); }); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index c9b71a72c..b8f7d1c32 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -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}