Add clusrun.
This commit is contained in:
Родитель
73911f910c
Коммит
4a7c6c2642
10
angular.json
10
angular.json
|
@ -30,7 +30,10 @@
|
|||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [
|
||||
"./node_modules/jquery/dist/jquery.min.js",
|
||||
"./node_modules/signalr/jquery.signalR.min.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
@ -126,7 +129,10 @@
|
|||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"scripts": [
|
||||
"./node_modules/jquery/dist/jquery.min.js",
|
||||
"./node_modules/signalr/jquery.signalR.min.js"
|
||||
],
|
||||
"codeCoverageExclude": ["src/app/api-client/**/*"]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1275,6 +1275,15 @@
|
|||
"@types/jasmine": "*"
|
||||
}
|
||||
},
|
||||
"@types/jquery": {
|
||||
"version": "3.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
|
||||
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||
|
@ -1299,6 +1308,21 @@
|
|||
"integrity": "sha512-lMC2G0ItF2xv4UCiwbJGbnJlIuUixHrioOhNGHSCsYCJ8l4t9hMCUimCytvFv7qy6AfSzRxhRHoGa+UqaqwyeA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/signalr": {
|
||||
"version": "2.2.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/signalr/-/signalr-2.2.35.tgz",
|
||||
"integrity": "sha512-YVIqZzmiNA8E1maMM57fqz44xJFTWxoNeuw3NfYv/j22lVhVXZeef1rOFo96ENpnrpEa+5ktKpDsGpozfKdTCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
|
||||
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/source-list-map": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
|
||||
|
@ -5387,6 +5411,11 @@
|
|||
"integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
|
||||
"dev": true
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
|
||||
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||
|
@ -8887,6 +8916,14 @@
|
|||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
|
||||
"dev": true
|
||||
},
|
||||
"signalr": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/signalr/-/signalr-2.4.1.tgz",
|
||||
"integrity": "sha512-HhIcA9kOE9WBs/DPHd+9jN90GDeSD7RRAETcmxn80laDBQmkQeHblzGBNw4rBzn1behe2WiFYQcbKyx11H3ADw==",
|
||||
"requires": {
|
||||
"jquery": ">=1.6.4"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
"@angular/platform-browser-dynamic": "~8.2.0",
|
||||
"@angular/router": "~8.2.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jquery": "^3.4.1",
|
||||
"rxjs": "~6.4.0",
|
||||
"signalr": "^2.4.1",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.9.1"
|
||||
},
|
||||
|
@ -37,7 +39,9 @@
|
|||
"@angular/language-service": "~8.2.0",
|
||||
"@types/jasmine": "~3.3.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/jquery": "^3.3.31",
|
||||
"@types/node": "~8.9.4",
|
||||
"@types/signalr": "^2.2.35",
|
||||
"codelyzer": "^5.0.0",
|
||||
"jasmine-core": "~3.4.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
|
|
|
@ -13,6 +13,7 @@ import { AuthService } from './services/auth.service';
|
|||
import { UserService } from './services/user.service';
|
||||
import { ApiService, BASE_PATH, Configuration } from './services/api.service';
|
||||
import { ApiConfigService } from './services/api-config.service';
|
||||
import { RemoteCommandService } from './services/remote-command.service'
|
||||
import { BreadcrumbComponent } from './breadcrumb/breadcrumb.component';
|
||||
import { SharedComponents } from './shared-components/shared-components.module'
|
||||
|
||||
|
@ -74,6 +75,7 @@ const routes: Routes = [{
|
|||
AuthService,
|
||||
UserService,
|
||||
ApiService,
|
||||
RemoteCommandService,
|
||||
{ provide: BASE_PATH, useValue: environment.API_BASE_PATH },
|
||||
{ provide: Configuration, useClass: ApiConfigService },
|
||||
],
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<h2 mat-dialog-title>
|
||||
Run a Command
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="command">
|
||||
<mat-form-field class="cmd-line">
|
||||
<mat-label>Comamnd Line</mat-label>
|
||||
<input matInput required autofocus [formControl]="cmdInput" (keyup.enter)="run()" />
|
||||
</mat-form-field>
|
||||
<button mat-button color="primary" [disabled]="!readyToRun" (click)="run()">Run</button>
|
||||
<button mat-button [disabled]="!running" (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
<div class="result">
|
||||
<div class="node-select">
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Node</th>
|
||||
<td mat-cell *matCellDef="let node">{{node.name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>State</th>
|
||||
<td mat-cell *matCellDef="let node">{{node.state}}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let node; columns: displayedColumns;" (click)="select(node)" [ngClass]="cssClass(node)"></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="output">
|
||||
<pre>
|
||||
{{selected?.output}}
|
||||
<span class="error">{{selected?.errorOut}}</span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="undefined">Close</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,60 @@
|
|||
mat-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.command {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.cmd-line {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.node-select {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.selected {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.output {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
margin-left: 1em;
|
||||
|
||||
pre {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
color: white;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommanderComponent } from './commander.component';
|
||||
|
||||
describe('CommanderComponent', () => {
|
||||
let component: CommanderComponent;
|
||||
let fixture: ComponentFixture<CommanderComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CommanderComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommanderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,143 @@
|
|||
import { Component, Inject, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatTableDataSource } from '@angular/material';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
|
||||
import { RemoteCommandService, RemoteCommand, CommandOutput } from '../../services/remote-command.service'
|
||||
|
||||
class NodeOut {
|
||||
name: string;
|
||||
eof: boolean;
|
||||
errorOut: string;
|
||||
output: string;
|
||||
|
||||
get state(): string {
|
||||
if (this.eof === undefined)
|
||||
return '';
|
||||
if (this.errorOut) {
|
||||
return 'Error';
|
||||
}
|
||||
return this.eof ? 'End' : 'Running';
|
||||
}
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
update(output: CommandOutput): void {
|
||||
this.eof = output.eof;
|
||||
if (!this.eof) {
|
||||
if (output.error) {
|
||||
if (this.errorOut) {
|
||||
this.errorOut += `\n${output.line}`;
|
||||
}
|
||||
else {
|
||||
this.errorOut = output.line;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.output) {
|
||||
this.output += `\n${output.line}`;
|
||||
}
|
||||
else {
|
||||
this.output = output.line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-commander',
|
||||
templateUrl: './commander.component.html',
|
||||
styleUrls: ['./commander.component.scss']
|
||||
})
|
||||
export class CommanderComponent implements OnInit, OnDestroy {
|
||||
private command: RemoteCommand;
|
||||
|
||||
dataSource: MatTableDataSource<NodeOut> = new MatTableDataSource();
|
||||
|
||||
displayedColumns = ['name', 'state'];
|
||||
|
||||
@ViewChild(MatSort, {static: true})
|
||||
private sort: MatSort;
|
||||
|
||||
selected: NodeOut;
|
||||
|
||||
cmdInput: FormControl;
|
||||
|
||||
get cmdLine(): string {
|
||||
try {
|
||||
return this.cmdInput.value.trim();
|
||||
}
|
||||
catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private nameToIndex: Map<string, number>;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public nodeNames: string[],
|
||||
private fb: FormBuilder,
|
||||
private commandService: RemoteCommandService,
|
||||
) {
|
||||
this.cmdInput = this.fb.control('');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.data = this.nodeNames.map(name => new NodeOut(name));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.command) {
|
||||
this.command.disconnect();
|
||||
this.command = null;
|
||||
}
|
||||
}
|
||||
|
||||
get readyToRun(): boolean {
|
||||
return !this.running && this.cmdLine.length != 0;
|
||||
}
|
||||
|
||||
get running(): boolean {
|
||||
return this.command && this.dataSource.data.findIndex(node => node.eof != true) >= 0;
|
||||
}
|
||||
|
||||
run(): void {
|
||||
if (!this.readyToRun) {
|
||||
return;
|
||||
}
|
||||
this.dataSource.data = this.nodeNames.map(name => new NodeOut(name));
|
||||
this.select(this.dataSource.data[0]);
|
||||
this.nameToIndex = new Map(this.nodeNames.map((name, idx) => [name, idx]));
|
||||
this.command = this.commandService.create(this.cmdLine, this.nodeNames);
|
||||
this.command.start(
|
||||
output => {
|
||||
console.log(output);
|
||||
let idx = this.nameToIndex.get(output.nodeName);
|
||||
let nodeOut = this.dataSource.data[idx];
|
||||
nodeOut.update(output);
|
||||
},
|
||||
err => {
|
||||
console.error(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this.command) {
|
||||
this.command.cancel();
|
||||
this.command = null;
|
||||
}
|
||||
}
|
||||
|
||||
select(node: NodeOut): void {
|
||||
this.selected = node;
|
||||
}
|
||||
|
||||
cssClass(node: NodeOut): string {
|
||||
return node == this.selected ? 'selected' : '';
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
<mat-list-item><button mat-button [disabled]="!canLoadMore" (click)="loadMoreData()">{{loadDataActionText}}</button></mat-list-item>
|
||||
<mat-list-item><button mat-button [disabled]="!anySelected" (click)="bringOnline()">Bring Online</button></mat-list-item>
|
||||
<mat-list-item><button mat-button [disabled]="!anySelected" (click)="takeOffline()">Take Offline</button></mat-list-item>
|
||||
<mat-list-item><button mat-button [disabled]="!anySelected" (click)="runCommand()">Run Command...</button></mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { UserService } from '../../services/user.service'
|
|||
import { ApiService } from '../../services/api.service';
|
||||
import { ColumnDef, ColumnSelectorComponent, ColumnSelectorInput, ColumnSelectorResult }
|
||||
from '../../shared-components/column-selector/column-selector.component'
|
||||
import { CommanderComponent } from '../commander/commander.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-list',
|
||||
|
@ -257,4 +258,9 @@ export class NodeListComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
get toggleActionListIcon(): string {
|
||||
return this.actionListHidden ? 'keyboard_arrow_left' : 'keyboard_arrow_right';
|
||||
}
|
||||
|
||||
runCommand(): void {
|
||||
let nodeNames = this.selection.selected.map(node => node.Name);
|
||||
let dialogRef = this.dialog.open(CommanderComponent, { data: nodeNames, disableClose: true, minWidth: '50%' });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { SharedComponents } from '../shared-components/shared-components.module'
|
|||
import { NodesComponent } from './nodes.component'
|
||||
import { NodeListComponent } from './node-list/node-list.component';
|
||||
import { NodeMapComponent } from './node-map/node-map.component';
|
||||
import { CommanderComponent } from './commander/commander.component';
|
||||
|
||||
const routes: Routes = [{
|
||||
path: '',
|
||||
|
@ -24,6 +25,7 @@ const routes: Routes = [{
|
|||
NodesComponent,
|
||||
NodeListComponent,
|
||||
NodeMapComponent,
|
||||
CommanderComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -32,5 +34,6 @@ const routes: Routes = [{
|
|||
MaterialModule,
|
||||
SharedComponents,
|
||||
],
|
||||
entryComponents: [CommanderComponent],
|
||||
})
|
||||
export class NodesModule { }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable, Optional, Inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs'
|
||||
import { Observable } from 'rxjs'
|
||||
import { DefaultService, BASE_PATH, Configuration, NodeMetric } from '../api-client'
|
||||
import { Node } from '../models/node'
|
||||
import { Job } from '../models/job'
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RemoteCommandService } from './remote-command.service';
|
||||
|
||||
describe('RemoteCommandService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: RemoteCommandService = TestBed.get(RemoteCommandService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import { Injectable, Inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
import { BASE_PATH } from './api.service'
|
||||
import { UserService } from './user.service'
|
||||
import { User } from '../models/user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RemoteCommandService {
|
||||
constructor(
|
||||
@Inject(BASE_PATH) private basePath: string,
|
||||
private userService: UserService,
|
||||
) {
|
||||
//NOTE: SignalR client relies on jQuery for XHR and thus auth header. However,
|
||||
//there's no way for WS auth, so SignalR falls back to Long Poll!
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'Authorization': this.basicAuthHeader
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private get basicAuthHeader(): string {
|
||||
let v = `${this.userService.user.username}:${this.userService.user.password}`;
|
||||
return `Basic ${btoa(v)}`;
|
||||
}
|
||||
|
||||
create(cmd: string, nodes: string[]): RemoteCommand {
|
||||
return new RemoteCommand(this.basePath, cmd, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandOutput {
|
||||
jobId: number;
|
||||
nodeName: string;
|
||||
line: string;
|
||||
eof: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export class RemoteCommand {
|
||||
private connection: SignalR.Hub.Connection;
|
||||
|
||||
private proxy: SignalR.Hub.Proxy;
|
||||
|
||||
private jobId: number;
|
||||
|
||||
constructor(private basePath: string, private cmd: string, private nodes: string[]) {}
|
||||
|
||||
start(output?: (output: CommandOutput) => void, errorOut?: (error: string) => void): void {
|
||||
if (this.connection) {
|
||||
return;
|
||||
}
|
||||
this.connection = $.hubConnection(this.basePath);
|
||||
this.proxy = this.connection.createHubProxy('CommandHub');
|
||||
|
||||
if (output) {
|
||||
this.proxy.on('OutputLine', (jobId: number, nodeName: string, line: string, eof: boolean, error: boolean) => {
|
||||
output({ jobId, nodeName, line, eof, error });
|
||||
});
|
||||
}
|
||||
if (errorOut) {
|
||||
this.connection.error((err: any) => errorOut(err));
|
||||
}
|
||||
let result = this.connection.start()
|
||||
.then(() => this.proxy.invoke('CreateCommand', this.cmd, this.nodes))
|
||||
.then((jobId) => { this.jobId = jobId; return this.proxy.invoke('StartCommand', jobId); });
|
||||
if (errorOut) {
|
||||
result.catch((err) => errorOut(err));
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this.jobId) {
|
||||
this.proxy.invoke('CancelCommand', this.jobId);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.connection) {
|
||||
this.connection.stop(true, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,10 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
"types": [
|
||||
"jquery",
|
||||
"signalr"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
"node",
|
||||
"jquery",
|
||||
"signalr"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
|
|
Загрузка…
Ссылка в новой задаче