+
+ + + + + + + + + +
+
- -
- -
-
- {{decorator.id}} - {{decorator.state}} -
-
-
- Pool: {{job.poolId}} - -
+ {{decorator.id}} + {{decorator.state}} + Pool: {{job.poolId}} +
- - -
- - - - - - - - +
+ +
diff --git a/app/components/job/home/job-home.html b/app/components/job/home/job-home.html index 57b6aeaef3..13dda39be1 100644 --- a/app/components/job/home/job-home.html +++ b/app/components/job/home/job-home.html @@ -1,8 +1,7 @@
- + +
diff --git a/app/components/node/details/node-details.html b/app/components/node/details/node-details.html index db3add65d4..44dc876c77 100644 --- a/app/components/node/details/node-details.html +++ b/app/components/node/details/node-details.html @@ -1,23 +1,17 @@
- -
+
+ -
-
- {{node.id}} - {{node.state}} -
- - + + +
+
+ {{node.id}} + {{node.state}}

Pool: {{poolId}}

- - - - +
diff --git a/app/components/pool/details/pool-details.html b/app/components/pool/details/pool-details.html index f801a98d83..4ae8e67a41 100644 --- a/app/components/pool/details/pool-details.html +++ b/app/components/pool/details/pool-details.html @@ -1,29 +1,25 @@
- -
+
+ -
-
- {{pool.id}} - {{pool.vmSize}}, {{pool.allocationState}} -
-
- -
- - + + + + + +
+
+ {{pool.id}} + {{pool.vmSize}}, {{pool.allocationState}}

Last resized {{poolDecorator.lastResized}}

{{poolDecorator.poolOs}}

- - - - - - - +
+
+ +
diff --git a/app/components/pool/home/pool-home.html b/app/components/pool/home/pool-home.html index 98dee76a51..bd0fea300c 100644 --- a/app/components/pool/home/pool-home.html +++ b/app/components/pool/home/pool-home.html @@ -1,8 +1,7 @@
- + +
diff --git a/app/components/task/action/add/task-create-basic-dialog.component.ts b/app/components/task/action/add/task-create-basic-dialog.component.ts index c719e5a4ef..518cfabb1f 100644 --- a/app/components/task/action/add/task-create-basic-dialog.component.ts +++ b/app/components/task/action/add/task-create-basic-dialog.component.ts @@ -29,7 +29,7 @@ export class TaskCreateBasicDialogComponent extends DynamicForm, + public sidebarRef: SidebarRef, protected taskService: TaskService, private notificationService: NotificationService) { super(TaskCreateDto); diff --git a/app/components/task/details/task-details.html b/app/components/task/details/task-details.html index bdb2a971f1..28b9f922d3 100644 --- a/app/components/task/details/task-details.html +++ b/app/components/task/details/task-details.html @@ -4,29 +4,22 @@
-
-
- -
- -
-
- {{task.id}} - {{task.displayName}} - {{task.state}} -
-
- -

Job: {{jobId}}

-
-
+
+ + + + + + +
+
+ {{task.id}} - {{task.displayName}} + {{task.state}} +

Job: {{jobId}}

+
+
- - - - - - diff --git a/app/components/task/home/task-home.html b/app/components/task/home/task-home.html index 1cea5f363c..d1ef08bc89 100644 --- a/app/components/task/home/task-home.html +++ b/app/components/task/home/task-home.html @@ -1,8 +1,7 @@
- + +
diff --git a/app/styles/base/layout.scss b/app/styles/base/layout.scss index de6acf1da2..c7fa123774 100644 --- a/app/styles/base/layout.scss +++ b/app/styles/base/layout.scss @@ -219,8 +219,7 @@ footer { .context-button-bar { position: relative !important; - margin-top: -34px !important; - margin-right: 10px !important; + margin: -34px 10px 0 !important; float: right; button { diff --git a/app/styles/job/details.scss b/app/styles/job/details.scss index 3ebd6cc37e..913e51f8b2 100644 --- a/app/styles/job/details.scss +++ b/app/styles/job/details.scss @@ -1,12 +1,19 @@ -bl-job-details { - .overview > .main-content { - display: flex; +md-card.overview { + padding: 0 !important; + display: flex; - > .info { - flex: 1; - } + > .actions { + width: $action-btn-size + 1; } + > .content { + padding: 18px; + flex: 1; + } + + > .tile { + border-left: 1px solid #d5d5d5; + } } bl-job-progress-status { diff --git a/app/styles/variables.scss b/app/styles/variables.scss index bbdc52c100..6ad570ab37 100644 --- a/app/styles/variables.scss +++ b/app/styles/variables.scss @@ -134,3 +134,7 @@ $listview-header-height : 90px; $footer-height : 0; $content-top-position : $header-height; $contentview-height : calc(100vh - #{$header-height}); + + +// Base component dimensions +$action-btn-size: 32px; diff --git a/app/styles/vendor/material-theme.scss b/app/styles/vendor/material-theme.scss index 5d92612c93..8441e06c68 100644 --- a/app/styles/vendor/material-theme.scss +++ b/app/styles/vendor/material-theme.scss @@ -99,21 +99,6 @@ md-card { } } - &.overview { - padding: 18px !important; - - md-card-actions { - margin-left: -10px !important; - margin-right: -10px !important; - padding-top: 4px !important; - } - - md-card-actions:last-child { - margin-bottom: -10px !important; - padding-bottom: 0 !important; - } - } - &.details { margin-top:8px !important; padding:0 !important; diff --git a/test/app/components/application/errors/application-error-display.component.spec.ts b/test/app/components/application/errors/application-error-display.component.spec.ts index 6f55549a5d..d3bb300b45 100644 --- a/test/app/components/application/errors/application-error-display.component.spec.ts +++ b/test/app/components/application/errors/application-error-display.component.spec.ts @@ -4,11 +4,11 @@ import { By } from "@angular/platform-browser"; import { BehaviorSubject } from "rxjs"; import { ApplicationErrorDisplayComponent } from "app/components/application/errors"; +import { SidebarManager } from "app/components/base/sidebar"; import { AccountResource, Application } from "app/models"; import { AccountService } from "app/services"; import * as Fixtures from "test/fixture"; import { BannerMockComponent } from "test/utils/mocks/components"; -import { SidebarManager } from "app/components/base/sidebar"; @Component({ template: ``, diff --git a/test/app/components/base/buttons/action-button.component.spec.ts b/test/app/components/base/buttons/action-button.component.spec.ts new file mode 100644 index 0000000000..e8715fe7f8 --- /dev/null +++ b/test/app/components/base/buttons/action-button.component.spec.ts @@ -0,0 +1,98 @@ +import { Component, DebugElement, NO_ERRORS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { MaterialModule, MdTooltip } from "@angular/material"; +import { ActionButtonComponent } from "app/components/base/buttons/action-button.component"; +import { click } from "test/utils/helpers"; + +@Component({ + template: ` + + + `, +}) +class TestComponent { + public disabled: boolean = false; + + public onAction: jasmine.Spy; + + public color = "primary"; + constructor() { + this.onAction = jasmine.createSpy("onAction"); + } +} + +describe("ActionButton", () => { + let fixture: ComponentFixture; + let testComponent: TestComponent; + let component: ActionButtonComponent; + let de: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MaterialModule], + declarations: [ActionButtonComponent, TestComponent], + schemas: [NO_ERRORS_SCHEMA], + }); + fixture = TestBed.createComponent(TestComponent); + testComponent = fixture.componentInstance; + de = fixture.debugElement.query(By.css("bl-action-btn")); + component = de.componentInstance; + fixture.detectChanges(); + }); + + it("should have the icon specified", () => { + const icon = de.query(By.css("i.fa.fa-stop")); + expect(icon).not.toBeFalsy(); + }); + + it("Should have the tooltip specified with title", () => { + const el = de.query(By.css(".action-btn")); + const tooltip = el.injector.get(MdTooltip); + expect(tooltip.message).toBe("Stop"); + }); + + it("should change color", () => { + expect(de.attributes["color"]).toBe("primary"); + + testComponent.color = "warn"; + fixture.detectChanges(); + expect(de.attributes["color"]).toBe("warn"); + + }); + + describe("when disabled", () => { + beforeEach(() => { + testComponent.disabled = true; + fixture.detectChanges(); + }); + + it("should have the disabled class", () => { + expect(de.classes["disabled"]).toBe(true); + }); + + it("should not trigger the event on click", () => { + click(de); + fixture.detectChanges(); + expect(testComponent.onAction).not.toHaveBeenCalled(); + }); + }); + + describe("when enabled", () => { + beforeEach(() => { + testComponent.disabled = false; + fixture.detectChanges(); + }); + + it("should have NOT the disabled class", () => { + expect(de.classes["disabled"]).toBe(false); + }); + + it("should trigger the event on click", () => { + click(de); + fixture.detectChanges(); + expect(testComponent.onAction).toHaveBeenCalledOnce(); + }); + }); +}); diff --git a/test/app/memory-leak/mem-leak.spec.ts b/test/app/memory-leak/mem-leak.spec.ts index 684a0b335a..fad8c731c4 100644 --- a/test/app/memory-leak/mem-leak.spec.ts +++ b/test/app/memory-leak/mem-leak.spec.ts @@ -5,6 +5,7 @@ import { TestBed, async } from "@angular/core/testing"; import { TaskDetailsModule } from "app/components/task/details"; export function main() { + // tslint:disable-next-line:ban fdescribe("Memory leak Testing", () => { for (let i = 0; i < 100000; i++) { describe(`${i}`, () => { From c151353fdcd205faa0d523f0ba8ffcd54e16a698 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 31 May 2017 09:57:12 -0700 Subject: [PATCH 04/37] Update storage account and vmSize table to fix active problem (#415) * Update storage account and vmSize table to fix active problem * ADd specs and update --- .../storage-account-picker.html | 9 ++--- .../base/abstract-list/abstract-list-base.ts | 13 +++++-- .../pool/action/add/vm-size-picker.html | 2 +- app/core/record/arm-record.ts | 14 ++++++++ app/core/record/index.ts | 1 + app/models/storage-account.ts | 4 +-- app/models/vm-size.ts | 35 ++++++++++--------- .../storage-account-picker.component.spec.ts | 10 +++--- .../storage-account-card.component.spec.ts | 2 +- .../base/table/table.component.spec.ts | 8 ++--- .../pool/action/add/vm-size-picker.spec.ts | 22 ++++++------ test/app/core/arm-record.spec.ts | 17 +++++++++ test/app/models/storage-account.spec.ts | 10 ++++++ test/app/models/vm-size.spec.ts | 10 ++++++ 14 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 app/core/record/arm-record.ts create mode 100644 test/app/core/arm-record.spec.ts create mode 100644 test/app/models/storage-account.spec.ts create mode 100644 test/app/models/vm-size.spec.ts diff --git a/app/components/account/base/storage-account-picker/storage-account-picker.html b/app/components/account/base/storage-account-picker/storage-account-picker.html index f85a80243d..588602c4a7 100644 --- a/app/components/account/base/storage-account-picker/storage-account-picker.html +++ b/app/components/account/base/storage-account-picker/storage-account-picker.html @@ -1,7 +1,7 @@

Storage accounts in the same region as your batch account({{account.location}})

- Name @@ -12,8 +12,9 @@

Storage accounts in the same region as your batch account({{account.location - + + @@ -23,12 +24,12 @@

Storage accounts in the same region as your batch account({{account.location

Other regions

- + Name Region - + diff --git a/app/components/base/abstract-list/abstract-list-base.ts b/app/components/base/abstract-list/abstract-list-base.ts index b78f49b33f..4df56e4c88 100644 --- a/app/components/base/abstract-list/abstract-list-base.ts +++ b/app/components/base/abstract-list/abstract-list-base.ts @@ -31,6 +31,7 @@ export class AbstractListBase implements AfterViewInit, OnDestroy { @Input() public set activeItem(key) { + this._activeItemInput = key; this.setActiveItem(key); } @@ -62,13 +63,18 @@ export class AbstractListBase implements AfterViewInit, OnDestroy { public get selectedItems() { return Object.keys(this._selectedItems); } public listFocused: boolean = false; - public focusedItem = new BehaviorSubject(null); + public focusedItem = new BehaviorSubject(null); /** * Map of the selected items. Used for better performance to check if an item is selected. */ private _selectedItems: { [key: string]: boolean } = {}; private _activeItemKey = new BehaviorSubject(null); + + /** + * Save the value provided in the activeItem input + */ + private _activeItemInput = null; private _subs: Subscription[] = []; constructor( @@ -79,7 +85,9 @@ export class AbstractListBase implements AfterViewInit, OnDestroy { this._subs.push(this._activeItemKey.subscribe(x => { this.selectedItems = x ? [x.key] : []; this.activatedItemChange.emit(x); - this.activeItemChange.emit(x && x.key); + if (!x || x.key !== this._activeItemInput) { + this.activeItemChange.emit(x && x.key); + } if (this.listFocused) { this.focusedItem.next(x && x.key); @@ -108,7 +116,6 @@ export class AbstractListBase implements AfterViewInit, OnDestroy { } else { this.items.changes.first().subscribe((newItems: QueryList) => { this._processInitialItems(newItems); - }); } diff --git a/app/components/pool/action/add/vm-size-picker.html b/app/components/pool/action/add/vm-size-picker.html index a96bbc0f0b..c7b1c1dc72 100644 --- a/app/components/pool/action/add/vm-size-picker.html +++ b/app/components/pool/action/add/vm-size-picker.html @@ -10,7 +10,7 @@ OS Disk Resource/Temp Disk - + {{size.title}} diff --git a/app/core/record/arm-record.ts b/app/core/record/arm-record.ts new file mode 100644 index 0000000000..ddf8fa3f30 --- /dev/null +++ b/app/core/record/arm-record.ts @@ -0,0 +1,14 @@ +import { Record } from "./record"; + +export interface ArmRecordAttributes { + id: string; +} + +/** + * ArmRecord is a subclass of record that unify the id for ARM record which have problems with the case. + */ +export class ArmRecord extends Record { + constructor(data: T) { + super(Object.assign({}, data, { id: data.id && data.id.toLowerCase() })); + } +} diff --git a/app/core/record/index.ts b/app/core/record/index.ts index 44c0753f12..5b0904bad3 100644 --- a/app/core/record/index.ts +++ b/app/core/record/index.ts @@ -1,3 +1,4 @@ +export * from "./arm-record"; export * from "./record"; export * from "./decorators"; export * from "./errors"; diff --git a/app/models/storage-account.ts b/app/models/storage-account.ts index eec6317cbb..7d044ba729 100644 --- a/app/models/storage-account.ts +++ b/app/models/storage-account.ts @@ -1,4 +1,4 @@ -import { Model, Prop, Record } from "app/core"; +import { ArmRecord, Model, Prop, Record } from "app/core"; interface StorageAccountPropertiesAttributes { creationTime: Date; @@ -36,7 +36,7 @@ export interface StorageAccountAttributes { } @Model() -export class StorageAccount extends Record { +export class StorageAccount extends ArmRecord { @Prop() public id: string; @Prop() public location: string; diff --git a/app/models/vm-size.ts b/app/models/vm-size.ts index 5c4e0acc37..5ff13dadfb 100644 --- a/app/models/vm-size.ts +++ b/app/models/vm-size.ts @@ -1,13 +1,4 @@ -import { Record } from "immutable"; - -const VmSizeRecord = Record({ - name: null, - numberOfCores: null, - osDiskSizeInMB: null, - resourceDiskSizeInMB: null, - memoryInMB: null, - maxDataDiskCount: null, -}); +import { Model, Prop, Record } from "app/core"; export interface VmSizeAttributes { name: string; @@ -18,34 +9,44 @@ export interface VmSizeAttributes { maxDataDiskCount: number; } -export class VmSize extends VmSizeRecord implements VmSizeAttributes { +@Model() +export class VmSize extends Record { + /** + * Id for the vmSize(This is computed form the name attribute) + */ + @Prop() public id: string; + /** * Specifies the name of the virtual machine size */ - public name: string; + @Prop() public name: string; /** * Specifies the number of available CPU cores. */ - public numberOfCores: number; + @Prop() public numberOfCores: number; /** * Specifies the size in MB of the operating system disk. */ - public osDiskSizeInMB: number; + @Prop() public osDiskSizeInMB: number; /** * Specifies the size in MB of the temporary or resource disk. */ - public resourceDiskSizeInMB: number; + @Prop() public resourceDiskSizeInMB: number; /** * Specifies the available RAM in MB. */ - public memoryInMB: number; + @Prop() public memoryInMB: number; /** * Specifies the maximum number of data disks that can be attached to the VM size. */ - public maxDataDiskCount: number; + @Prop() public maxDataDiskCount: number; + + constructor(data: VmSizeAttributes) { + super({ ...data, id: data.name && data.name.toLowerCase() }); + } } diff --git a/test/app/components/account/base/storage-account-picker/storage-account-picker.component.spec.ts b/test/app/components/account/base/storage-account-picker/storage-account-picker.component.spec.ts index b9d3c1d396..071d3600df 100644 --- a/test/app/components/account/base/storage-account-picker/storage-account-picker.component.spec.ts +++ b/test/app/components/account/base/storage-account-picker/storage-account-picker.component.spec.ts @@ -36,11 +36,11 @@ describe("StorageAccountPickerComponent", () => { beforeEach(() => { storageServiceSpy = { list: () => Observable.of(List([ - new StorageAccount({ id: "sub-1/storage-1", name: "storage-1", location: "westus" }), - new StorageAccount({ id: "sub-1/storage-2", name: "storage-2", location: "brazilsouth" }), - new StorageAccount({ id: "sub-1/storage-3", name: "storage-3", location: "westus" }), - new StorageAccount({ id: "sub-1/storage-4", name: "storage-4", location: "easteurope" }), - new StorageAccount({ id: "sub-1/storage-5", name: "storage-5", location: "eastus" }), + new StorageAccount({ id: "sub-1/storage-1", name: "storage-1", location: "westus" } as any), + new StorageAccount({ id: "sub-1/storage-2", name: "storage-2", location: "brazilsouth" } as any), + new StorageAccount({ id: "sub-1/storage-3", name: "storage-3", location: "westus" } as any), + new StorageAccount({ id: "sub-1/storage-4", name: "storage-4", location: "easteurope" } as any), + new StorageAccount({ id: "sub-1/storage-5", name: "storage-5", location: "eastus" } as any), ])), }; TestBed.configureTestingModule({ diff --git a/test/app/components/account/details/storage-account-card/storage-account-card.component.spec.ts b/test/app/components/account/details/storage-account-card/storage-account-card.component.spec.ts index 012c4c71dd..6bcbe284cb 100644 --- a/test/app/components/account/details/storage-account-card/storage-account-card.component.spec.ts +++ b/test/app/components/account/details/storage-account-card/storage-account-card.component.spec.ts @@ -23,7 +23,7 @@ const accountWithInvalidStorage = new AccountResource({ autoStorage: { storageAccountId: "sub-1/storage-wrong" }, }, }); -const storage1 = new StorageAccount({ id: "sub-1/storage-1", name: "storage-1", location: "westus" }); +const storage1 = new StorageAccount({ id: "sub-1/storage-1", name: "storage-1", location: "westus" } as any); @Component({ template: ``, diff --git a/test/app/components/base/table/table.component.spec.ts b/test/app/components/base/table/table.component.spec.ts index c536f7e8d1..ceaded0671 100644 --- a/test/app/components/base/table/table.component.spec.ts +++ b/test/app/components/base/table/table.component.spec.ts @@ -11,10 +11,10 @@ import { import { VmSize } from "app/models"; import { click } from "test/utils/helpers"; -const sizeA = new VmSize({ name: "Size A", numberOfCores: 1, resourceDiskSizeInMB: 1000 }); -const sizeB = new VmSize({ name: "Size B", numberOfCores: 8, resourceDiskSizeInMB: 2000 }); -const sizeC = new VmSize({ name: "Size C", numberOfCores: 4, resourceDiskSizeInMB: 80000 }); -const sizeD = new VmSize({ name: "Size D", numberOfCores: 2, resourceDiskSizeInMB: 4000 }); +const sizeA = new VmSize({ name: "Size A", numberOfCores: 1, resourceDiskSizeInMB: 1000 } as any); +const sizeB = new VmSize({ name: "Size B", numberOfCores: 8, resourceDiskSizeInMB: 2000 } as any); +const sizeC = new VmSize({ name: "Size C", numberOfCores: 4, resourceDiskSizeInMB: 80000 } as any); +const sizeD = new VmSize({ name: "Size D", numberOfCores: 2, resourceDiskSizeInMB: 4000 } as any); @Component({ template: ` diff --git a/test/app/components/pool/action/add/vm-size-picker.spec.ts b/test/app/components/pool/action/add/vm-size-picker.spec.ts index dafe17703d..2873b20a33 100644 --- a/test/app/components/pool/action/add/vm-size-picker.spec.ts +++ b/test/app/components/pool/action/add/vm-size-picker.spec.ts @@ -35,19 +35,19 @@ describe("VmSizePickerComponent", () => { memory: ["Standard_M*"], }), virtualMachineSizes: Observable.of(List([ - new VmSize({ name: "Standard_A1" }), - new VmSize({ name: "Standard_A2" }), - new VmSize({ name: "Standard_A3" }), - new VmSize({ name: "Standard_C1" }), - new VmSize({ name: "Standard_C2" }), - new VmSize({ name: "Standard_O1" }), + new VmSize({ name: "Standard_A1" } as any), + new VmSize({ name: "Standard_A2" } as any), + new VmSize({ name: "Standard_A3" } as any), + new VmSize({ name: "Standard_C1" } as any), + new VmSize({ name: "Standard_C2" } as any), + new VmSize({ name: "Standard_O1" } as any), ])), cloudServiceSizes: Observable.of(List([ - new VmSize({ name: "Standard_A1" }), - new VmSize({ name: "Standard_A2" }), - new VmSize({ name: "Standard_A3" }), - new VmSize({ name: "Standard_C1" }), - new VmSize({ name: "Standard_C2" }), + new VmSize({ name: "Standard_A1" } as any), + new VmSize({ name: "Standard_A2" } as any), + new VmSize({ name: "Standard_A3" } as any), + new VmSize({ name: "Standard_C1" } as any), + new VmSize({ name: "Standard_C2" } as any), ])), }; diff --git a/test/app/core/arm-record.spec.ts b/test/app/core/arm-record.spec.ts new file mode 100644 index 0000000000..71f5116f6a --- /dev/null +++ b/test/app/core/arm-record.spec.ts @@ -0,0 +1,17 @@ +import { ArmRecord, Model, Prop } from "app/core"; + +@Model() +class TestModel extends ArmRecord { + @Prop() public id: string; + + @Prop() public name: string; +} + +describe("Node Model", () => { + it("lower case the id", () => { + const model1 = new TestModel({ id: "sub-1/My-Resource-GrOUP/storage-1", name: "storage-1" } as any); + const model2 = new TestModel({ id: "sub-1/my-resource-group/storage-1", name: "storage-1" } as any); + expect(model1.id).toEqual(model2.id); + expect(model1).toEqualImmutable(model2); + }); +}); diff --git a/test/app/models/storage-account.spec.ts b/test/app/models/storage-account.spec.ts new file mode 100644 index 0000000000..e68fef4407 --- /dev/null +++ b/test/app/models/storage-account.spec.ts @@ -0,0 +1,10 @@ +import { StorageAccount } from "app/models"; + +describe("Node Model", () => { + it("lower case the id", () => { + const account1 = new StorageAccount({ id: "sub-1/My-Resource-GrOUP/storage-1", name: "storage-1" } as any); + const account2 = new StorageAccount({ id: "sub-1/my-resource-group/storage-1", name: "storage-1" } as any); + expect(account1.id).toEqual(account2.id); + expect(account1).toEqualImmutable(account2); + }); +}); diff --git a/test/app/models/vm-size.spec.ts b/test/app/models/vm-size.spec.ts new file mode 100644 index 0000000000..3cc694e32c --- /dev/null +++ b/test/app/models/vm-size.spec.ts @@ -0,0 +1,10 @@ +import { VmSize } from "app/models"; + +describe("Node Model", () => { + it("lower case the id", () => { + const size1 = new VmSize({ name: "Standard_A1"} as any); + const size2 = new VmSize({ name: "standard_a1" } as any); + expect(size1.id).toEqual(size1.id); + expect(size2).toEqualImmutable(size2); + }); +}); From 08ed6fe114027609ef954b090f7ddb2896285894 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 31 May 2017 11:49:10 -0700 Subject: [PATCH 05/37] Use css variables and abstract colors as primary, danger, warn, success (#416) * CSS var wip, doesn't seem compatible with material * Wip * More * update styles * cleanup some style --- .../storage-account-card.scss | 4 +- app/components/base/banner/banner.scss | 6 +- .../base/buttons/action-buttons.scss | 18 ++-- .../form/editable-table/editable-table.scss | 8 +- .../base/form/form-page/form-page.scss | 2 +- .../base/form/form-picker/form-picker.scss | 4 +- .../base/property-list/property-list.scss | 18 ++-- app/components/base/table/table.scss | 14 +-- app/components/base/tags/tags.scss | 16 +-- .../scale/autoscale-formula-picker.scss | 2 +- .../pool/base/pool-nodes-preview.scss | 2 +- app/styles/account/browse.scss | 43 +------- app/styles/base/advanced-filter.scss | 2 +- app/styles/base/breadcrumbs.scss | 4 +- app/styles/base/buttons.scss | 2 +- app/styles/base/forms.scss | 4 +- app/styles/base/graphs/gauge.scss | 2 +- app/styles/base/layout.scss | 8 +- app/styles/base/list.scss | 16 +-- app/styles/base/loading.scss | 2 +- app/styles/base/notifications.scss | 22 ++--- app/styles/base/scaffolding.scss | 67 +------------ app/styles/base/server-error.scss | 8 +- app/styles/common/type.scss | 9 +- app/styles/file/log.scss | 2 +- app/styles/job/base.scss | 8 +- app/styles/job/details.scss | 2 +- app/styles/job/form.scss | 14 +-- app/styles/node/connect.scss | 12 +-- app/styles/partials/header.scss | 6 +- app/styles/partials/icons.scss | 10 +- app/styles/partials/navigation.scss | 6 +- app/styles/partials/refresh-btn.scss | 6 +- app/styles/pool/create.scss | 28 +++--- app/styles/pool/graphs.scss | 2 +- app/styles/pool/heatmap.scss | 6 +- app/styles/task/timeline.scss | 10 +- app/styles/themes/classic.scss | 99 +++++++++++++++++++ app/styles/themes/core.scss | 89 +++++++++++++++++ app/styles/variables.scss | 73 +++----------- app/styles/vars/material-colors.scss | 35 ------- app/styles/vendor/material-theme.scss | 18 ++-- 42 files changed, 362 insertions(+), 347 deletions(-) create mode 100644 app/styles/themes/classic.scss create mode 100644 app/styles/themes/core.scss diff --git a/app/components/account/details/storage-account-card/storage-account-card.scss b/app/components/account/details/storage-account-card/storage-account-card.scss index d0c2fda61b..9a3a7f167a 100644 --- a/app/components/account/details/storage-account-card/storage-account-card.scss +++ b/app/components/account/details/storage-account-card/storage-account-card.scss @@ -20,7 +20,7 @@ bl-storage-account-card > .storage-account-card { font-size: 16px; } .error { - color: $red; + color: map-get($danger, 500); } } } @@ -37,7 +37,7 @@ bl-storage-account-card > .storage-account-card { &:hover { color: $whitesmoke; - background: map-get($md-prussian-blue, 300); + background: map-get($primary, 300); } } } diff --git a/app/components/base/banner/banner.scss b/app/components/base/banner/banner.scss index 198327a5ef..ccfa5e0459 100644 --- a/app/components/base/banner/banner.scss +++ b/app/components/base/banner/banner.scss @@ -85,7 +85,7 @@ bl-banner > md-card.mat-card { > .details-container { padding: 10px 24px; - border-top: 1px solid $red-dark; + border-top: 1px solid map-get($danger, 800); font-weight: 500; } } @@ -134,12 +134,12 @@ bl-banner > md-card.mat-card { } bl-banner > md-card.error { - @include banner-scheme($red, $red-dark); + @include banner-scheme(map-get($danger, 500), map-get($danger, 800)); color: $whiteSmoke-darker; } bl-banner > md-card.warning { - @include banner-scheme($yellow, $yellow-dark); + @include banner-scheme(map-get($warn, 500), map-get($warn, 800)); color: $whiteSmoke-darker; } diff --git a/app/components/base/buttons/action-buttons.scss b/app/components/base/buttons/action-buttons.scss index 87bfba5a5c..595bf5e658 100644 --- a/app/components/base/buttons/action-buttons.scss +++ b/app/components/base/buttons/action-buttons.scss @@ -2,11 +2,11 @@ bl-action-btn-group { text-align: center; - border-right: 1px solid #d5d5d5; + border-right: 1px solid $border-color; display: block; bl-action-btn { - border-bottom: 1px solid #d5d5d5; + border-bottom: 1px solid $border-color; } } @@ -42,33 +42,33 @@ bl-action-btn { &[color="primary"] { background: white; - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); &:hover, &:focus { background: $alto; - color: map-get($md-prussian-blue, 200); + color: map-get($primary, 200); } &[type="round"] { - border: 1px solid #d5d5d5; + border: 1px solid $border-color; } } &[color="danger"] { - background: $red; + background: map-get($danger, 500); color: $whitesmoke; &:hover, &:focus { - background: $red-dark; + background: map-get($danger, 800); } } &[color="warn"] { - background: $yellow; + background: map-get($warn, 500); color: $whitesmoke; &:hover, &:focus { - background: $yellow-dark; + background: map-get($warn, 800); } } } diff --git a/app/components/base/form/editable-table/editable-table.scss b/app/components/base/form/editable-table/editable-table.scss index 4fc0c3a60b..17dded8034 100644 --- a/app/components/base/form/editable-table/editable-table.scss +++ b/app/components/base/form/editable-table/editable-table.scss @@ -40,7 +40,7 @@ bl-editable-table { &:focus { background: white; - border-bottom: 2px solid map-get($md-prussian-blue, 500); + border-bottom: 2px solid map-get($primary, 500); } } @@ -56,18 +56,18 @@ bl-editable-table { outline: none; > .fa { - color: $red; + color: map-get($danger, 500); } &:focus, &:hover { - background: $red; + background: map-get($danger, 500); > .fa { color: $whitesmoke; } } &:active { - background: $red-dark; + background: map-get($danger, 800); > .fa { color: $whitesmoke; } diff --git a/app/components/base/form/form-page/form-page.scss b/app/components/base/form/form-page/form-page.scss index 03d21d0f38..19e85718e4 100644 --- a/app/components/base/form/form-page/form-page.scss +++ b/app/components/base/form/form-page/form-page.scss @@ -13,7 +13,7 @@ height: 30px; line-height: 30px; text-align: center; - background: map-get($md-prussian-blue, 300); + background: map-get($primary, 300); border-radius: 99%; font-size: 14px; color: $whitesmoke; diff --git a/app/components/base/form/form-picker/form-picker.scss b/app/components/base/form/form-picker/form-picker.scss index fca98d1d5f..b67dce9b20 100644 --- a/app/components/base/form/form-picker/form-picker.scss +++ b/app/components/base/form/form-picker/form-picker.scss @@ -22,8 +22,8 @@ } &:hover, &:focus { - border-color: map-get($md-prussian-blue, 300); - color: map-get($md-prussian-blue, 300); + border-color: map-get($primary, 300); + color: map-get($primary, 300); } diff --git a/app/components/base/property-list/property-list.scss b/app/components/base/property-list/property-list.scss index 068af659a0..612b224ab5 100644 --- a/app/components/base/property-list/property-list.scss +++ b/app/components/base/property-list/property-list.scss @@ -57,15 +57,15 @@ bl-property-group { width: 100%; &:not(:last-child) { - border-bottom: 1px solid $alto; + border-bottom: 1px solid $border-color; } .warning-message { - color: $yellow; + color: map-get($warn, 500); margin-bottom: 10px; > .fa { - color: $yellow; + color: map-get($warn, 500); } } @@ -82,7 +82,7 @@ bl-property-group { align-items: center; margin-right: 5px; font-size: 20px; - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); width: 15px; } @@ -139,7 +139,7 @@ bl-property-group { } &.link { - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); i { padding-left: 4px; } @@ -166,7 +166,7 @@ bl-property-group { } &:active { - color: map-get($md-prussian-blue, 200); + color: map-get($primary, 200); } } @@ -220,7 +220,7 @@ bl-property-group { .copied-notification { top: 0; z-index: 1; - background: $green; + background: map-get($success, 500); position: absolute; color: $whitesmoke; padding: 5px; @@ -254,7 +254,7 @@ bl-property-group { width: 26px !important; height: 26px !important; font-size: 16px; - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); } } } @@ -263,7 +263,7 @@ bl-property-group { bl-bool-property { &.enabled { .fa-toggle-on { - color: $green; + color: map-get($success, 500); } } } diff --git a/app/components/base/table/table.scss b/app/components/base/table/table.scss index 87aca221fb..73f392ba0b 100644 --- a/app/components/base/table/table.scss +++ b/app/components/base/table/table.scss @@ -31,11 +31,11 @@ bl-table { > tr { cursor:pointer; - border-top: 1px $athensGrey solid; + border-top: 1px $border-color solid; height: $table-row-height; &.selected { - background-color: map-get($md-prussian-blue, 100); + background-color: map-get($primary, 100); p { color: $mineShaftGray; @@ -43,7 +43,7 @@ bl-table { } &.focused { - background-color: map-get($md-prussian-blue, 200); + background-color: map-get($primary, 200); } &:hover:not(.selected) { @@ -51,7 +51,7 @@ bl-table { } &:last-child { - border-bottom: 1px $athensGrey solid; + border-bottom: 1px $border-color solid; } } } @@ -93,9 +93,9 @@ bl-table { } &.sorting { - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); > .sort-icon { - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); } } } @@ -157,5 +157,5 @@ bl-table.dashboard { } .cell-spinner { - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } diff --git a/app/components/base/tags/tags.scss b/app/components/base/tags/tags.scss index e67b0999ab..43fe47da8e 100644 --- a/app/components/base/tags/tags.scss +++ b/app/components/base/tags/tags.scss @@ -7,7 +7,7 @@ .tag { font-size: 11px; color: white; - background: map-get($md-prussian-blue, 300); + background: map-get($primary, 300); padding: 2px 5px; border-radius: 3px; @@ -18,13 +18,13 @@ .expand-tags { font-size: 16px; - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); padding: 0 5px; border-radius: 3px; &:hover { - color: map-get($md-prussian-blue, 50); - background: map-get($md-prussian-blue, 300); + color: map-get($primary, 50); + background: map-get($primary, 300); } } } @@ -53,10 +53,10 @@ bl-tags { .fa-pencil { cursor: pointer; - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); &:hover { - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); } } @@ -66,11 +66,11 @@ bl-tags { } .fa-check:hover { - color: $green; + color: map-get($success, 500);; } .fa-times:hover { - color: $red-light; + color: map-get($danger, 400); } } diff --git a/app/components/pool/action/scale/autoscale-formula-picker.scss b/app/components/pool/action/scale/autoscale-formula-picker.scss index 788a5b19c4..1f5cd96f62 100644 --- a/app/components/pool/action/scale/autoscale-formula-picker.scss +++ b/app/components/pool/action/scale/autoscale-formula-picker.scss @@ -75,7 +75,7 @@ .fa-file-code-o { font-size: 30px; margin-bottom: 10px; - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } } } diff --git a/app/components/pool/base/pool-nodes-preview.scss b/app/components/pool/base/pool-nodes-preview.scss index bdb6ec8acc..b5e3661a6c 100644 --- a/app/components/pool/base/pool-nodes-preview.scss +++ b/app/components/pool/base/pool-nodes-preview.scss @@ -11,7 +11,7 @@ bl-pool-nodes-preview { } &.resize-error { - color: $red; + color: map-get($danger, 500); font-weight: bold; } } diff --git a/app/styles/account/browse.scss b/app/styles/account/browse.scss index 474fc72b5a..e701d477ed 100644 --- a/app/styles/account/browse.scss +++ b/app/styles/account/browse.scss @@ -10,55 +10,16 @@ bl-account-home { } bl-account-list { - .subscription { - > .subscription-details { - display: flex; - align-items: center; - cursor: pointer; - - &:hover { - background-color: $altoGray; - } - - .icon { - color: map-get($md-prussian-blue, 300); - margin: 0 10px; - font-size: 2em; - width: 26px; - } - } - - > .accounts { - position: relative; - background: #f1f1f1; - min-height: 40px; - - .no-item-message { - height: 40px; - padding: 5px 0; - font-size: 14px; - } - - .quick-list-item { - padding-left: 20px; - } - border-top: 1px solid #e5e5e5; - border-bottom: 1px solid #e5e5e5; - } - } -} - -bl-account-list, bl-account-fav-list { .fa-star { font-size: 2em; color: #d5d5d5; &:hover { - color: map-get($md-prussian-blue, 100); + color: map-get($primary, 100); } &.favorite { - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } } } diff --git a/app/styles/base/advanced-filter.scss b/app/styles/base/advanced-filter.scss index 7fb99fdeea..c69bf9e1f7 100644 --- a/app/styles/base/advanced-filter.scss +++ b/app/styles/base/advanced-filter.scss @@ -8,7 +8,7 @@ .label { padding: 2px 5px; background: $alto; - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } .value { diff --git a/app/styles/base/breadcrumbs.scss b/app/styles/base/breadcrumbs.scss index 02349d0120..a7a4892ae1 100644 --- a/app/styles/base/breadcrumbs.scss +++ b/app/styles/base/breadcrumbs.scss @@ -3,8 +3,8 @@ bl-breadcrumb-group { $separation: 3px; $height: 28px; $half-height: $height / 2; - $color: lighten($blue-lighter, 5%); - $hover-color: $blue-light; + $color: map-get($primary, 500); + $hover-color: map-get($primary, 600); display: block; height: $height; diff --git a/app/styles/base/buttons.scss b/app/styles/base/buttons.scss index df194d8998..518deaad7a 100644 --- a/app/styles/base/buttons.scss +++ b/app/styles/base/buttons.scss @@ -6,7 +6,7 @@ bl-submit-btn { } .fa-check { - color: $green-dark; + color: map-get($success, 800); overflow: hidden; } diff --git a/app/styles/base/forms.scss b/app/styles/base/forms.scss index 88eb8a9c96..414c4aa322 100644 --- a/app/styles/base/forms.scss +++ b/app/styles/base/forms.scss @@ -86,7 +86,7 @@ fieldset { background-color: $silver-grey; i { - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } } @@ -244,7 +244,7 @@ bl-complex-form { > .form-footer { height: $footer-height; - // background: map-get($md-prussian-blue, 500); + // background: map-get($primary, 500); padding: 5px; display: flex; align-items: center; diff --git a/app/styles/base/graphs/gauge.scss b/app/styles/base/graphs/gauge.scss index 45a1ba01f1..50b76eeb51 100644 --- a/app/styles/base/graphs/gauge.scss +++ b/app/styles/base/graphs/gauge.scss @@ -2,7 +2,7 @@ bl-gauge { display: block; .chart-filled { - fill: map-get($md-prussian-blue, 400); + fill: map-get($primary, 400); } .chart-empty { diff --git a/app/styles/base/layout.scss b/app/styles/base/layout.scss index c7fa123774..6557b15d31 100644 --- a/app/styles/base/layout.scss +++ b/app/styles/base/layout.scss @@ -210,7 +210,7 @@ footer { position: absolute; left: 0; top: 0; - background-color: $fuzzyWuzzyBrown; + background-color: map-get($danger, 500); border-radius: 10px; width: 18px; height: 18px; @@ -257,7 +257,7 @@ footer { } .warn-text { - color: $yellow-dark; + color: map-get($warn, 800); } .route-home { @@ -313,7 +313,7 @@ ul.list-point { flex-direction: column; align-items: center; justify-content: center; - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); cursor: pointer; &:hover { @@ -341,7 +341,7 @@ ul.list-point { &:hover { background: $header-hover; - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); } > .fa { diff --git a/app/styles/base/list.scss b/app/styles/base/list.scss index 2851805ac1..0896ae6296 100644 --- a/app/styles/base/list.scss +++ b/app/styles/base/list.scss @@ -46,7 +46,7 @@ bl-quick-list { white-space: nowrap; &.selected { - background-color: map-get($md-prussian-blue, 100); + background-color: map-get($primary, 100); p { color: $mineShaftGray; @@ -54,7 +54,7 @@ bl-quick-list { } &.focused { - background-color: map-get($md-prussian-blue, 200); + background-color: map-get($primary, 200); } &:hover:not(.selected) { @@ -87,13 +87,13 @@ bl-quick-list { } .fa-hourglass-half { - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } } bl-quick-list-item-status { position: relative; - color: $blue-light; + color: map-get($primary, 500); flex: 0 0 8px; display: flex; align-items: stretch; @@ -101,16 +101,16 @@ bl-quick-list { width: 100%; &.warning { - background-color: $red; + background-color: map-get($danger, 500); } &.lightaccent { - background: map-get($md-prussian-blue, 100); + background: map-get($primary, 100); } &.accent { - background: map-get($md-prussian-blue, 300); + background: map-get($primary, 300); } &.important { @@ -120,7 +120,7 @@ bl-quick-list { transparent 5px, $concreteGray 5px, $concreteGray 10px - ), map-get($md-prussian-blue, 500); + ), map-get($primary, 500); } } } diff --git a/app/styles/base/loading.scss b/app/styles/base/loading.scss index 943446b19f..e3aabb6151 100644 --- a/app/styles/base/loading.scss +++ b/app/styles/base/loading.scss @@ -1,4 +1,4 @@ -$spinner-color: $blue-medium; +$spinner-color: map-get($primary, 600); bl-loading { display: block; diff --git a/app/styles/base/notifications.scss b/app/styles/base/notifications.scss index be0370faae..b950db0200 100644 --- a/app/styles/base/notifications.scss +++ b/app/styles/base/notifications.scss @@ -84,28 +84,28 @@ bl-notification { } &.success { - background-color: $green; + background-color: map-get($success, 500); a { - color: $green-dark; + color: map-get($success, 800); } } &.error { - background-color: $red-light; + background-color: map-get($danger, 300); a { - color: $red; + color: map-get($danger, 500); } } &.info { - color: $orient; + color: map-get($primary, 400); background-color: white; } &.warn { - background-color: $yellow-light; + background-color: map-get($warn, 400); a { - color: $yellow-dark; + color: map-get($warn, 800); } } @@ -168,7 +168,7 @@ bl-persistent-notifications-dropdown { position: absolute; left: 0; top: 0; - background-color: $fuzzyWuzzyBrown; + background-color: map-get($danger, 500); border-radius: 10px; width: 18px; height: 18px; @@ -188,7 +188,7 @@ bl-persistent-notifications-dropdown { margin-right: 10px; .fa-exclamation-triangle { - color: $yellow; + color: map-get($warn, 500); } } @@ -218,11 +218,11 @@ bl-persistent-notifications-dropdown { &:hover { color: white; - background-color: $crail; + background-color: map-get($danger, 400); } &:active { - background-color: $buttonBrown; + background-color: map-get($danger, 600); } } diff --git a/app/styles/base/scaffolding.scss b/app/styles/base/scaffolding.scss index 9c5df69168..5bec61a2b1 100644 --- a/app/styles/base/scaffolding.scss +++ b/app/styles/base/scaffolding.scss @@ -3,11 +3,6 @@ html, body, button { padding: 0; border: 0; height: 100%; - font-family: "Roboto", Helvetica, Arial, sans-serif; -} - -.center_this { - @include center(); } .text-left { text-align: left; } @@ -28,68 +23,8 @@ ol ul { margin-bottom: 0; } -// Horizontal rules -hr { - margin: $baseLineHeight 0; - border: 0; - border-top: 1px solid $athensGrey; - border-bottom: 1px solid $white; -} - -// Blockquotes -blockquote { - padding: 0 0 0 15px; - margin: 0 0 $baseLineHeight; - border-left: 5px solid $athensGrey; - - p { - margin-bottom: 0; - font-size: $baseFontSize * 1.25; - font-weight: 300; - line-height: 1.25; - } - - small { - display: block; - line-height: $baseLineHeight; - color: $silver-grey; - - &:before { - content: "\2014 \00A0"; - } - } - - // Float right with text-align: right - &.pull-right { - float: right; - padding-right: 15px; - padding-left: 0; - border-right: 5px solid $athensGrey; - border-left: 0; - - p, small { - text-align: right; - } - - small { - &:before { - content: ""; - } - - &:after { - content: "\00A0 \2014"; - } - } - } -} - -// Quotes -blockquote:before, blockquote:after { - content: ""; -} - .danger { - color: $red; + color: map-get($danger, 500); } .unaccent { diff --git a/app/styles/base/server-error.scss b/app/styles/base/server-error.scss index 64f50aa655..80af0b4362 100644 --- a/app/styles/base/server-error.scss +++ b/app/styles/base/server-error.scss @@ -7,7 +7,7 @@ bl-server-error { } .error-banner { - background: $red; + background: map-get($danger, 500); color: $whiteSmoke; > .content { padding: 10px; @@ -39,18 +39,18 @@ bl-server-error { z-index: 10; i { - border: 1px solid $red-dark; + border: 1px solid map-get($danger, 800); border-radius: 3px; padding: 3px; &:hover { - background-color: $red-dark; + background-color: map-get($danger, 800); } } } .troubleshoot-info { padding: 10px; - border-top: 1px solid $red-dark; + border-top: 1px solid map-get($danger, 800); table { font-size: 1em; diff --git a/app/styles/common/type.scss b/app/styles/common/type.scss index 856f1b9200..6466b29715 100644 --- a/app/styles/common/type.scss +++ b/app/styles/common/type.scss @@ -6,7 +6,7 @@ */ body { - font-family: $baseFontFamily; + font-family: "Roboto", Helvetica, Arial, sans-serif; font-size: $baseFontSize; line-height: $baseLineHeight; color: $textColor; @@ -59,3 +59,10 @@ h6 { font-size: $h6FontSize; line-height: 19px; } + +hr { + margin: $baseLineHeight 0; + border: 0; + border-top: 1px solid $border-color; + border-bottom: 1px solid $white; +} diff --git a/app/styles/file/log.scss b/app/styles/file/log.scss index c2520bc054..34cce56b9c 100644 --- a/app/styles/file/log.scss +++ b/app/styles/file/log.scss @@ -68,7 +68,7 @@ } &.following { - background: $green; + background: map-get($success, 500); color: $whiteSmoke-darker; } } diff --git a/app/styles/job/base.scss b/app/styles/job/base.scss index 42123cd173..7a0f5e6f34 100644 --- a/app/styles/job/base.scss +++ b/app/styles/job/base.scss @@ -1,13 +1,13 @@ bl-job-stats-preview { .task-success { display: inline-block; - color: $green-dark; + color: map-get($success, 800); margin-right: 5px; } .task-failed { display: inline-block; - color: $red; + color: map-get($danger, 500); } } @@ -17,10 +17,10 @@ bl-application-preview, bl-task-preview { } .success { - color: $green; + color: map-get($success, 500); } .failure { - color: $red; + color: map-get($danger, 500); } } diff --git a/app/styles/job/details.scss b/app/styles/job/details.scss index 913e51f8b2..65bca060ef 100644 --- a/app/styles/job/details.scss +++ b/app/styles/job/details.scss @@ -56,7 +56,7 @@ bl-job-progress-status { cursor: pointer; &.active { - color: map-get($md-prussian-blue, 600); + color: map-get($primary, 600); font-weight: bold; } } diff --git a/app/styles/job/form.scss b/app/styles/job/form.scss index b9f1730194..71119a6671 100644 --- a/app/styles/job/form.scss +++ b/app/styles/job/form.scss @@ -38,21 +38,21 @@ bl-pool-picker { &.active { color: $whitesmoke; - border-color: map-get($md-prussian-blue, 300); - background: map-get($md-prussian-blue, 300); + border-color: map-get($primary, 300); + background: map-get($primary, 300); > .pool-wrapper { > .icon { - color: map-get($md-prussian-blue, 50); + color: map-get($primary, 50); } > .info > .details { - color: map-get($md-prussian-blue, 50); + color: map-get($primary, 50); } } > .pool-tags .tag { - color: map-get($md-prussian-blue, 700); - background-color: map-get($md-prussian-blue, 50); + color: map-get($primary, 700); + background-color: map-get($primary, 50); } } @@ -62,7 +62,7 @@ bl-pool-picker { width: $tile-width; > .icon { - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); font-size: 30px; height: 30px; margin: 5px; diff --git a/app/styles/node/connect.scss b/app/styles/node/connect.scss index 9368699360..608cbb9205 100644 --- a/app/styles/node/connect.scss +++ b/app/styles/node/connect.scss @@ -18,12 +18,12 @@ bl-node-connect { &:hover { color: $whitesmoke; - background-color: map-get($md-prussian-blue, 400); + background-color: map-get($primary, 400); } &:active { color: $whitesmoke; - background-color: map-get($md-prussian-blue, 500); + background-color: map-get($primary, 500); } } } @@ -67,8 +67,8 @@ bl-ssh-key-picker > .ssh-key-picker { .saved-keys-wrapper { height: 120px; overflow: auto; - border-top: 1px solid #d5d5d5; - border-bottom: 1px solid #d5d5d5; + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; > .key { display: flex; @@ -81,7 +81,7 @@ bl-ssh-key-picker > .ssh-key-picker { } &:not(:last-child) { - border-bottom: 1px solid #d5d5d5; + border-bottom: 1px solid $border-color; } &:hover { @@ -98,7 +98,7 @@ bl-ssh-key-picker > .ssh-key-picker { .fa-key { font-size: 30px; margin-bottom: 10px; - color: map-get($md-prussian-blue, 400); + color: map-get($primary, 400); } } diff --git a/app/styles/partials/header.scss b/app/styles/partials/header.scss index a4a5e5cb09..a116f826f7 100644 --- a/app/styles/partials/header.scss +++ b/app/styles/partials/header.scss @@ -34,7 +34,7 @@ border-right: 1px solid $navigation-background; &.invalid { - color: $fuzzyWuzzyBrown; + color: map-get($danger, 500); } i { @@ -116,11 +116,11 @@ &:hover { color: white; - background-color: $crail; + background-color: map-get($danger, 400); } &:active { - background-color: $buttonBrown; + background-color: map-get($danger, 600); } } } diff --git a/app/styles/partials/icons.scss b/app/styles/partials/icons.scss index 43c7059f52..2813f11b2d 100644 --- a/app/styles/partials/icons.scss +++ b/app/styles/partials/icons.scss @@ -1,21 +1,21 @@ .fa-refresh { - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); } .fa-cog, .fa-gear { - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); } .fa-ban, .fa-times, .fa-warning, .fa-times-circle { - color: $red; + color: map-get($danger, 500); } .fa-check, .fa-check-circle-o, .fa-check-circle { - color: $green-dark; + color: map-get($success, 800); } .fa-question-circle-o { - color: $green-dark; + color: map-get($success, 800); } button { diff --git a/app/styles/partials/navigation.scss b/app/styles/partials/navigation.scss index 15cc5197cc..6cbc9995d4 100644 --- a/app/styles/partials/navigation.scss +++ b/app/styles/partials/navigation.scss @@ -1,7 +1,7 @@ nav { ul { margin: 0; - + li { padding: 0; @@ -9,14 +9,14 @@ nav { display: block; margin: 0 auto; } - + a { display: block; padding: 18px 0; } a.active { - background-color: lighten($navigation-hover, 4%); + background-color: map-get($primary, 500); color: $white; } } diff --git a/app/styles/partials/refresh-btn.scss b/app/styles/partials/refresh-btn.scss index 2b6b3070a7..aae1633d11 100644 --- a/app/styles/partials/refresh-btn.scss +++ b/app/styles/partials/refresh-btn.scss @@ -1,15 +1,15 @@ bl-refresh-btn { .fa-refresh { - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); } .fa-check { - color: $green-dark; + color: map-get($success, 800); // overflow: hidden; } .fa-warning { - color: $red; + color: map-get($danger, 500); } .success-container { diff --git a/app/styles/pool/create.scss b/app/styles/pool/create.scss index 3c89881a60..23a6e70d2b 100644 --- a/app/styles/pool/create.scss +++ b/app/styles/pool/create.scss @@ -26,16 +26,16 @@ bl-pool-os-picker { } &.active { - border-color: map-get($md-prussian-blue, 300); + border-color: map-get($primary, 300); > .main > .icon { - background: map-get($md-prussian-blue, 300); - border-color: map-get($md-prussian-blue, 300); - color: map-get($md-prussian-blue, 50); + background: map-get($primary, 300); + border-color: map-get($primary, 300); + color: map-get($primary, 50); } .select-version { - color: map-get($md-prussian-blue, 500); + color: map-get($primary, 500); font-weight: bold; } @@ -50,7 +50,7 @@ bl-pool-os-picker { padding: 10px 0; height: 120px; font-size: 60px; - color: map-get($md-prussian-blue, 300); + color: map-get($primary, 300); display: flex; align-items: center; justify-content: center; @@ -117,16 +117,16 @@ bl-vm-size-picker { } &.active { - border-color: map-get($md-prussian-blue, 300); + border-color: map-get($primary, 300); > .title { - background: map-get($md-prussian-blue, 300); - border-color: map-get($md-prussian-blue, 300); + background: map-get($primary, 300); + border-color: map-get($primary, 300); color: $whitesmoke; } > .pricing { - border-color: map-get($md-prussian-blue, 300); + border-color: map-get($primary, 300); } } @@ -172,19 +172,19 @@ bl-autoscale-formula-picker { } .cm-variables { - color: $green-dark; + color: map-get($success, 800); } .cm-functions { - color: $red-dark; + color: map-get($danger, 800); } .cm-math { - color: $prussian-blue; + color: map-get($primary, 700); } .cm-builtin { - color: $midnight; + color: map-get($primary, 800); } } diff --git a/app/styles/pool/graphs.scss b/app/styles/pool/graphs.scss index 4a5269aa1c..3d56717747 100644 --- a/app/styles/pool/graphs.scss +++ b/app/styles/pool/graphs.scss @@ -18,7 +18,7 @@ bl-pool-graphs { &.focused { .label { - color: map-get($md-prussian-blue, 600); + color: map-get($primary, 600); text-decoration: underline; } } diff --git a/app/styles/pool/heatmap.scss b/app/styles/pool/heatmap.scss index f6c193097d..dc8c653335 100644 --- a/app/styles/pool/heatmap.scss +++ b/app/styles/pool/heatmap.scss @@ -20,7 +20,7 @@ $color-width: 14px; g.node-group { cursor: pointer; rect { - stroke: map-get($md-prussian-blue, 500); + stroke: map-get($primary, 500); } text { @@ -33,7 +33,7 @@ $color-width: 14px; &.interactive { g.node-group:hover { rect { - stroke: map-get($md-prussian-blue, 500); + stroke: map-get($primary, 500); fill-opacity: 0.7; } } @@ -66,7 +66,7 @@ $color-width: 14px; &.highlighted { color: $concreteGray; - background: map-get($md-prussian-blue, 400); + background: map-get($primary, 400); } } diff --git a/app/styles/task/timeline.scss b/app/styles/task/timeline.scss index 4c2ea4ad62..c053378dbf 100644 --- a/app/styles/task/timeline.scss +++ b/app/styles/task/timeline.scss @@ -2,7 +2,7 @@ bl-task-timeline { $tile-size: 30px; $separator-width: 60px; $separator-height: 2px; - $state-reached-color: map-get($md-prussian-blue, 300); + $state-reached-color: map-get($primary, 300); .timeline { display: flex; @@ -44,12 +44,12 @@ bl-task-timeline { color: $dusty-grey; .warn { - color: $yellow; + color: map-get($warn, 500); } .error { font-weight: bold; - color: $red; + color: map-get($danger, 500); } } } @@ -68,11 +68,11 @@ bl-task-timeline { } &.warn > .tile { - background: $yellow; + background: map-get($warn, 500); } &.error > .tile { - background: $red; + background: map-get($danger, 500); } } diff --git a/app/styles/themes/classic.scss b/app/styles/themes/classic.scss new file mode 100644 index 0000000000..75a0e88963 --- /dev/null +++ b/app/styles/themes/classic.scss @@ -0,0 +1,99 @@ +$black-87-opacity: rgba(black, 0.87); +$white-87-opacity: rgba(white, 0.87); + +$contrast-theme-classic: ( + 50: $black-87-opacity, + 100: $black-87-opacity, + 200: $black-87-opacity, + 300: $black-87-opacity, + 400: $black-87-opacity, + 500: white, + 600: white, + 700: white, + 800: $white-87-opacity, + 900: $white-87-opacity, + A100: $black-87-opacity, + A200: white, + A400: white, + A700: white, +); + +$primary-theme-classic: ( + 50: #cde0ed, + 100: #94bdd9, + 200: #6ba3cb, + 300: #3e81b0, + 400: #36719a, + 500: #2e6083, + 600: #264f6c, + 700: #1e3f56, + 800: #162e3f, + 900: #0e1e28, + A100: #cde0ed, + A200: #94bdd9, + A400: #36719a, + A700: #1e3f56, + contrast: $contrast-theme-classic, +); + +$danger-theme-classic: ( + 50: #f5e7e7, + 100: #e6c4c4, + 200: #d59c9c, + 300: #c47474, + 400: #b75757, + 500: #aa3939, + 600: #a33333, + 700: #992c2c, + 800: #902424, + 900: #7f1717, + A100: #ffb7b7, + A200: #ff8484, + A400: #ff5151, + A700: #ff3737, + contrast: $contrast-theme-classic, +); + +$warn-theme-classic: ( + 50: #fef5e8, + 100: #fce6c6, + 200: #fad6a1, + 300: #f7c67b, + 400: #f6b95e, + 500: #f4ad42, + 600: #f3a63c, + 700: #f19c33, + 800: #ef932b, + 900: #ec831d, + A100: #ffffff, + A200: #fff6ee, + A400: #ffdabb, + A700: #ffcda1, + contrast: $contrast-theme-classic, +); + +$success-theme-classic: ( + 50: #eaf5ea, + 100: #c9e7cb, + 200: #a6d7a8, + 300: #82c785, + 400: #67bb6a, + 500: #4caf50, + 600: #45a849, + 700: #3c9f40, + 800: #339637, + 900: #248627, + A100: #c5ffc7, + A200: #92ff95, + A400: #5fff64, + A700: #46ff4b, + contrast: $contrast-theme-classic, +); + +body { + // Primary color + @include css-var-theme(--primary, $primary-theme-classic); + @include css-var-theme(--danger, $danger-theme-classic); + @include css-var-theme(--warn, $warn-theme-classic); + @include css-var-theme(--success, $success-theme-classic); +} diff --git a/app/styles/themes/core.scss b/app/styles/themes/core.scss new file mode 100644 index 0000000000..ff2609d0fb --- /dev/null +++ b/app/styles/themes/core.scss @@ -0,0 +1,89 @@ +@mixin prop($name, $var, $index) { + #{$name}-#{$index}: map-get($var, $index); +} + +@mixin css-var-theme($name, $var) { + @include prop($name, $var, 50); + @include prop($name, $var, 100); + @include prop($name, $var, 200); + @include prop($name, $var, 300); + @include prop($name, $var, 400); + @include prop($name, $var, 500); + @include prop($name, $var, 600); + @include prop($name, $var, 700); + @include prop($name, $var, 800); + @include prop($name, $var, 900); + @include prop($name, $var, A100); + @include prop($name, $var, A200); + @include prop($name, $var, A400); + @include prop($name, $var, A700); +}; + +$primary: ( + 50: var(--primary-50), + 100: var(--primary-100), + 200: var(--primary-200), + 300: var(--primary-300), + 400: var(--primary-400), + 500: var(--primary-500), + 600: var(--primary-600), + 700: var(--primary-700), + 800: var(--primary-800), + 900: var(--primary-900), + A100: var(--primary-A100), + A200: var(--primary-A200), + A400: var(--primary-A400), + A700: var(--primary-A700), +); +$primary: $primary; + +$danger: ( + 50: var(--danger-50), + 100: var(--danger-100), + 200: var(--danger-200), + 300: var(--danger-300), + 400: var(--danger-400), + 500: var(--danger-500), + 600: var(--danger-600), + 700: var(--danger-700), + 800: var(--danger-800), + 900: var(--danger-900), + A100: var(--danger-A100), + A200: var(--danger-A200), + A400: var(--danger-A400), + A700: var(--danger-A700), +); + +$warn: ( + 50: var(--warn-50), + 100: var(--warn-100), + 200: var(--warn-200), + 300: var(--warn-300), + 400: var(--warn-400), + 500: var(--warn-500), + 600: var(--warn-600), + 700: var(--warn-700), + 800: var(--warn-800), + 900: var(--warn-900), + A100: var(--warn-A100), + A200: var(--warn-A200), + A400: var(--warn-A400), + A700: var(--warn-A700), +); + +$success: ( + 50: var(--success-50), + 100: var(--success-100), + 200: var(--success-200), + 300: var(--success-300), + 400: var(--success-400), + 500: var(--success-500), + 600: var(--success-600), + 700: var(--success-700), + 800: var(--success-800), + 900: var(--success-900), + A100: var(--success-A100), + A200: var(--success-A200), + A400: var(--success-A400), + A700: var(--success-A700), +); diff --git a/app/styles/variables.scss b/app/styles/variables.scss index 6ad570ab37..53ec2391fd 100644 --- a/app/styles/variables.scss +++ b/app/styles/variables.scss @@ -1,42 +1,13 @@ +@import "./themes/core"; +@import "./themes/classic"; @import "./vars/material-colors"; @import "./vars/zindex"; -$orient: #00528c; -$regal-blue: #00416f; -$prussian-blue: #003153; -$midnight: #001e33; -$charcoal-blue: #000306; -$blue-lighter: $orient; -$blue-light: $regal-blue; -$blue-medium: $prussian-blue; -$blue-dark: $midnight; -$blue-darker: $charcoal-blue; - -// Red -$red-light: #d46a6a; -$red: #aa3939; -$red-dark: #801515; - -$grenadier : #dd2c00; -$fuzzyWuzzyBrown : #c74f54; -$buttonBrown : #c45151; -$crail : #c14848; -$vanillaIce : #efd0d0; - -$yellow-light: #ffc267; -$yellow: #f4ad42; -$yellow-dark: #e39014; - -//Green -$green-light: #77cc7d; -$green: #4caf50; -$green-dark: #388e3c; - -$header-background: $blue-light; -$header-hover: $blue-medium; -$navigation-background: $blue-medium; -$navigation-hover: $blue-light; +$header-background : map-get($primary, 700); +$header-hover : map-get($primary, 800); +$navigation-background : map-get($primary, 600); +$navigation-hover : map-get($primary, 700); // Black & white //--------------------------------------------- @@ -68,34 +39,22 @@ $mineShaftGray : $mineshaft-grey; // Status Colours //--------------------------------------------- -$warningText : #c09853; -$errorText : #b94a48; -$infoText : #3a87ad; -$successText : #468847; - +$warningText : map-get($warn, 500); +$errorText : map-get($danger, 500); +$infoText : map-get($primary, 500); +$successText : map-get($success, 500); // Text //--------------------------------------------- -$headings-color : $prussian-blue; -$link-color : map-get($md-prussian-blue, 700); -$link-color-hover : map-get($md-prussian-blue, 800); -$validation-error-color : $red; -$label-text : $dusty-grey; - -// Typography -//--------------------------------------------- -$segoeUiFontFamily : wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; -$segoiUiFontFamilySemibold : wf_segoe-ui_semibold, "Segoe UI Semibold", "Segoe WP Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; -$segoiUiFontFamilySemilight: wf_segoe-ui_semilight, "Segoe UI Semilight", "Segoe WP Semilight","Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; -$segoiUiFontFamilyBold : wf_segoe-ui_bold, "Segoe UI Bold", "Segoe WP Bold","Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; +$headings-color : map-get($primary, 700); +$link-color : map-get($primary, 700); +$link-color-hover : map-get($primary, 800); +$validation-error-color : map-get($danger, 500); +$label-text : $dusty-grey; -$baseFontSize : 10pt; -$baseFontFamily : $segoeUiFontFamily; +$baseFontSize : 13px; $baseLineHeight : 1.4em; -$lineHeightComputed : floor($baseFontSize * $baseLineHeight); -$altFontFamily : $segoeUiFontFamily; -$headingsFontFamily : $segoeUiFontFamily; $headingsFontWeight : semilight; $headingWeightBold : semibold; $headingsLineHeight : $baseLineHeight; diff --git a/app/styles/vars/material-colors.scss b/app/styles/vars/material-colors.scss index 80505f0333..e69de29bb2 100644 --- a/app/styles/vars/material-colors.scss +++ b/app/styles/vars/material-colors.scss @@ -1,35 +0,0 @@ -$black-87-opacity: rgba(black, 0.87); -$white-87-opacity: rgba(white, 0.87); - -$md-prussian-blue: ( - 50: #cde0ed, - 100: #94bdd9, - 200: #6ba3cb, - 300: #3e81b0, - 400: #36719a, - 500: #2e6083, - 600: #264f6c, - 700: #1e3f56, - 800: #162e3f, - 900: #0e1e28, - A100: #cde0ed, - A200: #94bdd9, - A400: #36719a, - A700: #1e3f56, - contrast: ( - 50: $black-87-opacity, - 100: $black-87-opacity, - 200: $black-87-opacity, - 300: $black-87-opacity, - 400: $black-87-opacity, - 500: white, - 600: white, - 700: white, - 800: $white-87-opacity, - 900: $white-87-opacity, - A100: $black-87-opacity, - A200: white, - A400: white, - A700: white, - ) -); diff --git a/app/styles/vendor/material-theme.scss b/app/styles/vendor/material-theme.scss index 8441e06c68..e4cbc47b22 100644 --- a/app/styles/vendor/material-theme.scss +++ b/app/styles/vendor/material-theme.scss @@ -8,19 +8,19 @@ // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. -$primary: mat-palette($md-prussian-blue); -$accent: mat-palette($mat-grey, A200, A100, A400); +$md-primary: mat-palette($primary-theme-classic); +$md-accent: mat-palette($mat-grey, A200, A100, A400); // The warn palette is optional (defaults to red). -$warn: mat-palette($mat-red); +$md-warn: mat-palette($mat-red); // Create the theme object (a Sass map containing all of the palettes). -$theme: mat-light-theme($primary, $accent, $warn); +$md-theme: mat-light-theme($md-primary, $md-accent, $md-warn); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. -@include angular-material-theme($theme); +@include angular-material-theme($md-theme); // TODO: can we pull in the material scss classes into the project? // really want to be able to override the styles more easily. @@ -42,7 +42,7 @@ md-tab-group:not(.form-tabs) { border-right: 1px solid #ddd; &.mat-tab-label-active { - background-color: map-get($md-prussian-blue, 100); + background-color: map-get($primary, 100); } &.mat-tab-disabled { @@ -115,17 +115,17 @@ md-radio-button { &.mat-radio-checked { .mat-radio-outer-circle { - border-color: map-get($md-prussian-blue, 400); + border-color: map-get($primary, 400); } .mat-radio-inner-circle { - background: map-get($md-prussian-blue, 400); + background: map-get($primary, 400); } } } [md-raised-button].mat-primary.mat-button-focus { - background: map-get($md-prussian-blue, 700); + background: map-get($primary, 700); } button[md-raised-button].small { From 44fc9477bbf7fd08926c4dde5868e0240d3a0ba1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 1 Jun 2017 11:50:32 -0700 Subject: [PATCH 06/37] Fix: Disabled editing tags for a completed job & small refactor (#421) * Disabled editing tags for a completed job & small refactor * revert table border too strong * Fix typing issues * Update app details to new format * Fix --- .../details/application-details.html | 33 ++--- .../application/home/application-home.html | 5 +- app/components/base/table/table.scss | 4 +- .../error-display/job-error-display.html | 2 +- .../details/job-configuration.component.ts | 4 +- app/components/job/details/job-details.html | 2 +- app/core/record/decorators.ts | 2 +- app/core/record/helpers.ts | 13 +- app/models/constraints.ts | 13 +- app/models/job-constraints.ts | 5 + app/models/job-execution-information.ts | 40 +++--- app/models/job.ts | 128 +++++++++--------- app/models/name-value-pair.ts | 14 +- app/models/scheduling-error.ts | 28 ++-- .../job-error-display.component.spec.ts | 2 +- test/app/core/record.spec.ts | 15 +- tslint.json | 3 +- 17 files changed, 176 insertions(+), 137 deletions(-) diff --git a/app/components/application/details/application-details.html b/app/components/application/details/application-details.html index 92fc80e146..69ea78dc99 100644 --- a/app/components/application/details/application-details.html +++ b/app/components/application/details/application-details.html @@ -1,25 +1,22 @@
- -
+
+ -
-
- {{decorator.id}} - - {{decorator.displayName}} -
(unlocked)
-
(locked)
-
-
- - - - - - - + + + + +
+
+ {{decorator.id}} + + {{decorator.displayName}} +
(unlocked)
+
(locked)
+
+
diff --git a/app/components/application/home/application-home.html b/app/components/application/home/application-home.html index 6104978686..4a4fd27f2e 100644 --- a/app/components/application/home/application-home.html +++ b/app/components/application/home/application-home.html @@ -1,8 +1,7 @@
- + +
diff --git a/app/components/base/table/table.scss b/app/components/base/table/table.scss index 73f392ba0b..0a1dd35cb5 100644 --- a/app/components/base/table/table.scss +++ b/app/components/base/table/table.scss @@ -31,7 +31,7 @@ bl-table { > tr { cursor:pointer; - border-top: 1px $border-color solid; + border-top: 1px $athensGrey solid; height: $table-row-height; &.selected { @@ -51,7 +51,7 @@ bl-table { } &:last-child { - border-bottom: 1px $border-color solid; + border-bottom: 1px $athensGrey solid; } } } diff --git a/app/components/job/details/error-display/job-error-display.html b/app/components/job/details/error-display/job-error-display.html index 3ba95cd1db..d3dee85a76 100644 --- a/app/components/job/details/error-display/job-error-display.html +++ b/app/components/job/details/error-display/job-error-display.html @@ -10,7 +10,7 @@
{{schedulingError.message}}
-
{{entry.key}}: {{entry.value}}
+
{{entry.name}}: {{entry.value}}
diff --git a/app/components/job/details/job-configuration.component.ts b/app/components/job/details/job-configuration.component.ts index ef1ab7e003..01d427ea0c 100644 --- a/app/components/job/details/job-configuration.component.ts +++ b/app/components/job/details/job-configuration.component.ts @@ -33,7 +33,7 @@ export class JobConfigurationComponent { public managerTask: JobManagerTaskDecorator; public prepTask: JobPreparationTaskDecorator; public releaseTask: JobReleaseTaskDecorator; - public environmentSettings: NameValuePair[] = []; + public environmentSettings: List = List([]); public jobMetadata: List = List([]); public poolInfo: any = {}; public hasStartTime: boolean; @@ -50,7 +50,7 @@ export class JobConfigurationComponent { this.prepTask = this.decorator.jobPreparationTask; this.releaseTask = this.decorator.jobReleaseTask; this.poolInfo = this.decorator.poolInfo || {}; - this.environmentSettings = this.job.commonEnvironmentSettings || []; + this.environmentSettings = this.job.commonEnvironmentSettings; this.jobMetadata = this.job.metadata; } } diff --git a/app/components/job/details/job-details.html b/app/components/job/details/job-details.html index a1c2d63cd8..080934cba6 100644 --- a/app/components/job/details/job-details.html +++ b/app/components/job/details/job-details.html @@ -17,7 +17,7 @@ {{decorator.id}} {{decorator.state}} Pool: {{job.poolId}} - +
diff --git a/app/core/record/decorators.ts b/app/core/record/decorators.ts index 4ac221bf5e..a6f36addf0 100644 --- a/app/core/record/decorators.ts +++ b/app/core/record/decorators.ts @@ -22,7 +22,7 @@ export function Prop(...args) { const type = Reflect.getMetadata("design:type", target, attr); if (!type) { throw new Error(`Cannot retrieve the type for RecordAttribute ${target.constructor.name}#${attr}` - + "Check your nested type is defined in another file or above this DtoAttr"); + + "Check your nested type is defined in another file or above this DtoAttr"); } updateTypeMetadata(ctr, attr, { type, list: false }); diff --git a/app/core/record/helpers.ts b/app/core/record/helpers.ts index 80c9ee4614..e9bb1fe9cf 100644 --- a/app/core/record/helpers.ts +++ b/app/core/record/helpers.ts @@ -7,7 +7,18 @@ const attrMetadataKey = "record:attrs"; export const primitives = new Set(["Array", "Number", "String", "Object", "Boolean"]); export function metadataForRecord(record: Record) { - return Reflect.getMetadata(attrMetadataKey, record.constructor) || {}; + return metadataForCtr(record.constructor); +} + +function metadataForCtr(ctr: any) { + const data = Reflect.getMetadata(attrMetadataKey, ctr) || {}; + const parent = Object.getPrototypeOf(ctr.prototype); + const parentCtr = parent.constructor; + if (parentCtr.name !== "Record") { + const parentData = metadataForCtr(parentCtr); + return { ...parentData, ...data }; + } + return data; } interface TypeMetadata { diff --git a/app/models/constraints.ts b/app/models/constraints.ts index e6cafa5777..f348942361 100644 --- a/app/models/constraints.ts +++ b/app/models/constraints.ts @@ -1,9 +1,16 @@ +import { Model, Prop, Record } from "app/core"; import { Duration } from "moment"; +export interface ConstraintsAttributes { + maxTaskRetryCount: Duration; + maxWallClockTime: number; +} + /** * Specifies the execution constraints for tasks or jobs. */ -export abstract class Constraints { - public maxWallClockTime: Duration; - public maxTaskRetryCount: number; +@Model() +export class Constraints extends Record { + @Prop() public maxWallClockTime: any; + @Prop() public maxTaskRetryCount: number; } diff --git a/app/models/job-constraints.ts b/app/models/job-constraints.ts index 9d1f06dbc2..e38844dfe3 100644 --- a/app/models/job-constraints.ts +++ b/app/models/job-constraints.ts @@ -1,7 +1,12 @@ +import { Model } from "app/core"; import { Constraints } from "./constraints"; /** * Specifies the execution constraints for jobs created on a schedule. */ +@Model() export class JobConstraints extends Constraints { + constructor(data: any) { + super(data); + } } diff --git a/app/models/job-execution-information.ts b/app/models/job-execution-information.ts index d6d8c474b3..b99f2cba16 100644 --- a/app/models/job-execution-information.ts +++ b/app/models/job-execution-information.ts @@ -1,14 +1,5 @@ -import { Record } from "immutable"; - -import { SchedulingError } from "./scheduling-error"; - -const JobExecutionInformationRecord = Record({ - startTime: null, - endTime: null, - poolId: null, - schedulingError: null, - terminateReason: null, -}); +import { Model, Prop, Record } from "app/core"; +import { SchedulingError, SchedulingErrorAttributes } from "./scheduling-error"; /** * Job terminate reason. @@ -29,19 +20,22 @@ export const JobTerminateReason = { UserTerminate: "UserTerminate" as JobTerminateReason, }; +export interface JobExecutionInformationAttributes { + startTime: Date; + endTime: Date; + poolId: string; + schedulingError: Partial; + terminateReason: JobTerminateReason; +} + /** * Contains information about the execution of a job in the Azure */ -export class JobExecutionInformation extends JobExecutionInformationRecord { - public startTime: Date; - public endTime: Date; - public poolId: string; - public schedulingError: SchedulingError; - public terminateReason: JobTerminateReason; - - constructor(data: any) { - super(Object.assign({}, data, { - schedulingError: data.schedulingError && new SchedulingError(data.schedulingError), - })); - } +@Model() +export class JobExecutionInformation extends Record { + @Prop() public startTime: Date; + @Prop() public endTime: Date; + @Prop() public poolId: string; + @Prop() public schedulingError: SchedulingError; + @Prop() public terminateReason: JobTerminateReason; } diff --git a/app/models/job.ts b/app/models/job.ts index e4980c40c2..490e7a09b7 100644 --- a/app/models/job.ts +++ b/app/models/job.ts @@ -1,86 +1,88 @@ -import { List, Record } from "immutable"; +import { List } from "immutable"; +import { ListProp, Model, Prop, Record } from "app/core"; import { ModelUtils } from "app/utils"; import { AllTasksCompleteAction, TaskFailureAction } from "./job-action"; import { JobConstraints } from "./job-constraints"; -import { JobExecutionInformation } from "./job-execution-information"; +import { JobExecutionInformation, JobExecutionInformationAttributes } from "./job-execution-information"; import { JobManagerTask } from "./job-manager-task"; import { JobPreparationTask } from "./job-preparation-task"; import { JobReleaseTask } from "./job-release-task"; import { JobStats } from "./job-stats"; -import { Metadata } from "./metadata"; -import { NameValuePair } from "./name-value-pair"; +import { Metadata, MetadataAttributes } from "./metadata"; +import { NameValuePair, NameValuePairAttributes } from "./name-value-pair"; -const JobRecord = Record({ - id: null, - displayName: null, - usesTaskDependencies: false, - url: null, - eTag: null, - lastModified: null, - creationTime: null, - state: null, - stateTransitionTime: null, - previousState: null, - previousStateTransitionTime: null, - priority: null, - onAllTasksComplete: AllTasksCompleteAction.noaction, - onTaskFailure: TaskFailureAction.noaction, - constraints: null, - jobManagerTask: null, - jobPreparationTask: null, - jobReleaseTask: null, - commonEnvironmentSettings: null, - poolInfo: null, - metadata: List([]), - executionInfo: null, - stats: null, - schedulingError: null, -}); +export interface JobAttributes { + id: string; + displayName: string; + usesTaskDependencies: boolean; + url: string; + eTag: string; + lastModified: Date; + creationTime: Date; + state: JobState; + stateTransitionTime: Date; + previousState: JobState; + previousStateTransitionTime: Date; + priority: number; + onAllTasksComplete: AllTasksCompleteAction; + onTaskFailure: TaskFailureAction; + constraints: Partial; + jobManagerTask: Partial; + jobPreparationTask: Partial; + jobReleaseTask: Partial; + commonEnvironmentSettings: NameValuePairAttributes[]; + poolInfo: any; + metadata: MetadataAttributes[]; + executionInfo: Partial; + stats: JobStats; +} /** * Class for displaying Batch job information. */ -export class Job extends JobRecord { - public id: string; - public displayName: string; - public usesTaskDependencies: boolean; - public url: string; - public eTag: string; - public lastModified: Date; - public creationTime: Date; - public state: JobState; - public stateTransitionTime: Date; - public previousState: JobState; - public previousStateTransitionTime: Date; - public priority: number; - public onAllTasksComplete: AllTasksCompleteAction; - public onTaskFailure: TaskFailureAction; +@Model() +export class Job extends Record { + @Prop() public id: string; + @Prop() public displayName: string; + @Prop() public usesTaskDependencies: boolean; + @Prop() public url: string; + @Prop() public eTag: string; + @Prop() public lastModified: Date; + @Prop() public creationTime: Date; + @Prop() public state: JobState; + @Prop() public stateTransitionTime: Date; + @Prop() public previousState: JobState; + @Prop() public previousStateTransitionTime: Date; + @Prop() public priority: number; + @Prop() public onAllTasksComplete: AllTasksCompleteAction = AllTasksCompleteAction.noaction; + @Prop() public onTaskFailure: TaskFailureAction = TaskFailureAction.noaction; - public constraints: JobConstraints; - public jobManagerTask: JobManagerTask; - public jobPreparationTask: JobPreparationTask; - public jobReleaseTask: JobReleaseTask; - public commonEnvironmentSettings: NameValuePair[]; - public poolInfo: any; - public metadata: List; - public executionInfo: JobExecutionInformation; - public stats: JobStats; + @Prop() public constraints: JobConstraints; + @Prop() public jobManagerTask: JobManagerTask; + @Prop() public jobPreparationTask: JobPreparationTask; + @Prop() public jobReleaseTask: JobReleaseTask; + @ListProp(NameValuePair) public commonEnvironmentSettings: List = List([]); + @Prop() public poolInfo: any; + @ListProp(Metadata) public metadata: List = List([]); + @Prop() public executionInfo: JobExecutionInformation; + @Prop() public stats: JobStats; /** * Tags are computed from the metadata using an internal key */ - public tags: List = List([]); + public readonly tags: List = List([]); + + /** + * If the job properties can be edited on the server. + * i.e. A competed job cannot be edited anymore. + */ + public readonly editable: boolean; - constructor(data: any = {}) { - super(Object.assign({}, data, { - jobPreparationTask: data.jobPreparationTask && new JobPreparationTask(data.jobPreparationTask), - jobReleaseTask: data.jobReleaseTask && new JobReleaseTask(data.jobReleaseTask), - jobManagerTask: data.jobManagerTask && new JobManagerTask(data.jobManagerTask), - executionInfo: data.executionInfo && new JobExecutionInformation(data.executionInfo), - metadata: List(data.metadata && data.metadata.map(x => new Metadata(x))), - })); + constructor(data: Partial = {}) { + super(data); this.tags = ModelUtils.tagsFromMetadata(this.metadata); + this.editable = this.state !== JobState.completed; } /** diff --git a/app/models/name-value-pair.ts b/app/models/name-value-pair.ts index bc3bfbc3ac..fb4dcb4cd2 100644 --- a/app/models/name-value-pair.ts +++ b/app/models/name-value-pair.ts @@ -1,7 +1,15 @@ +import { Model, Prop, Record } from "app/core"; + +export interface NameValuePairAttributes { + name: string; + value?: string; +} + /** * Common name value pair object */ -export class NameValuePair { - public name: string; - public value: string; +@Model() +export class NameValuePair extends Record { + @Prop() public name: string; + @Prop() public value: string; } diff --git a/app/models/scheduling-error.ts b/app/models/scheduling-error.ts index d712a70d46..a1e528fc83 100644 --- a/app/models/scheduling-error.ts +++ b/app/models/scheduling-error.ts @@ -1,19 +1,23 @@ -import { List, Record } from "immutable"; +import { List } from "immutable"; -const SchedulingErrorRecord = Record({ - code: null, - category: null, - message: null, - details: List([]), -}); +import { ListProp, Model, Prop, Record } from "app/core"; +import { NameValuePair, NameValuePairAttributes } from "./name-value-pair"; + +export interface SchedulingErrorAttributes { + code: string; + category: string; + message: string; + details: NameValuePairAttributes[]; +} /** * Job or task scheduling error. * Possible values are https://msdn.microsoft.com/en-us/library/azure/dn878162.aspx#BKMK_JobTaskError */ -export class SchedulingError extends SchedulingErrorRecord { - public code: string; - public category: string; - public message: string; - public details: List; +@Model() +export class SchedulingError extends Record { + @Prop() public code: string; + @Prop() public category: string; + @Prop() public message: string; + @ListProp(NameValuePair) public details: List; } diff --git a/test/app/components/job/details/error-display/job-error-display.component.spec.ts b/test/app/components/job/details/error-display/job-error-display.component.spec.ts index f8c923f393..6aa1aed275 100644 --- a/test/app/components/job/details/error-display/job-error-display.component.spec.ts +++ b/test/app/components/job/details/error-display/job-error-display.component.spec.ts @@ -124,7 +124,7 @@ describe("JobErrorDisplayComponent", () => { category: "UserError", message: "Auto pool has invalid settings", details: [ - { key: "some", value: "More info" }, + { name: "some", value: "More info" }, ], }, }, diff --git a/test/app/core/record.spec.ts b/test/app/core/record.spec.ts index 240586a350..03abe4b0e4 100644 --- a/test/app/core/record.spec.ts +++ b/test/app/core/record.spec.ts @@ -34,6 +34,12 @@ class SimpleTestRec extends Record { public c: number; } +@Model() +class InheritedTestRec extends SimpleTestRec { + @Prop() + public d: number; +} + describe("Record", () => { it("should throw an exeption when record doesn't extends Record class", () => { try { @@ -126,9 +132,9 @@ describe("Record", () => { }); it("toJS() should return compelex type toJS recursively", () => { - let a = new TestRec({ id: "id-1", nested: { name: "name-1" }, nestedList: [{ name: "name-2" }]}); + let a = new TestRec({ id: "id-1", nested: { name: "name-1" }, nestedList: [{ name: "name-2" }] }); - expect(a.toJS()).toEqual({ id: "id-1", nested: { name: "name-1" }, nestedList: [{ name: "name-2" }]}); + expect(a.toJS()).toEqual({ id: "id-1", nested: { name: "name-1" }, nestedList: [{ name: "name-2" }] }); }); it("should have access to values in constructor", () => { @@ -156,4 +162,9 @@ describe("Record", () => { expect(rec2.computedA).toEqual("A3"); expect(rec2.computedB).toEqual("B50"); }); + + it("should work with inherited models", () => { + const rec = new InheritedTestRec({ a: 1, b: 2, c: 3, d: 4, invalid: 10 }); + expect(rec.toJS()).toEqual({ id: null, a: 1, b: 2, c: 3, d: 4 }); + }); }); diff --git a/tslint.json b/tslint.json index 1b154ef44c..e7993ee1fa 100644 --- a/tslint.json +++ b/tslint.json @@ -30,7 +30,8 @@ "ban": [ true, ["main", "debugCrash"], // Show a warning if the main window is being shown on start for debug purposes - ["fdescribe"] + ["fdescribe"], + ["fit"] ], "no-empty-interface": false, "space-before-function-paren": [false], From de5e0cb7105a630088235b4cbf200e059132613d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 2 Jun 2017 09:47:56 -0700 Subject: [PATCH 07/37] Fix: dashboard tables return to line if window too small (#419) * Fix dashboard tables return to line if window too small * Move style * make table layout fixed to prevent overflow * Fix * give job status a fixed size column * remove empty style attr --- .../account/details/account-details.html | 10 ++--- .../account/details/account-details.scss | 7 +++ app/components/base/table/table.scss | 43 +------------------ 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/app/components/account/details/account-details.html b/app/components/account/details/account-details.html index d096d018be..718b38e098 100644 --- a/app/components/account/details/account-details.html +++ b/app/components/account/details/account-details.html @@ -33,8 +33,8 @@

Job status

- - + + @@ -62,9 +62,9 @@

Pool status

- - - + + + diff --git a/app/components/account/details/account-details.scss b/app/components/account/details/account-details.scss index 278c9b8c14..7e2e358f90 100644 --- a/app/components/account/details/account-details.scss +++ b/app/components/account/details/account-details.scss @@ -27,10 +27,17 @@ bl-account-details { text-align: justify; margin-top: 8px; margin-right: 4px; + padding: 12px; .account-card-content { height: 300px; } } } + + bl-table.dashboard { + bl-cell { + padding: 0 5px; + } + } } diff --git a/app/components/base/table/table.scss b/app/components/base/table/table.scss index 0a1dd35cb5..1e8f152a8f 100644 --- a/app/components/base/table/table.scss +++ b/app/components/base/table/table.scss @@ -15,6 +15,7 @@ bl-table { border-collapse: collapse; overflow: hidden; display: table; + table-layout: fixed; } bl-thead { @@ -114,48 +115,6 @@ bl-table { } } -/* - * Specifically for tables on the dashboard that need display: block & inline-block - * instead of display: table. - */ -bl-table.dashboard { - table { - display: block; - } - - bl-thead { - display: block; - } - - tbody { - display: block; - - > tr { - display: block; - padding: 6px; - } - } - - bl-cell, - bl-column { - &:first-child { - padding: 0; - } - - &:last-child { - padding: 0; - } - } - - bl-column { - display: inline-block; - } - - bl-cell { - display: inline-block; - } -} - .cell-spinner { color: map-get($primary, 400); } From b7672b0f5b58d74fba0a6598c63ea6fd5829d4aa Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 6 Jun 2017 10:15:48 -0700 Subject: [PATCH 08/37] Wait for the fetch to complete before returning an item (#430) --- app/services/core/rx-entity-proxy.ts | 32 +++++++++++----------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/app/services/core/rx-entity-proxy.ts b/app/services/core/rx-entity-proxy.ts index 942af0407c..a408b76ed8 100644 --- a/app/services/core/rx-entity-proxy.ts +++ b/app/services/core/rx-entity-proxy.ts @@ -84,26 +84,20 @@ export abstract class RxEntityProxy extends RxProxyBase(getProxy: RxEntityProxy): Observable { const obs = new AsyncSubject(); - const errorCallback = (e) => { - sub.unsubscribe(); - obs.error(e); - obs.complete(); - }; - - const sub = getProxy.item.subscribe({ - next: (item: TEntity) => { - if (item) { - sub.unsubscribe(); - obs.next(item); - obs.complete(); - getProxy.dispose(); - } - }, - error: errorCallback, - }); - getProxy.fetch().subscribe({ - error: errorCallback, + next: () => { + getProxy.item.first().subscribe((item: TEntity) => { + if (item) { + obs.next(item); + obs.complete(); + getProxy.dispose(); + } + }); + }, + error: (e) => { + obs.error(e); + obs.complete(); + }, }); return obs.asObservable(); From a1d72d6b0fcbac082a1646a58eb0220c79faad6a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 6 Jun 2017 11:10:09 -0700 Subject: [PATCH 09/37] Feature: Low priority support (#424) * WIP * WIp * Wip * Low pri is working * Fix test * pool resize dto * Autoscale formula syntax update * fix graphs * Table shows details * node is preemptible * low pri dashed colors * Node heatmap * Update * Fix resize broken * Update tests for low pri --- .../job/action/add/pool-picker.component.ts | 2 +- .../job-progress-status.component.ts | 2 +- .../node/details/node-configuration.html | 1 + .../pool/action/delete/delete-pool-task.ts | 4 +- .../resize/pool-resize-dialog.component.ts | 15 +- .../scale/pool-scale-picker.component.ts | 5 +- .../pool/action/scale/pool-scale-picker.html | 21 ++- .../pool/base/pool-nodes-preview.component.ts | 8 +- .../pool/base/pool-nodes-preview.html | 6 +- .../pool/browse/pool-list.component.ts | 14 +- app/components/pool/browse/pool-list.html | 10 +- .../pool-error-display.component.ts | 6 +- .../pool/details/pool-configuration.html | 6 +- .../pool/details/pool-details.component.ts | 10 -- app/components/pool/graphs/heatmap-color.ts | 3 + .../pool/graphs/nodes-heatmap.component.ts | 51 ++++++- app/components/pool/graphs/nodes-heatmap.html | 5 +- .../pool/graphs/pool-graphs.component.ts | 2 +- app/components/pool/graphs/pool-graphs.html | 4 +- app/core/record/record.ts | 5 + app/models/decorators/pool-decorator.ts | 26 +++- app/models/dtos/index.ts | 1 + app/models/dtos/pool-create.dto.ts | 5 +- app/models/dtos/pool-resize.dto.ts | 9 ++ app/models/forms/create-pool-model.ts | 9 +- app/models/node.ts | 53 +++---- app/models/pool.ts | 132 ++++++++++-------- app/services/core/data-cache.ts | 2 +- app/services/pool-service.ts | 6 +- app/utils/autoscale.ts | 3 +- client/api/batch-client-proxy/poolProxy.ts | 5 +- package.json | 2 +- .../action/add/pool-picker.component.spec.ts | 6 +- .../job-progress-status.component.spec.ts | 2 +- .../base/pool-nodes-preview.component.spec.ts | 28 ++-- .../graphs/nodes-heatmap.component.spec.ts | 25 +++- test/fixture.ts | 1 + yarn.lock | 6 +- 38 files changed, 319 insertions(+), 182 deletions(-) create mode 100644 app/models/dtos/pool-resize.dto.ts diff --git a/app/components/job/action/add/pool-picker.component.ts b/app/components/job/action/add/pool-picker.component.ts index 410d11f2c8..8bbcf83be4 100644 --- a/app/components/job/action/add/pool-picker.component.ts +++ b/app/components/job/action/add/pool-picker.component.ts @@ -91,7 +91,7 @@ export class PoolPickerComponent implements ControlValueAccessor, OnInit, OnDest public poolCoreCount(pool: Pool) { const cores = this.poolCores[pool.id] || 1; - return cores * pool.targetDedicated; + return cores * pool.targetNodes; } private _computeOptions(query: string = null) { diff --git a/app/components/job/details/job-progress-status/job-progress-status.component.ts b/app/components/job/details/job-progress-status/job-progress-status.component.ts index fae715e0bb..a460d0ba6a 100644 --- a/app/components/job/details/job-progress-status/job-progress-status.component.ts +++ b/app/components/job/details/job-progress-status/job-progress-status.component.ts @@ -45,7 +45,7 @@ export class JobProgressStatusComponent implements OnChanges, OnDestroy { this._subs.push(this.poolData.item.subscribe((pool) => { this.pool = pool; - this.maxRunningTasks = pool ? pool.targetDedicated * pool.maxTasksPerNode : 1; + this.maxRunningTasks = pool ? pool.targetNodes * pool.maxTasksPerNode : 1; this.updateGaugeOptions(); })); diff --git a/app/components/node/details/node-configuration.html b/app/components/node/details/node-configuration.html index f9951b70b6..b20a7e4ecb 100644 --- a/app/components/node/details/node-configuration.html +++ b/app/components/node/details/node-configuration.html @@ -10,5 +10,6 @@ + diff --git a/app/components/pool/action/delete/delete-pool-task.ts b/app/components/pool/action/delete/delete-pool-task.ts index 101ed29544..3bd4b91e73 100644 --- a/app/components/pool/action/delete/delete-pool-task.ts +++ b/app/components/pool/action/delete/delete-pool-task.ts @@ -18,7 +18,7 @@ export class DeletePoolTask extends LongRunningDeleteAction { protected waitForDelete(id: string, taskManager?: BackgroundTaskService) { this.poolService.getOnce(id).subscribe({ next: (pool: Pool) => { - const task = new WaitForDeletePoolPollTask(this.poolService, id, pool.currentDedicated); + const task = new WaitForDeletePoolPollTask(this.poolService, id, pool.currentNodes); if (taskManager) { taskManager.startTask(`Deleting pool '${id}'`, (bTask) => { return task.start(bTask.progress); @@ -72,7 +72,7 @@ export class WaitForDeletePoolPollTask { data.item.subscribe({ next: (pool: Pool) => { if (pool) { - const currentNodes = pool.currentDedicated; + const currentNodes = pool.currentNodes; progress.next(this._getProgress(currentNodes)); } }, diff --git a/app/components/pool/action/resize/pool-resize-dialog.component.ts b/app/components/pool/action/resize/pool-resize-dialog.component.ts index c93f04e37b..9cc54380e9 100644 --- a/app/components/pool/action/resize/pool-resize-dialog.component.ts +++ b/app/components/pool/action/resize/pool-resize-dialog.component.ts @@ -7,7 +7,7 @@ import { Observable } from "rxjs"; import { NotificationService } from "app/components/base/notifications"; import { SidebarRef } from "app/components/base/sidebar"; import { Pool } from "app/models"; -import { PoolEnableAutoScaleDto } from "app/models/dtos"; +import { PoolEnableAutoScaleDto, PoolResizeDto } from "app/models/dtos"; import { PoolScaleModel } from "app/models/forms"; import { PoolService } from "app/services"; @@ -22,7 +22,8 @@ export class PoolResizeDialogComponent { this._pool = pool; const interval = pool.autoScaleEvaluationInterval ? pool.autoScaleEvaluationInterval.asMinutes() : 15; this.scale.patchValue({ - targetDedicated: pool.targetDedicated, + targetDedicatedNodes: pool.targetDedicatedNodes, + targetLowPriorityNodes: pool.targetLowPriorityNodes, enableAutoScale: pool.enableAutoScale, autoScaleFormula: pool.autoScaleFormula, autoScaleEvaluationInterval: interval, @@ -52,15 +53,19 @@ export class PoolResizeDialogComponent { if (value.enableAutoScale) { obs = this._enableAutoScale(value); } else { - const targetDedicated = value.targetDedicated; - obs = this._disableAutoScale().flatMap(() => this.poolService.resize(id, targetDedicated)); + const targetDedicatedNodes = value.targetDedicatedNodes; + const targetLowPriorityNodes = value.targetLowPriorityNodes; + obs = this._disableAutoScale() + .flatMap(() => this.poolService.resize(id, new PoolResizeDto({ + targetDedicatedNodes, targetLowPriorityNodes, + }))); } const finalObs = obs.flatMap(() => this.poolService.getOnce(this.pool.id)).share(); finalObs.subscribe({ next: (pool) => { this.notificationService.success("Pool resize started!", - `Pool '${id}' will resize to ${pool.targetDedicated} nodes!`); + `Pool '${id}' will resize to ${pool.targetNodes} nodes!`); }, error: () => null, }); diff --git a/app/components/pool/action/scale/pool-scale-picker.component.ts b/app/components/pool/action/scale/pool-scale-picker.component.ts index a1ef3e07d5..61c7c6064a 100644 --- a/app/components/pool/action/scale/pool-scale-picker.component.ts +++ b/app/components/pool/action/scale/pool-scale-picker.component.ts @@ -25,7 +25,8 @@ export class PoolScalePickerComponent implements OnDestroy, ControlValueAccessor this.form = formBuilder.group({ enableAutoScale: false, autoScaleFormula: ["", this._invalidAutoscaleFormula()], - targetDedicated: [0, this._invalidTargetDedicated()], + targetDedicatedNodes: [0, this._invalidTargetNodes()], + targetLowPriorityNodes: [0, this._invalidTargetNodes()], autoScaleEvaluationInterval: [15], }); @@ -87,7 +88,7 @@ export class PoolScalePickerComponent implements OnDestroy, ControlValueAccessor }; } - private _invalidTargetDedicated() { + private _invalidTargetNodes() { return (control: FormControl): { [key: string]: any } => { if (!this.form || this.form.controls.enableAutoScale.value) { return null; diff --git a/app/components/pool/action/scale/pool-scale-picker.html b/app/components/pool/action/scale/pool-scale-picker.html index 4422bef751..8ab831c568 100644 --- a/app/components/pool/action/scale/pool-scale-picker.html +++ b/app/components/pool/action/scale/pool-scale-picker.html @@ -1,16 +1,27 @@
- - - - Target size is a required field +
+
+ + + +
+
+ + + +
+
+ Target dedicated is a required field + Target low priority is a required field
Auto scale formula is a required field - +
diff --git a/app/components/pool/base/pool-nodes-preview.component.ts b/app/components/pool/base/pool-nodes-preview.component.ts index cebc154dd4..01e2f0f406 100644 --- a/app/components/pool/base/pool-nodes-preview.component.ts +++ b/app/components/pool/base/pool-nodes-preview.component.ts @@ -1,6 +1,6 @@ import { Component, HostBinding, Input, OnChanges } from "@angular/core"; -import { Pool } from "app/models"; +import { Pool, PoolAllocationState } from "app/models"; import "./pool-nodes-preview.scss"; @@ -33,10 +33,10 @@ export class PoolNodesPreviewComponent implements OnChanges { const pool = this.pool; if (pool.resizeError) { return "There was a resize error"; - } else if (pool.currentDedicated !== pool.targetDedicated) { - return `Pool is resizing from ${pool.currentDedicated} to ${pool.targetDedicated} nodes`; + } else if (pool.allocationState === PoolAllocationState.resizing) { + return `Pool is resizing from ${pool.currentNodes} to ${pool.targetNodes} nodes`; } else { - return `Pool has ${pool.currentDedicated} nodes`; + return `Pool has ${pool.currentNodes} nodes`; } } } diff --git a/app/components/pool/base/pool-nodes-preview.html b/app/components/pool/base/pool-nodes-preview.html index 6862f91f62..826e203385 100644 --- a/app/components/pool/base/pool-nodes-preview.html +++ b/app/components/pool/base/pool-nodes-preview.html @@ -1,8 +1,8 @@
- {{pool.currentDedicated}} - + {{pool.currentNodes}} + ⇾ - {{pool.targetDedicated}} + {{pool.targetNodes}}
diff --git a/app/components/pool/browse/pool-list.component.ts b/app/components/pool/browse/pool-list.component.ts index 00b8b2fa89..a80a0e7031 100644 --- a/app/components/pool/browse/pool-list.component.ts +++ b/app/components/pool/browse/pool-list.component.ts @@ -4,6 +4,7 @@ import { import { MdDialog } from "@angular/material"; import { ActivatedRoute, Router } from "@angular/router"; import { autobind } from "core-decorators"; +import { List } from "immutable"; import { Observable, Subscription } from "rxjs"; import { BackgroundTaskService } from "app/components/base/background-task"; @@ -14,6 +15,7 @@ import { ListOrTableBase } from "app/components/base/selectable-list"; import { SidebarManager } from "app/components/base/sidebar"; import { TableComponent } from "app/components/base/table"; import { Pool } from "app/models"; +import { PoolDecorator } from "app/models/decorators"; import { PoolService } from "app/services"; import { RxListProxy } from "app/services/core"; import { Filter } from "app/utils/filter-builder"; @@ -57,8 +59,9 @@ export class PoolListComponent extends ListOrTableBase implements OnInit, OnDest } public get filter(): Filter { return this._filter; } + public pools: List = List([]); private _filter: Filter; - private _onPoolAddedSub: Subscription; + private _subs: Subscription[] = []; constructor( private poolService: PoolService, @@ -71,9 +74,12 @@ export class PoolListComponent extends ListOrTableBase implements OnInit, OnDest super(dialog); this.data = this.poolService.list(); this.status = this.data.status; - this._onPoolAddedSub = poolService.onPoolAdded.subscribe((poolId) => { + this._subs.push(poolService.onPoolAdded.subscribe((poolId) => { this.data.loadNewItem(poolService.get(poolId)); - }); + })); + this._subs.push(this.data.items.subscribe((pools) => { + this.pools = List(pools.map(x => new PoolDecorator(x))); + })); } public ngOnInit() { @@ -81,7 +87,7 @@ export class PoolListComponent extends ListOrTableBase implements OnInit, OnDest } public ngOnDestroy() { - this._onPoolAddedSub.unsubscribe(); + this._subs.forEach(x => x.unsubscribe()); } @autobind() diff --git a/app/components/pool/browse/pool-list.html b/app/components/pool/browse/pool-list.html index 7ad29de4ac..7eb09eb7c2 100644 --- a/app/components/pool/browse/pool-list.html +++ b/app/components/pool/browse/pool-list.html @@ -21,16 +21,16 @@

{{pool.id}}

State Allocation State VmSize - Current - Target + Dedicated + Low priority - + {{pool.id}} {{pool.state}} {{pool.allocationState}} {{pool.vmSize}} - {{pool.currentDedicated}} - {{pool.targetDedicated}} + {{pool.dedicatedNodes}} + {{pool.lowPriorityNodes}} diff --git a/app/components/pool/details/error-display/pool-error-display.component.ts b/app/components/pool/details/error-display/pool-error-display.component.ts index f7fac67795..88cb21b242 100644 --- a/app/components/pool/details/error-display/pool-error-display.component.ts +++ b/app/components/pool/details/error-display/pool-error-display.component.ts @@ -3,6 +3,7 @@ import { autobind } from "core-decorators"; import { shell } from "electron"; import { Pool, ResizeErrorCode } from "app/models"; +import { PoolResizeDto } from "app/models/dtos"; import { AccountService, PoolService } from "app/services"; import { ExternalLinks } from "app/utils/constants"; @@ -32,7 +33,10 @@ export class PoolErrorDisplayComponent { @autobind() public fixStopResizeError() { - const obs = this.poolService.resize(this.pool.id, this.pool.targetDedicated); + const obs = this.poolService.resize(this.pool.id, new PoolResizeDto({ + targetDedicatedNodes: this.pool.targetDedicatedNodes, + targetLowPriorityNodes: this.pool.targetLowPriorityNodes, + })); obs.subscribe(() => { this.refreshPool(); }); diff --git a/app/components/pool/details/pool-configuration.html b/app/components/pool/details/pool-configuration.html index 42b48dcd30..5bb7cbf674 100644 --- a/app/components/pool/details/pool-configuration.html +++ b/app/components/pool/details/pool-configuration.html @@ -3,8 +3,10 @@ - - + + + + diff --git a/app/components/pool/details/pool-details.component.ts b/app/components/pool/details/pool-details.component.ts index bc70343ecc..0f2383ec4e 100644 --- a/app/components/pool/details/pool-details.component.ts +++ b/app/components/pool/details/pool-details.component.ts @@ -106,16 +106,6 @@ export class PoolDetailsComponent implements OnInit, OnDestroy { }); } - public get nodesTooltipMessage() { - if (this.pool.resizeError) { - return "There was a resize error"; - } else if (this.pool.currentDedicated !== this.pool.targetDedicated) { - return `Pool is resizing from ${this.pool.currentDedicated} to ${this.pool.targetDedicated} nodes`; - } else { - return `Pool has ${this.pool.currentDedicated} nodes`; - } - } - @autobind() public updateTags(newTags: List) { return this.poolService.updateTags(this.pool, newTags).flatMap(() => { diff --git a/app/components/pool/graphs/heatmap-color.ts b/app/components/pool/graphs/heatmap-color.ts index 330cd5ccc2..43cee600a3 100644 --- a/app/components/pool/graphs/heatmap-color.ts +++ b/app/components/pool/graphs/heatmap-color.ts @@ -8,6 +8,8 @@ interface ColorMap { [key: string]: string; } * It also handles showing different colors for categories and substates */ export class HeatmapColor { + public keys: string[]; + private _colors: ColorMap = {}; private _lastHighlightedState: string = null; @@ -32,6 +34,7 @@ export class HeatmapColor { } else { this._colors = this._colorsForHighlight(highlightedState); } + this.keys = Object.keys(this._colors); this._lastHighlightedState = highlightedState; } diff --git a/app/components/pool/graphs/nodes-heatmap.component.ts b/app/components/pool/graphs/nodes-heatmap.component.ts index cdc9ed4033..499bbc9250 100644 --- a/app/components/pool/graphs/nodes-heatmap.component.ts +++ b/app/components/pool/graphs/nodes-heatmap.component.ts @@ -75,6 +75,9 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro @ViewChild("heatmap") public heatmapEl: ElementRef; + @ViewChild("svg") + public svgEl: ElementRef; + @Input() public set nodes(nodes: List) { if (nodes.size > maxNodes) { @@ -99,6 +102,7 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro private _erd: any; private _svg: d3.Selection; + private _defs: d3.Selection; private _width: number = 0; private _height: number = 0; private _nodeMap: { [id: string]: Node } = {}; @@ -129,10 +133,12 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro this.containerSizeChanged(); }); - this._svg = d3.select(this.heatmapEl.nativeElement).append("svg") + this._svg = d3.select(this.svgEl.nativeElement) .attr("width", this._width) .attr("height", this._height); + this._defs = this._svg.append("defs"); + this._setupLowPriColors(); this._processNewNodes(); } @@ -149,11 +155,12 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro public selectState(state: string) { this.highlightedState = state; + this.colors.updateColors(this.highlightedState); + this._setupLowPriColors(); this.redraw(); } public redraw() { - this.colors.updateColors(this.highlightedState); this._computeDimensions(); const tiles = this._nodes.map((node, index) => ({ node, index })); const groups = this._svg.selectAll("g.node-group").data(tiles.toJS()); @@ -216,6 +223,8 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro let color; if (tile.node.state === NodeState.running) { color = idleColor; + } else if (!tile.node.isDedicated) { + return `url(#${tile.node.state})`; } else { color = this.colors.get(tile.node.state); } @@ -238,7 +247,7 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro if (node.state !== NodeState.running || !node.recentTasks) { return []; } - return node.runningTasks.map((task, index) => ({ task, index })).toJS(); + return node.runningTasks.map((task, index) => ({ node, task, index })).toJS(); }); runningTaskRects.enter().append("rect") @@ -249,11 +258,45 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro }) .attr("width", z) .attr("height", taskWidth - 1) - .style("fill", runningColor); + .style("fill", (tile: any) => { + if (tile.node.isDedicated) { + return runningColor; + } else { + return `url(#${tile.node.state})`; + } + }); runningTaskRects.exit().remove(); } + private _setupLowPriColors() { + this._defs.selectAll("pattern").remove(); + for (let key of this.colors.keys) { + const pattern = this._defs.append("pattern") + .attr("id", key) + .attr("width", "8") + .attr("height", "10") + .attr("patternUnits", "userSpaceOnUse") + .attr("patternTransform", "rotate(45 50 50)"); + pattern.append("line") + .attr("stroke", this.colors.get(key)) + .attr("stroke-width", "12px") + .attr("y2", "10"); + + this._triggerWebkitSvgRedraw(); + } + + } + + /** + * Workaround for webkit not updating the svg if the defs only change(this will trigger the update). + */ + private _triggerWebkitSvgRedraw() { + this._svg.style("display", "inline-block"); + setTimeout(() => { + this._svg.style("display", "block"); + }); + } /** * Compute the dimension of the heatmap. * - rows diff --git a/app/components/pool/graphs/nodes-heatmap.html b/app/components/pool/graphs/nodes-heatmap.html index cf09d8f7f4..3807987050 100644 --- a/app/components/pool/graphs/nodes-heatmap.html +++ b/app/components/pool/graphs/nodes-heatmap.html @@ -3,5 +3,8 @@
-
+
+ + +
diff --git a/app/components/pool/graphs/pool-graphs.component.ts b/app/components/pool/graphs/pool-graphs.component.ts index 343335846d..3afd310bb1 100644 --- a/app/components/pool/graphs/pool-graphs.component.ts +++ b/app/components/pool/graphs/pool-graphs.component.ts @@ -78,7 +78,7 @@ export class PoolGraphsComponent implements OnChanges, OnDestroy { if (changes.pool) { this.data.updateParams({ poolId: this.pool.id }); this.data.refresh(false); - this.maxRunningTasks = this.pool ? this.pool.targetDedicated * this.pool.maxTasksPerNode : 0; + this.maxRunningTasks = this.pool ? this.pool.targetNodes * this.pool.maxTasksPerNode : 0; this.runningNodesHistory.reset(); this.runningTaskHistory.reset(); } diff --git a/app/components/pool/graphs/pool-graphs.html b/app/components/pool/graphs/pool-graphs.html index cbd0419d7d..b722ebe53e 100644 --- a/app/components/pool/graphs/pool-graphs.html +++ b/app/components/pool/graphs/pool-graphs.html @@ -12,7 +12,7 @@
Available nodes
- +
@@ -34,7 +34,7 @@
- +
diff --git a/app/core/record/record.ts b/app/core/record/record.ts index 5a331ee82c..b1aa516f3c 100644 --- a/app/core/record/record.ts +++ b/app/core/record/record.ts @@ -29,6 +29,11 @@ export class Record { return Object.assign({}, this._defaultValues, this._toJS()); } + public merge(other: Partial): this { + const ctr: any = this.constructor; + return new ctr({ ...this.toJS(), ...other as any}); + } + /** * DO NOT USE. For interal use only */ diff --git a/app/models/decorators/pool-decorator.ts b/app/models/decorators/pool-decorator.ts index f08620ef8e..0c51b5bc08 100644 --- a/app/models/decorators/pool-decorator.ts +++ b/app/models/decorators/pool-decorator.ts @@ -12,7 +12,8 @@ export class PoolDecorator extends DecoratorBase { public certificateReferences: any[]; public cloudServiceConfiguration: CloudServiceConfigurationDecorator; public creationTime: string; - public currentDedicated: string; + public currentDedicatedNodes: string; + public currentLowPriorityNodes: string; public displayName: string; public enableAutoScale: string; public enableInterNodeCommunication: string; @@ -23,7 +24,8 @@ export class PoolDecorator extends DecoratorBase { public resizeTimeout: string; public state: string; public stateTransitionTime: string; - public targetDedicated: string; + public targetDedicatedNodes: string; + public targetLowPriorityNodes: string; public autoScaleFormula: string; public autoScaleEvaluationInterval: string; public taskSchedulingPolicy: TaskSchedulingPolicyDecorator; @@ -35,6 +37,9 @@ export class PoolDecorator extends DecoratorBase { public lastResized: string; public userAccounts: string; + public dedicatedNodes: string; + public lowPriorityNodes: string; + constructor(private pool?: Pool) { super(pool); this.allocationState = this.stateField(pool.allocationState); @@ -42,7 +47,8 @@ export class PoolDecorator extends DecoratorBase { this.cloudServiceConfiguration = new CloudServiceConfigurationDecorator(pool.cloudServiceConfiguration || {} as any); this.creationTime = this.dateField(pool.creationTime); - this.currentDedicated = this.stringField(pool.currentDedicated); + this.currentDedicatedNodes = this.stringField(pool.currentDedicatedNodes); + this.currentLowPriorityNodes = this.stringField(pool.currentLowPriorityNodes); this.displayName = this.stringField(pool.displayName); this.enableAutoScale = this.booleanField(pool.enableAutoScale); this.enableInterNodeCommunication = this.booleanField(pool.enableInterNodeCommunication); @@ -53,7 +59,8 @@ export class PoolDecorator extends DecoratorBase { this.resizeTimeout = this.timespanField(pool.resizeTimeout); this.state = this.stateField(pool.state); this.stateTransitionTime = this.dateField(pool.stateTransitionTime); - this.targetDedicated = this.stringField(pool.targetDedicated); + this.targetDedicatedNodes = this.stringField(pool.targetDedicatedNodes); + this.targetLowPriorityNodes = this.stringField(pool.targetLowPriorityNodes); this.autoScaleFormula = this.stringField(pool.autoScaleFormula); this.autoScaleEvaluationInterval = this.timespanField(pool.autoScaleEvaluationInterval); this.taskSchedulingPolicy = @@ -69,6 +76,9 @@ export class PoolDecorator extends DecoratorBase { this.lastResized = moment(this.pool.allocationStateTransitionTime).fromNow(); this.userAccounts = pool.userAccounts.map(x => this._decorateUserAccount(x)).join(", "); + + this.dedicatedNodes = this._prettyNodes(pool.currentDedicatedNodes, pool.targetDedicatedNodes); + this.lowPriorityNodes = this._prettyNodes(pool.currentLowPriorityNodes, pool.targetLowPriorityNodes); } private _computePoolOs(): string { @@ -79,6 +89,14 @@ export class PoolDecorator extends DecoratorBase { return this.pool.osIconName(); } + private _prettyNodes(current: number, target: number) { + if (current === target) { + return target.toString(); + } else { + return `${current} → ${target}`; + } + } + private _decorateUserAccount(user: UserAccount) { if (user.elevationLevel === UserAccountElevationLevel.admin) { return `${user.name} (admin)`; diff --git a/app/models/dtos/index.ts b/app/models/dtos/index.ts index 208cc3eb5b..70b1beb773 100644 --- a/app/models/dtos/index.ts +++ b/app/models/dtos/index.ts @@ -10,3 +10,4 @@ export * from "./task-create.dto"; export * from "./metadata.dto"; export * from "./user-account.dto"; export * from "./user-identity.dto"; +export * from "./pool-resize.dto"; diff --git a/app/models/dtos/pool-create.dto.ts b/app/models/dtos/pool-create.dto.ts index 9929ae9955..887b10ab5e 100644 --- a/app/models/dtos/pool-create.dto.ts +++ b/app/models/dtos/pool-create.dto.ts @@ -32,7 +32,10 @@ export class PoolCreateDto extends Dto { public resizeTimeout?: moment.Duration; @DtoAttr() - public targetDedicated?: number; + public targetDedicatedNodes?: number; + + @DtoAttr() + public targetLowPriorityNodes?: number; @DtoAttr() public maxTasksPerNode?: number; diff --git a/app/models/dtos/pool-resize.dto.ts b/app/models/dtos/pool-resize.dto.ts new file mode 100644 index 0000000000..692116cbd9 --- /dev/null +++ b/app/models/dtos/pool-resize.dto.ts @@ -0,0 +1,9 @@ +import { Dto, DtoAttr } from "app/core"; + +export class PoolResizeDto extends Dto { + @DtoAttr() + public targetDedicatedNodes: number; + + @DtoAttr() + public targetLowPriorityNodes: number; +} diff --git a/app/models/forms/create-pool-model.ts b/app/models/forms/create-pool-model.ts index 4cb0e15502..579251fda0 100644 --- a/app/models/forms/create-pool-model.ts +++ b/app/models/forms/create-pool-model.ts @@ -27,7 +27,8 @@ export interface PoolScaleModel { enableAutoScale: boolean; autoScaleFormula: string; autoScaleEvaluationInterval: number; - targetDedicated: number; + targetDedicatedNodes: number; + targetLowPriorityNodes: number; } export interface CreatePoolModel { @@ -63,7 +64,8 @@ export function createPoolToData(output: CreatePoolModel): PoolCreateDto { data.autoScaleFormula = outputScale.autoScaleFormula; data.autoScaleEvaluationInterval = moment.duration({ minutes: outputScale.autoScaleEvaluationInterval }); } else { - data.targetDedicated = outputScale.targetDedicated; + data.targetDedicatedNodes = outputScale.targetDedicatedNodes; + data.targetLowPriorityNodes = outputScale.targetLowPriorityNodes; } if (output.os.source === PoolOsSources.PaaS) { @@ -89,7 +91,8 @@ export function poolToFormModel(pool: PoolCreateDto): CreatePoolModel { displayName: pool.displayName, vmSize: pool.vmSize, scale: { - targetDedicated: pool.targetDedicated, + targetDedicatedNodes: pool.targetDedicatedNodes, + targetLowPriorityNodes: pool.targetLowPriorityNodes, enableAutoScale: pool.enableAutoScale, autoScaleFormula: pool.autoScaleFormula, autoScaleEvaluationInterval: autoScaleInterval && autoScaleInterval.asMinutes(), diff --git a/app/models/node.ts b/app/models/node.ts index 215b0b477d..6245b0acc8 100644 --- a/app/models/node.ts +++ b/app/models/node.ts @@ -1,23 +1,9 @@ -import { Iterable, List, Record } from "immutable"; +import { Iterable, List } from "immutable"; +import { ListProp, Model, Prop, Record } from "app/core"; import { NodeRecentTask } from "app/models/node-recent-task"; import { TaskState } from "app/models/task"; -const NodeRecord = Record({ - id: null, - state: null, - totalTasksRun: 0, - schedulingState: null, - vmSize: null, - url: null, - stateTransitionTime: null, - lastBootTime: null, - allocationTime: null, - ipAddress: null, - affinityId: null, - recentTasks: null, -}); - export interface NodeAttributes { id: string; state: NodeState; @@ -31,30 +17,27 @@ export interface NodeAttributes { ipAddress: string; affinityId: string; recentTasks: Array>; + isDedicated: boolean; } /** * Class for displaying Batch node information. */ -export class Node extends NodeRecord { - public id: string; - public state: NodeState; - public totalTasksRun: number; - public schedulingState: string; - public vmSize: string; - public url: string; - public stateTransitionTime: Date; - public lastBootTime: Date; - public allocationTime: Date; - public ipAddress: string; - public affinityId: string; - public recentTasks: List; - - constructor(data: Partial) { - super(Object.assign({}, data, { - recentTasks: List(data.recentTasks && data.recentTasks.map(x => new NodeRecentTask(x))), - })); - } +@Model() +export class Node extends Record { + @Prop() public id: string; + @Prop() public state: NodeState; + @Prop() public totalTasksRun: number; + @Prop() public schedulingState: string; + @Prop() public vmSize: string; + @Prop() public url: string; + @Prop() public stateTransitionTime: Date; + @Prop() public lastBootTime: Date; + @Prop() public allocationTime: Date; + @Prop() public ipAddress: string; + @Prop() public affinityId: string; + @ListProp(NodeRecentTask) public recentTasks: List = List([]); + @Prop() public isDedicated: boolean; public get runningTasks(): Iterable { return this.recentTasks.filter(x => x.taskState === TaskState.running); diff --git a/app/models/pool.ts b/app/models/pool.ts index a04eeab429..f2b4a7fabe 100644 --- a/app/models/pool.ts +++ b/app/models/pool.ts @@ -18,7 +18,8 @@ export interface PoolAttributes { certificateReferences: any[]; cloudServiceConfiguration: Partial; creationTime: Date; - currentDedicated: number; + currentDedicatedNodes: number; + currentLowPriorityNodes: number; displayName: string; enableAutoscale: boolean; enableInterNodeCommunication: boolean; @@ -29,7 +30,8 @@ export interface PoolAttributes { resizeTimeout: Duration; state: string; stateTransitionTime: Date; - targetDedicated: number; + targetDedicatedNodes: number; + targetLowPriorityNodes: number; taskSchedulingPolicy: TaskSchedulingPolicy; url: string; virtualMachineConfiguration: Partial; @@ -44,65 +46,79 @@ export interface PoolAttributes { */ @Model() export class Pool extends Record { - @Prop() - public allocationState: string; - @Prop() - public allocationStateTransitionTime: Date; - @ListProp(Object) - public applicationPackageReferences: List; - @ListProp(Object) - public certificateReferences: List; - @Prop() - public cloudServiceConfiguration: CloudServiceConfiguration; - @Prop() - public creationTime: Date; - @Prop() - public currentDedicated: number; - @Prop() - public displayName: string; - @Prop() - public enableAutoScale: boolean; - @Prop() - public enableInterNodeCommunication: boolean; - @Prop() - public id: string; - @Prop() - public lastModified: Date; - @Prop() - public maxTasksPerNode: number = 1; - @Prop() - public resizeError: ResizeError; - @Prop() - public resizeTimeout: Duration; - @Prop() - public state: string; - @Prop() - public stateTransitionTime: Date; - @Prop() - public targetDedicated: number = 0; - @Prop() - public autoScaleFormula: string; - @Prop() - public autoScaleEvaluationInterval: Duration; - @Prop() - public taskSchedulingPolicy: any; - @Prop() - public url: string; - @Prop() - public virtualMachineConfiguration: VirtualMachineConfiguration; - @Prop() - public vmSize: string; - @Prop() - public startTask: StartTask; - @ListProp(Metadata) - public metadata: List = List([]); - @ListProp(UserAccount) - public userAccounts: List = List([]); + + @Prop() public allocationState: string; + + @Prop() public allocationStateTransitionTime: Date; + + @ListProp(Object) public applicationPackageReferences: List; + + @ListProp(Object) public certificateReferences: List; + + @Prop() public cloudServiceConfiguration: CloudServiceConfiguration; + + @Prop() public creationTime: Date; + + @Prop() public currentDedicatedNodes: number = 0; + + @Prop() public currentLowPriorityNodes: number = 0; + + @Prop() public displayName: string; + + @Prop() public enableAutoScale: boolean; + + @Prop() public enableInterNodeCommunication: boolean; + + @Prop() public id: string; + + @Prop() public lastModified: Date; + + @Prop() public maxTasksPerNode: number = 1; + + @Prop() public resizeError: ResizeError; + + @Prop() public resizeTimeout: Duration; + + @Prop() public state: string; + + @Prop() public stateTransitionTime: Date; + + @Prop() public targetDedicatedNodes: number = 0; + + @Prop() public targetLowPriorityNodes: number = 0; + + @Prop() public autoScaleFormula: string; + + @Prop() public autoScaleEvaluationInterval: Duration; + + @Prop() public taskSchedulingPolicy: any; + + @Prop() public url: string; + + @Prop() public virtualMachineConfiguration: VirtualMachineConfiguration; + + @Prop() public vmSize: string; + + @Prop() public startTask: StartTask; + + @ListProp(Metadata) public metadata: List = List([]); + + @ListProp(UserAccount) public userAccounts: List = List([]); /** * Tags are computed from the metadata using an internal key */ - public tags: List = List([]); + @Prop() public tags: List = List([]); + + /** + * Computed field sum of dedicated and low pri nodes + */ + public targetNodes: number; + + /** + * Computed field sum of dedicated and low pri nodes + */ + public currentNodes: number; private _osName: string; private _osIcon: string; @@ -112,6 +128,8 @@ export class Pool extends Record { this.tags = ModelUtils.tagsFromMetadata(this.metadata); this._osName = PoolUtils.getOsName(this); this._osIcon = PoolUtils.getComputePoolOsIcon(this._osName); + this.targetNodes = this.targetDedicatedNodes + this.targetLowPriorityNodes; + this.currentNodes = this.currentDedicatedNodes + this.currentLowPriorityNodes; } public osIconName(): string { diff --git a/app/services/core/data-cache.ts b/app/services/core/data-cache.ts index 759cb01fc8..62e9b8bcba 100644 --- a/app/services/core/data-cache.ts +++ b/app/services/core/data-cache.ts @@ -151,7 +151,7 @@ export class DataCache { if (!select) { return item; } const oldItem = this._items.getValue().get(key); if (!oldItem) { return item; } - let attributes = ObjectUtils.slice((item as any).toObject(), this._getAttributesList(select)); + let attributes = ObjectUtils.slice((item as any).toJS(), this._getAttributesList(select)); return (oldItem as any).merge(attributes); } } diff --git a/app/services/pool-service.ts b/app/services/pool-service.ts index 2a326a4702..2ecacae8e8 100644 --- a/app/services/pool-service.ts +++ b/app/services/pool-service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { Observable, Subject } from "rxjs"; import { Pool } from "app/models"; -import { PoolCreateDto, PoolEnableAutoScaleDto } from "app/models/dtos"; +import { PoolCreateDto, PoolEnableAutoScaleDto, PoolResizeDto } from "app/models/dtos"; import { ModelUtils, log } from "app/utils"; import { List } from "immutable"; import { BatchClientService } from "./batch-client.service"; @@ -79,8 +79,8 @@ export class PoolService extends ServiceBase { this._cache.deleteItemByKey(poolId); } - public resize(poolId: string, targetDedicated: number, options: any = {}) { - return this.callBatchClient((client) => client.pool.resize(poolId, targetDedicated, options), (error) => { + public resize(poolId: string, target: PoolResizeDto, options: any = {}) { + return this.callBatchClient((client) => client.pool.resize(poolId, target.toJS(), options), (error) => { log.error("Error resizing pool: " + poolId, Object.assign({}, error)); }); } diff --git a/app/utils/autoscale.ts b/app/utils/autoscale.ts index ead585ae1c..df9b34e5ab 100644 --- a/app/utils/autoscale.ts +++ b/app/utils/autoscale.ts @@ -5,7 +5,8 @@ const variables = [ "$DiskReadBytes", "$DiskReadOps", "$DiskWriteBytes", "$DiskWriteOps", "$FailedTasks", "$MemoryBytes", "$NetworkInBytes", "$NetworkOutBytes", "$PendingTasks", "$RunningTasks", "$SampleNodeCount", "$SucceededTasks", - "$TargetDedicated", "$WallClockSeconds", "$NodeDeallocationOption", + "$TargetDedicatedNodes", "$TargetLowPriorityNodes", "$WallClockSeconds", + "$NodeDeallocationOption", ]; const mathFunc = [ diff --git a/client/api/batch-client-proxy/poolProxy.ts b/client/api/batch-client-proxy/poolProxy.ts index 3fde6ba932..544c206d90 100644 --- a/client/api/batch-client-proxy/poolProxy.ts +++ b/client/api/batch-client-proxy/poolProxy.ts @@ -44,9 +44,10 @@ export default class PoolProxy { * @param targetDedicated: The desired number of nodes in the pool * @param options: Optional Parameters. */ - public resize(poolId: string, targetDedicated: number, options?: any): Promise { + public resize(poolId: string, target: any, options?: any): Promise { let resizeBody: any = {}; - resizeBody.targetDedicated = Number(targetDedicated); + resizeBody.targetDedicatedNodes = Number(target.targetDedicatedNodes); + resizeBody.targetLowPriorityNodes = Number(target.targetLowPriorityNodes); return this.client.pool.resize(poolId, resizeBody, wrapOptions(options)); } diff --git a/package.json b/package.json index 663ecae9fb..2e4f5e7f11 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "@angular/platform-browser-dynamic": "4.1.2", "@angular/router": "4.1.2", "@angular/tsc-wrapped": "4.1.2", - "azure-batch": "1.0.0-preview", + "azure-batch": "~2.0.0-preview", "azure-storage": "^2.1.0", "brace": "^0.10.0", "bunyan": "^1.8.4", diff --git a/test/app/components/job/action/add/pool-picker.component.spec.ts b/test/app/components/job/action/add/pool-picker.component.spec.ts index b0fea1100a..49a618539d 100644 --- a/test/app/components/job/action/add/pool-picker.component.spec.ts +++ b/test/app/components/job/action/add/pool-picker.component.spec.ts @@ -22,9 +22,9 @@ const config = { nodeAgentId: "centos.batch", }; -const pool1 = new Pool({ id: "pool-1", targetDedicated: 3, virtualMachineConfiguration: config }); -const pool2 = new Pool({ id: "pool-2", targetDedicated: 1, virtualMachineConfiguration: config }); -const pool3 = new Pool({ id: "pool-3", targetDedicated: 19, virtualMachineConfiguration: config }); +const pool1 = new Pool({ id: "pool-1", targetDedicatedNodes: 3, virtualMachineConfiguration: config }); +const pool2 = new Pool({ id: "pool-2", targetDedicatedNodes: 1, virtualMachineConfiguration: config }); +const pool3 = new Pool({ id: "pool-3", targetDedicatedNodes: 19, virtualMachineConfiguration: config }); describe("PoolPickerComponent", () => { let fixture: ComponentFixture; diff --git a/test/app/components/job/details/job-progress-status/job-progress-status.component.spec.ts b/test/app/components/job/details/job-progress-status/job-progress-status.component.spec.ts index 763ad8116e..232b822dbb 100644 --- a/test/app/components/job/details/job-progress-status/job-progress-status.component.spec.ts +++ b/test/app/components/job/details/job-progress-status/job-progress-status.component.spec.ts @@ -31,7 +31,7 @@ describe("JobProgressStatusComponent", () => { beforeEach(() => { poolServiceSpy = { get: () => new RxMockEntityProxy(Pool, { - item: new Pool({ id: "pool-1", maxTasksPerNode: 8, targetDedicated: 3 }), + item: new Pool({ id: "pool-1", maxTasksPerNode: 8, targetDedicatedNodes: 3 }), }), }; diff --git a/test/app/components/pool/base/pool-nodes-preview.component.spec.ts b/test/app/components/pool/base/pool-nodes-preview.component.spec.ts index e4650d1811..d547ea9005 100644 --- a/test/app/components/pool/base/pool-nodes-preview.component.spec.ts +++ b/test/app/components/pool/base/pool-nodes-preview.component.spec.ts @@ -33,14 +33,18 @@ describe("PoolNodesPreviewComponent", () => { describe("when pool is steady", () => { beforeEach(() => { - testComponent.pool = new Pool({ state: "steady", currentDedicated: 4, targetDedicated: 4 }); + testComponent.pool = new Pool({ + state: "steady", + currentDedicatedNodes: 4, targetDedicatedNodes: 4, + currentLowPriorityNodes: 1, targetLowPriorityNodes: 1, + }); fixture.detectChanges(); }); it("should just show the current dedicated", () => { const text = de.nativeElement.textContent; - expect(text).toContain("4"); - expect((text.match(/4/g) || []).length).toBe(1, "Should only have one 4 in the content"); + expect(text).toContain("5"); + expect((text.match(/5/g) || []).length).toBe(1, "Should only have one 5 in the content"); }); it("should not show the arrow", () => { @@ -51,14 +55,18 @@ describe("PoolNodesPreviewComponent", () => { describe("when pool is resizing", () => { beforeEach(() => { - testComponent.pool = new Pool({ state: "resizing", currentDedicated: 2, targetDedicated: 8 }); + testComponent.pool = new Pool({ + state: "resizing", + currentDedicatedNodes: 2, targetDedicatedNodes: 8, + currentLowPriorityNodes: 1, targetLowPriorityNodes: 1, + }); fixture.detectChanges(); }); it("should just show the current dedicated", () => { const text = de.nativeElement.textContent; - expect(text).toContain("2"); - expect(text).toContain("8"); + expect(text).toContain("3"); + expect(text).toContain("9"); }); it("should show the arrow", () => { @@ -70,7 +78,9 @@ describe("PoolNodesPreviewComponent", () => { describe("when there is a resize error", () => { beforeEach(() => { testComponent.pool = new Pool({ - state: "steady", currentDedicated: 2, targetDedicated: 8, + state: "steady", + currentDedicatedNodes: 2, targetDedicatedNodes: 8, + currentLowPriorityNodes: 1, targetLowPriorityNodes: 1, resizeError: { code: "StoppedResize" } as any, }); fixture.detectChanges(); @@ -78,8 +88,8 @@ describe("PoolNodesPreviewComponent", () => { it("should just show the current dedicated", () => { const text = de.nativeElement.textContent; - expect(text).toContain("2"); - expect(text).toContain("8"); + expect(text).toContain("3"); + expect(text).toContain("9"); }); it("should show the arrow", () => { diff --git a/test/app/components/pool/graphs/nodes-heatmap.component.spec.ts b/test/app/components/pool/graphs/nodes-heatmap.component.spec.ts index a9aed0efde..b6b08b2651 100644 --- a/test/app/components/pool/graphs/nodes-heatmap.component.spec.ts +++ b/test/app/components/pool/graphs/nodes-heatmap.component.spec.ts @@ -129,6 +129,22 @@ describe("NodesHeatmapLegendComponent", () => { }); }); + it("should use fill url(gradient) for low pri nodes", () => { + testComponent.nodes = createNodes(2, false); + fixture.detectChanges(); + const tiles = svg.selectAll("g.node-group"); + expect(tiles.size()).toBe(2); + tiles.each((d, i, groups) => { + const group = d3.select(groups[i]); + const bg = group.select("g.bg"); + + expect(bg.selectAll("rect").size()).toBe(1, "Should only have 1 rect"); + const rect = bg.select("rect"); + expect(rect).not.toBeFalsy("Should have a rect in bg"); + expect(rect.attr("style")).toContain("fill: url(\"#idle\")"); + }); + }); + it("should not fail when the size of the svg is 0x0", () => { testComponent.width = "160px"; testComponent.nodes = createNodes(4); @@ -153,9 +169,8 @@ describe("NodesHeatmapLegendComponent", () => { testComponent.nodes = createNodes(4); fixture.detectChanges(); - const group = svg.select("g.node-group:nth-child(2)"); - - const el: any = group.node(); + const groups = svg.selectAll("g.node-group"); + const el: any = groups.nodes()[1]; click(el); fixture.detectChanges(); expect(heatmap.selectedNodeId.value).toEqual("node-2"); @@ -189,10 +204,10 @@ describe("NodesHeatmapLegendComponent", () => { }); }); -function createNodes(count: number) { +function createNodes(count: number, dedicated = true) { const nodes: Node[] = []; for (let i = 0; i < count; i++) { - nodes.push(Fixture.node.create({ id: `node-${i + 1}`, state: NodeState.idle })); + nodes.push(Fixture.node.create({ id: `node-${i + 1}`, state: NodeState.idle, isDedicated: dedicated })); } return List(nodes); } diff --git a/test/fixture.ts b/test/fixture.ts index 711e5b8d1e..b810174b40 100644 --- a/test/fixture.ts +++ b/test/fixture.ts @@ -197,6 +197,7 @@ export const node = new FixtureFactory(Node, { id: "node-1", displayName: "MyImaginaryNode", state: "running", + isDedicated: true, }); export const subscription = new FixtureFactory(Subscription, { diff --git a/yarn.lock b/yarn.lock index 48fef09668..b9829155a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -639,9 +639,9 @@ aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -azure-batch@1.0.0-preview: - version "1.0.0-preview" - resolved "https://registry.yarnpkg.com/azure-batch/-/azure-batch-1.0.0-preview.tgz#2f281f1f8b7f381afa8eee5adbe6e8b48de40f0e" +azure-batch@~2.0.0-preview: + version "2.0.0-preview" + resolved "https://registry.yarnpkg.com/azure-batch/-/azure-batch-2.0.0-preview.tgz#8f8cc0be13dcba361d28e2eb38887ca0e5876ea5" dependencies: "@types/underscore" "^1.8.0" moment "^2.18.1" From 261e6be8ed5f43f0b54e79a9b2659693fed7718f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 6 Jun 2017 13:12:15 -0700 Subject: [PATCH 10/37] Fix: Only reset graphs if the pool id changes (#431) * Only reset graphs if the pool id changes * update max running tasks all the time * Also for heatmap * tslint fix --- app/components/pool/graphs/nodes-heatmap.component.ts | 10 ++++++++-- app/components/pool/graphs/pool-graphs.component.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/components/pool/graphs/nodes-heatmap.component.ts b/app/components/pool/graphs/nodes-heatmap.component.ts index 499bbc9250..04a8a6e1f3 100644 --- a/app/components/pool/graphs/nodes-heatmap.component.ts +++ b/app/components/pool/graphs/nodes-heatmap.component.ts @@ -1,5 +1,6 @@ import { - AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input, OnChanges, OnDestroy, ViewChild, + AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, + HostBinding, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild, } from "@angular/core"; import * as d3 from "d3"; import * as elementResizeDetectorMaker from "element-resize-detector"; @@ -115,8 +116,13 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro }); } - public ngOnChanges(changes) { + public ngOnChanges(changes: SimpleChanges) { if (changes.pool) { + const prev = changes.pool.previousValue; + const cur = changes.pool.currentValue; + if (prev && cur && prev.id === cur.id) { + return; + } this.selectedNodeId.next(null); if (this._svg) { this._svg.selectAll("g.node-group").remove(); diff --git a/app/components/pool/graphs/pool-graphs.component.ts b/app/components/pool/graphs/pool-graphs.component.ts index 3afd310bb1..7050e431ee 100644 --- a/app/components/pool/graphs/pool-graphs.component.ts +++ b/app/components/pool/graphs/pool-graphs.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from "@angular/core"; import { FormControl } from "@angular/forms"; import { Router } from "@angular/router"; import { autobind } from "core-decorators"; @@ -74,11 +74,16 @@ export class PoolGraphsComponent implements OnChanges, OnDestroy { this._poll = this.data.startPoll(refreshRate); } - public ngOnChanges(changes) { + public ngOnChanges(changes: SimpleChanges) { if (changes.pool) { + const prev = changes.pool.previousValue; + const cur = changes.pool.currentValue; + this.maxRunningTasks = this.pool ? this.pool.targetNodes * this.pool.maxTasksPerNode : 0; + if (prev && cur && prev.id === cur.id) { + return; + } this.data.updateParams({ poolId: this.pool.id }); this.data.refresh(false); - this.maxRunningTasks = this.pool ? this.pool.targetNodes * this.pool.maxTasksPerNode : 0; this.runningNodesHistory.reset(); this.runningTaskHistory.reset(); } From 085a9be43050a0b06d46e0f82b4f90d8f0c027b2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 6 Jun 2017 14:05:44 -0700 Subject: [PATCH 11/37] Fix custom image pool making list crash (#435) --- app/models/pool-os-disk.ts | 14 +++++++++++ app/models/virtual-machine-configuration.ts | 26 +++++++-------------- app/utils/pool-utils.ts | 14 ++++++++--- 3 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 app/models/pool-os-disk.ts diff --git a/app/models/pool-os-disk.ts b/app/models/pool-os-disk.ts new file mode 100644 index 0000000000..5c23495df7 --- /dev/null +++ b/app/models/pool-os-disk.ts @@ -0,0 +1,14 @@ +import { List } from "immutable"; + +import { ListProp, Model, Prop, Record } from "app/core"; + +export interface PoolOSDiskAttributes { + caching: string; + imageUris?: string[]; +} + +@Model() +export class PoolOSDisk extends Record { + @Prop() public caching: string; + @ListProp(String) public imageUris: List = List([]); +} diff --git a/app/models/virtual-machine-configuration.ts b/app/models/virtual-machine-configuration.ts index 982baedd74..105902140b 100644 --- a/app/models/virtual-machine-configuration.ts +++ b/app/models/virtual-machine-configuration.ts @@ -1,30 +1,22 @@ -import { Record } from "immutable"; - +import { Model, Prop, Record } from "app/core"; import { ImageReference, ImageReferenceAttributes } from "./image-reference"; +import { PoolOSDisk, PoolOSDiskAttributes } from "./pool-os-disk"; import { WindowsConfiguration } from "./windows-configuration"; -// tslint:disable:variable-name object-literal-sort-keys -const VirtualMachineConfigurationRecord = Record({ - imageReference: null, - nodeAgentSKUId: null, - windowsConfiguration: null, -}); - export interface VirtualMachineConfigurationAttributes { imageReference: ImageReferenceAttributes; nodeAgentSKUId: string; windowsConfiguration: WindowsConfiguration; + osDisk?: PoolOSDiskAttributes; } /** * Class for displaying Batch VirtualMachineConfiguration information. */ -export class VirtualMachineConfiguration extends VirtualMachineConfigurationRecord { - public imageReference: ImageReference; - public nodeAgentSKUId: string; - public windowsConfiguration: WindowsConfiguration; - - constructor(data: VirtualMachineConfigurationAttributes) { - super(data); - } +@Model() +export class VirtualMachineConfiguration extends Record { + @Prop() public imageReference: ImageReference; + @Prop() public nodeAgentSKUId: string; + @Prop() public windowsConfiguration: WindowsConfiguration; + @Prop() public osDisk: PoolOSDisk; } diff --git a/app/utils/pool-utils.ts b/app/utils/pool-utils.ts index b7ff8c7057..0b3eca94ab 100644 --- a/app/utils/pool-utils.ts +++ b/app/utils/pool-utils.ts @@ -96,8 +96,14 @@ export class PoolUtils { } if (pool.virtualMachineConfiguration) { - if (pool.virtualMachineConfiguration.imageReference.publisher === - "MicrosoftWindowsServer") { + const config = pool.virtualMachineConfiguration; + if (config.osDisk) { + return "Custom Image"; + } + if (!config.imageReference) { + return "Unkown"; + } + if (config.imageReference.publisher === "MicrosoftWindowsServer") { return `Windows Server ${pool.virtualMachineConfiguration.imageReference.sku}`; } @@ -110,7 +116,9 @@ export class PoolUtils { } public static getComputePoolOsIcon(osName): string { - if (osName.includes("Windows")) { + if (osName === "Custom Image") { + return "cloud"; + } else if (osName.includes("Windows")) { return "windows"; } From d570e0f57109628f8359076aa5c780e6a466e072 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 6 Jun 2017 14:17:27 -0700 Subject: [PATCH 12/37] Feature: Scheduling error refactor to Failure info with more error possibilities (#433) * Rename scheduling error to failure info * Update messages * Remove console.log * fix tests --- .../job/browse/job-list.component.ts | 8 +-- .../job-error-display.component.ts | 4 +- .../error-display/job-error-display.html | 12 ++-- .../display/task-list-display.component.ts | 6 +- .../sub-task-display-list.component.ts | 6 +- .../sub-task-properties.component.ts | 4 +- .../sub-tasks/sub-task-properties.html | 10 ++-- .../details/task-configuration.component.ts | 2 +- .../task/details/task-configuration.html | 2 +- .../details/task-error-display.component.ts | 60 ++++++++++++------- .../task/details/task-error-display.html | 7 ++- ...decorator.ts => failure-info-decorator.ts} | 6 +- app/models/decorators/index.ts | 2 +- .../job-execution-info-decorator.ts | 6 +- app/models/decorators/sub-task-decorator.ts | 6 +- .../task-execution-info-decorator.ts | 6 +- .../{scheduling-error.ts => failure-info.ts} | 10 ++-- app/models/index.ts | 2 +- app/models/job-execution-information.ts | 6 +- app/models/subtask-information.ts | 53 ++++++++-------- app/models/task-execution-information.ts | 35 +++++++---- app/models/task-exit-conditions.ts | 6 +- app/models/task.ts | 2 +- .../job-error-display.component.spec.ts | 2 +- .../task-error-display.component.spec.ts | 17 +++++- .../task-timeline.component.spec.ts | 4 ++ test/app/models/task.spec.ts | 17 ++++-- test/fixture.ts | 4 +- 28 files changed, 187 insertions(+), 118 deletions(-) rename app/models/decorators/{scheduling-error-decorator.ts => failure-info-decorator.ts} (88%) rename app/models/{scheduling-error.ts => failure-info.ts} (72%) diff --git a/app/components/job/browse/job-list.component.ts b/app/components/job/browse/job-list.component.ts index e0254368e9..d5eedc1e9c 100644 --- a/app/components/job/browse/job-list.component.ts +++ b/app/components/job/browse/job-list.component.ts @@ -12,7 +12,7 @@ import { QuickListComponent, QuickListItemStatus } from "app/components/base/qui import { ListOrTableBase } from "app/components/base/selectable-list"; import { TableComponent } from "app/components/base/table"; import { Job, JobState } from "app/models"; -import { SchedulingErrorDecorator } from "app/models/decorators"; +import { FailureInfoDecorator } from "app/models/decorators"; import { JobService } from "app/services"; import { RxListProxy } from "app/services/core"; import { Filter } from "app/utils/filter-builder"; @@ -91,7 +91,7 @@ export class JobListComponent extends ListOrTableBase implements OnInit, OnDestr } public jobStatus(job: Job): QuickListItemStatus { - if (job.executionInfo && job.executionInfo.schedulingError) { + if (job.executionInfo && job.executionInfo.failureInfo) { return QuickListItemStatus.warning; } else { switch (job.state) { @@ -112,8 +112,8 @@ export class JobListComponent extends ListOrTableBase implements OnInit, OnDestr } public jobStatusText(job: Job): string { - if (job.executionInfo && job.executionInfo.schedulingError) { - return new SchedulingErrorDecorator(job.executionInfo.schedulingError).summary; + if (job.executionInfo && job.executionInfo.failureInfo) { + return new FailureInfoDecorator(job.executionInfo.failureInfo).summary; } else { switch (job.state) { case JobState.completed: diff --git a/app/components/job/details/error-display/job-error-display.component.ts b/app/components/job/details/error-display/job-error-display.component.ts index 2fda17cfd7..bb9b78f939 100644 --- a/app/components/job/details/error-display/job-error-display.component.ts +++ b/app/components/job/details/error-display/job-error-display.component.ts @@ -23,9 +23,9 @@ export class JobErrorDisplayComponent { return this.job && this.job.executionInfo; } - public get schedulingError() { + public get failureInfo() { const info = this.executionInfo; - return info && info.schedulingError; + return info && info.failureInfo; } public get jobFailed() { diff --git a/app/components/job/details/error-display/job-error-display.html b/app/components/job/details/error-display/job-error-display.html index d3dee85a76..4f301835c7 100644 --- a/app/components/job/details/error-display/job-error-display.html +++ b/app/components/job/details/error-display/job-error-display.html @@ -4,12 +4,12 @@
Job timed out after running for {{jobRunningTime}}.
- -
{{schedulingError.message}}
-
{{schedulingError.code}}
-
{{schedulingError.message}}
-
-
+ +
{{failureInfo.message}}
+
{{failureInfo.code}}
+
{{failureInfo.message}}
+
+
{{entry.name}}: {{entry.value}}
diff --git a/app/components/task/browse/display/task-list-display.component.ts b/app/components/task/browse/display/task-list-display.component.ts index 44c05716ae..8d08487f9d 100644 --- a/app/components/task/browse/display/task-list-display.component.ts +++ b/app/components/task/browse/display/task-list-display.component.ts @@ -9,7 +9,7 @@ import { ListOrTableBase } from "app/components/base/selectable-list"; import { TableComponent } from "app/components/base/table"; import { DeleteTaskDialogComponent, TerminateTaskDialogComponent } from "app/components/task/action"; import { Task, TaskState } from "app/models"; -import { SchedulingErrorDecorator } from "app/models/decorators"; +import { FailureInfoDecorator } from "app/models/decorators"; import { TaskService } from "app/services"; import { DateUtils } from "app/utils"; @@ -46,8 +46,8 @@ export class TaskListDisplayComponent extends ListOrTableBase { } public taskStatusText(task: Task): string { - if (task.executionInfo && task.executionInfo.schedulingError) { - return new SchedulingErrorDecorator(task.executionInfo.schedulingError).summary; + if (task.executionInfo && task.executionInfo.failureInfo) { + return new FailureInfoDecorator(task.executionInfo.failureInfo).summary; } else if (task.executionInfo && task.executionInfo.exitCode !== 0) { return `Task failed with exitCode: ${task.executionInfo.exitCode}`; } diff --git a/app/components/task/details/sub-tasks/sub-task-display-list.component.ts b/app/components/task/details/sub-tasks/sub-task-display-list.component.ts index 6f5b195abd..aa45be35c4 100644 --- a/app/components/task/details/sub-tasks/sub-task-display-list.component.ts +++ b/app/components/task/details/sub-tasks/sub-task-display-list.component.ts @@ -5,7 +5,7 @@ import { LoadingStatus } from "app/components/base/loading"; import { QuickListComponent, QuickListItemStatus } from "app/components/base/quick-list"; import { SelectableList } from "app/components/base/selectable-list"; import { SubtaskInformation, TaskState } from "app/models"; -import { SchedulingErrorDecorator } from "app/models/decorators"; +import { FailureInfoDecorator } from "app/models/decorators"; @Component({ selector: "bl-sub-task-display-list", @@ -43,8 +43,8 @@ export class SubTaskDisplayListComponent extends SelectableList { } public taskStatusText(task: SubtaskInformation): string { - if (task.schedulingError) { - return new SchedulingErrorDecorator(task.schedulingError).summary; + if (task.failureInfo) { + return new FailureInfoDecorator(task.failureInfo).summary; } else if (task.exitCode !== 0) { return `Subtask failed with exitCode: ${task.exitCode}`; } diff --git a/app/components/task/details/sub-tasks/sub-task-properties.component.ts b/app/components/task/details/sub-tasks/sub-task-properties.component.ts index 9099e92f6d..d196d748d8 100644 --- a/app/components/task/details/sub-tasks/sub-task-properties.component.ts +++ b/app/components/task/details/sub-tasks/sub-task-properties.component.ts @@ -12,11 +12,11 @@ export class SubTaskPropertiesComponent { @Input() public set task(value: SubtaskInformation) { this.decorator = new SubTaskDecorator(value || {} as any); - this.schedulingError = this.decorator.schedulingError || {}; + this.failureInfo = this.decorator.failureInfo || {}; this.nodeInfo = this.decorator.nodeInfo || {}; } public decorator: SubTaskDecorator; - public schedulingError: any; + public failureInfo: any; public nodeInfo: any; } diff --git a/app/components/task/details/sub-tasks/sub-task-properties.html b/app/components/task/details/sub-tasks/sub-task-properties.html index 257a5548eb..7aa211b06a 100644 --- a/app/components/task/details/sub-tasks/sub-task-properties.html +++ b/app/components/task/details/sub-tasks/sub-task-properties.html @@ -19,10 +19,10 @@ - - - - - + + + + + diff --git a/app/components/task/details/task-configuration.component.ts b/app/components/task/details/task-configuration.component.ts index 7edeea1446..aa4600be0b 100644 --- a/app/components/task/details/task-configuration.component.ts +++ b/app/components/task/details/task-configuration.component.ts @@ -79,7 +79,7 @@ export class TaskConfigurationComponent { this.exitConditionData = { noAction, terminateJob, - schedulingError: this._jobActionString(this._task.exitConditions.schedulingError), + failureInfo: this._jobActionString(this._task.exitConditions.failureInfo), default: this._jobActionString(this._task.exitConditions.default), }; } diff --git a/app/components/task/details/task-configuration.html b/app/components/task/details/task-configuration.html index 5a245e137d..b5ec699183 100644 --- a/app/components/task/details/task-configuration.html +++ b/app/components/task/details/task-configuration.html @@ -76,7 +76,7 @@ - + diff --git a/app/components/task/details/task-error-display.component.ts b/app/components/task/details/task-error-display.component.ts index 0e25d3bdb0..05c2338f65 100644 --- a/app/components/task/details/task-error-display.component.ts +++ b/app/components/task/details/task-error-display.component.ts @@ -1,9 +1,9 @@ -import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from "@angular/core"; import { autobind } from "core-decorators"; import { SidebarManager } from "app/components/base/sidebar"; import { RerunTaskFormComponent } from "app/components/task/action"; -import { Task, TaskState } from "app/models"; +import { FailureInfo, Task, TaskState } from "app/models"; import { TaskService } from "app/services"; import { DateUtils, ObservableUtils } from "app/utils"; @@ -12,36 +12,40 @@ import { DateUtils, ObservableUtils } from "app/utils"; templateUrl: "task-error-display.html", changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TaskErrorDisplayComponent { +export class TaskErrorDisplayComponent implements OnChanges { @Input() public jobId: string; @Input() public task: Task; + public hasCompleted: boolean = false; + public failureInfo: FailureInfo; + public code: number; + public hasFailureExitCode: boolean = false; + public errorMessage: string; + constructor( private taskService: TaskService, private sidebarManager: SidebarManager) { } - public get hasCompleted(): boolean { - return Boolean(this.task && this.task.state === TaskState.completed); - } + public ngOnChanges(changes: SimpleChanges) { + if (changes.task) { + if (this.task) { - public get code() { - return this.task && this.task.executionInfo && this.task.executionInfo.exitCode; - } - - public get hasFailureExitCode(): boolean { - return this.hasCompleted && this.code !== 0; - } - - public get exitCodeMessage(): string { - if (this.task.didTimeout) { - const time: any = this.task.constraints && this.task.constraints.maxWallClockTime; - return `Task timed out after running for ${DateUtils.prettyDuration(time)}`; - } else { - return `Task completed with exit code '${this.code}'`; + const exec = this.task.executionInfo; + this.failureInfo = exec && exec.failureInfo; + this.hasCompleted = Boolean(this.task && this.task.state === TaskState.completed); + this.code = exec && this.task.executionInfo.exitCode; + this.hasFailureExitCode = this.hasCompleted && this.code !== 0; + this._computeExitCodeMessage(); + } else { + this.failureInfo = null; + this.code = null; + this.hasFailureExitCode = false; + this.errorMessage = ""; + } } } @@ -59,4 +63,20 @@ export class TaskErrorDisplayComponent { ref.component.jobId = this.jobId; ref.component.setValueFromEntity(this.task); } + + private _computeExitCodeMessage() { + if (!this.failureInfo) { + this.errorMessage = ""; + return; + } + + if (this.task.didTimeout) { + const time: any = this.task.constraints && this.task.constraints.maxWallClockTime; + this.errorMessage = `Task timed out after running for ${DateUtils.prettyDuration(time)}`; + } else if (this.failureInfo.code === "FailureExitCode") { + this.errorMessage = `Task completed with exit code '${this.code}'`; + } else { + this.errorMessage = this.failureInfo.message; + } + } } diff --git a/app/components/task/details/task-error-display.html b/app/components/task/details/task-error-display.html index eaf109293d..69c47664f4 100644 --- a/app/components/task/details/task-error-display.html +++ b/app/components/task/details/task-error-display.html @@ -1,4 +1,9 @@ - + + +
{{failureInfo.code}}
+
+
{{errorMessage}}
diff --git a/app/models/decorators/scheduling-error-decorator.ts b/app/models/decorators/failure-info-decorator.ts similarity index 88% rename from app/models/decorators/scheduling-error-decorator.ts rename to app/models/decorators/failure-info-decorator.ts index b6ff6d73aa..3d0a0d457b 100644 --- a/app/models/decorators/scheduling-error-decorator.ts +++ b/app/models/decorators/failure-info-decorator.ts @@ -1,7 +1,7 @@ -import { SchedulingError } from "app/models"; +import { FailureInfo } from "app/models"; import { DecoratorBase } from "app/utils/decorators"; -export class SchedulingErrorDecorator extends DecoratorBase { +export class FailureInfoDecorator extends DecoratorBase { public category: string; public code: string; public message: string; @@ -17,7 +17,7 @@ export class SchedulingErrorDecorator extends DecoratorBase { */ public exists: boolean; - constructor(error: SchedulingError) { + constructor(error: FailureInfo) { super(error); this.exists = Boolean(error && error.category); diff --git a/app/models/decorators/index.ts b/app/models/decorators/index.ts index 51cd0bbaba..6f0c9a7afb 100644 --- a/app/models/decorators/index.ts +++ b/app/models/decorators/index.ts @@ -1,6 +1,7 @@ export * from "./application-decorator"; export * from "./cloud-service-configuration-decorator"; export * from "./compute-node-info-decorator"; +export * from "./failure-info-decorator"; export * from "./image-reference-decorator"; export * from "./job-constraints-decorator"; export * from "./job-decorator"; @@ -9,7 +10,6 @@ export * from "./job-manager-task-decorator"; export * from "./job-preparation-task-decorator"; export * from "./job-release-task-decorator"; export * from "./pool-decorator"; -export * from "./scheduling-error-decorator"; export * from "./start-task.decorator"; export * from "./sub-task-decorator"; export * from "./node-decorator"; diff --git a/app/models/decorators/job-execution-info-decorator.ts b/app/models/decorators/job-execution-info-decorator.ts index abd67d04ad..ff405dc1fa 100644 --- a/app/models/decorators/job-execution-info-decorator.ts +++ b/app/models/decorators/job-execution-info-decorator.ts @@ -1,12 +1,12 @@ import { JobExecutionInformation } from "app/models"; import { DecoratorBase } from "app/utils/decorators"; -import { SchedulingErrorDecorator } from "./scheduling-error-decorator"; +import { FailureInfoDecorator } from "./failure-info-decorator"; export class JobExecutionInfoDecorator extends DecoratorBase { public startTime: string; public endTime: string; public poolId: string; - public schedulingError: SchedulingErrorDecorator; + public failureInfo: FailureInfoDecorator; public terminateReason: string; constructor(executionInfo: JobExecutionInformation) { @@ -15,7 +15,7 @@ export class JobExecutionInfoDecorator extends DecoratorBase { public startTime: string; @@ -14,7 +14,7 @@ export class SubTaskDecorator extends DecoratorBase { public previousStateTransitionTime: string; public nodeInfo: {}; - public schedulingError: {}; + public failureInfo: {}; constructor(task?: SubtaskInformation) { super(task); @@ -29,7 +29,7 @@ export class SubTaskDecorator extends DecoratorBase { this.previousStateTransitionTime = this.dateField(task.previousStateTransitionTime); this.nodeInfo = new ComputeNodeInfoDecorator(task.nodeInfo || {} as any); - this.schedulingError = new SchedulingErrorDecorator(task.schedulingError || {} as any); + this.failureInfo = new FailureInfoDecorator(task.failureInfo || {} as any); } // todo: base class ... diff --git a/app/models/decorators/task-execution-info-decorator.ts b/app/models/decorators/task-execution-info-decorator.ts index 63c6bed148..b709070434 100644 --- a/app/models/decorators/task-execution-info-decorator.ts +++ b/app/models/decorators/task-execution-info-decorator.ts @@ -1,12 +1,12 @@ import { TaskExecutionInformation } from "app/models"; import { DecoratorBase } from "app/utils/decorators"; -import { SchedulingErrorDecorator } from "./scheduling-error-decorator"; +import { FailureInfoDecorator } from "./failure-info-decorator"; export class TaskExecutionInfoDecorator extends DecoratorBase { public startTime: string; public endTime: string; public exitCode: string; - public schedulingError: SchedulingErrorDecorator; + public failureInfo: FailureInfoDecorator; public retryCount: string; public lastRetryTime: string; public requeueCount: string; @@ -18,7 +18,7 @@ export class TaskExecutionInfoDecorator extends DecoratorBase { +export class FailureInfo extends Record { @Prop() public code: string; @Prop() public category: string; @Prop() public message: string; diff --git a/app/models/index.ts b/app/models/index.ts index 71d02732da..9e1d729b82 100644 --- a/app/models/index.ts +++ b/app/models/index.ts @@ -12,6 +12,7 @@ export * from "./cloud-service-configuration"; export * from "./compute-node-information"; export * from "./constraints"; export * from "./image-reference"; +export * from "./failure-info"; export * from "./file"; export * from "./file-type"; export * from "./file-properties"; @@ -38,7 +39,6 @@ export * from "./pool"; export * from "./resize-error"; export * from "./resource-descriptor"; export * from "./resource-file"; -export * from "./scheduling-error"; export * from "./settings"; export * from "./spec-cost"; export * from "./ssh-public-key"; diff --git a/app/models/job-execution-information.ts b/app/models/job-execution-information.ts index b99f2cba16..44e7752ea0 100644 --- a/app/models/job-execution-information.ts +++ b/app/models/job-execution-information.ts @@ -1,5 +1,5 @@ import { Model, Prop, Record } from "app/core"; -import { SchedulingError, SchedulingErrorAttributes } from "./scheduling-error"; +import { FailureInfo, FailureInfoAttributes } from "./failure-info"; /** * Job terminate reason. @@ -24,7 +24,7 @@ export interface JobExecutionInformationAttributes { startTime: Date; endTime: Date; poolId: string; - schedulingError: Partial; + failureInfo: FailureInfoAttributes; terminateReason: JobTerminateReason; } @@ -36,6 +36,6 @@ export class JobExecutionInformation extends Record { + @Prop() public id: string; + @Prop() public startTime: Date; + @Prop() public endTime: Date; + @Prop() public exitCode: number; + @Prop() public state: TaskState; + @Prop() public stateTransitionTime: Date; + @Prop() public previousState: TaskState; + @Prop() public previousStateTransitionTime: Date; - public nodeInfo: ComputeNodeInformation; - public schedulingError: SchedulingError; + @Prop() public nodeInfo: ComputeNodeInformation; + @Prop() public failureInfo: FailureInfo; } diff --git a/app/models/task-execution-information.ts b/app/models/task-execution-information.ts index a544eb5178..c0d9eecfa8 100644 --- a/app/models/task-execution-information.ts +++ b/app/models/task-execution-information.ts @@ -1,15 +1,30 @@ -import { SchedulingError } from "./scheduling-error"; +import { Model, Prop, Record } from "app/core"; +import { FailureInfo, FailureInfoAttributes } from "./failure-info"; + +export interface TaskExecutionInformationAttributes { + startTime: Date; + endTime?: Date; + state: string; + taskRootDirectory?: string; + taskRootDirectoryUrl?: string; + exitCode?: number; + failureInfo?: FailureInfoAttributes; + retryCount: number; + lastRetryTime?: Date; + result?: string; +} /** * Contains information about the execution of a task in the Azure */ -export class TaskExecutionInformation { - public startTime: Date; - public endTime: Date; - public exitCode: number; - public schedulingError: SchedulingError; - public retryCount: number; - public lastRetryTime: Date; - public requeueCount: number; - public lastRequeueTime: Date; +@Model() +export class TaskExecutionInformation extends Record { + @Prop() public startTime: Date; + @Prop() public endTime: Date; + @Prop() public exitCode: number; + @Prop() public failureInfo: FailureInfo; + @Prop() public retryCount: number; + @Prop() public lastRetryTime: Date; + @Prop() public requeueCount: number; + @Prop() public lastRequeueTime: Date; } diff --git a/app/models/task-exit-conditions.ts b/app/models/task-exit-conditions.ts index ceb0e719fa..c7390b5751 100644 --- a/app/models/task-exit-conditions.ts +++ b/app/models/task-exit-conditions.ts @@ -7,21 +7,21 @@ import { JobAction } from "./job-action"; const TaskExitConditionsRecord = Record({ exitCodes: [], exitCodeRanges: [], - schedulingError: null, + failureInfo: null, default: null, }); export class TaskExitConditions extends TaskExitConditionsRecord { public exitCodes: List; public exitCodeRanges: List; - public schedulingError: ExitOptions; + public failureInfo: ExitOptions; public default: ExitOptions; constructor(data: any = {}) { super(Object.assign({}, data, ObjectUtils.compact({ exitCodes: data.exitCodes && List(data.exitCodes.map(x => new ExitCodeMapping(x))), exitCodeRanges: data.exitCodeRanges && List(data.exitCodeRanges.map(x => new ExitCodeRangeMapping(x))), - schedulingError: new ExitOptions(data.schedulingError), + failureInfo: new ExitOptions(data.failureInfo), default: new ExitOptions(data.default), }))); } diff --git a/app/models/task.ts b/app/models/task.ts index 38ad82d251..e9c60c914d 100644 --- a/app/models/task.ts +++ b/app/models/task.ts @@ -82,7 +82,7 @@ export class Task extends TaskRecord { public get didTimeout() { const info = this.executionInfo; const constraints = this.constraints; - if (!(info && info.exitCode && constraints && constraints.maxWallClockTime)) { + if (!(info && info.failureInfo && info.exitCode && constraints && constraints.maxWallClockTime)) { return false; } if (info.exitCode === 0) { diff --git a/test/app/components/job/details/error-display/job-error-display.component.spec.ts b/test/app/components/job/details/error-display/job-error-display.component.spec.ts index 6aa1aed275..ee6bd0fcae 100644 --- a/test/app/components/job/details/error-display/job-error-display.component.spec.ts +++ b/test/app/components/job/details/error-display/job-error-display.component.spec.ts @@ -119,7 +119,7 @@ describe("JobErrorDisplayComponent", () => { maxWallClockTime: moment.duration("PT2M"), }, executionInfo: { - schedulingError: { + failureInfo: { code: "InvalidAutoPoolSettings", category: "UserError", message: "Auto pool has invalid settings", diff --git a/test/app/components/task/details/task-error-display.component.spec.ts b/test/app/components/task/details/task-error-display.component.spec.ts index 9bcde92ac2..10698b9d9c 100644 --- a/test/app/components/task/details/task-error-display.component.spec.ts +++ b/test/app/components/task/details/task-error-display.component.spec.ts @@ -54,6 +54,11 @@ describe("TaskErrorDisplayComponent", () => { testComponent.task = new Task({ state: TaskState.completed, executionInfo: { + failureInfo: { + category: "UserError", + code: "FailureExitCode", + message: "Task has wrong exit code", + }, exitCode: 1, }, }); @@ -69,11 +74,21 @@ describe("TaskErrorDisplayComponent", () => { expect(fixture.debugElement.queryAll(By.css("bl-banner")).length).toBe(1); }); - it("Should show the code and message", () => { + it("Should contain the error code", () => { + const banner = fixture.debugElement.query(By.css("bl-banner")); + expect(banner.nativeElement.textContent).toContain("FailureExitCode"); + }); + + it("Should custom message", () => { const banner = fixture.debugElement.query(By.css("bl-banner")); expect(banner.nativeElement.textContent).toContain("Task completed with exit code '1'"); }); + it("Should not show the failure info message", () => { + const banner = fixture.debugElement.query(By.css("bl-banner")); + expect(banner.nativeElement.textContent).not.toContain("Task has wrong exit code"); + }); + it("should propose increase quota as a first fix", () => { const banner = fixture.debugElement.query(By.css("bl-banner")).componentInstance; expect(banner.fixMessage).toContain("Rerun task"); diff --git a/test/app/components/task/details/task-lifetime/task-timeline.component.spec.ts b/test/app/components/task/details/task-lifetime/task-timeline.component.spec.ts index 3e45d114e3..0f0ea70262 100644 --- a/test/app/components/task/details/task-lifetime/task-timeline.component.spec.ts +++ b/test/app/components/task/details/task-lifetime/task-timeline.component.spec.ts @@ -28,6 +28,10 @@ function createTask(state: string, timeout = "PT6M") { endTime: moment().subtract(20, "minutes").toDate(), retryCount: 3, exitCode: -3, + failureInfo: { + category: "UserError", + code: "TaskEnded", + }, }, constraints: { maxWallClockTime: moment.duration(timeout), diff --git a/test/app/models/task.spec.ts b/test/app/models/task.spec.ts index 5ed31d8d60..493dc5ac45 100644 --- a/test/app/models/task.spec.ts +++ b/test/app/models/task.spec.ts @@ -2,15 +2,24 @@ import * as moment from "moment"; import { Task } from "app/models"; -function createTask(exitCode: number, startTime: Date, endTime: Date, maxWallClockTime = "PT4M") { +function createTask(exitCode: number, startTime: Date, endTime: Date, hasFailureInfo = false) { + let failureInfo = null; + + if (hasFailureInfo) { + failureInfo = { + category: "UserError", + code: "TaskEnded", + }; + } return new Task({ executionInfo: { startTime, endTime, exitCode, + failureInfo, }, constraints: { - maxWallClockTime: moment.duration(maxWallClockTime), + maxWallClockTime: moment.duration("PT4M"), }, }); } @@ -34,13 +43,13 @@ describe("Task Model", () => { it("should return false if runnnig time is less than clock time", () => { const current = moment(); - const task = createTask(0, current.clone().subtract(3, "minute").toDate(), current.toDate()); + const task = createTask(0, current.clone().subtract(3, "minute").toDate(), current.toDate(), true); expect(task.didTimeout).toBe(false); }); it("should return true if runnnig time is more than clock time", () => { const current = moment(); - const task = createTask(-10, current.clone().subtract(5, "minute").toDate(), current.toDate()); + const task = createTask(-10, current.clone().subtract(5, "minute").toDate(), current.toDate(), true); expect(task.didTimeout).toBe(true); }); }); diff --git a/test/fixture.ts b/test/fixture.ts index b810174b40..c2532b81e6 100644 --- a/test/fixture.ts +++ b/test/fixture.ts @@ -59,7 +59,7 @@ export const job = new FixtureFactory(Job, { endTime: new Date(2015, 5, 1, 10, 4, 31), poolId: "pool-1", terminateReason: "because i said so", - schedulingError: { + failureInfo: { category: "cat1", code: "code1", message: "this is a message", @@ -98,7 +98,7 @@ export const task = new FixtureFactory(Task, { lastRetryTime: new Date(2015, 5, 21, 0, 0, 0), requeueCount: 1, lastRequeueTime: new Date(2015, 5, 21, 0, 0, 0), - schedulingError: { + failureInfo: { category: "cat1", code: "code1", message: "this is a message", From 726f4ff1eaf812c5a8e99e4c9843deae28125c71 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 7 Jun 2017 09:42:24 -0700 Subject: [PATCH 13/37] Fix: List classic storage accounts too (#438) * Also list classic storage accounts * Classic storage account --- .../storage-account-picker.html | 8 ++++++-- app/core/record/arm-record.ts | 5 +++++ app/models/storage-account.ts | 16 +++++++++++++--- app/services/storage-account.service.ts | 4 +++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/components/account/base/storage-account-picker/storage-account-picker.html b/app/components/account/base/storage-account-picker/storage-account-picker.html index 588602c4a7..a5238c2393 100644 --- a/app/components/account/base/storage-account-picker/storage-account-picker.html +++ b/app/components/account/base/storage-account-picker/storage-account-picker.html @@ -6,16 +6,17 @@

Storage accounts in the same region as your batch account({{account.location Name Region + No storage account - + - + {{account.isClassic ? "Classic" : ""}} diff --git a/app/core/record/arm-record.ts b/app/core/record/arm-record.ts index ddf8fa3f30..7a54a32bb3 100644 --- a/app/core/record/arm-record.ts +++ b/app/core/record/arm-record.ts @@ -1,13 +1,18 @@ +import { Model, Prop } from "./decorators"; import { Record } from "./record"; export interface ArmRecordAttributes { id: string; + type: string; } /** * ArmRecord is a subclass of record that unify the id for ARM record which have problems with the case. */ +@Model() export class ArmRecord extends Record { + @Prop() public type: string; + constructor(data: T) { super(Object.assign({}, data, { id: data.id && data.id.toLowerCase() })); } diff --git a/app/models/storage-account.ts b/app/models/storage-account.ts index 7d044ba729..4594430286 100644 --- a/app/models/storage-account.ts +++ b/app/models/storage-account.ts @@ -1,4 +1,4 @@ -import { ArmRecord, Model, Prop, Record } from "app/core"; +import { ArmRecord, ArmRecordAttributes, Model, Prop, Record } from "app/core"; interface StorageAccountPropertiesAttributes { creationTime: Date; @@ -27,14 +27,16 @@ class StorageAccountProperties extends Record; + type: StorageAccountType; } +export type StorageAccountType = "Microsoft.Storage/storageAccounts" | "Microsoft.ClassicStorage/storageAccounts"; + @Model() export class StorageAccount extends ArmRecord { @Prop() public id: string; @@ -46,4 +48,12 @@ export class StorageAccount extends ArmRecord { @Prop() public kind: string; @Prop() public properties: StorageAccountProperties; + + public type: StorageAccountType; + public isClassic: boolean; + + constructor(data: StorageAccountAttributes) { + super(data); + this.isClassic = this.type === "Microsoft.ClassicStorage/storageAccounts"; + } } diff --git a/app/services/storage-account.service.ts b/app/services/storage-account.service.ts index c8e22094e7..6d2fac0c4e 100644 --- a/app/services/storage-account.service.ts +++ b/app/services/storage-account.service.ts @@ -36,7 +36,9 @@ export class StorageAccountService { public list(subscriptionId: string): Observable> { const search = new URLSearchParams(); - search.set("$filter", "resourceType eq 'Microsoft.Storage/storageAccounts'"); + search.set("$filter", + "resourceType eq 'Microsoft.Storage/storageAccounts'" + + "or resourceType eq 'Microsoft.ClassicStorage/storageAccounts'"); const options = new RequestOptions({ search }); return this.subscriptionService.get(subscriptionId) From cd76696ef00b17ef3f0aac6931baba7f83558a29 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 7 Jun 2017 13:56:19 -0700 Subject: [PATCH 14/37] Feature: Entities are refreshing every 10 seconds automatically (#436) * Polling can be started automatically * Job and node poll * Task data * Remove unused import * Fix specs * REmove console.log --- .../browse/application-list.component.ts | 2 +- .../job/action/delete/delete-job-action.ts | 18 ++---- .../job/browse/job-list.component.ts | 2 +- .../job/details/job-details.component.ts | 1 + .../job-progress-status.component.ts | 1 + .../node/details/node-details.component.ts | 2 + .../pool/action/delete/delete-pool-task.ts | 32 ++++------ .../pool/browse/pool-list.component.ts | 2 +- .../pool/details/pool-details.component.ts | 1 + .../task/action/delete/delete-task-action.ts | 19 ++---- .../task/browse/task-list.component.ts | 2 +- .../task/details/task-details.component.ts | 2 + app/services/core/rx-entity-proxy.ts | 64 ++++++++++++++++--- app/services/core/rx-list-proxy.ts | 9 +-- app/services/core/rx-proxy-base.ts | 15 ++++- app/services/job-service.ts | 3 +- app/services/node-service.ts | 3 +- app/services/pool-service.ts | 3 +- app/services/task-service.ts | 3 +- app/utils/constants.ts | 4 ++ .../services/core/rx-batch-list-proxy.spec.ts | 6 +- 21 files changed, 123 insertions(+), 71 deletions(-) diff --git a/app/components/application/browse/application-list.component.ts b/app/components/application/browse/application-list.component.ts index 40d4abd8ec..9680e58077 100644 --- a/app/components/application/browse/application-list.component.ts +++ b/app/components/application/browse/application-list.component.ts @@ -53,7 +53,7 @@ export class ApplicationListComponent extends ListOrTableBase implements OnInit, this.status = this.data.status; this._subs.push(applicationService.onApplicationAdded.subscribe((applicationId) => { - this.data.loadNewItem(applicationService.get(applicationId)); + this.data.loadNewItem(applicationService.getOnce(applicationId)); })); } diff --git a/app/components/job/action/delete/delete-job-action.ts b/app/components/job/action/delete/delete-job-action.ts index eaa99f322f..7255d2d326 100644 --- a/app/components/job/action/delete/delete-job-action.ts +++ b/app/components/job/action/delete/delete-job-action.ts @@ -47,23 +47,17 @@ export class WaitForDeleteJobPoller { @autobind() public start(progress: BehaviorSubject): Observable { const obs = new AsyncSubject(); - const data = this.jobService.get(this.jobId); - const errorCallback = (e) => { - progress.next(100); - clearInterval(interval); - obs.complete(); - }; - let interval = setInterval(() => { - data.fetch().subscribe({ - error: errorCallback, + this.jobService.getOnce(this.jobId).subscribe({ + error: (e) => { + progress.next(100); + clearInterval(interval); + obs.complete(); + }, }); }, 5000); progress.next(-1); - data.item.subscribe({ - error: errorCallback, - }); return obs; } diff --git a/app/components/job/browse/job-list.component.ts b/app/components/job/browse/job-list.component.ts index d5eedc1e9c..7f67b04aa6 100644 --- a/app/components/job/browse/job-list.component.ts +++ b/app/components/job/browse/job-list.component.ts @@ -73,7 +73,7 @@ export class JobListComponent extends ListOrTableBase implements OnInit, OnDestr this.data = this.jobService.list(this._baseOptions); this.status = this.data.status; this._onJobAddedSub = jobService.onJobAdded.subscribe((jobId) => { - this.data.loadNewItem(jobService.get(jobId)); + this.data.loadNewItem(jobService.getOnce(jobId)); }); } diff --git a/app/components/job/details/job-details.component.ts b/app/components/job/details/job-details.component.ts index 0b10dda18f..ae8f933fd3 100644 --- a/app/components/job/details/job-details.component.ts +++ b/app/components/job/details/job-details.component.ts @@ -73,6 +73,7 @@ export class JobDetailsComponent implements OnInit, OnDestroy { public ngOnDestroy() { this._paramsSubscriber.unsubscribe(); + this.data.dispose(); } public get filterPlaceholderText() { diff --git a/app/components/job/details/job-progress-status/job-progress-status.component.ts b/app/components/job/details/job-progress-status/job-progress-status.component.ts index a460d0ba6a..12fd3db344 100644 --- a/app/components/job/details/job-progress-status/job-progress-status.component.ts +++ b/app/components/job/details/job-progress-status/job-progress-status.component.ts @@ -73,6 +73,7 @@ export class JobProgressStatusComponent implements OnChanges, OnDestroy { public ngOnDestroy() { this._polls.forEach(x => x.destroy()); this._subs.forEach(x => x.unsubscribe()); + this.poolData.dispose(); } public countRunningTasks() { diff --git a/app/components/node/details/node-details.component.ts b/app/components/node/details/node-details.component.ts index 0548be0cd2..7e0516e9a6 100644 --- a/app/components/node/details/node-details.component.ts +++ b/app/components/node/details/node-details.component.ts @@ -77,6 +77,8 @@ export class NodeDetailsComponent implements OnInit, OnDestroy { public ngOnDestroy() { this._paramsSubscribers.forEach(x => x.unsubscribe()); + this.poolData.dispose(); + this.data.dispose(); } @autobind() diff --git a/app/components/pool/action/delete/delete-pool-task.ts b/app/components/pool/action/delete/delete-pool-task.ts index 3bd4b91e73..36076b8c45 100644 --- a/app/components/pool/action/delete/delete-pool-task.ts +++ b/app/components/pool/action/delete/delete-pool-task.ts @@ -7,7 +7,7 @@ import { PoolService } from "app/services"; import { LongRunningDeleteAction } from "app/services/core"; export class DeletePoolTask extends LongRunningDeleteAction { - constructor(private poolService: PoolService, poolIds: string[]) { + constructor(private poolService: PoolService, poolIds: string[]) { super("pool", poolIds); } @@ -53,31 +53,25 @@ export class WaitForDeletePoolPollTask { @autobind() public start(progress: BehaviorSubject): Observable { const obs = new AsyncSubject(); - const data = this.poolService.get(this.poolId); let interval; - const errorCallback = (e) => { - progress.next(100); - clearInterval(interval); - obs.complete(); - }; - interval = setInterval(() => { - data.fetch().subscribe({ - error: errorCallback, + this.poolService.getOnce(this.poolId).subscribe({ + next: (pool: Pool) => { + if (pool) { + const currentNodes = pool.currentNodes; + progress.next(this._getProgress(currentNodes)); + } + }, + error: (e) => { + progress.next(100); + clearInterval(interval); + obs.complete(); + }, }); }, this.refreshRate); progress.next(10); - data.item.subscribe({ - next: (pool: Pool) => { - if (pool) { - const currentNodes = pool.currentNodes; - progress.next(this._getProgress(currentNodes)); - } - }, - error: errorCallback, - }); return obs; } diff --git a/app/components/pool/browse/pool-list.component.ts b/app/components/pool/browse/pool-list.component.ts index a80a0e7031..27c82d86ee 100644 --- a/app/components/pool/browse/pool-list.component.ts +++ b/app/components/pool/browse/pool-list.component.ts @@ -75,7 +75,7 @@ export class PoolListComponent extends ListOrTableBase implements OnInit, OnDest this.data = this.poolService.list(); this.status = this.data.status; this._subs.push(poolService.onPoolAdded.subscribe((poolId) => { - this.data.loadNewItem(poolService.get(poolId)); + this.data.loadNewItem(poolService.getOnce(poolId)); })); this._subs.push(this.data.items.subscribe((pools) => { this.pools = List(pools.map(x => new PoolDecorator(x))); diff --git a/app/components/pool/details/pool-details.component.ts b/app/components/pool/details/pool-details.component.ts index 0f2383ec4e..c0c087141d 100644 --- a/app/components/pool/details/pool-details.component.ts +++ b/app/components/pool/details/pool-details.component.ts @@ -69,6 +69,7 @@ export class PoolDetailsComponent implements OnInit, OnDestroy { public ngOnDestroy() { this._paramsSubscriber.unsubscribe(); + this.data.dispose(); } public get filterPlaceholderText() { diff --git a/app/components/task/action/delete/delete-task-action.ts b/app/components/task/action/delete/delete-task-action.ts index a787c27748..5500bb210d 100644 --- a/app/components/task/action/delete/delete-task-action.ts +++ b/app/components/task/action/delete/delete-task-action.ts @@ -48,24 +48,17 @@ export class WaitForDeleteTaskPoller { @autobind() public start(progress: BehaviorSubject): Observable { const obs = new AsyncSubject(); - const data = this.taskService.get(this.jobId, this.taskId); - const errorCallback = (e) => { - progress.next(100); - clearInterval(interval); - obs.complete(); - }; - let interval = setInterval(() => { - data.fetch().subscribe({ - error: errorCallback, + this.taskService.getOnce(this.jobId, this.taskId).subscribe({ + error: (e) => { + progress.next(100); + clearInterval(interval); + obs.complete(); + }, }); }, 5000); progress.next(-1); - data.item.subscribe({ - error: errorCallback, - }); - return obs; } } diff --git a/app/components/task/browse/task-list.component.ts b/app/components/task/browse/task-list.component.ts index 45cbcca3b5..2d9f878f85 100644 --- a/app/components/task/browse/task-list.component.ts +++ b/app/components/task/browse/task-list.component.ts @@ -68,7 +68,7 @@ export class TaskListComponent extends SelectableList implements OnInit { super(); this._onTaskAddedSub = taskService.onTaskAdded.subscribe((item: TaskParams) => { - this.data.loadNewItem(taskService.get(item.jobId, item.id)); + this.data.loadNewItem(taskService.getOnce(item.jobId, item.id)); }); } diff --git a/app/components/task/details/task-details.component.ts b/app/components/task/details/task-details.component.ts index 9ede2f01e3..bec8fa1d50 100644 --- a/app/components/task/details/task-details.component.ts +++ b/app/components/task/details/task-details.component.ts @@ -82,6 +82,8 @@ export class TaskDetailsComponent implements OnInit, OnDestroy { public ngOnDestroy() { this._paramsSubscribers.forEach(x => x.unsubscribe()); + this.jobData.dispose(); + this.data.dispose(); } @autobind() diff --git a/app/services/core/rx-entity-proxy.ts b/app/services/core/rx-entity-proxy.ts index a408b76ed8..fa069c2cc9 100644 --- a/app/services/core/rx-entity-proxy.ts +++ b/app/services/core/rx-entity-proxy.ts @@ -3,10 +3,16 @@ import { AsyncSubject, BehaviorSubject, Observable } from "rxjs"; import { LoadingStatus } from "app/components/base/loading"; import { ServerError } from "app/models"; +import { PollObservable } from "app/services/core"; import { HttpCode } from "app/utils/constants"; import { RxProxyBase, RxProxyBaseConfig } from "./rx-proxy-base"; export interface RxEntityProxyConfig extends RxProxyBaseConfig { + /** + * If you want to have the entity proxy poll automatically for you every given milliseconds. + * @default Disabled + */ + poll?: number; } export abstract class RxEntityProxy extends RxProxyBase { @@ -17,6 +23,7 @@ export abstract class RxEntityProxy extends RxProxyBase; private _itemKey = new BehaviorSubject(null); + private _pollTracker: PollObservable; /** * @param _type Class for TEntity used to instantiate @@ -27,11 +34,15 @@ export abstract class RxEntityProxy extends RxProxyBase, config: RxEntityProxyConfig) { super(type, config); this.params = config.initialParams || {} as TParams; - this.item = this._itemKey.map((key) => { + this.item = this._itemKey.distinctUntilChanged().map((key) => { return this.cache.items.map((items) => { return items.get(key); }); - }).switch(); + }).switch().distinctUntilChanged().takeUntil(this.isDisposed); + + if (config.poll) { + this._pollTracker = this.startPoll(5000); + } } /** @@ -59,10 +70,31 @@ export abstract class RxEntityProxy extends RxProxyBase { return this.fetch(); } + /** + * Stop the automatically started poll if applicable + */ + public stopPoll() { + if (this._pollTracker) { + this._pollTracker.destroy(); + } + } + + /** + * Abstract method implementation of what to do when the polling calls. + */ protected pollRefresh() { return this.refresh(); } @@ -83,22 +115,38 @@ export abstract class RxEntityProxy extends RxProxyBase(getProxy: RxEntityProxy): Observable { const obs = new AsyncSubject(); + getProxy.stopPoll(); getProxy.fetch().subscribe({ next: () => { - getProxy.item.first().subscribe((item: TEntity) => { - if (item) { - obs.next(item); - obs.complete(); - getProxy.dispose(); - } + getProxy.item.subscribe((item: TEntity) => { + obs.next(item); + obs.complete(); + getProxy.dispose(); }); }, error: (e) => { obs.error(e); obs.complete(); + getProxy.dispose(); }, }); return obs.asObservable(); } + +// const sub = new BehaviorSubject(0); +// const until = new AsyncSubject(); +// let i = 0; +// setInterval(() => { +// sub.next(i++); +// if (i === 4) { +// until.next(true); +// until.complete(); +// } +// }, 2000); + +// const obs = sub.takeUntil(until); +// obs.subscribe((x) => { +// console.log("New value", x); +// }); diff --git a/app/services/core/rx-list-proxy.ts b/app/services/core/rx-list-proxy.ts index 572ef16df4..dc603228d0 100644 --- a/app/services/core/rx-list-proxy.ts +++ b/app/services/core/rx-list-proxy.ts @@ -5,7 +5,6 @@ import { AsyncSubject, BehaviorSubject, Observable } from "rxjs"; import { LoadingStatus } from "app/components/base/loading"; import { log } from "app/utils"; import { ListOptions, ListOptionsAttributes } from "./list-options"; -import { RxEntityProxy } from "./rx-entity-proxy"; import { RxProxyBase, RxProxyBaseConfig } from "./rx-proxy-base"; export interface RxListProxyConfig extends RxProxyBaseConfig { @@ -129,9 +128,8 @@ export abstract class RxListProxy extends RxProxyBase): Observable { - const obs = entityProxy.fetch().flatMap(() => entityProxy.item.first()); - obs.subscribe({ + public loadNewItem(getOnceObs: Observable): Observable { + getOnceObs.subscribe({ next: (newItem) => { this._addItemToList(newItem); }, error: (error) => { @@ -139,8 +137,7 @@ export abstract class RxListProxy extends RxProxyBase; + /** + * Sets to after calling dispose() + */ + public isDisposed: AsyncSubject; + protected _status = new BehaviorSubject(LoadingStatus.Loading); protected _newDataStatus = new BehaviorSubject(LoadingStatus.Loading); protected _error = new BehaviorSubject(null); @@ -86,6 +91,7 @@ export abstract class RxProxyBase { if (status === LoadingStatus.Loading) { @@ -163,7 +169,12 @@ export abstract class RxProxyBase) { @@ -265,7 +276,7 @@ export abstract class RxProxyBase this._cache, getFn: (client, params: PoolParams) => client.pool.get(params.id, options), initialParams: { id: poolId }, + poll: Constants.PollRate.entity, }); } diff --git a/app/services/task-service.ts b/app/services/task-service.ts index 6ed723e84e..ce212bf86f 100644 --- a/app/services/task-service.ts +++ b/app/services/task-service.ts @@ -4,7 +4,7 @@ import { Observable, Subject } from "rxjs"; import { SubtaskInformation, Task } from "app/models"; import { TaskCreateDto } from "app/models/dtos"; -import { log } from "app/utils"; +import { Constants, log } from "app/utils"; import { FilterBuilder } from "app/utils/filter-builder"; import { BatchClientService } from "./batch-client.service"; import { @@ -96,6 +96,7 @@ export class TaskService extends ServiceBase { return client.task.get(params.jobId, params.id, options); }, initialParams: { id: taskId, jobId: initialJobId }, + poll: Constants.PollRate.entity, }); } diff --git a/app/utils/constants.ts b/app/utils/constants.ts index c37bb667dc..528954c464 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -143,3 +143,7 @@ export const APIErrorCodes = { export const MetadataInternalKey = { tags: "_bl_tags", }; + +export const PollRate = { + entity: 10000, +}; diff --git a/test/app/services/core/rx-batch-list-proxy.spec.ts b/test/app/services/core/rx-batch-list-proxy.spec.ts index 90eae3eb01..3cc4135124 100644 --- a/test/app/services/core/rx-batch-list-proxy.spec.ts +++ b/test/app/services/core/rx-batch-list-proxy.spec.ts @@ -6,7 +6,7 @@ import { List, OrderedSet } from "immutable"; import { LoadingStatus } from "app/components/base/loading"; import { BatchError, ServerError } from "app/models"; -import { DataCache, RxBatchEntityProxy, RxBatchListProxy } from "app/services/core"; +import { DataCache, RxBatchEntityProxy, RxBatchListProxy, getOnceProxy } from "app/services/core"; import { BatchClientServiceMock } from "test/utils/mocks"; import { FakeModel } from "./fake-model"; @@ -209,7 +209,7 @@ describe("RxBatchListProxy", () => { { id: "3", state: "running", name: "Fake3" }, ]; - proxy.loadNewItem(entityProxy as any).subscribe(() => { + proxy.loadNewItem(getOnceProxy(entityProxy)).subscribe(() => { expect(items).toEqualImmutable(List(expected.map((x) => new FakeModel(x)))); done(); }); @@ -225,7 +225,7 @@ describe("RxBatchListProxy", () => { { id: "4", state: "running", name: "Fake4" }, ].concat(data[0]); - proxy.loadNewItem(entityProxy as any).subscribe(() => { + proxy.loadNewItem(getOnceProxy(entityProxy)).subscribe(() => { expect(items).toEqualImmutable(List(expected.map((x) => new FakeModel(x)))); done(); }); From a3203cb60defacdd9f8f57388ab0c65174d8ba66 Mon Sep 17 00:00:00 2001 From: ascobie Date: Thu, 8 Jun 2017 13:03:41 +1200 Subject: [PATCH 15/37] Support quick adding of other output files (#423) * ability to add new output file to be read from node * load task files * get task files from api, reset tabs to default * start tests * tests * address feedback * better centering of icons in context-button-bar buttons --- .../file/details/file-content.component.ts | 4 +- app/components/file/details/file-content.html | 8 +- .../task/details/output/task-log.component.ts | 180 ++++++++++++++-- .../task/details/output/task-log.html | 26 ++- app/styles/base/buttons.scss | 4 + app/styles/base/layout.scss | 6 +- app/styles/task/output.scss | 13 +- app/styles/vendor/material-theme.scss | 3 + .../details/output/task-log.component.spec.ts | 198 ++++++++++++++++++ test/fixture.ts | 14 +- 10 files changed, 421 insertions(+), 35 deletions(-) create mode 100644 test/app/components/task/details/output/task-log.component.spec.ts diff --git a/app/components/file/details/file-content.component.ts b/app/components/file/details/file-content.component.ts index a22e5c30fc..81bc936e53 100644 --- a/app/components/file/details/file-content.component.ts +++ b/app/components/file/details/file-content.component.ts @@ -37,6 +37,7 @@ export class FileContentComponent implements OnChanges, OnDestroy, AfterViewInit public currentSubscription: Subscription; public nodeNotFound = false; + public fileCleanupOperation = false; public fileContentFailure = false; private _refreshInterval; @@ -66,6 +67,7 @@ export class FileContentComponent implements OnChanges, OnDestroy, AfterViewInit this.loading = true; this.lastContentLength = 0; this.nodeNotFound = false; + this.fileCleanupOperation = false; this.fileContentFailure = false; this._updateFileContent(); @@ -216,7 +218,7 @@ export class FileContentComponent implements OnChanges, OnDestroy, AfterViewInit this.nodeNotFound = true; return; } else if (e.status === Constants.HttpCode.Conflict) { - this.nodeNotFound = true; + this.fileCleanupOperation = true; return; } else if (!e.status && e.body.message.startsWith("An incorrect number of bytes")) { // gets an undefined error code for binary files. diff --git a/app/components/file/details/file-content.html b/app/components/file/details/file-content.html index 66ade54c75..09bef151c4 100644 --- a/app/components/file/details/file-content.html +++ b/app/components/file/details/file-content.html @@ -1,4 +1,4 @@ -
+
- The node this task ran on was not found. + The node this task ran on, or the file was not found. +
+ +
+ The files for the specified task have been cleaned from the node.
diff --git a/app/components/task/details/output/task-log.component.ts b/app/components/task/details/output/task-log.component.ts index e446935bdc..7f9e097ec8 100644 --- a/app/components/task/details/output/task-log.component.ts +++ b/app/components/task/details/output/task-log.component.ts @@ -1,58 +1,194 @@ -import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; -import { Subscription } from "rxjs"; +import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { BehaviorSubject, Observable, Subscription } from "rxjs"; -import { File, Task } from "app/models"; +import { File, ServerError, Task, TaskState } from "app/models"; import { FileService } from "app/services"; +import { PollObservable } from "app/services/core"; import { prettyBytes } from "app/utils"; -const outputFileNames = ["stdout.txt", "stderr.txt"]; +const defaultOutputFileNames = ["stdout.txt", "stderr.txt"]; @Component({ selector: "bl-task-log", templateUrl: "task-log.html", }) -export class TaskLogComponent implements OnChanges, OnDestroy { +export class TaskLogComponent implements OnInit, OnChanges, OnDestroy { @Input() public jobId: string; @Input() public task: Task; - public outputFileNames = outputFileNames; - public outputFilename: "stdout.txt" | "stderr.txt" = "stdout.txt"; + public outputFileNames = defaultOutputFileNames.slice(); + public selectedOutputFile: "stdout.txt" | "stderr.txt" = defaultOutputFileNames[0] as any; public fileSizes: { [filename: string]: string } = {}; + public filterControl = new FormControl(); + public filteredOptions: Observable; + public addingFile = false; - private _dataSubs: Subscription[] = []; + private _fileSizeSubs: Subscription[] = []; + private _taskFileSubscription: Subscription; + private _initialQueryOptions = { maxItems: 500 }; + private _options: BehaviorSubject; + private _currentTaskId: string = null; + private _poller: PollObservable; + private _refreshInterval: number = 5000; + private _fileMap = {}; constructor(private fileService: FileService) { + this._options = new BehaviorSubject([]); + this.filteredOptions = this._options; } + public ngOnInit() { + this.filteredOptions = this.filterControl.valueChanges + .map((nameFilter) => { + return nameFilter + ? this._options.value.filter(option => new RegExp(`${nameFilter}`, "gi").test(option)) + : this._options.value; + }); + } + public ngOnChanges(inputs) { if (inputs.jobId || inputs.task) { - this._updateFileData(); + /** + * ngOnChanges is fired multiple times for the same task selection + * so this should cut down on chatter. + */ + if (this.task && this._currentTaskId !== this.task.id) { + this._currentTaskId = this.task.id; + this.fileSizes = {}; + this.addingFile = false; + + this._loadTaskFilesData(); + this._updateFileData(); + } } } + /** + * Navigating away from the job and task so reset the tabs + */ public ngOnDestroy() { - this._clearSubscriptions(); + this.resetTabs(); + this._clearTaskFilesSubscription(); + this._clearFileSizeSubscriptions(); + if (this._poller) { + this._poller.destroy(); + } } - private _updateFileData() { - this._clearSubscriptions(); - outputFileNames.map((filename) => { - const data = this.fileService.getFilePropertiesFromTask(this.jobId, this.task.id, filename); - this._dataSubs.push(data.item.subscribe((file: File) => { - if (file) { - const props = file.properties; - this.fileSizes[filename] = prettyBytes(props && props.contentLength); + /** + * Enable/disable the filter control + */ + public toggleFilter() { + this.addingFile = !this.addingFile; + this.filterControl.setValue(null); + } + + /** + * Fires when an item is selected from the autocomplete option list + * @param event - the selection event + * @param item - selected item, add this to a tab if it doesn't already exist + */ + public optionSelected($event: any, item: string) { + if (item && !this.outputFileNames.find(existing => existing === item)) { + this.outputFileNames.push(item); + this.selectedOutputFile = item as any; + this.toggleFilter(); + this._updateFileData(); + } + } + + public resetTabs() { + this.addingFile = false; + this.outputFileNames = defaultOutputFileNames.slice(); + this.selectedOutputFile = defaultOutputFileNames[0] as any; + } + + /** + * Get the task files from the node so we can use them to populate + * the autocomplete control. + */ + private _loadTaskFilesData() { + this._clearTaskFilesSubscription(); + const taskFileData = this.fileService.listFromTask( + this.jobId, + this.task.id, + true, + this._initialQueryOptions, + (error: ServerError) => { + // todo: should i ignore all errors for this call? + return false; + }, + ); + + // poll for files if the job has not completed + if (this.task.state !== TaskState.completed) { + this._poller = taskFileData.startPoll(this._refreshInterval); + } + + this._taskFileSubscription = taskFileData.items.subscribe((items) => { + items.map((file: File) => { + if (this._canAddFileToMap(file)) { + this._fileMap[file.name] = {}; } - })); + }); + + this._options.next(Object.keys(this._fileMap)); + }); + + taskFileData.fetchNext(true); + } + + /** + * Get the sizes for the output file name collection + */ + private _updateFileData() { + this._clearFileSizeSubscriptions(); + this.outputFileNames.map((filename) => { + if (this._shouldGetFileSize(filename)) { + const data = this.fileService.getFilePropertiesFromTask(this.jobId, this.task.id, filename); + this._fileSizeSubs.push(data.item.subscribe((file: File) => { + if (file) { + const props = file.properties; + this.fileSizes[filename] = prettyBytes(props && props.contentLength); + } + })); - data.fetch(); + data.fetch(); + } }); } - private _clearSubscriptions() { - this._dataSubs.forEach(x => x.unsubscribe()); + /** + * Only get the size of this file if either the task hasn't completed, or + * we don't have the current file size for the file. + */ + private _shouldGetFileSize(filename: string) { + return this.task.state !== TaskState.completed || !this.fileSizes[filename]; + } + + /** + * Ignore directories and any file that is either already in the map, or + * is one of the defaults. + */ + private _canAddFileToMap(file: File) { + return !file.isDirectory && + !(file.name in this._fileMap) && + file.name !== defaultOutputFileNames[0] && + file.name !== defaultOutputFileNames[1]; + } + + private _clearFileSizeSubscriptions() { + this._fileSizeSubs.forEach(x => x.unsubscribe()); + } + + private _clearTaskFilesSubscription() { + this._fileMap = {}; + if (this._taskFileSubscription) { + this._taskFileSubscription.unsubscribe(); + } } } diff --git a/app/components/task/details/output/task-log.html b/app/components/task/details/output/task-log.html index 63dc54ab95..747ca18dfe 100644 --- a/app/components/task/details/output/task-log.html +++ b/app/components/task/details/output/task-log.html @@ -1,13 +1,25 @@
-
+
{{filename}} ({{fileSizes[filename] || " - B"}})
- - - +
+ + + +
+
+ +
+
+ +
-
+
+ + + + {{ option }} + + diff --git a/app/styles/base/buttons.scss b/app/styles/base/buttons.scss index 518deaad7a..3bc51aae9e 100644 --- a/app/styles/base/buttons.scss +++ b/app/styles/base/buttons.scss @@ -37,6 +37,10 @@ bl-loading-button { } } +.mat-mini-fab[disabled] { + background-color: rgba(225, 225, 225, 1) !important; +} + @keyframes spin-left-then-right { 0% { transform: rotate(0); diff --git a/app/styles/base/layout.scss b/app/styles/base/layout.scss index 6557b15d31..e30e5c97b5 100644 --- a/app/styles/base/layout.scss +++ b/app/styles/base/layout.scss @@ -219,9 +219,13 @@ footer { .context-button-bar { position: relative !important; - margin: -34px 10px 0 !important; + margin: -35px 10px 0 !important; float: right; + bl-action-btn { + line-height: 31px; + } + button { margin-left: 5px; } diff --git a/app/styles/task/output.scss b/app/styles/task/output.scss index 98c7811e09..4f037e8ec6 100644 --- a/app/styles/task/output.scss +++ b/app/styles/task/output.scss @@ -24,10 +24,21 @@ bl-task-log, bl-task-outputs { &:hover { background: $dove-grey; } + + .mat-input-wrapper { + margin: 0; + + .mat-input-underline { + .mat-input-ripple { + height: 0; + } + } + } } > .file-tab-more { - font-size: 20px; + font-size: 16px; + padding-top: 6px; } } } diff --git a/app/styles/vendor/material-theme.scss b/app/styles/vendor/material-theme.scss index e4cbc47b22..a16d619f02 100644 --- a/app/styles/vendor/material-theme.scss +++ b/app/styles/vendor/material-theme.scss @@ -172,3 +172,6 @@ md-input-container.bl-textarea { margin-top: 6px; } +.mat-autocomplete-panel { + width : 30vw; +} diff --git a/test/app/components/task/details/output/task-log.component.spec.ts b/test/app/components/task/details/output/task-log.component.spec.ts new file mode 100644 index 0000000000..ea8013cb1c --- /dev/null +++ b/test/app/components/task/details/output/task-log.component.spec.ts @@ -0,0 +1,198 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MdAutocomplete } from "@angular/material"; +import { By } from "@angular/platform-browser"; +import { RouterTestingModule } from "@angular/router/testing"; + +import { TaskLogComponent } from "app/components/task/details/output"; +import { File } from "app/models"; +import { FileService } from "app/services"; +import * as Fixtures from "test/fixture"; +import { RxMockEntityProxy, RxMockListProxy } from "test/utils/mocks"; + +const fileSizeMap: Map = new Map() + .set("stdout.txt", 10) + .set("stderr.txt", 20) + .set("banana.txt", 30) + .set("apple.txt", 40) + .set("pear.txt", 50); + +describe("TaskDependenciesComponent", () => { + let fixture: ComponentFixture; + let component: TaskLogComponent; + + let anyComponent: any; + let fileListProxy: RxMockListProxy; + let fileServiceSpy: any; + + beforeEach(() => { + fileListProxy = new RxMockListProxy(File, { + cacheKey: "name", + items: [ + Fixtures.file.create({ name: "stdout.txt" }), + Fixtures.file.create({ name: "stderr.txt" }), + Fixtures.file.create({ name: "banana.txt" }), + Fixtures.file.create({ name: "apple.txt" }), + Fixtures.file.create({ name: "pear.txt" }), + ], + }); + + fileServiceSpy = { + listFromTask: (jobid: string, taskId: string, recursive?: boolean, options?: any) => fileListProxy, + + getFilePropertiesFromTask: (jobid: string, taskId: string, filename: string) => + new RxMockEntityProxy(File, { + cacheKey: "name", + item: Fixtures.file.create({ + name: filename, + properties: { contentLength: fileSizeMap.get(filename) || 100 }, + }), + }), + }; + + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [ + MdAutocomplete, TaskLogComponent, + ], + providers: [ + { provide: FileService, useValue: fileServiceSpy }, + ], + schemas: [NO_ERRORS_SCHEMA], + }); + + fixture = TestBed.createComponent(TaskLogComponent); + component = fixture.componentInstance; + component.jobId = "bobs-job-1"; + component.task = Fixtures.task.create({ id: "bobs-task" }); + anyComponent = component as any; + fixture.detectChanges(); + }); + + describe("on initial default load", () => { + it("initial ui state is correct", () => { + expect(component.jobId).toBe("bobs-job-1"); + expect(component.outputFileNames.length).toBe(2); + expect(component.selectedOutputFile).toBe("stdout.txt"); + expect(Object.keys(component.fileSizes).length).toBe(0); + expect(component.filterControl.value).toBeNull(); + expect(component.addingFile).toBeFalsy(); + + expect(anyComponent._currentTaskId).toBeNull(); + expect(anyComponent._refreshInterval).toBe(5000); + }); + + it("shows default tabs", () => { + const container = fixture.debugElement.query(By.css(".file-tabs")); + expect(container.nativeElement.textContent).toContain("stdout.txt ( - B)"); + expect(container.nativeElement.textContent).toContain("stderr.txt ( - B)"); + expect(container).toBeVisible(); + }); + + it("shows default add file tab", () => { + const container = fixture.debugElement.query(By.css(".file-tab-more")); + expect(container).toBeVisible(); + }); + + it("reset tabs button not shown", () => { + const container = fixture.debugElement.query(By.css("div[title=\"Reset to default\"]")); + expect(container).toBeNull(); + }); + }); + + describe("loads file sizes based on current job and task", () => { + beforeEach(() => { + component.ngOnChanges({ jobId: component.jobId, task: component.task }); + fixture.detectChanges(); + }); + + it("current task id set", () => { + expect(anyComponent._currentTaskId).toBe("bobs-task"); + }); + + it("has loaded file sizes", () => { + expect(Object.keys(component.fileSizes).length).toBe(2); + expect(component.fileSizes["stdout.txt"]).toBe("10 B"); + expect(component.fileSizes["stderr.txt"]).toBe("20 B"); + }); + + it("ui shows file sizes", () => { + const container = fixture.debugElement.query(By.css(".file-tabs")); + expect(container.nativeElement.textContent).toContain("stdout.txt (10 B)"); + expect(container.nativeElement.textContent).toContain("stderr.txt (20 B)"); + }); + + it("default file tab is stdout.txt", () => { + const container = fixture.debugElement.query(By.css(".file-tab.active")); + expect(container.nativeElement.textContent).toContain("stdout.txt (10 B)"); + }); + }); + + describe("loads additional task output files", () => { + beforeEach(() => { + component.ngOnChanges({ jobId: component.jobId, task: component.task }); + fixture.detectChanges(); + }); + + it("has 3 extra outputs for auto-complete filter", () => { + expect(anyComponent._options.value.length).toBe(3); + }); + + it("won't add either default file to _options list", () => { + let file = { name: "stdout.txt", isDirectory: false }; + expect(anyComponent._canAddFileToMap(file)).toBe(false); + + file.name = "stderr.txt"; + expect(anyComponent._canAddFileToMap(file)).toBe(false); + + file.isDirectory = true; + expect(anyComponent._canAddFileToMap(file)).toBe(false); + }); + + it("hasn't added new files to tab list", () => { + expect(component.outputFileNames.length).toBe(2); + }); + }); + + describe("can add a new file to a tab", () => { + beforeEach(() => { + component.ngOnChanges({ jobId: component.jobId, task: component.task }); + fixture.detectChanges(); + }); + + it("click add shows filter", () => { + const tabElements = fixture.debugElement.queryAll(By.css(".file-tab-more")); + tabElements[0].nativeElement.click(); + fixture.detectChanges(); + expect(component.addingFile).toBe(true); + }); + + it("can filter options", (done) => { + component.filteredOptions.subscribe((items) => { + expect(items.length).toBe(1); + expect(items[0]).toBe("banana.txt"); + done(); + }); + + component.filterControl.setValue("banana"); + }); + + it("selecting option loads file into tab list and gets file size", () => { + component.optionSelected({}, "banana.txt"); + expect(component.outputFileNames.length).toBe(3); + expect(component.selectedOutputFile).toBe("banana.txt"); + expect(Object.keys(component.fileSizes).length).toBe(3); + expect(component.fileSizes["banana.txt"]).toBe("30 B"); + }); + + it("clicking reset will remove additional tabs", () => { + component.optionSelected({}, "apple.txt"); + expect(component.outputFileNames.length).toBe(3); + expect(component.selectedOutputFile).toBe("apple.txt"); + + component.resetTabs(); + expect(component.outputFileNames.length).toBe(2); + expect(component.selectedOutputFile).toBe("stdout.txt"); + }); + }); +}); diff --git a/test/fixture.ts b/test/fixture.ts index c2532b81e6..0a511bb43f 100644 --- a/test/fixture.ts +++ b/test/fixture.ts @@ -1,7 +1,7 @@ import { Type } from "@angular/core"; import * as moment from "moment"; -import { AccountResource, Application, ApplicationPackage, Job, Node, PackageState, Pool, +import { AccountResource, Application, ApplicationPackage, File, Job, Node, PackageState, Pool, Subscription, SubtaskInformation, Task, } from "app/models"; @@ -237,3 +237,15 @@ export const applicationPackage = new FixtureFactory(Applica storageUrl: "", storageUrlExpiry: null, }); + +export const file = new FixtureFactory(File, { + name: "file-1", + url: "url/to/file", + isDirectory: false, + properties: { + contentLength: 0, + contentType: "text/plain", + creationTime: new Date(), + lastModified: new Date(), + }, +}); From e9e6979a48d5455a34576320ce37ba2efb090a42 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 8 Jun 2017 16:41:23 -0700 Subject: [PATCH 16/37] Make navigation bar thiner and define focus style (#444) --- app/components/shared/main-navigation.html | 12 ++++-- app/styles/partials/navigation.scss | 48 ++++++++++------------ app/styles/variables.scss | 2 +- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/components/shared/main-navigation.html b/app/components/shared/main-navigation.html index 3cbae2b080..eb2dffa58e 100644 --- a/app/components/shared/main-navigation.html +++ b/app/components/shared/main-navigation.html @@ -1,14 +1,18 @@ diff --git a/app/styles/partials/navigation.scss b/app/styles/partials/navigation.scss index 6cbc9995d4..e28c17968f 100644 --- a/app/styles/partials/navigation.scss +++ b/app/styles/partials/navigation.scss @@ -1,36 +1,32 @@ -nav { - ul { - margin: 0; +bl-app-nav > ul { + margin: 0; + user-select: none; - li { - padding: 0; - - i.fa { - display: block; - margin: 0 auto; - } + > li > a { + height: $navigationbar-width; + display: block; + padding: 13px 0; + outline: none; + color: $navigation-text; + -webkit-user-drag: none; - a { - display: block; - padding: 18px 0; - } + i.fa { + display: block; + margin: 0 auto; + } - a.active { - background-color: map-get($primary, 500); - color: $white; - } + .label { + margin-top: 5px; } - li:hover { + &:hover, &:focus { background-color: $navigation-hover; + color: $white; } - } - a { - color: $navigation-text; - } - - a:hover { - color: $white; + &.active { + background-color: map-get($primary, 500); + color: $white; + } } } diff --git a/app/styles/variables.scss b/app/styles/variables.scss index 53ec2391fd..c8de4a9d2d 100644 --- a/app/styles/variables.scss +++ b/app/styles/variables.scss @@ -86,7 +86,7 @@ $border-color: $alto; // Layout sizing //--------------------------------------------- $header-height : 30px; -$navigationbar-width : 70px; +$navigationbar-width : 60px; $listview-width : 350px; $listview-filtering-width : 55vw; $listview-header-height : 90px; From beb728683c7613c26724e7d2dab3b7bc423ead38 Mon Sep 17 00:00:00 2001 From: ascobie Date: Fri, 9 Jun 2017 14:46:47 +1200 Subject: [PATCH 17/37] Fix: start-task tab padding (#442) * add label to property list and allow removing collapse functionality * edit start task element padding --- .../property-list/property-group.component.ts | 7 ++++++- .../base/property-list/property-group.html | 2 +- .../pool/start-task/start-task-picker.html | 17 +++++++++-------- .../pool/start-task/start-task-properties.html | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/components/base/property-list/property-group.component.ts b/app/components/base/property-list/property-group.component.ts index bdba47c809..4b5d3ad563 100644 --- a/app/components/base/property-list/property-group.component.ts +++ b/app/components/base/property-list/property-group.component.ts @@ -17,6 +17,9 @@ export class PropertyGroupComponent { @Input() public warningMessage: string = null; + @Input() + public collapsable: boolean = true; + @Input() public set collapsed(collapsed) { this._collapsed = collapsed; @@ -30,6 +33,8 @@ export class PropertyGroupComponent { private _collapsed = false; public toogleCollapsed() { - this.collapsed = !this.collapsed; + if (this.collapsable) { + this.collapsed = !this.collapsed; + } } } diff --git a/app/components/base/property-list/property-group.html b/app/components/base/property-list/property-group.html index 22f6e94fa7..bddefbd8fc 100644 --- a/app/components/base/property-list/property-group.html +++ b/app/components/base/property-list/property-group.html @@ -1,5 +1,5 @@
-
+
diff --git a/app/components/pool/start-task/start-task-picker.html b/app/components/pool/start-task/start-task-picker.html index 7b0fe9401b..90e5d4f832 100644 --- a/app/components/pool/start-task/start-task-picker.html +++ b/app/components/pool/start-task/start-task-picker.html @@ -16,15 +16,16 @@ Wait for success
- -
+
- -

Resource files

- - -

Environment variables

- +
+

Resource files

+ +
+
+

Environment variables

+ +
diff --git a/app/components/pool/start-task/start-task-properties.html b/app/components/pool/start-task/start-task-properties.html index 67d8159f37..4f53e00b6b 100644 --- a/app/components/pool/start-task/start-task-properties.html +++ b/app/components/pool/start-task/start-task-properties.html @@ -3,7 +3,7 @@ - + From 352b81788e1c63ce79411bf60ac81d57ef445a20 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 9 Jun 2017 13:19:39 -0700 Subject: [PATCH 18/37] Feature: Display batch account quotas & refactor pool resize errors (#445) * Low pri quota * Show real values * Pool resize error wip * Fix * Fix quotas * fix account dropdown case sensitive * Fix batch --- app/app.module.ts | 2 + app/components/account/account.module.ts | 3 +- .../browse/account-dropdown.component.ts | 5 +- .../account/browse/account-dropdown.html | 2 +- .../account/details/account-details.html | 5 +- .../account/details/account-details.scss | 13 --- .../account-quotas-card.component.ts | 33 ++++++++ .../account-quotas-card.html | 20 +++++ .../account-quotas-card.scss | 31 +++++++ .../details/account-quotas-card/index.ts | 1 + .../storage-account-card.html | 2 +- .../pool/base/pool-nodes-preview.component.ts | 4 +- .../pool/browse/pool-list.component.ts | 4 +- .../pool-error-display.component.ts | 14 ++-- .../error-display/pool-error-display.html | 48 +++++++---- app/models/account.ts | 83 ++++++++++++------- app/models/pool.ts | 4 +- app/models/resize-error.ts | 36 ++++---- app/services/account.service.ts | 23 ++--- app/services/azure-http.service.ts | 10 +-- app/services/compute.service.ts | 49 +++++++++++ app/services/index.ts | 1 + app/services/vm-size.service.ts | 5 +- app/utils/arm-resource-utils.ts | 12 +++ app/utils/constants.ts | 2 +- .../storage-account-picker.component.spec.ts | 5 +- .../storage-account-card.component.spec.ts | 6 +- .../pool/action/add/vm-size-picker.spec.ts | 2 +- .../base/pool-nodes-preview.component.spec.ts | 2 +- .../pool-error-display.component.spec.ts | 24 +----- test/app/services/account.service.spec.ts | 4 +- 31 files changed, 298 insertions(+), 157 deletions(-) create mode 100644 app/components/account/details/account-quotas-card/account-quotas-card.component.ts create mode 100644 app/components/account/details/account-quotas-card/account-quotas-card.html create mode 100644 app/components/account/details/account-quotas-card/account-quotas-card.scss create mode 100644 app/components/account/details/account-quotas-card/index.ts create mode 100644 app/services/compute.service.ts diff --git a/app/app.module.ts b/app/app.module.ts index 2ce9a714fb..e5e436b52a 100644 --- a/app/app.module.ts +++ b/app/app.module.ts @@ -37,6 +37,7 @@ import { AzureHttpService, BatchClientService, CommandService, + ComputeService, ElectronRemote, ElectronShell, FileService, @@ -97,6 +98,7 @@ const modules = [ ArmHttpService, BatchClientService, CommandService, + ComputeService, ElectronRemote, ElectronShell, FileService, diff --git a/app/components/account/account.module.ts b/app/components/account/account.module.ts index 47818bdc15..b189c63363 100644 --- a/app/components/account/account.module.ts +++ b/app/components/account/account.module.ts @@ -8,13 +8,14 @@ import { AccountCreateDialogComponent } from "./action/add/account-create-dialog import { DeleteAccountDialogComponent } from "./action/delete/delete-account-dialog.component"; import { AccountBrowseModule } from "./browse"; import { AccountDefaultComponent, AccountDetailsComponent } from "./details"; +import { AccountQuotasCardComponent } from "./details/account-quotas-card"; import { StorageAccountCardComponent } from "./details/storage-account-card"; import { AccountHomeComponent } from "./home"; const components = [ AccountCreateDialogComponent, AccountDefaultComponent, AccountDetailsComponent, AccountHomeComponent, DeleteAccountDialogComponent, StorageAccountCardComponent, - EditStorageAccountFormComponent, StorageAccountPickerComponent, + EditStorageAccountFormComponent, StorageAccountPickerComponent, AccountQuotasCardComponent, ]; const modules = [ diff --git a/app/components/account/browse/account-dropdown.component.ts b/app/components/account/browse/account-dropdown.component.ts index 057fd7bb56..a1b70d6208 100644 --- a/app/components/account/browse/account-dropdown.component.ts +++ b/app/components/account/browse/account-dropdown.component.ts @@ -2,6 +2,7 @@ import { AfterViewInit, ChangeDetectorRef, Component } from "@angular/core"; import { Router } from "@angular/router"; import { AccountResource } from "app/models"; import { AccountService, AccountStatus } from "app/services"; +import { ArmResourceUtils } from "app/utils"; @Component({ selector: "bl-account-dropdown", @@ -23,9 +24,7 @@ export class AccountDropDownComponent implements AfterViewInit { accountService.currentAccountId.subscribe((accountId) => { if (accountId) { this.selectedId = accountId; - this.selectedAccountAlias = accountService.getNameFromAccountId(accountId); - // this.router.navigate(["/accounts", this.selectedId]); - + this.selectedAccountAlias = ArmResourceUtils.getAccountNameFromResourceId(accountId); } else { this.selectedAccountAlias = "No account selected!"; } diff --git a/app/components/account/browse/account-dropdown.html b/app/components/account/browse/account-dropdown.html index 314294380f..f372876c58 100644 --- a/app/components/account/browse/account-dropdown.html +++ b/app/components/account/browse/account-dropdown.html @@ -8,7 +8,7 @@