Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Show usage of quotas on pool and job page too #1098

Merged
merged 15 commits into from
Feb 27, 2018
4 changes: 4 additions & 0 deletions app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { BatchLabsErrorHandler } from "app/error-handler";

// services
import { HttpModule } from "@angular/http";
import { CommonModule } from "app/components/common";
import { LayoutModule } from "app/components/layout";
import { MiscModule } from "app/components/misc";
import { MaterialModule } from "app/core";
Expand Down Expand Up @@ -76,6 +77,7 @@ import {
PredefinedFormulaService,
PricingService,
PythonRpcService,
QuotaService,
ResourceAccessService,
SSHKeyService,
SettingsService,
Expand Down Expand Up @@ -134,6 +136,7 @@ const graphApiServices = [AADApplicationService, AADGraphHttpService, MsGraphHtt
BatchClientService,
CacheDataService,
CommandService,
CommonModule,
ComputeService,
ElectronRemote,
ElectronShell,
Expand All @@ -158,6 +161,7 @@ const graphApiServices = [AADApplicationService, AADGraphHttpService, MsGraphHtt
PollService,
PoolService,
PricingService,
QuotaService,
PythonRpcService,
ResourceAccessService,
SettingsService,
Expand Down
3 changes: 2 additions & 1 deletion app/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { BrowserModule } from "@angular/platform-browser";
import { RouterModule } from "@angular/router";

import { BaseModule } from "app/components/base";
import { CommonModule } from "app/components/common";
import { MaterialModule } from "app/core";

export const commonModules = [
BrowserModule, MaterialModule, RouterModule,
FormsModule, ReactiveFormsModule,
BaseModule,
CommonModule, BaseModule,
];
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, NgZone, OnDestroy, OnInit, ViewContainerRef } from "@angular/core";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { autobind } from "app/core";
import { Subscription } from "rxjs";
Expand Down Expand Up @@ -44,6 +44,7 @@ export class AccountDetailsComponent implements OnInit, OnDestroy {

constructor(
router: Router,
private changeDetector: ChangeDetectorRef,
private activatedRoute: ActivatedRoute,
private accountService: AccountService,
private dialogService: DialogService,
Expand All @@ -56,6 +57,7 @@ export class AccountDetailsComponent implements OnInit, OnDestroy {
this.data = this.accountService.view();
this.data.item.subscribe((account) => {
this.account = account;
this.changeDetector.markForCheck();
if (account) {
this._loadQuickAccessLists();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,242 +1,66 @@
import { Component, Input, OnChanges, OnDestroy } from "@angular/core";
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnDestroy, OnInit,
} from "@angular/core";
import { Subscription } from "rxjs";

import { LoadingStatus } from "app/components/base/loading";
import { AccountResource, Pool } from "app/models";
import { ComputeService, PoolListParams, PoolService, VmSizeService } from "app/services";
import { ListView } from "app/services/core";
import { ComponentUtils } from "app/utils";
import { AccountResource, BatchQuotas } from "app/models";
import { ElectronShell, QuotaService } from "app/services";

import { List } from "immutable";
import { Observable } from "rxjs/Observable";
import { ContextMenu, ContextMenuItem, ContextMenuService } from "app/components/base/context-menu";
import { Constants } from "common";
import "./account-quotas-card.scss";

type ProgressColorClass = "high-usage" | "medium-usage" | "low-usage";

@Component({
selector: "bl-account-quotas-card",
templateUrl: "account-quotas-card.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountQuotasCardComponent implements OnChanges, OnDestroy {
export class AccountQuotasCardComponent implements OnDestroy, OnInit {
@Input() public account: AccountResource;
public get bufferValue(): number {
return 100;
}
public poolData: ListView<Pool, PoolListParams>;
public vmSizeCores: StringMap<number>;

private _usedPool: number = null;
private _totalPoolQuota: number = null;
public bufferValue = 100;

private _dedicatedCoreLoadingStatus = LoadingStatus.Loading;
private _usedDedicatedCore: number = null;
private _dedicatedCoreQuota: number = null;
public quotas: BatchQuotas = new BatchQuotas();
public use: BatchQuotas = new BatchQuotas();
public loadingUse = true;

private _lowPriorityCoreStatus = LoadingStatus.Loading;
private _usedLowPrioirtyCore: number = null;
private _lowPriorityCoreQuota: number = null;
private _quotaSub: Subscription;
private _usageSub: Subscription;

private _poolListViewSub: Subscription;
private _vmSizeSub: Subscription;
private _computeServiceSub: Subscription;
constructor(
private quotaService: QuotaService,
private changeDetector: ChangeDetectorRef,
private contextMenuService: ContextMenuService,
private shell: ElectronShell) {
this._quotaSub = this.quotaService.quotas.subscribe((quotas) => {
this.quotas = quotas;
this.changeDetector.markForCheck();
});

constructor(private computeService: ComputeService,
private poolService: PoolService,
private vmSizeService: VmSizeService) {
this.vmSizeCores = {...vmSizeService.additionalVmSizeCores};
const vmSizeObs = Observable.merge(
this.vmSizeService.virtualMachineSizes, this.vmSizeService.cloudServiceSizes);
this._vmSizeSub = vmSizeObs.subscribe(vmSizes => {
if (vmSizes) {
vmSizes.forEach(vmSize => this.vmSizeCores[vmSize.id] = vmSize.numberOfCores);
}
this._usageSub = this.quotaService.usage.subscribe((quota) => {
this.loadingUse = false;
this.use = quota;
this.changeDetector.markForCheck();
});
}

public ngOnChanges(changes) {
if (changes.account) {
if (this.account.isBatchManaged) {
this._dedicatedCoreQuota = this.account.properties.dedicatedCoreQuota;
this._lowPriorityCoreQuota = this.account.properties.lowPriorityCoreQuota;
this._listUsage();
}

if (this.isByosAccount(changes)) {
if (this._computeServiceSub) {
this._computeServiceSub.unsubscribe();
}
this._computeServiceSub = this.computeService.getCoreQuota().subscribe((dedicatedCoreQuota) => {
if (this.isByosAccount(changes)) {
this._dedicatedCoreQuota = dedicatedCoreQuota;
this._lowPriorityCoreQuota = null;
this._listUsage(true);
}
});
}
}
public ngOnInit() {
this.quotaService.updateUsages();
}

public ngOnDestroy(): void {
if (this._poolListViewSub) {
this._poolListViewSub.unsubscribe();
}
if (this._vmSizeSub) {
this._vmSizeSub.unsubscribe();
}
if (this._computeServiceSub) {
this._computeServiceSub.unsubscribe();
}
}

/**
* Get pool usage progress bar percent
*/
public get poolUsagePercent() {
return this._calculatePercentage(this._usedPool, this._totalPoolQuota);
}

/**
* Get friendly message displayed for pools
* Format: {{used}}/{{total}} ({{Percent}})
*/
public get poolUsageStatus(): string {
const used = this._usedPool;
const total = this._totalPoolQuota;
if (used !== null && total !== null) {
return `${used}/${total} (${Math.floor(this.poolUsagePercent)}%)`;
}
return "N/A";
}

/**
* Get dedicated cores usage progress bar percent
*/
public get dedicatedCoresPercent() {
return this._calculatePercentage(this._usedDedicatedCore, this._dedicatedCoreQuota);
}

/**
* Get friendly message displayed for dedicated cores
* Format: {{used}}/{{total}} ({{Percent}})
*/
public get dedicatedCoreStatus(): string {
switch (this._dedicatedCoreLoadingStatus) {
case LoadingStatus.Loading:
return "Loading.";
case LoadingStatus.Ready:
const used = this._usedDedicatedCore;
const total = this._dedicatedCoreQuota;
if (used !== null && total !== null) {
return `${used}/${total} (${Math.floor(this.dedicatedCoresPercent)}%)`;
}
case LoadingStatus.Error:
default:
return "N/A";
}
}

/**
* Get low priority cores usage progress bar percent
*/
public get lowPriorityCoresPercent() {
return this._calculatePercentage(this._usedLowPrioirtyCore, this._lowPriorityCoreQuota);
}

/**
* Get friendly message displayed for low priority cores
* Format: {{used}}/{{total}} ({{Percent}})
*/
public get lowPriorityCoreStatus(): string {
switch (this._lowPriorityCoreStatus) {
case LoadingStatus.Loading:
return "Loading.";
case LoadingStatus.Ready:
const used = this._usedLowPrioirtyCore;
const total = this._lowPriorityCoreQuota;
if (used !== null && total !== null) {
return `${used}/${total} (${Math.floor(this.lowPriorityCoresPercent)}%)`;
}
case LoadingStatus.Error:
default:
return "N/A";
}
}

/**
* Defines usage progress bar color for pool usage, dedicated/lowPriority cores usage.
* Use 3 different states (error, warn and success) to represent high usage, medium usage and low usage
* @param percent
*/
public getColorClass(percent: number): ProgressColorClass {
if (percent <= 100 && percent >= 90) {
return "high-usage";
} else if (percent >= 50) {
return "medium-usage";
}
return "low-usage";
}

/**
* Fetch latest pool list and core usages
*/
private _listUsage(isByos?: boolean) {
this._dedicatedCoreLoadingStatus = LoadingStatus.Loading;
this._lowPriorityCoreStatus = LoadingStatus.Loading;
this.poolData = this.poolService.listView();
this.poolData.fetchNext();
if (this._poolListViewSub) {
this._poolListViewSub.unsubscribe();
}
this._poolListViewSub = this.poolData.items.subscribe((pools) => {
this._usedPool = pools.size;
this._loadCoreUsages(pools);
this._dedicatedCoreLoadingStatus = LoadingStatus.Ready;
if (isByos) {
this._lowPriorityCoreStatus = LoadingStatus.Error;
} else {
this._lowPriorityCoreStatus = LoadingStatus.Ready;
}
}, (error) => {
this._dedicatedCoreLoadingStatus = LoadingStatus.Error;
this._lowPriorityCoreStatus = LoadingStatus.Error;
});
this._totalPoolQuota = this.account.properties.poolQuota;
}

/**
* Calculate pool dedicated cores and low priority core usage
* @param pools
*/
private _loadCoreUsages(pools: List<Pool>) {
this._usedDedicatedCore = 0;
this._usedLowPrioirtyCore = 0;
if (!pools || pools.size === 0) {
return;
}
pools.forEach(pool => {
if (pool && pool.vmSize) {
const key = pool.vmSize.toLowerCase();
if (this.vmSizeCores[key]) {
this._usedDedicatedCore += (this.vmSizeCores[key] * pool.currentDedicatedNodes);
this._usedLowPrioirtyCore += (this.vmSizeCores[key] * pool.currentLowPriorityNodes);
}
}
});
this._quotaSub.unsubscribe();
this._usageSub.unsubscribe();
}

/**
* Calculate percentage of used pools, dedicated/lowPriority cores
* @param used
* @param total
*/
private _calculatePercentage(used: number, total: number): number {
if (used !== null && total !== null && total > 0) {
return (used / total) * 100;
}
return 0;
@HostListener("contextmenu")
public showContextMenu() {
this.contextMenuService.openMenu(new ContextMenu([
new ContextMenuItem("Refresh", () => this.quotaService.refresh()),
new ContextMenuItem("Request quota increase", () => this._gotoQuotaRequest()),
]));
}

private isByosAccount(changes) {
return ComponentUtils.recordChangedId(changes.account) && !this.account.isBatchManaged;
private _gotoQuotaRequest() {
this.shell.openExternal(Constants.ExternalLinks.supportRequest);
}
}
Loading