From 176aa8af0c3a431876509601c07aecd1b74034fa Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 24 Jul 2017 17:36:53 -0700 Subject: [PATCH 1/6] Confirm delete node --- app/components/pool/graphs/nodes-heatmap.component.ts | 7 ++++++- app/services/node-service.ts | 6 ++++++ src/client/api/batch-client-proxy/nodeProxy.ts | 10 ++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/components/pool/graphs/nodes-heatmap.component.ts b/app/components/pool/graphs/nodes-heatmap.component.ts index e7e5996347..ff1888dfb8 100644 --- a/app/components/pool/graphs/nodes-heatmap.component.ts +++ b/app/components/pool/graphs/nodes-heatmap.component.ts @@ -429,9 +429,10 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro private _buildContextMenu(node: Node) { return new ContextMenu([ new ContextMenuItem({ label: "Go to", click: () => this._gotoNode(node) }), + new ContextMenuItem({ label: "Connect", click: () => this._connectTo(node) }), new ContextMenuItem({ label: "Reboot", click: () => this._reboot(node) }), new ContextMenuItem({ label: "Reimage", click: () => this._reimage(node) }), - new ContextMenuItem({ label: "Connect", click: () => this._connectTo(node) }), + new ContextMenuItem({ label: "Delete", click: () => this._delete(node) }), ]); } @@ -443,6 +444,10 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro this.nodeService.reimage(this.pool.id, node.id).cascade(() => this.nodeService.getOnce(this.pool.id, node.id)); } + private _delete(node: Node) { + this.nodeService.delete(this.pool.id, node.id).cascade(() => this.nodeService.getOnce(this.pool.id, node.id)); + } + private _gotoNode(node: Node) { this.router.navigate(["/pools", this.pool.id, "nodes", node.id]); } diff --git a/app/services/node-service.ts b/app/services/node-service.ts index 0b3ad39ed6..435c62e1d6 100644 --- a/app/services/node-service.ts +++ b/app/services/node-service.ts @@ -177,6 +177,12 @@ export class NodeService extends ServiceBase { return observable; } + public delete(poolId: string, nodeId: string): Observable { + return this.callBatchClient((client) => client.node.delete(poolId, nodeId, {}), (error) => { + log.error("Error deleting node: " + nodeId, Object.assign({}, error)); + }); + } + public listNodeAgentSkus(initialOptions: any = { pageSize: 1000 }): RxListProxy<{}, NodeAgentSku> { return new RxBatchListProxy<{}, NodeAgentSku>(NodeAgentSku, this.batchService, { cache: (params) => this._nodeAgentSkusCache, diff --git a/src/client/api/batch-client-proxy/nodeProxy.ts b/src/client/api/batch-client-proxy/nodeProxy.ts index 552cb148d9..611ac1283e 100644 --- a/src/client/api/batch-client-proxy/nodeProxy.ts +++ b/src/client/api/batch-client-proxy/nodeProxy.ts @@ -29,6 +29,16 @@ export default class NodeProxy { return this.client.computeNodeOperations.reboot(poolId, nodeId, wrapOptions(options)); } + /** + * Reinstalls the operating system on the specified compute node. + * @param poolId: The id of the pool. + * @param nodeId: The id of the node to delete + * @param options: Optional Parameters. + */ + public delete(poolId: string, nodeId: string, options?: any): Promise { + return this.client.pool.removeNodes(poolId, { nodeList: [nodeId] }, wrapOptions(options)); + } + /** * Reinstalls the operating system on the specified compute node. * http://azure.github.io/azure-sdk-for-node/azure-batch/latest/ComputeNodeOperations.html#reimage From 25b564a202222947206bb7ea25d8e10905eae797 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 25 Jul 2017 10:08:24 -0700 Subject: [PATCH 2/6] Ability to delete node --- app/components/base/base.module.ts | 3 ++ .../dialogs/confirmation-dialog.component.ts | 29 +++++++++++++++++ .../base/dialogs/confirmation-dialog.html | 6 ++++ app/components/base/dialogs/dialog.service.ts | 22 +++++++++++++ app/components/base/dialogs/dialogs.module.ts | 31 +++++++++++++++++++ app/components/base/dialogs/index.ts | 3 ++ .../simple-dialog/simple-dialog.component.ts | 2 +- .../node/details/node-details.component.ts | 16 +++++++++- app/components/node/details/node-details.html | 1 + 9 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 app/components/base/dialogs/confirmation-dialog.component.ts create mode 100644 app/components/base/dialogs/confirmation-dialog.html create mode 100644 app/components/base/dialogs/dialog.service.ts create mode 100644 app/components/base/dialogs/dialogs.module.ts create mode 100644 app/components/base/dialogs/index.ts diff --git a/app/components/base/base.module.ts b/app/components/base/base.module.ts index 65d1848434..45be068c6b 100644 --- a/app/components/base/base.module.ts +++ b/app/components/base/base.module.ts @@ -13,6 +13,7 @@ import { BreadcrumbModule } from "./breadcrumbs"; import { ButtonsModule } from "./buttons"; import { ChartsModule } from "./charts"; import { ContextMenuModule } from "./context-menu"; +import { DialogsModule } from "./dialogs"; import { DropdownModule } from "./dropdown"; import { EditorModule } from "./editor"; import { ElapsedTimeComponent } from "./elapsed-time"; @@ -43,6 +44,7 @@ const modules = [ BackgroundTaskModule, ChartsModule, ContextMenuModule, + DialogsModule, DropdownModule, EditorModule, FocusSectionModule, @@ -79,6 +81,7 @@ const components = [ declarations: components, entryComponents: [ DeleteSelectedItemsDialogComponent, + SimpleDialogComponent, ], exports: [...modules, ...components], imports: [ diff --git a/app/components/base/dialogs/confirmation-dialog.component.ts b/app/components/base/dialogs/confirmation-dialog.component.ts new file mode 100644 index 0000000000..e7b25ce97d --- /dev/null +++ b/app/components/base/dialogs/confirmation-dialog.component.ts @@ -0,0 +1,29 @@ +import { Component } from "@angular/core"; +import { MdDialogRef } from "@angular/material"; +import { autobind } from "core-decorators"; +import { AsyncSubject, Observable } from "rxjs"; + +@Component({ + selector: "bl-confirmation-dialog", + templateUrl: "confirmation-dialog.html", +}) +export class ConfirmationDialogComponent { + public title: string; + public description: string; + public execute: () => Observable; + + public response = new AsyncSubject(); + + constructor(public dialogRef: MdDialogRef) { + this.response.next(false); + } + + @autobind() + public submit() { + return this.execute(); + } + + public done() { + this.response.complete(); + } +} diff --git a/app/components/base/dialogs/confirmation-dialog.html b/app/components/base/dialogs/confirmation-dialog.html new file mode 100644 index 0000000000..d991d1c527 --- /dev/null +++ b/app/components/base/dialogs/confirmation-dialog.html @@ -0,0 +1,6 @@ + +
+

{{description}}

+
+
diff --git a/app/components/base/dialogs/dialog.service.ts b/app/components/base/dialogs/dialog.service.ts new file mode 100644 index 0000000000..c13cd82ba1 --- /dev/null +++ b/app/components/base/dialogs/dialog.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@angular/core"; +import { MdDialog } from "@angular/material"; +import { Observable } from "rxjs"; +import { ConfirmationDialogComponent } from "./confirmation-dialog.component"; + +export interface ConfirmOptions { + description?: string; + yes: () => Observable; +} + +@Injectable() +export class DialogService { + constructor(private mdDialog: MdDialog) { } + + public confirm(title: string = "Are you sure?", options: ConfirmOptions) { + const ref = this.mdDialog.open(ConfirmationDialogComponent); + const component = ref.componentInstance; + component.title = title; + component.description = options.description; + component.execute = options.yes; + } +} diff --git a/app/components/base/dialogs/dialogs.module.ts b/app/components/base/dialogs/dialogs.module.ts new file mode 100644 index 0000000000..b89cc95013 --- /dev/null +++ b/app/components/base/dialogs/dialogs.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { MaterialModule } from "@angular/material"; +import { BrowserModule } from "@angular/platform-browser"; + +import { FormModule } from "app/components/base/form"; +import { ConfirmationDialogComponent } from "./confirmation-dialog.component"; +import { DialogService } from "./dialog.service"; + +@NgModule({ + declarations: [ + ConfirmationDialogComponent, + ], + exports: [ + ConfirmationDialogComponent, + ], + imports: [ + BrowserModule, + FormsModule, + MaterialModule, + FormModule, + ], + providers: [ + DialogService, + ], + entryComponents: [ + ConfirmationDialogComponent, + ], +}) +export class DialogsModule { +} diff --git a/app/components/base/dialogs/index.ts b/app/components/base/dialogs/index.ts new file mode 100644 index 0000000000..1b720ea492 --- /dev/null +++ b/app/components/base/dialogs/index.ts @@ -0,0 +1,3 @@ +export * from "./confirmation-dialog.component"; +export * from "./dialog.service"; +export * from "./dialogs.module"; diff --git a/app/components/base/simple-dialog/simple-dialog.component.ts b/app/components/base/simple-dialog/simple-dialog.component.ts index 2a256bdf10..84dca7e888 100644 --- a/app/components/base/simple-dialog/simple-dialog.component.ts +++ b/app/components/base/simple-dialog/simple-dialog.component.ts @@ -16,7 +16,7 @@ export class SimpleDialogComponent { public done = new EventEmitter(); @Input() - public title: string; + public title: string = "Are you sure?"; @Input() public subtitle: string; diff --git a/app/components/node/details/node-details.component.ts b/app/components/node/details/node-details.component.ts index 5a740db66a..59f3cca658 100644 --- a/app/components/node/details/node-details.component.ts +++ b/app/components/node/details/node-details.component.ts @@ -1,9 +1,12 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; +import { MdDialog, MdDialogConfig } from "@angular/material"; import { ActivatedRoute } from "@angular/router"; import { autobind } from "core-decorators"; import { Subscription } from "rxjs"; +import { DialogService } from "app/components/base/dialogs"; import { SidebarManager } from "app/components/base/sidebar"; +import { SimpleDialogComponent } from "app/components/base/simple-dialog"; import { Node, Pool } from "app/models"; import { FileService, NodeParams, NodeService, PoolService } from "app/services"; import { RxEntityProxy } from "app/services/core"; @@ -33,8 +36,9 @@ export class NodeDetailsComponent implements OnInit, OnDestroy { constructor( private route: ActivatedRoute, - nodeService: NodeService, + private nodeService: NodeService, private poolService: PoolService, + private dialog: DialogService, fileService: FileService, private sidebarManager: SidebarManager) { @@ -92,4 +96,14 @@ export class NodeDetailsComponent implements OnInit, OnDestroy { ref.component.node = this.node; ref.component.pool = this.pool; } + + @autobind() + public delete() { + this.dialog.confirm("Are you sure you want to delete this node?", { + yes: () => { + return this.nodeService.delete(this.pool.id, this.nodeId) + .cascade(() => this.nodeService.getOnce(this.pool.id, this.node.id)); + }, + }); + } } diff --git a/app/components/node/details/node-details.html b/app/components/node/details/node-details.html index fbf05f9a35..085caf4abe 100644 --- a/app/components/node/details/node-details.html +++ b/app/components/node/details/node-details.html @@ -5,6 +5,7 @@ +
From db048ac6a6566c9b89e7e838c08503f77544049e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 25 Jul 2017 10:14:44 -0700 Subject: [PATCH 3/6] Fix grow gcol --- app/styles/partials/grid.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/styles/partials/grid.scss b/app/styles/partials/grid.scss index d4559754d8..cdc71753bc 100644 --- a/app/styles/partials/grid.scss +++ b/app/styles/partials/grid.scss @@ -1,9 +1,15 @@ .grow { display: flex; - margin: 0 -5px; - > .gcol { flex: 1; margin: 0 5px; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } } } From 32d5e6f7e7b8d025522a4e3c95a065f63327c036 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 25 Jul 2017 10:34:27 -0700 Subject: [PATCH 4/6] Show notifications --- app/components/base/dialogs/dialog.service.ts | 10 ++++++- .../node/details/node-details.component.ts | 2 -- .../pool/graphs/nodes-heatmap.component.ts | 30 +++++++++++++++---- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/app/components/base/dialogs/dialog.service.ts b/app/components/base/dialogs/dialog.service.ts index c13cd82ba1..29c4310902 100644 --- a/app/components/base/dialogs/dialog.service.ts +++ b/app/components/base/dialogs/dialog.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@angular/core"; -import { MdDialog } from "@angular/material"; +import { ComponentType, MdDialog, MdDialogConfig, MdDialogRef } from "@angular/material"; import { Observable } from "rxjs"; import { ConfirmationDialogComponent } from "./confirmation-dialog.component"; @@ -8,6 +8,10 @@ export interface ConfirmOptions { yes: () => Observable; } +/** + * Dialog service is a service to help open commonly used dialog such as a confirmation dialog. + * It can also open a dialog the same way material does so you only need to inject this service and not the MdDialog + */ @Injectable() export class DialogService { constructor(private mdDialog: MdDialog) { } @@ -19,4 +23,8 @@ export class DialogService { component.description = options.description; component.execute = options.yes; } + + public open(type: ComponentType, config?: MdDialogConfig): MdDialogRef { + return this.mdDialog.open(type, config); + } } diff --git a/app/components/node/details/node-details.component.ts b/app/components/node/details/node-details.component.ts index 59f3cca658..1ba2a1b977 100644 --- a/app/components/node/details/node-details.component.ts +++ b/app/components/node/details/node-details.component.ts @@ -1,12 +1,10 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { MdDialog, MdDialogConfig } from "@angular/material"; import { ActivatedRoute } from "@angular/router"; import { autobind } from "core-decorators"; import { Subscription } from "rxjs"; import { DialogService } from "app/components/base/dialogs"; import { SidebarManager } from "app/components/base/sidebar"; -import { SimpleDialogComponent } from "app/components/base/simple-dialog"; import { Node, Pool } from "app/models"; import { FileService, NodeParams, NodeService, PoolService } from "app/services"; import { RxEntityProxy } from "app/services/core"; diff --git a/app/components/pool/graphs/nodes-heatmap.component.ts b/app/components/pool/graphs/nodes-heatmap.component.ts index ff1888dfb8..6dd1459f89 100644 --- a/app/components/pool/graphs/nodes-heatmap.component.ts +++ b/app/components/pool/graphs/nodes-heatmap.component.ts @@ -6,12 +6,13 @@ import { Router } from "@angular/router"; import * as d3 from "d3"; import * as elementResizeDetectorMaker from "element-resize-detector"; import { List } from "immutable"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { ContextMenu, ContextMenuItem, ContextMenuService } from "app/components/base/context-menu"; +import { NotificationService } from "app/components/base/notifications"; import { SidebarManager } from "app/components/base/sidebar"; import { NodeConnectComponent } from "app/components/node/connect"; -import { Job, Node, NodeState, Pool } from "app/models"; +import { Job, Node, NodeState, Pool, ServerError } from "app/models"; import { NodeService } from "app/services"; import { log } from "app/utils"; import { HeatmapColor } from "./heatmap-color"; @@ -117,6 +118,7 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro private contextMenuService: ContextMenuService, private nodeService: NodeService, private sidebarManager: SidebarManager, + private notificationService: NotificationService, private router: Router, ) { this.colors = new HeatmapColor(stateTree); @@ -437,15 +439,33 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro } private _reboot(node: Node) { - this.nodeService.reboot(this.pool.id, node.id).cascade(() => this.nodeService.getOnce(this.pool.id, node.id)); + this._nodeAction(node, this.nodeService.reboot(this.pool.id, node.id)).cascade(() => { + this.notificationService.success("Node reimaging!", `Node ${node.id} started reimaging`); + }); } private _reimage(node: Node) { - this.nodeService.reimage(this.pool.id, node.id).cascade(() => this.nodeService.getOnce(this.pool.id, node.id)); + this._nodeAction(node, this.nodeService.reimage(this.pool.id, node.id)).cascade(() => { + this.notificationService.success("Node rebooting!", `Node ${node.id} started rebooting`); + }); } private _delete(node: Node) { - this.nodeService.delete(this.pool.id, node.id).cascade(() => this.nodeService.getOnce(this.pool.id, node.id)); + this._nodeAction(node, this.nodeService.delete(this.pool.id, node.id)).cascade(() => { + this.notificationService.success("Node deleting!", `Node ${node.id} is being removed from the pool.`); + }); + } + + private _nodeAction(node: Node, action: Observable): Observable { + action.subscribe({ + next: () => { + this.nodeService.getOnce(this.pool.id, node.id); + }, + error: (error: ServerError) => { + this.notificationService.error(error.body.code, error.body.message); + }, + }); + return action; } private _gotoNode(node: Node) { From 700db2f22ffb367fe721068e54fc7599c2d3a328 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 25 Jul 2017 11:01:07 -0700 Subject: [PATCH 5/6] Fix low pri stripes --- .../pool/graphs/nodes-heatmap.component.ts | 13 +++++++++++++ app/components/pool/graphs/nodes-heatmap.html | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/components/pool/graphs/nodes-heatmap.component.ts b/app/components/pool/graphs/nodes-heatmap.component.ts index 6dd1459f89..5c05f1285b 100644 --- a/app/components/pool/graphs/nodes-heatmap.component.ts +++ b/app/components/pool/graphs/nodes-heatmap.component.ts @@ -163,6 +163,19 @@ export class NodesHeatmapComponent implements AfterViewInit, OnChanges, OnDestro .attr("width", this._width) .attr("height", this._height); + const defs = this._svg.append("defs"); + const pattern = defs.append("pattern") + .attr("id", "low-pri-stripes") + .attr("width", "10") + .attr("height", "10") + .attr("patternUnits", "userSpaceOnUse") + .attr("patternTransform", "rotate(45 50 50)"); + + pattern.append("line") + .attr("stroke", "#fff") + .attr("stroke-width", "2px") + .attr("y2", "10"); + this._processNewData(); } diff --git a/app/components/pool/graphs/nodes-heatmap.html b/app/components/pool/graphs/nodes-heatmap.html index 801d961d3e..e0b09910de 100644 --- a/app/components/pool/graphs/nodes-heatmap.html +++ b/app/components/pool/graphs/nodes-heatmap.html @@ -4,11 +4,11 @@
- +
From cb2b8f7c8445671eeaf80a7dbf6772cb43951c93 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 25 Jul 2017 11:22:04 -0700 Subject: [PATCH 6/6] Fix specs and add docs --- docs/dialog.md | 52 +++++++++++++++++++ docs/readme.md | 1 + .../graphs/nodes-heatmap.component.spec.ts | 6 ++- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 docs/dialog.md diff --git a/docs/dialog.md b/docs/dialog.md new file mode 100644 index 0000000000..571d961614 --- /dev/null +++ b/docs/dialog.md @@ -0,0 +1,52 @@ +# How to open a popup dialog + +You can open a popup dialog in batchlabs to ask for confirmation to the user, fill a small form, etc. + +First you need to import the `DialogService` + +```ts +import { DialogService } from "app/components/base/dialogs"; + +constructor(private dialog: DialogService) {} +``` + +## Common dialogs + +**1. Confirmation dialog** + +Simplest confirmation dialog that do some action when user confirm. +```ts +this.dialog.confirm("Are you sure?", { + yes: () => this.otherService.doSomething(), +}) +``` + +You can also have a description to detail a bit more what the user is about to confirm +```ts +this.dialog.confirm("Are you sure?", { + description: "This cannot be reverted.", + yes: () => this.otherService.doSomething(), +}) +``` + + +## Custom dialogs + +You can also open any component as a dialog. For that you'll need to first add the component to the entryComponents list of the module + +```ts +@NgModule({ + ... + entryComponents: [ + MyDialogComponent + ], + ... +}) +``` + +then just call open(Note that this is just a proxy function to material MdDialog open) + +```ts +const ref = this.dialog.open(MyDialogComponent); +reg.componentInstance # If you want to set some variables. +``` diff --git a/docs/readme.md b/docs/readme.md index f3bd6d3d8a..a363f3df03 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -8,6 +8,7 @@ ## How to * [Context Menu](context-menu.md) +* [Open a dialog](dialog.md) * [Store user data/Cache data](store-user-data.md) 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 e6871aae53..08e1d2900b 100644 --- a/test/app/components/pool/graphs/nodes-heatmap.component.spec.ts +++ b/test/app/components/pool/graphs/nodes-heatmap.component.spec.ts @@ -11,7 +11,7 @@ import { Node, NodeState, Pool } from "app/models"; import { NodeService } from "app/services"; import * as Fixture from "test/fixture"; import { click, dblclick, rightClick } from "test/utils/helpers"; -import { ContextMenuServiceMock } from "test/utils/mocks"; +import { ContextMenuServiceMock, NotificationServiceMock } from "test/utils/mocks"; @Component({ template: ` @@ -48,9 +48,10 @@ describe("NodesHeatmapComponent", () => { let svg: d3.Selection; let contextMenuService: ContextMenuServiceMock; let routerSpy; - + let notificationService; beforeEach(() => { contextMenuService = new ContextMenuServiceMock(); + notificationService = new NotificationServiceMock(); routerSpy = { navigate: jasmine.createSpy("router.navigate"), }; @@ -63,6 +64,7 @@ describe("NodesHeatmapComponent", () => { { provide: SidebarManager, useValue: {} }, { provide: Router, useValue: routerSpy }, contextMenuService.asProvider(), + notificationService.asProvider(), ], });