From 8eb44a4d6bf56168255c83358cdca0c5a46bcb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=80=D0=B8=D1=81=D1=82=D0=B8=D0=BD=D0=B0=20=D0=98?= =?UTF-8?q?=D0=B2=D0=B0=D1=89=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Mon, 8 Oct 2018 11:29:46 +0700 Subject: [PATCH] feat(service-offering): show all service offerings even those which don't fit resources (#1299) PR close #1229 --- src/app/app-routing.module.ts | 2 +- src/app/core/services/system-tags.service.ts | 0 src/app/home/home.component.ts | 2 +- .../reducers/vm/redux/vm-creation.effects.ts | 18 +- .../custom-service-offering.component.html | 6 +- .../custom-service-offering.component.scss | 5 + .../custom-service-offering.component.ts | 23 +- .../service-offering-dialog.component.html | 6 + .../service-offering-dialog.component.scss | 5 + .../service-offering-dialog.component.ts | 16 +- .../service-offering-list.component.html | 1 - .../service-offering-list.component.ts | 10 +- .../volume-actions/volume-resize.container.ts | 4 +- src/app/shared/models/account-user.model.ts | 8 +- src/app/shared/models/account.model.ts | 50 ++-- src/app/shared/models/config/index.ts | 1 + src/app/shared/models/offering.model.ts | 12 +- .../shared/models/service-offering.model.ts | 16 +- .../template-tags/tags.component.html | 9 - .../template/template-tags/tags.component.ts | 0 .../template-tags.component.html | 17 +- .../template-tags/template-tags.component.ts | 2 +- .../selectors/service-offering.selectors.ts | 40 +-- ...mpute-offering-view-model.selector.spec.ts | 212 ++++++++++++++++ .../compute-offering-view-model.selector.ts | 234 ++++++++++++++---- src/app/vm/selectors/view-models/index.ts | 5 +- .../compute-offering.view-model.ts | 1 + .../service-offering-selector.component.html | 3 + .../service-offering-selector.component.scss | 5 + .../service-offering-selector.component.ts | 8 +- .../containers/vm-creation.container.ts | 6 +- .../vm/vm-creation/data/vm-creation-state.ts | 5 +- .../vm-creation-service-offering.container.ts | 3 + .../vm/vm-creation/vm-creation.component.html | 2 +- .../vm/vm-creation/vm-creation.component.ts | 13 +- src/app/vm/web-shell/web-shell.service.ts | 0 src/i18n/en.json | 3 + src/i18n/ru.json | 3 + src/testutils/data/accounts.ts | 73 ++++++ src/testutils/data/compute-offerings.ts | 37 +++ src/testutils/data/index.ts | 3 + .../fixtures/serviceOfferings.json | 12 +- 42 files changed, 702 insertions(+), 179 deletions(-) delete mode 100644 src/app/core/services/system-tags.service.ts delete mode 100644 src/app/template/template-tags/tags.component.html delete mode 100644 src/app/template/template-tags/tags.component.ts create mode 100644 src/app/vm/selectors/view-models/compute-offering-view-model.selector.spec.ts delete mode 100644 src/app/vm/web-shell/web-shell.service.ts create mode 100644 src/testutils/data/accounts.ts create mode 100644 src/testutils/data/compute-offerings.ts create mode 100644 src/testutils/data/index.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e09b69dad4..d8f0a2ca89 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -57,7 +57,7 @@ const routes: Routes = [ path: '**', redirectTo: 'instances' } - ] + ], }, { path: '**', diff --git a/src/app/core/services/system-tags.service.ts b/src/app/core/services/system-tags.service.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 4f3ad53c3f..f8e1ed5932 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -30,7 +30,7 @@ export class HomeComponent extends WithUnsubscribe() implements OnInit { this.auth.loggedIn.pipe( takeUntil(this.unsubscribe$), - filter(isLoggedIn => !!isLoggedIn)) + filter(isLoggedIn => isLoggedIn)) .subscribe(() => { this.store.dispatch(new authActions.LoadUserAccountRequest({ name: this.auth.user.account, diff --git a/src/app/reducers/vm/redux/vm-creation.effects.ts b/src/app/reducers/vm/redux/vm-creation.effects.ts index 34f726be10..d6c13bc4c4 100644 --- a/src/app/reducers/vm/redux/vm-creation.effects.ts +++ b/src/app/reducers/vm/redux/vm-creation.effects.ts @@ -44,6 +44,7 @@ import * as fromTemplates from '../../templates/redux/template.reducers'; import * as fromVMs from './vm.reducers'; import * as fromVMModule from '../../../vm/selectors'; import { KeyboardLayout } from '../../../shared/types'; +import { ComputeOfferingViewModel } from '../../../vm/view-models'; interface VmCreationParams { affinityGroupNames?: string; @@ -160,11 +161,14 @@ export class VirtualMachineCreationEffects { this.store.pipe(select(fromDiskOfferings.selectAll)), this.store.pipe(select(configSelectors.get('defaultComputeOffering'))) ), - map(( - [action, vmCreationState, zones, templates, serviceOfferings, diskOfferings, defaultComputeOfferings]: [ - vmActions.VmFormUpdate, VmCreationState, Zone[], BaseTemplateModel[], ServiceOffering[], DiskOffering[], - DefaultComputeOffering[] - ]) => { + map(([action, vmCreationState, zones, templates, serviceOfferings, diskOfferings, defaultComputeOfferings]: [ + vmActions.VmFormUpdate, + VmCreationState, Zone[], + BaseTemplateModel[], + ComputeOfferingViewModel[], + DiskOffering[], + DefaultComputeOffering[] + ]) => { if (action.payload.zone) { let updates = {}; @@ -670,10 +674,10 @@ export class VirtualMachineCreationEffects { } private getPreselectedOffering( - offerings: ServiceOffering[], + offerings: ComputeOfferingViewModel[], zone: Zone, defaultComputeOfferingConfiguration: DefaultComputeOffering[] - ): ServiceOffering { + ): ComputeOfferingViewModel { const firstOffering = offerings[0]; const configForCurrentZone = defaultComputeOfferingConfiguration.find(config => config.zoneId === zone.id); if (!configForCurrentZone) { diff --git a/src/app/service-offering/custom-service-offering/custom-service-offering.component.html b/src/app/service-offering/custom-service-offering/custom-service-offering.component.html index a994ada989..5c11cfee5d 100644 --- a/src/app/service-offering/custom-service-offering/custom-service-offering.component.html +++ b/src/app/service-offering/custom-service-offering/custom-service-offering.component.html @@ -47,6 +47,10 @@
{{ 'SERVICE_OFFERING.CUSTOM_SERVICE_OFFERING.MEMORY' | translate }}
+ + {{ 'ERRORS.COMPUTE_OFFERING.RESOURCE_LIMIT_EXCEEDED' | translate }} + +
diff --git a/src/app/service-offering/custom-service-offering/custom-service-offering.component.scss b/src/app/service-offering/custom-service-offering/custom-service-offering.component.scss index a91094ec88..ae72dcc5d0 100644 --- a/src/app/service-offering/custom-service-offering/custom-service-offering.component.scss +++ b/src/app/service-offering/custom-service-offering/custom-service-offering.component.scss @@ -9,3 +9,8 @@ h5 { margin-top: 0; } } + +.error-message { + color: #f44336; + font-size: 13px; +} diff --git a/src/app/service-offering/custom-service-offering/custom-service-offering.component.ts b/src/app/service-offering/custom-service-offering/custom-service-offering.component.ts index 1a1efe1e9a..8e652e3fb4 100644 --- a/src/app/service-offering/custom-service-offering/custom-service-offering.component.ts +++ b/src/app/service-offering/custom-service-offering/custom-service-offering.component.ts @@ -1,25 +1,29 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; import { ComputeOfferingViewModel } from '../../vm/view-models'; +import { Account } from '../../shared/models'; @Component({ selector: 'cs-custom-service-offering', templateUrl: 'custom-service-offering.component.html', styleUrls: ['custom-service-offering.component.scss'] }) -export class CustomServiceOfferingComponent { +export class CustomServiceOfferingComponent implements OnInit { public offering: ComputeOfferingViewModel; public hardwareForm: FormGroup; + public account: Account; constructor( @Inject(MAT_DIALOG_DATA) data, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef ) { - const { offering } = data; - this.offering = offering; + this.offering = data.offering; + this.account = data.account; + } + public ngOnInit() { this.createForm(); } @@ -37,9 +41,12 @@ export class CustomServiceOfferingComponent { private createForm() { // input text=number provide all other validation for current restrictions this.hardwareForm = new FormGroup({ - cpuNumber: new FormControl(this.offering.cpunumber, Validators.required), - cpuSpeed: new FormControl(this.offering.cpuspeed, Validators.required), - memory: new FormControl(this.offering.memory, Validators.required), + cpuNumber: new FormControl( + { value: this.offering.cpunumber, disabled: !this.offering.isAvailableByResources }, Validators.required), + cpuSpeed: new FormControl( + { value: this.offering.cpuspeed, disabled: !this.offering.isAvailableByResources }, Validators.required), + memory: new FormControl( + { value: this.offering.memory, disabled: !this.offering.isAvailableByResources }, Validators.required), }); } } diff --git a/src/app/service-offering/service-offering-dialog/service-offering-dialog.component.html b/src/app/service-offering/service-offering-dialog/service-offering-dialog.component.html index 622219ed11..6555a2ff02 100644 --- a/src/app/service-offering/service-offering-dialog/service-offering-dialog.component.html +++ b/src/app/service-offering/service-offering-dialog/service-offering-dialog.component.html @@ -14,6 +14,7 @@

[classes]="classes" [selectedClasses]="selectedClasses" [query]="query" + [account]="account" [offeringList]="serviceOfferings" [selectedOffering]="serviceOffering" [showFields]="showFields" @@ -25,7 +26,12 @@

*ngIf="showRebootMessage" >{{ "SERVICE_OFFERING.VM_WILL_BE_RESTARTED" | translate }}

+ + {{ 'ERRORS.COMPUTE_OFFERING.RESOURCE_LIMIT_EXCEEDED' | translate }} + + +
-
diff --git a/src/app/service-offering/service-offering-list/service-offering-list.component.ts b/src/app/service-offering/service-offering-list/service-offering-list.component.ts index bbcda815a9..18eacf1d3d 100644 --- a/src/app/service-offering/service-offering-list/service-offering-list.component.ts +++ b/src/app/service-offering/service-offering-list/service-offering-list.component.ts @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; import { classesFilter } from '../../reducers/service-offerings/redux/service-offerings.reducers'; -import { ComputeOfferingClass, ServiceOffering } from '../../shared/models'; +import { Account, ComputeOfferingClass, ServiceOffering } from '../../shared/models'; import { CustomServiceOfferingComponent } from '../custom-service-offering/custom-service-offering.component'; import { Language } from '../../shared/types'; import { ComputeOfferingViewModel } from '../../vm/view-models'; @@ -20,9 +20,10 @@ export class ServiceOfferingListComponent implements OnChanges { @Input() public classes: Array; @Input() public selectedClasses: Array; @Input() public query: string; - @Input() public selectedOffering: ServiceOffering; + @Input() public selectedOffering: ComputeOfferingViewModel; @Input() public isLoading = false; @Input() public showFields: boolean; + @Input() public account: Account; @Output() public selectedOfferingChange = new EventEmitter(); public list: Array<{ soClass: ComputeOfferingClass, items: MatTableDataSource }>; @@ -61,7 +62,8 @@ export class ServiceOfferingListComponent implements OnChanges { return this.dialog.open(CustomServiceOfferingComponent, { width: '370px', data: { - offering + offering, + account: this.account } }).afterClosed(); @@ -100,7 +102,7 @@ export class ServiceOfferingListComponent implements OnChanges { } } - public filterOfferings(list: ServiceOffering[], soClass: ComputeOfferingClass) { + public filterOfferings(list: ComputeOfferingViewModel[], soClass: ComputeOfferingClass) { const classesMap = [soClass].reduce((m, i) => ({ ...m, [i.id]: i }), {}); return list.filter(offering => classesFilter(offering, this.classes, classesMap)); } diff --git a/src/app/shared/actions/volume-actions/volume-resize.container.ts b/src/app/shared/actions/volume-actions/volume-resize.container.ts index 4b54524831..b82616e1af 100644 --- a/src/app/shared/actions/volume-actions/volume-resize.container.ts +++ b/src/app/shared/actions/volume-actions/volume-resize.container.ts @@ -32,7 +32,7 @@ export class VolumeResizeContainerComponent implements OnInit { public volume: Volume; - public maxSize = 2; + public maxSize = '2'; constructor( public authService: AuthService, @@ -51,7 +51,7 @@ export class VolumeResizeContainerComponent implements OnInit { take(1), filter(account => !!account)) .subscribe((account: Account) => { - this.maxSize = Number(account.primarystorageavailable); + this.maxSize = account.primarystorageavailable; }); } diff --git a/src/app/shared/models/account-user.model.ts b/src/app/shared/models/account-user.model.ts index 7e1ba82377..7e109b3215 100644 --- a/src/app/shared/models/account-user.model.ts +++ b/src/app/shared/models/account-user.model.ts @@ -8,22 +8,22 @@ export interface AccountUser extends BaseModelInterface { firstname: string; lastname: string; email: string; - password?: string; created: string; state: string; account: string; accounttype: number; - roleid: string; roletype: AccountType; rolename: AccountType; + roleid: string; domain: string; domainid: string; timezone: string; accountid: string; iscallerchilddomain: boolean; isdefault: boolean; - secretkey: string; - apikey: string; + password?: string; + apikey?: string; + secretkey?: string; } export interface ApiKeys { diff --git a/src/app/shared/models/account.model.ts b/src/app/shared/models/account.model.ts index d839a12943..7a0aff6900 100644 --- a/src/app/shared/models/account.model.ts +++ b/src/app/shared/models/account.model.ts @@ -41,55 +41,55 @@ export class AccountData { export interface Account extends BaseModelInterface { accounttype: AccountType; - cpuavailable: number; - cpulimit: number; + cpuavailable: string; + cpulimit: string; cputotal: number; domain: string; - fullDomain: string; domainid: string; id: string; - ipavailable: number; - iplimit: number; + ipavailable: string; + iplimit: string; iptotal: number; isdefault: false; - memoryavailable: number; - memorylimit: number; + memoryavailable: string; + memorylimit: string; memorytotal: number; name: string; - networkavailable: number; - networklimit: number; + networkavailable: string; + networklimit: string; networktotal: number; primarystorageavailable: string; - primarystoragelimit: number; + primarystoragelimit: string; primarystoragetotal: number; - role: string; roleid: string; rolename: string; roletype: string; - receivedbytes: number; - sentbytes: number; - secondarystorageavailable: number; - secondarystoragelimit: number; + receivedbytes?: number; + sentbytes?: number; + secondarystorageavailable: string; + secondarystoragelimit: string; secondarystoragetotal: number; - snapshotavailable: number; - snapshotlimit: number; + snapshotavailable: string; + snapshotlimit: string; snapshottotal: number; state: string; - templateavailable: number; - templatelimit: number; + templateavailable: string; + templatelimit: string; templatetotal: number; user: Array; - vmavailable: number; - vmlimit: number; + vmavailable: string; + vmlimit: string; vmrunning: number; vmstopped: number; vmtotal: number; - volumeavailable: number; - volumelimit: number; + volumeavailable: string; + volumelimit: string; volumetotal: number; - vpcavailable: number; - vpclimit: number; + vpcavailable: string; + vpclimit: string; vpctotal: number; + role?: string; + fullDomain?: string; } export const isAdmin = (account: Account) => account.accounttype !== AccountType.User; diff --git a/src/app/shared/models/config/index.ts b/src/app/shared/models/config/index.ts index c5a2d3909a..a816dbc71d 100644 --- a/src/app/shared/models/config/index.ts +++ b/src/app/shared/models/config/index.ts @@ -9,3 +9,4 @@ export * from './image-group.model'; export * from './service-offering-availability.interface'; export * from './offering-compatibility-policy.interface'; export * from './sidenav-config-element.interface'; +export * from './custom-compute-offering-parameters.interface'; diff --git a/src/app/shared/models/offering.model.ts b/src/app/shared/models/offering.model.ts index bf29039ca7..e45f020385 100644 --- a/src/app/shared/models/offering.model.ts +++ b/src/app/shared/models/offering.model.ts @@ -9,15 +9,15 @@ export interface Offering extends BaseModelInterface { id: string; name: string; displaytext: string; - diskBytesReadRate: number; - diskBytesWriteRate: number; - diskIopsReadRate: number; - diskIopsWriteRate: number; iscustomized: boolean; - miniops: number; - maxiops: number; storagetype: string; provisioningtype: string; + diskBytesReadRate?: number; + diskBytesWriteRate?: number; + diskIopsReadRate?: number; + diskIopsWriteRate?: number; + miniops?: number; + maxiops?: number; } export const isOfferingLocal = (offering: Offering) => offering.storagetype === StorageTypes.local; diff --git a/src/app/shared/models/service-offering.model.ts b/src/app/shared/models/service-offering.model.ts index 07229c19f1..99c6b3687b 100644 --- a/src/app/shared/models/service-offering.model.ts +++ b/src/app/shared/models/service-offering.model.ts @@ -4,19 +4,19 @@ import { userTagKeys } from '../../tags/tag-keys'; export interface ServiceOffering extends Offering { created: string; - cpunumber: number; - cpuspeed: number; - memory: number; - networkrate: string; offerha: boolean; limitcpuuse: boolean; isvolatile: boolean; issystem: boolean; defaultuse: boolean; - deploymentplanner: string; - domain: string; - hosttags: string; - tags: Array; + cpunumber?: number; + cpuspeed?: number; + memory?: number; + tags?: Array; + domain?: string; + hosttags?: string; + deploymentplanner?: string; + networkrate?: string; } export const ServiceOfferingType = { diff --git a/src/app/template/template-tags/tags.component.html b/src/app/template/template-tags/tags.component.html deleted file mode 100644 index 363508e192..0000000000 --- a/src/app/template/template-tags/tags.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
- -
diff --git a/src/app/template/template-tags/tags.component.ts b/src/app/template/template-tags/tags.component.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/template/template-tags/template-tags.component.html b/src/app/template/template-tags/template-tags.component.html index 1baf64c5bb..363508e192 100644 --- a/src/app/template/template-tags/template-tags.component.html +++ b/src/app/template/template-tags/template-tags.component.html @@ -1,8 +1,9 @@ - +
+ +
diff --git a/src/app/template/template-tags/template-tags.component.ts b/src/app/template/template-tags/template-tags.component.ts index 38b1cfd21c..11a752e420 100644 --- a/src/app/template/template-tags/template-tags.component.ts +++ b/src/app/template/template-tags/template-tags.component.ts @@ -10,7 +10,7 @@ import { KeyValuePair } from '../../tags/tags-view/tags-view.component'; @Component({ selector: 'cs-template-tags', - templateUrl: 'tags.component.html' + templateUrl: 'template-tags.component.html' }) export class TemplateTagsComponent extends TagsComponent { @Input() public entity: BaseTemplateModel; diff --git a/src/app/vm/selectors/service-offering.selectors.ts b/src/app/vm/selectors/service-offering.selectors.ts index 81648d6a76..98f36864e2 100644 --- a/src/app/vm/selectors/service-offering.selectors.ts +++ b/src/app/vm/selectors/service-offering.selectors.ts @@ -1,7 +1,6 @@ import { createSelector } from '@ngrx/store'; import { VmCompatibilityPolicy } from '../shared/vm-compatibility-policy'; -import { ResourceStats } from '../../shared/services/resource-usage.service'; import { ComputeOfferingViewModel } from '../view-models'; import { isOfferingLocal } from '../../shared/models/offering.model'; import { @@ -10,7 +9,6 @@ import { ServiceOfferingAvailability } from '../../shared/models/config'; import { ServiceOffering, ServiceOfferingType, Zone } from '../../shared/models'; -import { getComputeOfferingViewModel } from './view-models'; import { configSelectors } from '../../root-store'; import * as fromZones from '../../reducers/zones/redux/zones.reducers'; import * as fromAuths from '../../reducers/auth/redux/auth.reducers'; @@ -21,6 +19,10 @@ import { filterSelectedViewMode, getSelectedOffering, } from '../../reducers/service-offerings/redux/service-offerings.reducers'; +import { + getComputeOfferingForVmCreation, + getComputeOfferingForVmEditing +} from './view-models/compute-offering-view-model.selector'; const isComputeOfferingAvailableInZone = ( offering: ServiceOffering, @@ -50,32 +52,8 @@ const getOfferingsAvailableInZone = ( }); }; -const getAvailableByResourcesSync = ( - serviceOfferings: ComputeOfferingViewModel[], - availability: ServiceOfferingAvailability, - resourceUsage: ResourceStats, - zone: Zone -) => { - const availableInZone = getOfferingsAvailableInZone(serviceOfferings, availability, zone); - - return availableInZone.filter(offering => { - let enoughCpus; - let enoughMemory; - - if (offering.iscustomized) { - enoughCpus = resourceUsage.available.cpus >= offering.customOfferingRestrictions.cpunumber.min; - enoughMemory = resourceUsage.available.memory >= offering.customOfferingRestrictions.memory.min; - } else { - enoughCpus = resourceUsage.available.cpus >= offering.cpunumber; - enoughMemory = resourceUsage.available.memory >= offering.memory; - } - - return enoughCpus && enoughMemory; - }); -}; - export const getAvailableOfferingsForVmCreation = createSelector( - getComputeOfferingViewModel, + getComputeOfferingForVmCreation, configSelectors.get('serviceOfferingAvailability'), fromVMs.getVMCreationZone, fromAuths.getUserAccount, @@ -84,13 +62,12 @@ export const getAvailableOfferingsForVmCreation = createSelector( return []; } - const resourceUsage = ResourceStats.fromAccount([user]); - return getAvailableByResourcesSync(serviceOfferings, availability, resourceUsage, zone); + return getOfferingsAvailableInZone(serviceOfferings, availability, zone); } ); export const getAvailableOfferings = createSelector( - getComputeOfferingViewModel, + getComputeOfferingForVmEditing, getSelectedOffering, configSelectors.get('serviceOfferingAvailability'), configSelectors.get('offeringCompatibilityPolicy'), @@ -108,8 +85,7 @@ export const getAvailableOfferings = createSelector( return []; } - const resourceUsage = ResourceStats.fromAccount([user]); - const availableOfferings = getAvailableByResourcesSync(serviceOfferings, availability, resourceUsage, zone); + const availableOfferings = getOfferingsAvailableInZone(serviceOfferings, availability, zone); const filterByCompatibilityPolicy = VmCompatibilityPolicy.getFilter(compatibilityPolicy, currentOffering); diff --git a/src/app/vm/selectors/view-models/compute-offering-view-model.selector.spec.ts b/src/app/vm/selectors/view-models/compute-offering-view-model.selector.spec.ts new file mode 100644 index 0000000000..433df25fc9 --- /dev/null +++ b/src/app/vm/selectors/view-models/compute-offering-view-model.selector.spec.ts @@ -0,0 +1,212 @@ +import { account, customComputeOffering, fixedComputeOffering, vm } from '../../../../testutils/data'; +import { nonCustomizableProperties } from '../../../core/config/default-configuration'; +import { ComputeOfferingViewModel } from '../../view-models'; +import { Account } from '../../../shared/models'; +import { CustomComputeOfferingParameters } from '../../../shared/models/config/index'; +import { + getComputeOfferingForVmCreation, + getComputeOfferingForVmEditing +} from './compute-offering-view-model.selector'; + +describe('GetComputeOfferingForVmCreationSelector', () => { + it('isAvailableByResources should be true in fixed compute offering params which satisfy memory and cpu resources', + () => { + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + account, + [fixedComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(true); + }); + + it('should be false in fixed compute offering params which unsatisfied memory resources', () => { + const limitedAccount: Account = { ...account, memoryavailable: String(fixedComputeOffering.memory - 10) }; + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + limitedAccount, + [fixedComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(false); + }); + + it('should be false in fixed compute offering params which unsatisfied cpu resources', () => { + const limitedAccount: Account = { ...account, cpuavailable: String(fixedComputeOffering.cpunumber - 1) }; + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + limitedAccount, + [fixedComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(false); + }); + + it('should be true in custom compute offering params which satisfy memory and cpu resources', () => { + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + account, + [customComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(true); + }); + + it('should be false in custom compute offering params which unsatisfied memory resources', () => { + const memoryavailable = String(nonCustomizableProperties.customComputeOfferingHardwareValues.memory - 10); + const limitedAccount: Account = { ...account, memoryavailable }; + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + limitedAccount, + [customComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(false); + }); + + it('should be false in custom compute offering params which unsatisfied cpu resources', () => { + const cpuavailable = String(nonCustomizableProperties.customComputeOfferingHardwareValues.cpunumber - 1); + const limitedAccount: Account = { ...account, cpuavailable }; + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + limitedAccount, + [customComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(false); + }); + + it('must set values within restrictions and resources for custom compute offering', () => { + /** + * Min Value Max Resource + * cpu 2 7 8 5 => Value = MaxRestrictions = 5 + * memory 512 4000 8192 2000 => Value = MaxRestrictions = 2000 + */ + const cpuavailable = '5'; + const memoryavailable = '2000'; + const limitedAccount: Account = { ...account, memoryavailable, cpuavailable }; + + const customComputeOfferingParameters: CustomComputeOfferingParameters[] = [ + { + offeringId: customComputeOffering.id, + cpunumber: { min: 2, max: 8, value: 7 }, + cpuspeed: { min: 1000, max: 3000, value: 1500 }, + memory: { min: 512, max: 8192, value: 4000 } + } + ]; + + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + limitedAccount, + [customComputeOffering], + customComputeOfferingParameters, + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.cpunumber).toBe(5); + expect(computeOfferingViewModel.memory).toBe(2000); + expect(computeOfferingViewModel.customOfferingRestrictions.cpunumber.max).toBe(5); + expect(computeOfferingViewModel.customOfferingRestrictions.memory.max).toBe(2000); + }); + + it('must set default values within restrictions and resources for custom compute offering', () => { + /** + * Min Value Max Resource + * cpu 2 7 8 5 => Value = MaxRestrictions = 5 + * memory 512 4000 4000 8000 => Value = MaxRestrictions = 4000 + */ + const cpuavailable = '5'; + const memoryavailable = '8000'; + const limitedAccount: Account = { ...account, memoryavailable, cpuavailable }; + + const customComputeOfferingParameters: CustomComputeOfferingParameters[] = [ + { + offeringId: customComputeOffering.id, + cpunumber: { min: 2, max: 8, value: 7 }, + cpuspeed: { min: 1000, max: 3000, value: 1500 }, + memory: { min: 512, max: 4000, value: 4000 } + } + ]; + + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmCreation.projector( + limitedAccount, + [customComputeOffering], + customComputeOfferingParameters, + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [] + ); + expect(computeOfferingViewModel.cpunumber).toBe(5); + expect(computeOfferingViewModel.memory).toBe(4000); + expect(computeOfferingViewModel.customOfferingRestrictions.cpunumber.max).toBe(5); + expect(computeOfferingViewModel.customOfferingRestrictions.memory.max).toBe(4000); + }); +}); + +describe('GetComputeOfferingForVmEditingSelector', () => { + it('isAvailableByResources should be true in fixed compute offering params which satisfy memory and cpu resources', + () => { + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmEditing.projector( + account, + [fixedComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [], + vm + ); + expect(computeOfferingViewModel.isAvailableByResources).toEqual(true); + }); + + it('isAvailableByResources should be true, cause satisfy resources plus used resources in editing vm', + () => { + const cpuavailable = '0'; + const memoryavailable = '512'; + const limitedAccount: Account = { ...account, memoryavailable, cpuavailable }; + const updatedVm = { ...vm, memory: '512', cpuNumber: 1 }; + + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmEditing.projector( + limitedAccount, + [fixedComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [], + updatedVm + ); + + expect(computeOfferingViewModel.isAvailableByResources).toEqual(true); + }); + + it('isAvailableByResources should be false, cause unsatisfy resources plus used resources in editing vm', + () => { + const cpuavailable = '0'; + const memoryavailable = '0'; + const limitedAccount: Account = { ...account, memoryavailable, cpuavailable }; + const updatedVm = { ...vm, memory: '512', cpuNumber: 1 }; + + const [computeOfferingViewModel]: ComputeOfferingViewModel[] = getComputeOfferingForVmEditing.projector( + limitedAccount, + [fixedComputeOffering], + [], + nonCustomizableProperties.defaultCustomComputeOfferingRestrictions, + nonCustomizableProperties.customComputeOfferingHardwareValues, + [], + updatedVm + ); + + expect(computeOfferingViewModel.isAvailableByResources).toEqual(false); + }); +}); diff --git a/src/app/vm/selectors/view-models/compute-offering-view-model.selector.ts b/src/app/vm/selectors/view-models/compute-offering-view-model.selector.ts index aeb9630ae9..19b655dee2 100644 --- a/src/app/vm/selectors/view-models/compute-offering-view-model.selector.ts +++ b/src/app/vm/selectors/view-models/compute-offering-view-model.selector.ts @@ -10,6 +10,13 @@ import { import { ComputeOfferingViewModel } from '../../view-models'; import { configSelectors, UserTagsSelectors } from '../../../root-store'; import * as computeOffering from '../../../reducers/service-offerings/redux/service-offerings.reducers'; +import * as fromAuth from '../../../reducers/auth/redux/auth.reducers'; +import * as fromVms from '../../../reducers/vm/redux/vm.reducers'; + +interface Resources { + cpuNumber: number | string; + memory: number | string; +} const getFixedAndCustomOfferingsArrays = (offerings: ServiceOffering[]) => { const offeringsArrays = { @@ -30,7 +37,7 @@ const getCustomOfferingHardwareParameters = ( offering: ServiceOffering, offeringsParameters: CustomComputeOfferingParameters[] ): CustomComputeOfferingParameters | undefined => { - return offeringsParameters.find(parameters => parameters.offeringId === offering.id) + return offeringsParameters && offeringsParameters.find(parameters => parameters.offeringId === offering.id) }; const getCustomHardwareValues = ( @@ -71,7 +78,7 @@ const getCustomHardwareRestrictions = ( }; const getHardwareValuesFromTags = ( - serviceOffering: ComputeOfferingViewModel, + serviceOffering: ServiceOffering, tags: Tag[] ): CustomComputeOfferingHardwareValues | null => { const getValue = (param) => { @@ -90,59 +97,202 @@ const getHardwareValuesFromTags = ( return null; }; +const checkAvailabilityForFixedByResources = ( + cpuNumber: number, + memory: number, + availableResources: Resources +): boolean => { + const isEnoughCpuNumber = availableResources.cpuNumber === 'Unlimited' || cpuNumber <= availableResources.cpuNumber; + const isEnoughMemory = availableResources.memory === 'Unlimited' || memory <= availableResources.memory; + return isEnoughCpuNumber && isEnoughMemory; +}; + +const checkAvailabilityForCustomByResources = ( + cpuNumberRestrictions: HardwareLimits, + memoryRestrictions: HardwareLimits, + availableResources: Resources +): boolean => { + const isEnoughCpuNumber = availableResources.cpuNumber === 'Unlimited' + || cpuNumberRestrictions.min <= availableResources.cpuNumber; + const isEnoughMemory = availableResources.memory === 'Unlimited' + || memoryRestrictions.min <= availableResources.memory; + return isEnoughCpuNumber && isEnoughMemory; +}; + const getValueThatSatisfiesRestrictions = (defaultValue: number, restrictions: HardwareLimits) => { if (restrictions.min > defaultValue) { return restrictions.min; - } else if (defaultValue > restrictions.max) { + } + if (defaultValue > restrictions.max) { return restrictions.max; } return defaultValue; }; -export const getComputeOfferingViewModel = createSelector( - computeOffering.selectAll, - configSelectors.get('customComputeOfferingParameters'), - configSelectors.get('defaultCustomComputeOfferingRestrictions'), - configSelectors.get('customComputeOfferingHardwareValues'), - UserTagsSelectors.getServiceOfferingParamTags, - ( +const getValueThatSatisfiesResources = (defaultValue: number, resourceLimit: string | number): number => { + const limit = Number(resourceLimit); + if (!isNaN(limit) && limit < defaultValue) { + return limit; + } + + return defaultValue; +}; + +const getRestrictionsThatSatisfiesResources = ( + restrictions: CustomComputeOfferingHardwareRestrictions, + resources: Resources +): CustomComputeOfferingHardwareRestrictions => { + const cpuResource = Number(resources.cpuNumber); + const memoryResource = Number(resources.memory); + let maxCpuNumber = restrictions.cpunumber.max; + if (!isNaN(cpuResource)) { + maxCpuNumber = restrictions.cpunumber.max > cpuResource ? cpuResource : restrictions.cpunumber.max; + } + let maxMemory = restrictions.memory.max; + if (!isNaN(memoryResource)) { + maxMemory = restrictions.memory.max > memoryResource ? memoryResource : restrictions.memory.max; + } + return { + ...restrictions, + cpunumber: { + min: restrictions.cpunumber.min, + max: maxCpuNumber + }, + memory: { + min: restrictions.memory.min, + max: maxMemory + } + }; +}; + +const getComputeOfferingViewModel = ( offerings, customComputeOfferingParameters, defaultRestrictions, defaultHardwareValues, - tags + tags, + availableResources ): ComputeOfferingViewModel[] => { - const { customOfferings, fixedOfferings } = getFixedAndCustomOfferingsArrays(offerings); - - const customOfferingsWithMetadata: ComputeOfferingViewModel[] = customOfferings - .map((offering: ServiceOffering) => { - const customParameters = getCustomOfferingHardwareParameters(offering, customComputeOfferingParameters); - const customHardwareValues = getCustomHardwareValues(customParameters); - const customHardwareRestrictions = getCustomHardwareRestrictions(customParameters); - const hardwareValuesFromTags = getHardwareValuesFromTags(offering, tags); - - const prioritizedHardwareValues = hardwareValuesFromTags || customHardwareValues || defaultHardwareValues; - const prioritizedRestrictions = customHardwareRestrictions || defaultRestrictions; - - const cpunumber = getValueThatSatisfiesRestrictions( - prioritizedHardwareValues.cpunumber, prioritizedRestrictions.cpunumber); - const cpuspeed = getValueThatSatisfiesRestrictions( - prioritizedHardwareValues.cpuspeed, prioritizedRestrictions.cpuspeed); - const memory = getValueThatSatisfiesRestrictions( - prioritizedHardwareValues.memory, prioritizedRestrictions.memory); - - - const offeringViewModel: ComputeOfferingViewModel = { - ...offering, - cpunumber, - cpuspeed, - memory, - customOfferingRestrictions: prioritizedRestrictions - }; - return offeringViewModel; - }); - - return [...fixedOfferings, ...customOfferingsWithMetadata]; + const { customOfferings, fixedOfferings } = getFixedAndCustomOfferingsArrays(offerings); + + const customOfferingsWithMetadata: ComputeOfferingViewModel[] = customOfferings + .map((offering: ServiceOffering) => { + const customParameters = getCustomOfferingHardwareParameters(offering, customComputeOfferingParameters); + const customHardwareValues = getCustomHardwareValues(customParameters); + const customHardwareRestrictions = getCustomHardwareRestrictions(customParameters); + const hardwareValuesFromTags = getHardwareValuesFromTags(offering, tags); + + const prioritizedHardwareValues = hardwareValuesFromTags || customHardwareValues || defaultHardwareValues; + const prioritizedRestrictions = customHardwareRestrictions || defaultRestrictions; + + + const isAvailableByResources = checkAvailabilityForCustomByResources( + prioritizedRestrictions.cpunumber, prioritizedRestrictions.memory, availableResources); + + let cpunumber = getValueThatSatisfiesRestrictions( + prioritizedHardwareValues.cpunumber, prioritizedRestrictions.cpunumber); + const cpuspeed = getValueThatSatisfiesRestrictions( + prioritizedHardwareValues.cpuspeed, prioritizedRestrictions.cpuspeed); + let memory = getValueThatSatisfiesRestrictions( + prioritizedHardwareValues.memory, prioritizedRestrictions.memory); + + if (isAvailableByResources) { + cpunumber = getValueThatSatisfiesResources(cpunumber, availableResources.cpuNumber); + memory = getValueThatSatisfiesResources(memory, availableResources.memory); + } + + const customOfferingRestrictions = getRestrictionsThatSatisfiesResources( + prioritizedRestrictions, availableResources); + + const offeringViewModel: ComputeOfferingViewModel = { + ...offering, + cpunumber, + cpuspeed, + memory, + customOfferingRestrictions, + isAvailableByResources + }; + return offeringViewModel; + }); + + const fixedOfferingWithMeta = fixedOfferings.map(offering => { + const offeringViewModel: ComputeOfferingViewModel = { + ...offering, + isAvailableByResources: checkAvailabilityForFixedByResources( + offering.cpunumber, offering.memory, availableResources) + }; + return offeringViewModel; + }); + + return [...fixedOfferingWithMeta, ...customOfferingsWithMetadata]; + }; + +export const getComputeOfferingForVmEditing = createSelector( + fromAuth.getUserAccount, + computeOffering.selectAll, + configSelectors.get('customComputeOfferingParameters'), + configSelectors.get('defaultCustomComputeOfferingRestrictions'), + configSelectors.get('customComputeOfferingHardwareValues'), + UserTagsSelectors.getServiceOfferingParamTags, + fromVms.getSelectedVM, + (account, + offerings, + customComputeOfferingParameters, + defaultRestrictions, + defaultHardwareValues, + tags, + vm): ComputeOfferingViewModel[] => { + const memoryUsed = vm.memory; + const cpuNumberUsed = vm.cpuNumber; + + const cpuNumber = account && account.cpuavailable === 'Unlimited' + ? account.cpuavailable + : Number(account.cpuavailable) + cpuNumberUsed; + const memory = account && account.memoryavailable === 'Unlimited' + ? account.memoryavailable + : Number(account.memoryavailable) + memoryUsed; + + const availableResources: Resources = { cpuNumber, memory }; + + return getComputeOfferingViewModel( + offerings, + customComputeOfferingParameters, + defaultRestrictions, + defaultHardwareValues, + tags, + availableResources); + } +); + +export const getComputeOfferingForVmCreation = createSelector( + fromAuth.getUserAccount, + computeOffering.selectAll, + configSelectors.get('customComputeOfferingParameters'), + configSelectors.get('defaultCustomComputeOfferingRestrictions'), + configSelectors.get('customComputeOfferingHardwareValues'), + UserTagsSelectors.getServiceOfferingParamTags, + (account, + offerings, + customComputeOfferingParameters, + defaultRestrictions, + defaultHardwareValues, + tags): ComputeOfferingViewModel[] => { + + /** + * '0' used to prevent an error when account is not loaded yet + * it happened when you go to vm creation dialog by url + */ + const availableResources: Resources = { + cpuNumber: account && account.cpuavailable || '0', + memory: account && account.memoryavailable || '0' + }; + return getComputeOfferingViewModel( + offerings, + customComputeOfferingParameters, + defaultRestrictions, + defaultHardwareValues, + tags, + availableResources); } ); diff --git a/src/app/vm/selectors/view-models/index.ts b/src/app/vm/selectors/view-models/index.ts index 65a8ed8a70..470eaeb895 100644 --- a/src/app/vm/selectors/view-models/index.ts +++ b/src/app/vm/selectors/view-models/index.ts @@ -1 +1,4 @@ -export { getComputeOfferingViewModel } from './compute-offering-view-model.selector'; +export { + getComputeOfferingForVmCreation, + getComputeOfferingForVmEditing +} from './compute-offering-view-model.selector'; diff --git a/src/app/vm/view-models/compute-offering.view-model.ts b/src/app/vm/view-models/compute-offering.view-model.ts index 329cf697bf..a9122b5eac 100644 --- a/src/app/vm/view-models/compute-offering.view-model.ts +++ b/src/app/vm/view-models/compute-offering.view-model.ts @@ -1,5 +1,6 @@ import { CustomComputeOfferingHardwareRestrictions, ServiceOffering } from '../../shared/models'; export interface ComputeOfferingViewModel extends ServiceOffering { + isAvailableByResources: boolean; customOfferingRestrictions?: CustomComputeOfferingHardwareRestrictions; } diff --git a/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.html b/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.html index 784a5c83d8..f3247023f6 100644 --- a/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.html +++ b/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.html @@ -14,6 +14,9 @@

{{ offeringName | async }} + + {{ 'ERRORS.COMPUTE_OFFERING.RESOURCE_LIMIT_EXCEEDED' | translate }} + {{ 'VM_PAGE.VM_CREATION.NO_OFFERINGS' | translate }} diff --git a/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.scss b/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.scss index 113be9a32a..e8ed895110 100644 --- a/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.scss +++ b/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.scss @@ -18,3 +18,8 @@ text-align: left; padding-top: 18px; } + +.error-message { + color: #f44336; + font-size: 10pt; +} diff --git a/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.ts b/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.ts index cb28bbaf85..cff8122f7f 100644 --- a/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.ts +++ b/src/app/vm/vm-creation/components/service-offering-selector/service-offering-selector.component.ts @@ -13,13 +13,13 @@ import { VmCreationServiceOfferingContainerComponent } from '../../service-offer @Component({ selector: 'cs-service-offering-selector', templateUrl: 'service-offering-selector.component.html', - styleUrls: ['service-offering-selector.component.scss'], + styleUrls: ['service-offering-selector.component.scss'] }) export class ServiceOfferingSelectorComponent { @Input() public serviceOfferings: Array; @Output() public change: EventEmitter; - private _serviceOffering: ServiceOffering; + private _serviceOffering: ComputeOfferingViewModel; constructor( private dialog: MatDialog, @@ -29,11 +29,11 @@ export class ServiceOfferingSelectorComponent { } @Input() - public get serviceOffering(): ServiceOffering { + public get serviceOffering(): ComputeOfferingViewModel { return this._serviceOffering; } - public set serviceOffering(serviceOffering: ServiceOffering) { + public set serviceOffering(serviceOffering: ComputeOfferingViewModel) { this._serviceOffering = serviceOffering; } diff --git a/src/app/vm/vm-creation/containers/vm-creation.container.ts b/src/app/vm/vm-creation/containers/vm-creation.container.ts index 0cec90d411..eb9f6498ba 100644 --- a/src/app/vm/vm-creation/containers/vm-creation.container.ts +++ b/src/app/vm/vm-creation/containers/vm-creation.container.ts @@ -9,7 +9,6 @@ import { AffinityGroup, DiskOffering, InstanceGroup, - ServiceOffering, SSHKeyPair, Zone } from '../../../shared/models'; @@ -38,6 +37,7 @@ import * as fromVMs from '../../../reducers/vm/redux/vm.reducers'; import * as zoneActions from '../../../reducers/zones/redux/zones.actions'; import * as fromZones from '../../../reducers/zones/redux/zones.reducers'; import { getAvailableOfferingsForVmCreation } from '../../selectors'; +import { ComputeOfferingViewModel } from '../../view-models'; @Component({ selector: 'cs-vm-creation-container', @@ -127,14 +127,14 @@ export class VmCreationContainerComponent implements OnInit { } public ngOnInit() { - this.store.dispatch(new vmActions.VmCreationFormInit()); + this.store.dispatch(new vmActions.VmCreationFormInit()) } public onDisplayNameChange(displayName: string) { this.store.dispatch(new vmActions.VmFormUpdate({ displayName })); } - public onServiceOfferingChange(serviceOffering: ServiceOffering) { + public onServiceOfferingChange(serviceOffering: ComputeOfferingViewModel) { this.store.dispatch(new vmActions.VmFormUpdate({ serviceOffering })); } diff --git a/src/app/vm/vm-creation/data/vm-creation-state.ts b/src/app/vm/vm-creation/data/vm-creation-state.ts index aa237295dd..6869bb285f 100644 --- a/src/app/vm/vm-creation/data/vm-creation-state.ts +++ b/src/app/vm/vm-creation/data/vm-creation-state.ts @@ -1,6 +1,7 @@ -import { AffinityGroup, DiskOffering, InstanceGroup, ServiceOffering, SSHKeyPair, Zone } from '../../../shared/models'; +import { AffinityGroup, DiskOffering, InstanceGroup, SSHKeyPair, Zone } from '../../../shared/models'; import { BaseTemplateModel } from '../../../template/shared'; import { VmCreationSecurityGroupData } from '../security-group/vm-creation-security-group-data'; +import { ComputeOfferingViewModel } from '../../view-models'; export interface NotSelected { name: string; @@ -17,7 +18,7 @@ export interface VmCreationState { rootDiskSize: number; rootDiskMinSize: number; securityGroupData: VmCreationSecurityGroupData; - serviceOffering: ServiceOffering; + serviceOffering: ComputeOfferingViewModel; sshKeyPair: SSHKeyPair | NotSelected; template: BaseTemplateModel; zone: Zone; diff --git a/src/app/vm/vm-creation/service-offering/vm-creation-service-offering.container.ts b/src/app/vm/vm-creation/service-offering/vm-creation-service-offering.container.ts index 358a055e63..87e2c2577c 100644 --- a/src/app/vm/vm-creation/service-offering/vm-creation-service-offering.container.ts +++ b/src/app/vm/vm-creation/service-offering/vm-creation-service-offering.container.ts @@ -9,6 +9,7 @@ import * as serviceOfferingActions from '../../../reducers/service-offerings/red import * as fromServiceOfferings from '../../../reducers/service-offerings/redux/service-offerings.reducers'; // tslint:disable-next-line import { ServiceOfferingFromMode } from '../../../service-offering/service-offering-dialog/service-offering-dialog.component'; +import * as fromAccounts from '../../../reducers/accounts/redux/accounts.reducers'; @Component({ selector: 'cs-vm-creation-service-offering-container', @@ -21,6 +22,7 @@ import { ServiceOfferingFromMode } from '../../../service-offering/service-offer [selectedClasses]="selectedClasses$ | async" [viewMode]="viewMode$ | async" [query]="query$ | async" + [account]="account$ | async" (onServiceOfferingUpdate)="updateServiceOffering($event)" (onServiceOfferingChange)="changeServiceOffering($event)" (viewModeChange)="onViewModeChange($event)" @@ -35,6 +37,7 @@ export class VmCreationServiceOfferingContainerComponent implements OnInit, Afte readonly query$ = this.store.pipe(select(fromServiceOfferings.filterQuery)); readonly selectedClasses$ = this.store.pipe(select(fromServiceOfferings.filterSelectedClasses)); readonly viewMode$ = this.store.pipe(select(fromServiceOfferings.filterSelectedViewMode)); + readonly account$ = this.store.pipe(select(fromAccounts.selectUserAccount)); public formMode = ServiceOfferingFromMode.SELECT; diff --git a/src/app/vm/vm-creation/vm-creation.component.html b/src/app/vm/vm-creation/vm-creation.component.html index 2a0ef778b6..b65d29c667 100644 --- a/src/app/vm/vm-creation/vm-creation.component.html +++ b/src/app/vm/vm-creation/vm-creation.component.html @@ -248,7 +248,7 @@

{{ 'VM_PAGE.VM_CREATION.SSH_KEY_PAIR' | translate }}

mat-button color="primary" type="submit" - [disabled]="!vmCreateForm.valid || nameIsTaken() || !vmCreationState.template" + [disabled]="isSubmitButtonDisabled(vmCreateForm.valid)" > {{ 'COMMON.CREATE' | translate }} diff --git a/src/app/vm/vm-creation/vm-creation.component.ts b/src/app/vm/vm-creation/vm-creation.component.ts index 141f3a2a75..d31c019ead 100644 --- a/src/app/vm/vm-creation/vm-creation.component.ts +++ b/src/app/vm/vm-creation/vm-creation.component.ts @@ -92,8 +92,10 @@ export class VmCreationComponent { } public showResizeSlider(): boolean { - const template = isTemplate(this.vmCreationState.template); - return template || (!template && this.isCustomizedDiskOffering()); + return this.vmCreationState.template + && !isTemplate(this.vmCreationState.template) + && this.isCustomizedDiskOffering() + && !!this.vmCreationState.rootDiskMinSize; } public rootDiskSizeLimit(): number { @@ -156,4 +158,11 @@ export class VmCreationComponent { e.preventDefault(); this.deploy.emit(this.vmCreationState); } + + public isSubmitButtonDisabled(isFormValid: boolean): boolean { + return !isFormValid + || this.nameIsTaken() + || !this.vmCreationState.template + || !this.vmCreationState.serviceOffering.isAvailableByResources; + } } diff --git a/src/app/vm/web-shell/web-shell.service.ts b/src/app/vm/web-shell/web-shell.service.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/i18n/en.json b/src/i18n/en.json index 78c6ff2545..9950594de9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -118,6 +118,9 @@ }, "SNAPSHOT_POLICIES": { "HOURLY_TURN_OFF": "Hourly schedule is turned off" + }, + "COMPUTE_OFFERING": { + "RESOURCE_LIMIT_EXCEEDED": "The service offering cannot be selected because it doesn't fit the account resources available." } }, "NOTIFICATIONS": { diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 75f735e45c..f2d39a3ecc 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -118,6 +118,9 @@ }, "SNAPSHOT_POLICIES": { "HOURLY_TURN_OFF": "Почасовое расписание отключено" + }, + "COMPUTE_OFFERING": { + "RESOURCE_LIMIT_EXCEEDED": "Вычислительное предложение не может быть выбрано. Размер выбранного предложения превышает доступные ресурсы аккаунта." } }, "NOTIFICATIONS": { diff --git a/src/testutils/data/accounts.ts b/src/testutils/data/accounts.ts new file mode 100644 index 0000000000..8e20942bb3 --- /dev/null +++ b/src/testutils/data/accounts.ts @@ -0,0 +1,73 @@ +import { Account } from '../../app/shared/models'; +import { AccountUser } from '../../app/shared/models/account-user.model'; +import { AccountType } from '../../app/shared/models/account.model'; + +const user: AccountUser = { + id: 'de80acbc-5984-48fd-90dd-061c5c3f9ba9', + username: 'test', + firstname: 'firstname', + lastname: 'lastname', + email: 'hey@bk.ru', + created: '2018-09-24T04:58:53+0000', + state: 'enabled', + account: 'test', + accounttype: 0, + roleid: 'ff636770-acbe-11e8-b088-0242ac110002', + roletype: AccountType.User, + rolename: AccountType.User, + domainid: 'ef2a7031-acbe-11e8-b088-0242ac110002', + domain: 'ROOT', + timezone: 'Etc/GMT 12', + accountid: '22dd3d42-37c7-42fe-bb60-cbf9ec805538', + iscallerchilddomain: false, + isdefault: false, +}; + +export const account: Account = { + id: '22dd3d42-37c7-42fe-bb60-cbf9ec805538', + name: 'test', + accounttype: AccountType.User, + roleid: 'ff636770-acbe-11e8-b088-0242ac110002', + roletype: 'User', + rolename: 'User', + domainid: 'ef2a7031-acbe-11e8-b088-0242ac110002', + domain: 'ROOT', + vmlimit: '20', + vmtotal: 0, + vmavailable: '20', + iplimit: '20', + iptotal: 0, + ipavailable: '0', + volumelimit: '20', + volumetotal: 0, + volumeavailable: '20', + snapshotlimit: '20', + snapshottotal: 0, + snapshotavailable: '20', + templatelimit: '20', + templatetotal: 0, + templateavailable: '20', + vmstopped: 0, + vmrunning: 0, + networklimit: '20', + networktotal: 0, + networkavailable: '20', + vpclimit: '20', + vpctotal: 0, + vpcavailable: '20', + cpulimit: '5', + cputotal: 0, + cpuavailable: 'Infinity', + memorylimit: '40000', + memorytotal: 0, + memoryavailable: 'Infinity', + primarystoragelimit: '1000', + primarystoragetotal: 0, + primarystorageavailable: '1000', + secondarystoragelimit: '400', + secondarystoragetotal: 0, + secondarystorageavailable: '400.0', + state: 'enabled', + user: [user], + isdefault: false, +}; diff --git a/src/testutils/data/compute-offerings.ts b/src/testutils/data/compute-offerings.ts new file mode 100644 index 0000000000..6da80a105c --- /dev/null +++ b/src/testutils/data/compute-offerings.ts @@ -0,0 +1,37 @@ +import { ServiceOffering } from '../../app/shared/models'; + +export const fixedComputeOffering: ServiceOffering = { + id: '36de12ed-17f1-441f-903f-ab274832c318', + name: 'Medium Instance', + displaytext: 'Medium Instance', + cpunumber: 1, + cpuspeed: 1000, + memory: 1024, + created: '2018-08-31T01:50:04+0000', + storagetype: 'shared', + provisioningtype: 'thin', + offerha: false, + limitcpuuse: false, + isvolatile: false, + issystem: false, + defaultuse: false, + iscustomized: false, +}; + + +export const customComputeOffering: ServiceOffering = { + id: '9f55af11-99de-40b7-ab36-45c576296766', + name: 'custom', + displaytext: 'any', + created: '2018-08-31T01:55:05+0000', + storagetype: 'shared', + provisioningtype: 'thin', + offerha: false, + limitcpuuse: false, + isvolatile: false, + domain: 'ROOT', + issystem: false, + defaultuse: false, + iscustomized: true, +}; + diff --git a/src/testutils/data/index.ts b/src/testutils/data/index.ts new file mode 100644 index 0000000000..2d7277d5dd --- /dev/null +++ b/src/testutils/data/index.ts @@ -0,0 +1,3 @@ +export * from './compute-offerings'; +export * from './vitrual-machines'; +export * from './accounts'; diff --git a/src/testutils/mocks/model-services/fixtures/serviceOfferings.json b/src/testutils/mocks/model-services/fixtures/serviceOfferings.json index ccd2f5cd3f..51b81d43af 100644 --- a/src/testutils/mocks/model-services/fixtures/serviceOfferings.json +++ b/src/testutils/mocks/model-services/fixtures/serviceOfferings.json @@ -14,7 +14,8 @@ "isvolatile": false, "issystem": false, "defaultuse": false, - "iscustomized": false + "iscustomized": false, + "isAvailableByResources": true }, { "id": "b1196c0e-0c1a-4416-bea8-f6a62309fac5", @@ -31,7 +32,8 @@ "isvolatile": false, "issystem": false, "defaultuse": false, - "iscustomized": false + "iscustomized": false, + "isAvailableByResources": true }, { "id": "a18d52b6-268e-421c-9d0c-1c1635d3b9b9", @@ -50,7 +52,8 @@ "iscustomized": true, "cpunumber": 2, "cpuspeed": 500, - "memory": 1024 + "memory": 1024, + "isAvailableByResources": true }, { "id": "a18d52b6-268e-421c-9d0c-1c1635d3b9b9", @@ -66,6 +69,7 @@ "domain": "develop", "issystem": false, "defaultuse": false, - "iscustomized": true + "iscustomized": true, + "isAvailableByResources": true } ]