Merge pull request #15 from BabysbreathJJ/master

Modify dokerfile to adapt to new CI and remove portal code from code base.
This commit is contained in:
Robert Zhang 2019-03-21 16:05:08 +08:00 коммит произвёл GitHub
Родитель a96be19661 f960888ffc
Коммит b9478985d2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
273 изменённых файлов: 90 добавлений и 28616 удалений

Просмотреть файл

@ -1,41 +1,60 @@
FROM microsoft/aspnetcore-build:2.0 AS allbuild
WORKDIR /src
COPY *.sln ./
COPY Frontend/Frontend.csproj Frontend/
COPY Common/DTO/DTO.csproj Common/DTO/
COPY Common/Utilities/Utilities.csproj Common/Utilities/
COPY Services/Common/ServicesCommon.csproj Services/Common/
COPY Services/JobMonitor/JobMonitor.csproj Services/JobMonitor/
COPY Services/Dashboard/Dashboard.csproj Services/Dashboard/
COPY Services/TaskDispatcher/TaskDispatcher.csproj Services/TaskDispatcher/
COPY Services/NodeAgent/NodeAgent.csproj Services/NodeAgent/
COPY Bootstrap/Bootstrap.csproj Bootstrap/
RUN dotnet restore
COPY . .
WORKDIR /src/Frontend
RUN dotnet publish -c Release -o /app/Frontend
WORKDIR /src/Bootstrap
RUN dotnet publish -c Release -o /app/Bootstrap
WORKDIR /src/Services/Dashboard
RUN dotnet publish -c Release -o /app/Dashboard
WORKDIR /src/Services/JobMonitor
RUN dotnet publish -c Release -o /app/JobMonitor
WORKDIR /src/Services/TaskDispatcher
RUN dotnet publish -c Release -o /app/TaskDispatcher
WORKDIR /src/Services/NodeAgent
RUN dotnet publish -c Release -o /app/NodeAgent
# The image of hpcacmbuild.azurecr.io/public/hpcpack/hpcacm/runtime is built using src/Dcoker/Dockerfile-runtime,
# it should be repalced by your own build image.
FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm/portal AS portalbuild
WORKDIR /src
COPY portal/ portal/
WORKDIR /src/portal
RUN npm install
RUN ng build --prod
# allbuild/app is the diretory which stores all applications and its dependencies,
# we recommend you put the build result under the directory of src/allbuild/app,
# or under other directoies which you should modify other dockerfiles to get build result.
# The content of allbuild/app is generated by commands as below:
# cd src/Frontend/Frontend.csproj
# dotnet publish -c Release -o src/allbuild/app/Frontend
# cd src/Bootstrap/Bootstrap.csproj
# donet publish -c Release -o src/allbuild/app/Bootstrap
# cd src/Services/Dashboard/Dashboard.csproj
# donet publish -c Release -o src/allbuild/app/Dashboard
# cd src/Services/JobMonitor/JobMonitor.csproj
# donet publish -c Release -o src/allbuild/app/JobMonitor
# cd src/Services/TaskDispatcher/TaskDispatcher.csproj
# donet publish -c Release -o src/allbuild/app/TaskDispatcher
# cd src/Services/NodeAgent/NodeAgent.csproj
# donet publish -c Release -o src/allbuild/app/NodeAgent
# You could also get allbuild result in docker build environment, but it's not a good practise, the corresponding docker file shows as below:
# FROM microsoft/aspnetcore-build:2.0 AS allbuild
# WORKDIR /src
# COPY *.sln ./
# COPY Frontend/Frontend.csproj Frontend/
# COPY Common/DTO/DTO.csproj Common/DTO/
# COPY Common/Utilities/Utilities.csproj Common/Utilities/
# COPY Services/Common/ServicesCommon.csproj Services/Common/
# COPY Services/JobMonitor/JobMonitor.csproj Services/JobMonitor/
# COPY Services/Dashboard/Dashboard.csproj Services/Dashboard/
# COPY Services/TaskDispatcher/TaskDispatcher.csproj Services/TaskDispatcher/
# COPY Services/NodeAgent/NodeAgent.csproj Services/NodeAgent/
# COPY Bootstrap/Bootstrap.csproj Bootstrap/
# RUN dotnet restore
# COPY . .
# WORKDIR /src/Frontend
# RUN dotnet publish -c Release -o /app/Frontend
# WORKDIR /src/Bootstrap
# RUN dotnet publish -c Release -o /app/Bootstrap
# WORKDIR /src/Services/Dashboard
# RUN dotnet publish -c Release -o /app/Dashboard
# WORKDIR /src/Services/JobMonitor
# RUN dotnet publish -c Release -o /app/JobMonitor
# WORKDIR /src/Services/TaskDispatcher
# RUN dotnet publish -c Release -o /app/TaskDispatcher
# WORKDIR /src/Services/NodeAgent
# RUN dotnet publish -c Release -o /app/NodeAgent
# FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm/runtime AS final
# WORKDIR /app/scripts
# COPY Docker/scripts/* ./
# WORKDIR /app
# COPY --from=allbuild /app .
# ENTRYPOINT ["dotnet", "Bootstrap/Bootstrap.dll"]
FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm/runtime AS final
WORKDIR /app/scripts
COPY Docker/scripts/* ./
WORKDIR /app
COPY --from=allbuild /app .
COPY --from=portalbuild /src/portal/dist /app/Frontend/wwwroot
COPY allbuild/app .
ENTRYPOINT ["dotnet", "Bootstrap/Bootstrap.dll"]

Просмотреть файл

@ -1,4 +1,6 @@
# The image of hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest is built using src/Dcoker/Dockerfile,
# it should be repalced by your own build image.
FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest as final
WORKDIR /app/Dashboard
ENTRYPOINT ["dotnet", "Dashboard.dll"]

Просмотреть файл

@ -1,5 +1,33 @@
# The image of hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest is built using src/Dcoker/Dockerfile,
# it should be repalced by your own build image.
# portal/dist is the directory that stores portal build result.
# The portal code stores in https://github.com/Azure/hpcpack-acm-portal,
# we recommend you put the build result under the directory of src/portal/dist,
# or under other directoies which you should modify this dockerfile to get build result.
# You could also get portal build code using docker build environment, but it's not a good practise, then the frontend image docker file shows as below:
# FROM ubuntu AS portal
# RUN apt-get update \
# && apt-get install -y gnupg2 \
# apt-utils \
# curl \
# && curl -sL https://deb.nodesource.com/setup_8.x | bash - \
# && apt-get install -y nodejs \
# git \
# && git clone https://github.com/Azure/hpcpack-acm-portal.git /app/portal
# WORKDIR /app/portal
# RUN npm install \
# && npm install -g @angular/cli \
# && ng build --prod
# FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest as final
# EXPOSE 5000
# WORKDIR /app/Frontend
# COPY --from=portal /app/portal/dist /app/Frontend/wwwroot
# ENTRYPOINT ["/bin/bash", "/app/scripts/start-frontend.sh"]
FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest as final
EXPOSE 5000
WORKDIR /app/Frontend
COPY portal/dist /app/Frontend/wwwroot
ENTRYPOINT ["/bin/bash", "/app/scripts/start-frontend.sh"]

Просмотреть файл

@ -1,4 +1,6 @@
# The image of hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest is built using src/Dcoker/Dockerfile,
# it should be repalced by your own build image.
FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest as final
WORKDIR /app/JobMonitor
ENTRYPOINT ["dotnet", "JobMonitor.dll"]

Просмотреть файл

@ -1,7 +0,0 @@
FROM ubuntu AS angular
RUN apt-get update && apt-get install -y gnupg2
RUN apt-get install -y apt-utils
RUN apt-get install -y curl
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y nodejs
RUN npm install --unsafe-perm -g @angular/cli

Просмотреть файл

@ -1,4 +1,6 @@
# The image of hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest is built using src/Dcoker/Dockerfile,
# it should be repalced by your own build image.
FROM hpcacmbuild.azurecr.io/public/hpcpack/hpcacm:latest as final
WORKDIR /app/TaskDispatcher
ENTRYPOINT ["dotnet", "TaskDispatcher.dll"]

Просмотреть файл

@ -1,13 +0,0 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
src/portal/.gitignore поставляемый
Просмотреть файл

@ -1,42 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db

Просмотреть файл

@ -1,10 +0,0 @@
# This is the Dockerfile for building dev/test box.
FROM teracy/angular-cli:1.5.0
RUN \
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrom.list' \
&& apt-get update && apt-get install -y google-chrome-stable
RUN groupadd -r dev && useradd --no-log-init -r -m -s /bin/bash -g dev dev

Просмотреть файл

@ -1,77 +0,0 @@
# HPC Portal
## Angular CLI
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.5.3.
### Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
### Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
### Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
### Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
### Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
### Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
## Docker on Windows
To develop in Docker on Windows, firstly [register for a Docker account](https://www.docker.com/) and [get Docker](https://www.docker.com/get-docker) on Windows. Then you can use a Docker container as a "runtime box" for your Angular project. Do it as the followings.
Open a "cmd" shell, and log in with your Docker account by
`docker login`
Then cd into the portal project root(like ".../hpc-acm/src/portal") and execute:
`docker run --rm -it -v %cd%:/opt/app -w /opt/app --name portal -p 4200:4200 louirobert/angular-cli-with-chrome:1.0.0 /bin/bash`
It mounts the current directory `%cd%` to `/opt/app` in the Docker container's system, sets `/opt/app` as the working dirand opens an interactive Bash shell inside the docker container. It also maps the port 4200 of the container to the host's 4200 port.
For the first time(or when you update package.json), you need to install(or update) npm packages. Do it inside the docker container's shell:
`npm install`
Then start the dev server by:
`npm start`
Then you got it!
If you already had `npm install`, then you can start devlopment simply by an one line command from Windows "cmd" shell:
`docker run --rm -v %cd%:/opt/app -w /opt/app --name portal -p 4200:4200 louirobert/angular-cli-with-chrome:1.0.0 /bin/bash -c "npm start"`
Still, it has to be under the portal project root for `%cd%` to work.
After use, stop the container by
`docker stop portal`
Note that simply "Ctrl+C" doesn't stop a container(which can be observed by `docker container list`).
To run unit test, you need to run the docker container as a non-privileged user dev and with a `--privileged` argument(it has something to do with the Chrome browser for test):
`docker run --rm -it -v %cd%:/opt/app -w /opt/app --name portal2 --user dev --privileged -p 9876:9876 louirobert/angular-cli-with-chrome:1.0.0 /bin/bash`
Note: port 9876 is used by karma test server. If no need to access it(that means no test debugger) from outside of the container, then it doesn't have to be mapped.
Then execute:
`npm test`
in the container's shell.

Просмотреть файл

@ -1,122 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"hpc": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "hpc:build"
},
"configurations": {}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "hpc:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
"src/styles.css"
],
"assets": [
"src/assets",
"src/favicon.ico"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"hpc-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "hpc:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "hpc",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "css"
},
"@schematics/angular:directive": {
"prefix": "app"
}
}
}

Просмотреть файл

@ -1,14 +0,0 @@
import { AppPage } from './app.po';
describe('hpc App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});

Просмотреть файл

@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

Просмотреть файл

@ -1,14 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

Просмотреть файл

@ -1,31 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
singleRun: false,
});
};

11623
src/portal/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,60 +0,0 @@
{
"name": "hpc",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0 --disable-host-check --poll 2000",
"build": "ng build --prod",
"test": "ng test --source-map --poll 2000",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "7.1.1",
"@angular/cdk": "^7.1.1",
"@angular/common": "7.1.1",
"@angular/compiler": "7.1.1",
"@angular/core": "7.1.1",
"@angular/forms": "7.1.1",
"@angular/http": "7.1.1",
"@angular/material": "7.1.1",
"@angular/platform-browser": "7.1.1",
"@angular/platform-browser-dynamic": "7.1.1",
"@angular/router": "7.1.1",
"angular-in-memory-web-api": "^0.7.0",
"angular-tree-component": "^8.0.1",
"angular2-chartjs": "^0.5.1",
"core-js": "^2.5.5",
"hoek": "^6.1.2",
"moment": "^2.19.3",
"ng2-dragula": "^2.1.1",
"rxjs": "^6.3.3",
"rxjs-compat": "^6.3.3",
"ssri": "^6.0.1",
"tslib": "^1.9.3",
"zone.js": "^0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.11.1",
"@angular/cli": "7.1.1",
"@angular/compiler-cli": "7.1.1",
"@angular/language-service": "7.1.1",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "^4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.6"
}
}

Просмотреть файл

@ -1,28 +0,0 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

Просмотреть файл

@ -1,15 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: 'app/main/main.module#MainModule'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule],
})
export class AppRoutingModule { }

Просмотреть файл

@ -1,2 +0,0 @@
<router-outlet>
</router-outlet>

Просмотреть файл

@ -1,91 +0,0 @@
.app-container {
min-height: 900px;
margin-top: 64px;
}
.main-container {
padding: 1em;
}
mat-sidenav.sidenav {
position: fixed;
top: 64px;
left: 0;
}
mat-toolbar {
display: flex;
justify-content: space-between;
}
.left {
display: flex;
align-items: center;
}
.home {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
}
.home:visited {
color: inherit;
}
.home h1 {
display: inline-block;
margin-left: 4px;
}
.active {
background-color: #fafafa;
color: #3f51b5;
/* border-left: 3px solid #3f51b5; */
}
.title {
margin-left: 10px;
}
app-breadcrumb {
margin-left: 1.5em;
font-size: 72%;
}
.notification-num {
position: absolute;
display: block;
width: 16px;
height: 16px;
line-height: 16px;
top: 2px;
right: 2px;
border-radius: 50%;
background-color: #ff0833;
}
.notification-num.hidden {
display: none;
}
.mat-menu-item {
display: flex;
align-items: center;
}
.menu-item-text {
margin-left: 0.5em;
}
.nav-icon {
font-size: 1.1em;
}
.toolbar {
position: fixed;
top: 0;
left: 0;
z-index: 100;
}

Просмотреть файл

@ -1,63 +0,0 @@
import { TestBed, ComponentFixture, async } from '@angular/core/testing';
import { Component, Directive, Input } from '@angular/core';
import { AppComponent } from './app.component';
import { MaterialsModule } from './materials.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from './services/auth.service';
import { ApiService } from './services/api.service';
@Component({ selector: 'router-outlet', template: '' })
class RouterOutletStubComponent { }
const authServiceStub = {
isLoggedIn: true,
user: { name: 'Test User' },
logout: () => { },
getUserInfo:() => {}
}
const apiServiceStub = {}
const routerStub = {
navigate: () => { },
}
const activatedRouteStub = {}
fdescribe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
RouterOutletStubComponent,
],
imports: [
NoopAnimationsModule,
MaterialsModule,
],
providers: [
{ provide: AuthService, useValue: authServiceStub },
{ provide: ApiService, useValue: apiServiceStub },
{ provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub },
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
fixture.detectChanges();
});
});

Просмотреть файл

@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { AuthService } from './services/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(public authService: AuthService) {
this.authService.getUserInfo();
}
}

Просмотреть файл

@ -1,55 +0,0 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { MaterialsModule } from './materials.module';
import { AppRoutingModule } from './app-routing.module';
import { AuthService } from './services/auth.service';
import { LoginGuardService } from './services/login-guard.service';
import { ApiService } from './services/api.service';
import { UserSettingsService } from './services/user-settings.service';
import { LocalStorageService } from './services/local-storage.service';
import { InMemoryDataService } from './services/in-memory-data.service';
import { AppComponent } from './app.component';
import { WidgetsModule } from './widgets/widgets.module';
import { JobStateService } from './services/job-state/job-state.service';
import { TableService } from './services/table/table.service';
import { VirtualScrollService } from './services/virtual-scroll/virtual-scroll.service';
import { DateFormatterService } from './services/date-formatter/date-formatter.service';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DiagReportService } from './services/diag-report/diag-report.service';
import { DragulaModule } from 'ng2-dragula';
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
MaterialsModule,
WidgetsModule,
AppRoutingModule,
DragulaModule.forRoot(),
ScrollingModule
],
providers: [
AuthService,
LoginGuardService,
ApiService,
JobStateService,
DateFormatterService,
TableService,
VirtualScrollService,
UserSettingsService,
LocalStorageService,
DiagReportService
],
bootstrap: [AppComponent]
})
export class AppModule { }

Просмотреть файл

@ -1,33 +0,0 @@
ol {
display: block;
margin: 0;
padding: 0;
}
li {
display: inline-block;
/* The following rule should really be placed outside. However, due to
* Angular's issue, it has to be here. */
vertical-align: sub;
}
li + li {
margin-left: 5px;
}
li + li::before {
content: '/';
}
a {
text-decoration: none;
color: inherit;
}
a:visited {
color: inherit;
}
.last {
color: #c5c5c5;
}

Просмотреть файл

@ -1,9 +0,0 @@
<ol>
<!-- Do not format the following line! It's for removing a space between li. -->
<li><a routerLink="">Home</a></li><li *ngFor="let item of breadcrumbs; last as last">
<span *ngIf="last; else link" class="last">{{ item.label }}</span>
<ng-template #link>
<a [routerLink]="[item.url, item.params]">{{ item.label }}</a>
</ng-template>
</li>
</ol>

Просмотреть файл

@ -1,56 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs/observable/of';
import { BreadcrumbComponent } from './breadcrumb.component';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
import { Directive, Input } from '@angular/core';
const activatedRouteStub = {
paramMap: of({ get: () => 1 })
}
const routerStub = {
navigate: () => { },
events: of(new NavigationEnd(0, '', ''))
}
@Directive({
selector: '[routerLink]',
host: { '(click)': 'onClick()' }
})
class RouterLinkDirectiveStub {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
fdescribe('BreadcrumbComponent', () => {
let component: BreadcrumbComponent;
let fixture: ComponentFixture<BreadcrumbComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
RouterLinkDirectiveStub,
BreadcrumbComponent
],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useValue: routerStub },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BreadcrumbComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -1,67 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, NavigationEnd, PRIMARY_OUTLET } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { filter } from "rxjs/operators";
@Component({
selector: 'app-breadcrumb',
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.css']
})
export class BreadcrumbComponent implements OnInit {
public breadcrumbs = [];
private subcription: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router
) {
this.subcription = this.router.events.pipe(
filter(event => {
return event instanceof NavigationEnd
})
).subscribe((event) => {
let root = this.route.root;
this.breadcrumbs = this.getBreadcrumbs(root);
});
}
ngOnInit() {
}
ngOnDestroy() {
if (this.subcription)
this.subcription.unsubscribe();
}
private getBreadcrumbs(route, url = "", breadcrumbs = []) {
const ROUTE_DATA_BREADCRUMB: string = "breadcrumb";
let children: ActivatedRoute[] = route.children;
if (children.length === 0)
return breadcrumbs;
let child;
for (child of children) {
if (child.outlet === PRIMARY_OUTLET)
break;
}
if (!child.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB))
return this.getBreadcrumbs(child, url, breadcrumbs);
let routeURL: string = child.snapshot.url.map(segment => segment.path).join("/");
if (routeURL === '')
return this.getBreadcrumbs(child, url, breadcrumbs);
url += `/${routeURL}`;
breadcrumbs.push({
label: child.snapshot.data[ROUTE_DATA_BREADCRUMB],
params: child.snapshot.params,
url: url
});
return this.getBreadcrumbs(child, url, breadcrumbs);
}
}

Просмотреть файл

@ -1,18 +0,0 @@
<h2 mat-dialog-title>Run Command</h2>
<mat-dialog-content>
<mat-form-field class="cmd-line" *ngIf="isSingleCmd">
<input [(ngModel)]="command" matInput placeholder="Your command here, press enter to excute" value="" (keyup.enter)="runCmd()">
</mat-form-field>
<mat-form-field class="cmd-line" *ngIf="!isSingleCmd">
<textarea matInput cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="2" cdkAutosizeMaxRows="15"
placeholder="New script block here, press ctrl+enter to excute" [(ngModel)]='command' (keyup.control.Enter)="runCmd()"></textarea>
</mat-form-field>
<mat-form-field>
<input [(ngModel)]="timeout" (keyup.enter)="runCmd()" matInput placeholder="command timeout" type="number">
<span matSuffix>sec</span>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-stroked-button [mat-dialog-close]="false">Cancel</button>
<button mat-flat-button color='primary' (click)="runCmd()">Run</button>
</mat-dialog-actions>

Просмотреть файл

@ -1,16 +0,0 @@
mat-vertical-stepper {
clear: both;
}
mat-dialog-actions {
justify-content: flex-end;
button {
margin-right: 10px;
font-size: 13px;
}
}
.cmd-line {
width: 100%;
}

Просмотреть файл

@ -1,41 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommandInputComponent } from './command-input.component';
import { MaterialsModule } from '../../materials.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material';
import { FormsModule } from '@angular/forms';
class MatDialogModuleMock { }
fdescribe('CommandInputComponent', () => {
let component: CommandInputComponent;
let fixture: ComponentFixture<CommandInputComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CommandInputComponent],
imports: [MaterialsModule, MatDialogModule, NoopAnimationsModule, FormsModule],
providers: [
{ provide: MatDialogRef, useClass: MatDialogModuleMock },
{ provide: MAT_DIALOG_DATA, useValue: { command: 'test command', isSingleCmd: true } }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommandInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
fixture.whenStable().then(() => {
// ngModel should be available here
let text = fixture.nativeElement.querySelector('input').value;
expect(text).toEqual('test command');
})
});
});

Просмотреть файл

@ -1,39 +0,0 @@
import { Component, OnInit, Inject, NgZone, ViewChild } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { take } from 'rxjs/operators';
@Component({
templateUrl: './command-input.component.html',
styleUrls: ['./command-input.component.scss']
})
export class CommandInputComponent implements OnInit {
public command: string = '';
public timeout: number = 1800;
public isSingleCmd: boolean;
@ViewChild('autosize') autosize: CdkTextareaAutosize;
constructor(
public dialogRef: MatDialogRef<CommandInputComponent>,
private ngZone: NgZone,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.command = data.command;
this.timeout = data.timeout;
this.isSingleCmd = data.isSingleCmd;
}
ngOnInit() { }
runCmd() {
let params = { command: this.command, timeout: this.timeout };
this.dialogRef.close(params);
}
triggerResize() {
// Wait for changes to be applied, then trigger textarea resize.
this.ngZone.onStable.pipe(take(1))
.subscribe(() => this.autosize.resizeToFitContent(true));
}
}

Просмотреть файл

@ -1,38 +0,0 @@
<div class="control static up button-text" *ngIf="bof; else prevButton">
Begin of Output
</div>
<ng-template #prevButton>
<button mat-button class="control up" [ngClass]="{ attention: !loading && !disabled }" (click)="loadPrev.emit(output)" [disabled]="loading || disabled"
matTooltip="Load more content before">
<span *ngIf="loading == 'prev'; else upArrow" class="button-text">Loading...</span>
<ng-template #upArrow>
<i class="material-icons">keyboard_arrow_up</i>
</ng-template>
</button>
</ng-template>
<div class="content">
<pre (scroll)="onScroll($event)" #output>{{content}}</pre>
<div class="up-to-top">
<button mat-icon-button matTooltip="Go to the begin of output" (click)="gotoTop.emit(output)" [disabled]="loading || disabled">
<i class="material-icons upward">arrow_upward</i>
</button>
</div>
</div>
<div class="control static down button-text" *ngIf="eof; else nextButton">
End of Output
</div>
<ng-template #nextButton>
<button mat-button class="control down" [ngClass]="{ attention: !loading && !disabled }" (click)="loadNext.emit(output)"
[disabled]="loading || disabled" matTooltip="Load more content after">
<span *ngIf="loading == 'next'; else downArrow" class="button-text">Loading...</span>
<ng-template #downArrow>
<i class="material-icons">keyboard_arrow_down</i>
</ng-template>
</button>
</ng-template>
<div class="control" *ngIf="loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>

Просмотреть файл

@ -1,58 +0,0 @@
.content {
position: relative;
border: 1px solid #eee;
}
.content .up-to-top {
position: absolute;
bottom: 2em;
right: 1.5em;
color: floralwhite;
background-color: rgba(0, 0, 0, 0.4);
}
pre {
color: white;
background-color: black;
overflow: auto;
box-sizing: border-box;
margin: 0;
padding: 1em;
height: 600px;
white-space: pre-wrap;
}
@keyframes blink {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.attention {
animation: blink 1s 5;
}
button.control {
width: 100%;
background-color: #ffffff;
border: 1px solid #eee;
color: #3f51b5;
}
.control+.control {
margin-top: 5px;
}
.control.static {
text-align: center;
background-color: #ffffff;
padding: 8px;
border: 1px solid #eee;
}
.button-text {
color: #3f51b5;
}

Просмотреть файл

@ -1,31 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommandOutputComponent } from './command-output.component';
import { MaterialsModule } from '../../materials.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
fdescribe('CommandOutputComponent', () => {
let component: CommandOutputComponent;
let fixture: ComponentFixture<CommandOutputComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CommandOutputComponent],
imports: [MaterialsModule, NoopAnimationsModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommandOutputComponent);
component = fixture.componentInstance;
component.content = 'test content';
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
let text = fixture.nativeElement.querySelector('pre').textContent;
expect(text).toEqual('test content');
});
});

Просмотреть файл

@ -1,90 +0,0 @@
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'command-output',
templateUrl: './command-output.component.html',
styleUrls: ['./command-output.component.scss']
})
export class CommandOutputComponent implements OnInit {
@Output()
loadPrev = new EventEmitter<any>();
@Output()
loadNext = new EventEmitter<any>();
@Output()
gotoTop = new EventEmitter<any>();
@Input()
content: string = '';
@Input()
disabled: boolean = false;
@Input()
loading: string | boolean = false;
//Got Begin of File
@Input()
bof: boolean = false;
//Got End of File
@Input()
eof: boolean = false;
@ViewChild('output')
private output: ElementRef;
constructor() { }
ngOnInit() {
}
private scrollPos = 0;
private scrollTimer;
private scrollDelay = 200;
private scrollThreshold = 0.20;
onScroll($event, debounced = false, downward = undefined) {
if (this.disabled) {
return;
}
if (!debounced) {
if (this.scrollTimer) {
clearTimeout(this.scrollTimer);
}
let top = $event.srcElement.scrollTop;
let downward = top >= this.scrollPos;
this.scrollTimer = setTimeout(() => this.onScroll($event, true, downward), this.scrollDelay);
this.scrollPos = top;
}
else {
clearTimeout(this.scrollTimer);
this.scrollTimer = null;
let elem = $event.srcElement;
let height = elem.scrollHeight;
let up = elem.scrollTop / height;
let mid = elem.clientHeight / height;
let down = 1 - up - mid;
if (downward) {
if (down <= this.scrollThreshold) {
this.loadNext.emit(elem);
}
}
else if (up <= this.scrollThreshold) {
this.loadPrev.emit(elem);
}
}
}
scrollToBottom(): void {
let elem = this.output.nativeElement;
elem.scrollTop = elem.scrollHeight;
}
}

Просмотреть файл

@ -1,23 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CommandComponent } from './command.component';
import { ResultListComponent } from './result-list/result-list.component';
import { ResultDetailComponent } from './result-detail/result-detail.component';
import { MultiCmdsComponent } from './multi-cmds/multi-cmds.component';
const routes: Routes = [{
path: '',
component: CommandComponent,
children: [
{ path: 'results', component: ResultListComponent, data: { breadcrumb: "Results" } },
{ path: 'results/:id', component: ResultDetailComponent, data: { breadcrumb: "Result" } },
{ path: 'multi-cmds', component: MultiCmdsComponent, data: { breadcrumb: 'Multi-cmds' } },
{ path: '', redirectTo: 'results', pathMatch: 'full' },
],
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class CommandRoutingModule { }

Просмотреть файл

Просмотреть файл

@ -1 +0,0 @@
<router-outlet></router-outlet>

Просмотреть файл

@ -1,29 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { CommandComponent } from './command.component';
@Component({ selector: 'router-outlet', template: '' })
class RouterOutletStubComponent {}
fdescribe('CommandComponent', () => {
let component: CommandComponent;
let fixture: ComponentFixture<CommandComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CommandComponent, RouterOutletStubComponent ],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommandComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -1,13 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-command',
templateUrl: './command.component.html',
styleUrls: ['./command.component.css']
})
export class CommandComponent implements OnInit {
constructor() {}
ngOnInit() {
}
}

Просмотреть файл

@ -1,33 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'
import { ChartModule } from 'angular2-chartjs';
import { MaterialsModule } from '../materials.module';
import { WidgetsModule } from '../widgets/widgets.module';
import { CommandRoutingModule } from './command-routing.module';
import { CommandComponent } from './command.component';
import { ResultListComponent } from './result-list/result-list.component';
import { ResultDetailComponent } from './result-detail/result-detail.component';
import { CommandOutputComponent } from './command-output/command-output.component';
import { NodeSelectorComponent } from './node-selector/node-selector.component';
import { CommandInputComponent } from './command-input/command-input.component';
import { SharedModule } from '../shared.module';
import { TaskErrorComponent } from './node-selector/task-error/task-error.component';
import { MultiCmdsComponent } from './multi-cmds/multi-cmds.component';
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [
CommonModule,
CommandRoutingModule,
MaterialsModule,
WidgetsModule,
FormsModule,
ChartModule,
SharedModule,
ScrollingModule
],
declarations: [CommandComponent, ResultListComponent, ResultDetailComponent, CommandOutputComponent, NodeSelectorComponent, CommandInputComponent, TaskErrorComponent, MultiCmdsComponent],
entryComponents: [CommandInputComponent, TaskErrorComponent],
})
export class CommandModule { }

Просмотреть файл

@ -1,129 +0,0 @@
<div class="title main">
<div class="job-state">
<div class="name">
<div class="job-progress">
<div class="state-text" [ngClass]="stateClass(result.state)">{{result.state}}</div>
<div class="progress">
<mat-progress-bar mode="determinate" [value]="result.progress * 100" class="progress-bar"></mat-progress-bar>
<div class="progress-number">{{result.progress | percent}}</div>
</div>
</div>
<div *ngIf="isSingleCmd(result.command)" class="job-info"> {{id}} - {{result.command}} </div>
<div *ngIf="!isSingleCmd(result.command)" class="job-info"> {{id}} - <div class="block-script-title" (click)="toggleScriptBlock()">Scipt
Block</div>
</div>
</div>
</div>
<div class="operations" *ngIf="!initializing">
<div class="operation" (click)="newCommand()">
<i class="material-icons rerun">content_copy</i>
<div class="operation-name">Clone</div>
</div>
<div class="cancel-job">
<div class="operation" *ngIf="!isOver" (click)="cancelCommand()">
<i class="material-icons operation-icon cancel">clear</i>
<div class="operation-name">Cancel</div>
</div>
<div class="operation-text" *ngIf="!isOver && canceling">
<div class="operation-name">Waiting for cancel request finish...</div>
</div>
</div>
</div>
</div>
<pre class="block-script-content" *ngIf="scriptBlock">{{result.command}}</pre>
<ng-container *ngIf="isLoaded; else waiting">
<div class="container-fluid">
<div class="row">
<div class="col-md-3 selection">
<node-selector [nodes]="result.nodes" [loadFinished]='loadFinished' [maxPageSize]="maxPageSize" (select)="selectNode($event)"
(updateLastIdEvent)="onUpdateLastIdEvent($event)" [nodeOutputs]="nodeOutputs" [empty]="empty">
</node-selector>
</div>
<mat-tab-group mat-align-tabs="start" class="col-md-9" [selectedIndex]="selected.value" (selectedIndexChange)="changeTab($event)">
<mat-tab *ngFor="let tab of this.tabs; index as i">
<ng-template mat-tab-label>
<div class="cmd-tab">
<mat-icon class="tab-icon" color="primary" *ngIf='isJobOver(tab.state)'>call_to_action</mat-icon>
<mat-spinner *ngIf='!isJobOver(tab.state)' [diameter]="15"></mat-spinner>
<div class="tab-text">{{tab.id}} - {{tab.command}}</div>
</div>
<button mat-icon-button (click)="closeTab(i)" *ngIf="tabs.length > 1">
<mat-icon class="tab-icon">close</mat-icon>
</button>
</ng-template>
<div class="output">
<command-output (loadPrev)="loadPrevAndScroll(selectedNode, $event)" (loadNext)="loadNext(selectedNode)"
(gotoTop)="loadFromBeginAndScroll(selectedNode, $event)" [content]="currentOutput?.content" [disabled]="isOutputDisabled"
[loading]="loading" [bof]="currentOutput?.start === 0" [eof]="currentOutput?.end">
</command-output>
<div class="control bottom">
<a [href]="currentOutputUrl" *ngIf="currentOutputUrl">
<i class="material-icons">file_download</i> Download the whole output
</a>
<mat-checkbox color="primary" [disabled]="loading && loading != 'auto'" [checked]="autoload" (change)="toggleAutoload($event.checked)">Autoscroll</mat-checkbox>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-6">
<mat-radio-group [(ngModel)]="commandLine" (change)="changeMode(commandLine)">
<mat-radio-button value="single" color="primary" class="radio-btn">
Single Line Command
</mat-radio-button>
<mat-radio-button value="multiple" color="primary" class="radio-btn">
Script Block ( Linux )
</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div class="row">
<div class="col-md-3">
</div>
<mat-form-field class="col-md-3">
<input matInput placeholder="timeout" type="number" class="timeout-text" [(ngModel)]="timeout">
<span matSuffix>sec</span>
</mat-form-field>
</div>
<div class="row justify-content-end">
<mat-form-field class="col-md-7" *ngIf="commandLine == 'single'">
<input matInput placeholder="New single line command here, press enter to excute" [(ngModel)]='newCmd'
(keyup.ArrowUp)="getPreviousCmd()" (keyup.ArrowDown)="getNextCmd()" (keyup.Enter)="excuteCmd()">
</mat-form-field>
<mat-form-field class="col-md-7" *ngIf="commandLine == 'multiple'">
<textarea matInput cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="2"
cdkAutosizeMaxRows="15" placeholder="New script block here, press ctrl+enter to excute" [(ngModel)]='newCmd'
(keyup.ArrowUp)="getPreviousCmd()" (keyup.ArrowDown)="getNextCmd()" (keyup.control.Enter)="excuteCmd()"></textarea>
</mat-form-field>
<div class="col-md-2 excute-btn">
<button mat-flat-button color="primary" (click)="excuteCmd()">Excute</button>
</div>
</div>
</div>
</ng-container>
<ng-template #waiting>
<div class="waiting">
<p>{{errorMsg}}</p>
<div class="container-fluid">
<div class="row">
<div class="col-md-3 selection">
<node-selector [nodes]="result?.nodes">
</node-selector>
</div>
<div class="col-md-9 output">
<command-output [disabled]="true" [loading]="true">
</command-output>
</div>
</div>
</div>
</div>
</ng-template>

Просмотреть файл

@ -1,98 +0,0 @@
@import "../../stylesheets/job-result.scss";
.waiting {
mat-spinner {
margin: 1em auto;
}
text-align: center;
}
.actions {
margin-top: 1em;
}
.container-fluid {
margin-top: 1em;
}
.selection,
.output {
max-height: 800px;
margin-bottom: 1em;
}
.control.bottom {
display: flex;
justify-content: space-between;
padding: 5px;
margin-top: 0.5em;
a {
color: inherit;
}
a .material-icons {
vertical-align: top;
}
}
.cmd-tab {
display: flex;
align-items: center;
max-width: 200px;
}
.tab-icon {
font-size: 18px;
line-height: 24px;
}
.tab-text {
@include ellipsis-text;
}
:host ::ng-deep .mat-tab-labels .mat-tab-label {
padding: 0 2px;
.mat-tab-label-content {
width: 100%;
justify-content: space-between;
}
}
.radio-btn {
padding-right: 15px;
margin-bottom: 15px;
font-size: 14px;
}
.timeout-text {
font-size: 14px;
}
:host ::ng-deep .output .content pre {
height: 530px;
}
.excute-btn {
@include display-flex(start, center);
font-size: 13px;
}
.block-script-title {
display: inline-block;
text-decoration: underline solid $color;
&:hover {
cursor: pointer;
}
}
.block-script-content {
max-height: 50vh;
overflow-y: auto;
background: #fff;
padding: 10px 15px;
font-size: 14px;
}

Просмотреть файл

@ -1,158 +0,0 @@
import { async, ComponentFixture, TestBed, flush, fakeAsync } from '@angular/core/testing';
import { MultiCmdsComponent } from './multi-cmds.component';
import { Component, Output, EventEmitter, Input, SimpleChanges, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of } from 'rxjs';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { MaterialsModule } from '../../materials.module';
import { ApiService } from '../../services/api.service';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { TableService } from '../../services/table/table.service';
import { JobStateService } from '../../services/job-state/job-state.service';
@Component({ selector: 'command-output', template: '' })
class CommandOutputStubComponent {
@Output()
loadPrev = new EventEmitter<any>();
@Output()
loadNext = new EventEmitter<any>();
@Output()
gotoTop = new EventEmitter<any>();
@Input()
content: string = '';
@Input()
disabled: boolean = false;
@Input()
loading: string | boolean = false;
//Got Begin of File
@Input()
bof: boolean = false;
//Got End of File
@Input()
eof: boolean = false;
scrollToBottom() { }
}
@Component({ selector: 'node-selector', template: '' })
class NodeSelectorStubComponent {
@Input()
nodes: any[];
@Output()
select = new EventEmitter();
selectedNode: any;
ngOnChanges(changes: SimpleChanges) {
if (changes.nodes) {
let prevNode = this.selectedNode;
this.selectedNode = this.nodes[0];
this.select.emit({ node: this.nodes[0], prevNode });
}
}
}
class ApiServiceStub {
static job = { commandLine: 'TEST COMMAND', state: 'Finished', targetNodes: ['TEST NODE'] };
static tasks = [{ id: 1, name: 'TEST NODE', state: 'Finished' }];
static taskResult1 = { resultKey: 'key001' };
static outputContent = 'TEST CONTENT';
static outputUrl = 'TESTURL';
command: any;
constructor() {
this.command = jasmine.createSpyObj('Command', ['get', 'getTasksByPage', 'getTaskResult', 'getOutput', 'getDownloadUrl']);
this.command.get.and.returnValue(of(ApiServiceStub.job));
this.command.getTasksByPage.and.returnValue(of(ApiServiceStub.tasks));
this.command.getTaskResult.and.returnValue(of(ApiServiceStub.taskResult1));
let value = { content: ApiServiceStub.outputContent, size: ApiServiceStub.outputContent.length, offset: 0, end: true };
this.command.getOutput.and.returnValues(of(value));
this.command.getDownloadUrl.and.returnValue(ApiServiceStub.outputUrl);
}
}
const activatedRouteStub = {
queryParams: of({ firstJobId: 1 })
}
class JobStateServiceStub {
stateClass(state) {
return 'finished';
}
stateIcon(state) {
return 'done';
}
}
class TableServiceStub {
updateData(newData, dataSource, propertyName) {
return newData;
}
}
fdescribe('MultiCmdsComponent', () => {
let component: MultiCmdsComponent;
let fixture: ComponentFixture<MultiCmdsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
MultiCmdsComponent,
NodeSelectorStubComponent,
CommandOutputStubComponent
],
imports: [
NoopAnimationsModule,
FormsModule,
MaterialsModule
],
providers: [
{ provide: ApiService, useClass: ApiServiceStub },
{ provide: JobStateService, useClass: JobStateServiceStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: TableService, useClass: TableServiceStub }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MultiCmdsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', fakeAsync(() => {
flush();
expect(component).toBeTruthy();
let text = fixture.nativeElement.querySelector('.job-state .name').textContent;
expect(text).toContain(ApiServiceStub.job.commandLine);
text = fixture.nativeElement.querySelector('.state-text').textContent;
expect(text).toContain('Finished');
expect(component.currentOutput.content).toEqual(ApiServiceStub.outputContent);
let selectedNode = component.selectedNode;
expect(selectedNode.name).toEqual(ApiServiceStub.tasks[0].name);
expect(selectedNode.state).toEqual(ApiServiceStub.tasks[0].state);
let tabs = component.tabs;
expect(tabs.length).toEqual(1);
expect(tabs[0].id).toEqual(ApiServiceStub.tasks[0].id);
}));
});

Просмотреть файл

@ -1,768 +0,0 @@
import { Component, OnInit, ViewChild, ViewChildren, QueryList, NgZone } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { ApiService, Loop } from '../../services/api.service';
import { CommandOutputComponent } from '../command-output/command-output.component';
import { CommandInputComponent } from '../command-input/command-input.component';
import { ConfirmDialogComponent } from '../../widgets/confirm-dialog/confirm-dialog.component';
import { JobStateService } from '../../services/job-state/job-state.service';
import { FormControl } from '@angular/forms';
import { TableService } from '../../services/table/table.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { take } from 'rxjs/operators';
@Component({
selector: 'multi-cmds',
templateUrl: './multi-cmds.component.html',
styleUrls: ['./multi-cmds.component.scss'],
})
export class MultiCmdsComponent implements OnInit {
@ViewChildren(CommandOutputComponent)
private outputs: QueryList<CommandOutputComponent>;
@ViewChild('autosize') autosize: CdkTextareaAutosize;
public id: string;
public result: any;
private gotTasks: boolean = false;
private subcription: Subscription;
private jobLoop: object;
private nodesLoop: object;
private nodeLoop: object;
private errorMsg: string;
private autoload = true;
private outputInitOffset = -8192;
private outputPageSize = 8192;
public canceling = false;
public tabs = [];
public newCmd = '';
private commandIndex = 0;
private scriptIndex = 0;
public cmds = [];
public commandLine = 'single';
public timeout = 1800;
private lastId = 0;
public maxPageSize = 100;
public scrolled = false;
public loadFinished = false;
private reverse = true;
private selectedNodes = [];
pivot = Math.round(this.maxPageSize / 2) - 1;
startIndex = 0;
lastScrolled = 0;
public listLoading = false;
public empty = true;
private endId = -1;
public scriptBlock: boolean = false;
constructor(
private route: ActivatedRoute,
private api: ApiService,
private jobStateService: JobStateService,
private dialog: MatDialog,
private tableService: TableService,
private ngZone: NgZone,
) { }
ngOnInit() {
this.subcription = this.route.queryParams.subscribe(map => {
this.result = { state: 'unknown', command: '', nodes: [] };
this.id = map.firstJobId;
this.tabs = [];
this.tabs.push({ id: this.id, outputs: {}, command: '', state: '' });
this.updateJob(this.id);
this.updateNodes(this.id);
});
}
get initializing() {
return this.result.state == 'unknown' || this.result.state == '';
}
isSingleCmd(cmd) {
let match = /\r|\n/.exec(cmd);
return match ? false : true;
}
public toggleScriptBlock() {
this.scriptBlock = !this.scriptBlock;
}
get isLoaded(): boolean {
return this.gotTasks;
}
public stateClass(state) {
return this.jobStateService.stateClass(state);
}
updateJob(id) {
this.jobLoop = Loop.start(
//observable:
this.api.command.get(id),
//observer:
{
next: (job: any) => {
if (id != this.id) {
return;
}
this.result.state = job.state;
this.result.command = job.commandLine;
this.result.progress = job.progress;
this.tabs[this.selected.value].state = job.state;
if (!this.tabs[this.selected.value].command) {
this.tabs[this.selected.value].command = job.commandLine;
this.timeout = job.maximumRuntimeSeconds;
this.cmds.push({ mode: this.isSingleCmd(job.commandLine) ? 'single' : 'multiple', cmd: job.commandLine });
}
return true;
},
error: (err) => {
this.errorMsg = err;
}
}
);
}
private getTasksRequest() {
return this.api.command.getTasksByPage(this.id, this.lastId, this.maxPageSize);
}
updateNodes(id) {
this.nodesLoop = Loop.start(
//observable:
this.getTasksRequest(),
//observer:
{
next: (tasks) => {
if (id != this.id) {
return;
}
this.empty = false;
if (tasks.length > 0) {
this.gotTasks = true;
this.result.nodes = this.tableService.updateData(tasks, this.result.nodes, 'id');
if (this.endId != -1 && tasks[tasks.length - 1].id != this.endId) {
this.listLoading = false;
}
}
if (this.reverse && tasks.length < this.maxPageSize) {
this.loadFinished = true;
}
return this.getTasksRequest();
},
error: (err) => {
this.errorMsg = JSON.stringify(err);
}
}
);
}
onUpdateLastIdEvent(data) {
this.lastId = data.lastId;
this.endId = data.endId;
}
ngOnDestroy() {
if (this.subcription) {
this.subcription.unsubscribe();
}
this.stopCurrentLoop();
}
stopCurrentLoop() {
if (this.jobLoop) {
Loop.stop(this.jobLoop);
}
if (this.nodesLoop) {
Loop.stop(this.nodesLoop);
}
if (this.nodeLoop) {
Loop.stop(this.nodeLoop);
}
this.tabs.forEach(tab => {
for (let node in tab.outputs) {
let loop = tab.outputs[node].keyLoop;
tab.outputs[node].loading = false;
if (loop) {
Loop.stop(loop);
}
}
});
}
//This should work but not in fact! Because this.selector is set later than
//selectNode is called. That seems the NodeSelectorComponent can fire events
//before Angular captures it in this.selector. A surprise!
//
//get selectedNode(): any {
// return this.selector ? this.selector.selectedNode : null;
//}
selectedNode: any;
selectNode({ node, prevNode }) {
this.selectedNode = node;
if (prevNode) {
this.stopNodeOutputKeyLoop(prevNode);
this.stopAutoload(prevNode);
}
if (!node) {
return;
}
if (this.autoload) {
this.startAutoload(node);
}
else {
this.loadOnce(node);
}
}
isSelected(node) {
return node && this.selectedNode && node.name === this.selectedNode.name;
}
getNodeOutputKey(node, onGot) {
return Loop.start(
//observable:
Observable.create((observer) => {
this.api.command.getTaskResult(this.id, node.id).subscribe(
result => {
observer.next(result.resultKey);
observer.complete();
},
error => {
observer.error(error);
}
);
}),
//observer:
{
next: (key) => {
if (key) {
onGot(key);
return false;
}
//TODO: When is it impossible to get a key?
//if (this.isNodeOver(node)) {
// onGot(null);
// return false;
//}
return true;
},
error: (err) => {
if (err.status == 404 && !this.isNodeOver(node)) {
// return value is assigned to looper.ended in observer.err
// false means continue to query key result
return false;
}
else if (err.status == 404 && node.state == 'Finished') {
return false;
}
else {
onGot(err);
return true;
}
}
},
//interval(in ms):
1000,
);
}
getNodeOutput(node): any {
if (!this.tabs[this.selected.value]) {
return;
}
let selectedJobOutputs = this.tabs[this.selected.value].outputs;
let output = null;
if (selectedJobOutputs) {
output = selectedJobOutputs[node.name];
}
if (!output) {
selectedJobOutputs[node.name] = {
content: '',
next: this.outputInitOffset,
start: undefined,
end: undefined,
loading: false,
key: null,
error: ''
};
output = selectedJobOutputs[node.name];
let onKeyReady = (callback) => {
if (output.key === null) {
if (!output.keyLoop) {
output.loading = 'key';
output.keyLoop = this.getNodeOutputKey(node, (key) => {
let keyType = typeof (key);
output.loading = false;
if (key && keyType == 'string') {
output.key = key;
callback(true);
}
else if (key && keyType == 'object') {
output.error = key;
callback(false);
}
else {
output.key = false;
callback(false);
}
});
}
}
else if (output.key === false) { //No key, for no output
callback(false);
}
else {
callback(true);
}
}
(output as any).onKeyReady = onKeyReady;
}
return output;
}
stopNodeOutputKeyLoop(node) {
let output = this.tabs[this.selected.value].outputs[node.name];
if (output && output.keyLoop) {
Loop.stop(output.keyLoop);
output.keyLoop = null;
if (output.loading === 'key') {
output.loading = false;
}
}
}
updateNodeOutput(output, result): boolean {
//NOTE: There may be two inflight updates for the same piece of output, one
//by autoload and the other one by manual trigger. Drop the one arrives later.
if (output.next > result.offset) {
return false;
}
//Update start field when and only when it's not updated yet.
if (typeof (output.start) === 'undefined' && result.offset >= 0) {
output.start = result.offset;
}
//NOTE: result.end depends on passing an opt.over parameter to API getOutput.
output.end = result.end;
if (result.content) {
output.content += result.content;
}
output.next = result.offset + result.size;
return result.content ? true : false;
}
updateNodeOutputBackward(output, result): boolean {
//Update next field when and only when it's not updated yet.
if (output.next === this.outputInitOffset) {
output.next = result.offset + result.size;
}
if (result.content) {
output.content = result.content + output.content;
}
output.start = result.offset;
return result.content ? true : false;
}
stopAutoload(node): void {
let output = this.getNodeOutput(node)
output.loading = false;
if (this.nodeLoop) {
Loop.stop(this.nodeLoop);
this.nodeLoop = null;
}
}
startAutoload(node): void {
let output = this.getNodeOutput(node)
if (output.end || output.loading) {
return;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'auto';
this.nodeLoop = Loop.start(
//observable:
this.api.command.getOutput(output.key, output.next, this.outputPageSize),
//observer:
{
next: (result) => {
if (this.updateNodeOutput(output, result)) {
setTimeout(() => this.scrollOutputToBottom(), 0);
}
let over = output.end || !this.autoload;
if (over) {
output.loading = false;
}
return over ? false :
this.api.command.getOutput(output.key, output.next, this.outputPageSize);
},
error: (err) => {
output.loading = false;
output.error = err;
return true;
}
},
//interval(in ms):
0,
);
});
}
loadOnce(node) {
let output = this.getNodeOutput(node)
if (output.content || output.end || output.loading) {
return;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'once';
let opt = { fulfill: true, timeout: 2000 };
this.api.command.getOutput(output.key, output.next, this.outputPageSize, opt as any).subscribe(
result => {
output.loading = false;
this.updateNodeOutput(output, result);
},
error => {
output.loading = false;
output.error = error;
}
);
});
}
get loading(): boolean {
let output = this.currentOutput;
return output && output.loading;
}
get currentOutput(): any {
if (!this.selectedNode)
return;
return this.getNodeOutput(this.selectedNode);
}
get isOutputDisabled(): boolean {
return !this.selectedNode || this.currentOutput ? (!this.currentOutput.key) : true;
}
get currentOutputUrl(): string {
return this.isOutputDisabled ? '' : this.api.command.getDownloadUrl(this.currentOutput.key);
}
scrollOutputToBottom(): void {
this.outputs.forEach(e => {
e.scrollToBottom();
});
}
isJobOver(state): boolean {
return state == 'Finished' || state == 'Failed' || state == 'Canceled';
}
get isOver(): boolean {
let state = this.result.state;
return this.isJobOver(state);
}
isNodeOver(node): boolean {
let state = node.state;
return this.isJobOver(state);
}
toggleAutoload(enabled) {
this.autoload = enabled;
if (enabled) {
this.startAutoload(this.selectedNode);
}
else {
this.stopAutoload(this.selectedNode);
}
}
private scrollTop;
private scrollHeight;
loadPrevAndScroll(node, elem) {
this.scrollTop = elem.scrollTop;
this.scrollHeight = elem.scrollHeight;
this.loadPrev(node,
() => elem.scrollTop = elem.scrollHeight - this.scrollHeight + this.scrollTop);
}
loadPrev(node, onload = undefined) {
let output = this.getNodeOutput(node);
if (output.start === 0 || output.loading) {
return;
}
let prev;
let pageSize = this.outputPageSize;
if (output.start) {
prev = output.start - this.outputPageSize;
if (prev < 0) {
prev = 0;
pageSize = output.start;
}
}
else {
prev = this.outputInitOffset;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'prev';
let opt = { fulfill: true };
this.api.command.getOutput(output.key, prev, pageSize, opt as any)
.subscribe(
result => {
output.loading = false;
if (this.updateNodeOutputBackward(output, result) && onload
&& this.selectedNode && this.selectedNode.name == node.name) {
setTimeout(onload, 0);
}
},
error => {
output.loading = false;
output.error = error;
});
});
}
loadNext(node) {
let output = this.getNodeOutput(node)
if (output.end || output.loading) {
return;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'next';
let opt = { fulfill: true, timeout: 2000 };
this.api.command.getOutput(output.key, output.next, this.outputPageSize, opt as any)
.subscribe(
result => {
output.loading = false;
this.updateNodeOutput(output, result);
},
error => {
output.loading = false;
output.error = error;
});
});
}
loadFromBeginAndScroll(node, elem) {
this.loadFromBegin(node, () => elem.scrollTop = 0);
}
loadFromBegin(node, onload) {
let output = this.getNodeOutput(node)
if (output.loading) {
return;
}
if (output.start === 0 && onload) {
setTimeout(onload, 0);
return;
}
//Reset output for loading from the begin
output.content = '';
output.start = undefined;
output.end = undefined;
output.next = 0;
output.error = '';
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'top';
let opt = { fulfill: true, timeout: 2000 };
this.api.command.getOutput(output.key, output.next, this.outputPageSize, opt as any).subscribe(
result => {
output.loading = false;
this.updateNodeOutput(output, result);
setTimeout(onload, 0);
},
error => {
output.loading = false;
output.error = error;
});
});
}
selected = new FormControl(0);
excuteCmd() {
if (this.newCmd) {
let names = this.result.nodes.map(node => node.name);
this.api.command.create(this.newCmd, names, this.timeout).subscribe(obj => {
this.id = obj.id;
this.tabs.push({ id: this.id, outputs: {}, command: this.newCmd, state: '' });
this.cmds.push({ mode: this.isSingleCmd(this.newCmd) ? 'single' : 'multiple', cmd: this.newCmd });
this.selected.setValue(this.tabs.length - 1);
this.newCmd = '';
this.commandIndex = this.cmds.length - 1;
this.scriptIndex = this.cmds.length - 1;
});
}
}
newCommand() {
let dialogRef = this.dialog.open(CommandInputComponent, {
width: '98%',
data: { command: this.result.command, timeout: this.timeout, isSingleCmd: this.isSingleCmd(this.result.command) }
});
dialogRef.afterClosed().subscribe(params => {
if (params && params.command) {
let names = this.result.nodes.map(node => node.name);
this.api.command.create(params.command, names, params.timeout).subscribe(obj => {
this.id = obj.id;
this.tabs.push({ id: this.id, outputs: {}, command: params.command, state: '' });
this.cmds.push({ mode: this.isSingleCmd(params.command) ? 'single' : 'multiple', cmd: params.command });
this.selected.setValue(this.tabs.length - 1);
this.newCmd = '';
this.commandIndex = this.cmds.length - 1;
this.scriptIndex = this.cmds.length - 1;
});
}
});
}
changeTab(e) {
this.id = this.tabs[e].id;
this.selected.setValue(e);
this.stopCurrentLoop();
this.updateJob(this.id);
this.updateNodes(this.id);
if (this.autoload) {
this.startAutoload(this.selectedNode);
}
else {
this.loadOnce(this.selectedNode);
}
}
cancelCommand() {
let dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '45%',
data: {
title: 'Cancel',
message: 'Are you sure to cancel the current run of command?'
}
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.canceling = true;
this.api.command.cancel(this.id).subscribe(res => {
this.canceling = false;
});
}
});
}
closeTab(index) {
this.tabs.splice(index, 1);
}
getPreviousCmd() {
let index = this.commandLine == 'single' ? this.commandIndex : this.scriptIndex;
let previousCmd = this.cmds[index];
let tempMode;
let targetCmd;
while (index >= 0) {
let tempCmd = this.cmds[index--];
tempMode = tempCmd['mode'];
targetCmd = tempCmd['cmd'];
if (tempMode == this.commandLine)
break;
}
if (tempMode != this.commandLine) {
if (previousCmd['mode'] == this.commandLine) {
this.newCmd = previousCmd['cmd'];
}
}
else {
this.newCmd = targetCmd;
if (this.commandLine == 'single') {
this.commandIndex = index < 0 ? 0 : index;
}
else {
this.scriptIndex = index < 0 ? 0 : index;
}
}
}
getNextCmd() {
let index = this.commandLine == 'single' ? this.commandIndex : this.scriptIndex;
if (index + 1 == this.cmds.length) {
this.newCmd = '';
return;
}
let tempMode;
while (index + 1 < this.cmds.length) {
let tempCmd = this.cmds[++index];
tempMode = tempCmd['mode'];
this.newCmd = tempCmd['cmd'];
if (tempMode == this.commandLine)
break;
}
if (tempMode != this.commandLine) {
this.newCmd = '';
}
else {
if (this.commandLine == 'single') {
this.commandIndex = index;
}
else {
this.scriptIndex = index;
}
}
}
changeMode() {
this.commandIndex = (this.cmds.length - 1) > 0 ? this.cmds.length - 1 : 0;
this.scriptIndex = (this.cmds.length - 1) > 0 ? this.cmds.length - 1 : 0;
this.newCmd = '';
}
triggerResize() {
// Wait for changes to be applied, then trigger textarea resize.
this.ngZone.onStable.pipe(take(1))
.subscribe(() => this.autosize.resizeToFitContent(true));
}
}

Просмотреть файл

@ -1,47 +0,0 @@
<mat-form-field>
<mat-select placeholder="State" [(ngModel)]="state" (selectionChange)="filter()">
<mat-option *ngFor="let opt of states" [value]="opt">
{{ opt }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Name" [(ngModel)]="name" (keyup)="filter()">
</mat-form-field>
<div class="list-container">
<div class="list-header" [ngClass]="{'list-header-scrolled': showScrollBar}">
<div class="header node-name">
Name
</div>
<div class="header node-state">
state
</div>
</div>
<div class="list-content">
<cdk-virtual-scroll-viewport itemSize="40" #content class="list-content" (scrolledIndexChange)="indexChanged($event)">
<div *cdkVirtualFor="let node of nodes; templateCacheSize: 0; trackBy: trackByFn.bind(this)" class="list-item"
(click)="selectNode(node)" [ngClass]="{ selected: isSelected(node) }">
<div class="icon-cell node-name">
<div class="cell-text" *ngIf="!hasError(node)">{{node.name}}</div>
<div class="cell-text" *ngIf="hasError(node)">
<a class="error" (click)="showError(node)">{{node.name}}</a>
</div>
</div>
<div class="icon-cell node-state">
<i class="material-icons cell-icon" [ngClass]="stateClass(node.state)">{{stateIcon(node.state)}}</i>
<div class="cell-text">{{node.state}}</div>
</div>
</div>
</cdk-virtual-scroll-viewport>
<app-scroll-to-top [scrolled]="scrolled" [targetEle]="content"></app-scroll-to-top>
<div class="list-loading" *ngIf="empty">
<mat-spinner></mat-spinner>
</div>
</div>
</div>
<app-loading-progress-bar [loadFinished]="loadFinished" [hidden]="!loading || !scrolled" class="virtual-scroll-loading"></app-loading-progress-bar>

Просмотреть файл

@ -1,43 +0,0 @@
@import "../../stylesheets/table.scss";
@import "../../stylesheets/info.scss";
mat-form-field {
width: 100%;
}
.list-item {
cursor: pointer;
}
.list-item:hover {
background: rgba(0, 0, 0, .04);
}
.node-name {
flex: 1;
}
.node-state {
flex: 0.5;
}
.selected {
background-color: rgba(0, 0, 0, 0.08);
}
.mat-form-field input {
font-size: .9em;
}
.list-content {
height: 50vh;
}
.mat-select {
font-size: .9em;
}
.list-item a {
color: #d64205;
text-decoration-line: underline;
}

Просмотреть файл

@ -1,76 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NodeSelectorComponent } from './node-selector.component';
import { MaterialsModule } from '../../materials.module';
import { FormsModule } from '@angular/forms';
import { JobStateService } from '../../services/job-state/job-state.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SimpleChange, NO_ERRORS_SCHEMA } from '@angular/core';
import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { TableService } from '../../services/table/table.service';
class JobStateServiceStub {
stateClass(state) {
return 'finished';
}
stateIcon(state) {
return 'done';
}
}
class TableServiceStub {
trackByFn(item, colums) {
return false;
}
isContentScrolled(){
return false;
}
}
fdescribe('NodeSelectorComponent', () => {
let component: NodeSelectorComponent;
let fixture: ComponentFixture<NodeSelectorComponent>;
let viewport: CdkVirtualScrollViewport;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NodeSelectorComponent],
imports: [
MaterialsModule,
FormsModule,
NoopAnimationsModule,
ScrollingModule
],
providers: [
{ provide: JobStateService, useClass: JobStateServiceStub },
{ provide: TableService, useClass: TableServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NodeSelectorComponent);
component = fixture.componentInstance;
viewport = component.cdkVirtualScrollViewport;
component.nodes = [
{
name: "testNode",
state: 'Finished'
}
];
component.nodeOutputs = {};
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
component.ngOnChanges({
nodes: new SimpleChange([], component.nodes, false)
});
fixture.detectChanges();
expect(viewport.getDataLength()).toEqual(1);
})
});

Просмотреть файл

@ -1,137 +0,0 @@
import { Component, OnChanges, SimpleChanges, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { JobStateService } from '../../services/job-state/job-state.service';
import { MatDialog } from '@angular/material';
import { TaskErrorComponent } from './task-error/task-error.component';
import { VirtualScrollService } from '../../services/virtual-scroll/virtual-scroll.service';
import { TableService } from '../../services/table/table.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
selector: 'node-selector',
templateUrl: './node-selector.component.html',
styleUrls: ['./node-selector.component.scss']
})
export class NodeSelectorComponent implements OnChanges {
@ViewChild('content') cdkVirtualScrollViewport: CdkVirtualScrollViewport;
@Input()
nodes: Array<any>;
@Input()
nodeOutputs: any;
@Input()
loadFinished = false;
@Output()
select = new EventEmitter();
@Output()
updateLastIdEvent = new EventEmitter();
@Input()
empty = true;
@Input()
maxPageSize = 50;
state = 'All';
name = '';
selectedNode: any;
hasError(node) {
return this.nodeOutputs && this.nodeOutputs[node.name] && this.nodeOutputs[node.name]['error'] !== '';
}
public states = ['All', 'Queued', 'Running', 'Finished', 'Failed', 'Canceled'];
public displayedColumns = ['name', 'state'];
public scrolled = false;
pivot = Math.round(this.maxPageSize / 2) - 1;
startIndex = 0;
lastScrolled = 0;
public loading = false;
private endId = -1;
private lastId = 0;
constructor(
private jobStateService: JobStateService,
private dialog: MatDialog,
private tableService: TableService,
private virtualScrollService: VirtualScrollService
) { }
stateClass(state) {
return this.jobStateService.stateClass(state);
}
stateIcon(state) {
return this.jobStateService.stateIcon(state);
}
isSelected(node) {
return node && this.selectedNode && node.name === this.selectedNode.name;
}
ngOnChanges(changes: SimpleChanges) {
this.filter();
}
public filter() {
let res = this.nodes.filter(e => {
if (this.state != 'All' && e.state != this.state)
return false;
if (e.name.toLowerCase().indexOf(this.name.toLowerCase()) < 0)
return false;
return true;
});
this.nodes = res;
if (!this.selectedNode || res.findIndex((e) => e.name == this.selectedNode.name) < 0) {
this.selectNode(res[0]);
}
}
selectNode(node) {
if ((node && this.selectedNode && node.name == this.selectedNode.name)
|| (!node && !this.selectedNode)) {
return;
}
let prevNode = this.selectedNode;
this.selectedNode = node;
this.select.emit({ node, prevNode });
}
showError(node) {
let dialog = this.dialog.open(TaskErrorComponent, {
width: '70%',
data: this.nodeOutputs[node.name].error
});
}
trackByFn(index, item) {
return this.tableService.trackByFn(item, this.displayedColumns);
}
indexChanged($event) {
let result = this.virtualScrollService.indexChangedCalc(this.maxPageSize, this.pivot, this.cdkVirtualScrollViewport, this.nodes, this.lastScrolled, this.startIndex);
this.pivot = result.pivot;
this.lastScrolled = result.lastScrolled;
this.lastId = result.lastId == undefined ? this.lastId : result.lastId;
this.endId = result.endId == undefined ? this.endId : result.endId;
this.loading = result.loading;
this.startIndex = result.startIndex;
this.scrolled = result.scrolled;
this.updateLastIdEvent.emit({ lastId: this.lastId, endId: this.endId });
}
get showScrollBar() {
return this.tableService.isContentScrolled(this.cdkVirtualScrollViewport.elementRef.nativeElement);
}
}

Просмотреть файл

@ -1,15 +0,0 @@
<div class="dialog-title" mat-dialog-title>
<div class="job-info title">
<i class="material-icons diag-title-icon">event_note</i>
<div>Error Information</div>
</div>
<div class="close-icon" (click)="close()">
<i class="material-icons">close</i>
</div>
</div>
<mat-divider></mat-divider>
<mat-dialog-content>
<p class="error-message">
{{data.message}}
</p>
</mat-dialog-content>

Просмотреть файл

@ -1,3 +0,0 @@
@import "../../../stylesheets/mixin.scss";
@import "../../../stylesheets/info.scss";
@import "../../../stylesheets/dialog.scss";

Просмотреть файл

@ -1,40 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TaskErrorComponent } from './task-error.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { MaterialsModule } from '../../../materials.module';
class MatDialogModuleMock {
public close() { }
}
fdescribe('TaskErrorComponent', () => {
let component: TaskErrorComponent;
let fixture: ComponentFixture<TaskErrorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TaskErrorComponent],
imports: [MaterialsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: { message: 'error message' } },
{ provide: MatDialogRef, useClass: MatDialogModuleMock }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TaskErrorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
let errorMsg = fixture.nativeElement.querySelector('.error-message').textContent;
expect(errorMsg).toEqual(' error message ');
});
});

Просмотреть файл

@ -1,21 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'app-task-error',
templateUrl: './task-error.component.html',
styleUrls: ['./task-error.component.scss']
})
export class TaskErrorComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<TaskErrorComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) { }
ngOnInit() {
}
public close() {
this.dialogRef.close();
}
}

Просмотреть файл

@ -1,79 +0,0 @@
<div class="title main">
<div class="job-state">
<div class="name">
<div class="job-progress">
<div class="state-text" [ngClass]="stateClass(result.state)">{{result.state}}</div>
<div class="progress">
<mat-progress-bar mode="determinate" [value]="result.progress * 100" class="progress-bar"></mat-progress-bar>
<div class="progress-number">{{result.progress | percent}}</div>
</div>
</div>
<div *ngIf="isSingleCmd" class="job-info"> {{id}} - {{result.command}} </div>
<div *ngIf="!isSingleCmd" class="job-info"> {{id}} - <div class="block-script-title" (click)="toggleScriptBlock()">Scipt
Block</div>
</div>
</div>
</div>
<div class="operations" *ngIf="!initializing">
<div class="operation" (click)="newCommand()">
<i class="material-icons rerun">content_copy</i>
<div class="operation-name">Clone</div>
</div>
<div class="cancel-job">
<div class="operation" *ngIf="!isOver" (click)="cancelCommand()">
<i class="material-icons operation-icon cancel">clear</i>
<div class="operation-name">Cancel</div>
</div>
<div class="operation-text" *ngIf="!isOver && canceling">
<div class="operation-name">Waiting for cancel request finish...</div>
</div>
</div>
</div>
</div>
<pre class="block-script-content" *ngIf="scriptBlock">{{result.command}}</pre>
<ng-container *ngIf="isLoaded; else waiting">
<div class="container-fluid">
<div class="row">
<div class="col-md-3 selection">
<node-selector [nodes]="result.nodes" [loadFinished]='loadFinished' [maxPageSize]="maxPageSize" (select)="selectNode($event)"
(updateLastIdEvent)="onUpdateLastIdEvent($event)" [nodeOutputs]="nodeOutputs" [empty]="empty" #selector>
</node-selector>
</div>
<div class="col-md-9 output">
<command-output (loadPrev)="loadPrevAndScroll(selectedNode, $event)" (loadNext)="loadNext(selectedNode)"
(gotoTop)="loadFromBeginAndScroll(selectedNode, $event)" [content]="currentOutput?.content" [disabled]="isOutputDisabled"
[loading]="loading" [bof]="currentOutput?.start === 0" [eof]="currentOutput?.end" #output>
</command-output>
<div class="control bottom">
<a [href]="currentOutputUrl" *ngIf="currentOutputUrl">
<i class="material-icons">file_download</i> Download the whole output
</a>
<mat-checkbox color="primary" [disabled]="loading && loading != 'auto'" [checked]="autoload" (change)="toggleAutoload($event.checked)">Autoscroll</mat-checkbox>
</div>
</div>
</div>
</div>
</ng-container>
<ng-template #waiting>
<div class="waiting">
<p>{{errorMsg}}</p>
<div class="container-fluid">
<div class="row">
<div class="col-md-3 selection">
<node-selector [nodes]="result?.nodes">
</node-selector>
</div>
<div class="col-md-9 output">
<command-output [disabled]="true" [loading]="true">
</command-output>
</div>
</div>
</div>
</div>
</ng-template>

Просмотреть файл

@ -1,55 +0,0 @@
@import "../../stylesheets/job-result.scss";
.waiting {
mat-spinner {
margin: 1em auto;
}
text-align: center;
}
.actions {
margin-top: 1em;
}
.container-fluid {
margin-top: 1em;
}
.selection,
.output {
max-height: 800px;
margin-bottom: 1em;
}
.control.bottom {
display: flex;
justify-content: space-between;
padding: 5px;
margin-top: 0.5em;
a {
color: inherit;
}
a .material-icons {
vertical-align: top;
}
}
.block-script-title {
display: inline-block;
text-decoration: underline solid $color;
&:hover {
cursor: pointer;
}
}
.block-script-content {
max-height: 50vh;
overflow-y: auto;
background: #fff;
padding: 10px 15px;
font-size: 14px;
}

Просмотреть файл

@ -1,161 +0,0 @@
import { async, fakeAsync, flush, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef, SimpleChanges, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of } from 'rxjs/observable/of';
import { _throw } from 'rxjs/observable/throw';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../../services/api.service';
import { FormsModule } from '@angular/forms'
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialsModule } from '../../materials.module';
import { ResultDetailComponent } from './result-detail.component';
import { JobStateService } from '../../services/job-state/job-state.service';
import { TableService } from '../../services/table/table.service';
@Component({ selector: 'command-output', template: '' })
class CommandOutputStubComponent {
@Output()
loadPrev = new EventEmitter<any>();
@Output()
loadNext = new EventEmitter<any>();
@Output()
gotoTop = new EventEmitter<any>();
@Input()
content: string = '';
@Input()
disabled: boolean = false;
@Input()
loading: string | boolean = false;
//Got Begin of File
@Input()
bof: boolean = false;
//Got End of File
@Input()
eof: boolean = false;
scrollToBottom() { }
}
@Component({ selector: 'node-selector', template: '' })
class NodeSelectorStubComponent {
@Input()
nodes: any[];
@Output()
select = new EventEmitter();
selectedNode: any;
ngOnChanges(changes: SimpleChanges) {
if (changes.nodes) {
let prevNode = this.selectedNode;
this.selectedNode = this.nodes[0];
this.select.emit({ node: this.nodes[0], prevNode });
}
}
}
class ApiServiceStub {
static job = { commandLine: 'TEST COMMAND', state: 'Finished', targetNodes: ['TEST NODE'] };
static tasks = [{ id: 1, name: 'TEST NODE', state: 'Finished' }];
static taskResult1 = { resultKey: 'key001' };
static outputContent = 'TEST CONTENT';
static outputUrl = 'TESTURL';
command: any;
constructor() {
this.command = jasmine.createSpyObj('Command', ['get', 'getTasksByPage', 'getTaskResult', 'getOutput', 'getDownloadUrl']);
this.command.get.and.returnValue(of(ApiServiceStub.job));
this.command.getTasksByPage.and.returnValue(of(ApiServiceStub.tasks));
this.command.getTaskResult.and.returnValue(of(ApiServiceStub.taskResult1));
let value = { content: ApiServiceStub.outputContent, size: ApiServiceStub.outputContent.length, offset: 0, end: true };
this.command.getOutput.and.returnValues(of(value));
this.command.getDownloadUrl.and.returnValue(ApiServiceStub.outputUrl);
}
}
const activatedRouteStub = {
paramMap: of({ get: () => 1 })
}
const routerStub = {
navigate: () => { },
}
class JobStateServiceStub {
stateClass(state) {
return 'finished';
}
stateIcon(state) {
return 'done';
}
}
class TableServiceStub {
updateData(newData, dataSource, propertyName) {
return newData;
}
}
fdescribe('ClusrunResultDetailComponent', () => {
let component: ResultDetailComponent;
let fixture: ComponentFixture<ResultDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
NodeSelectorStubComponent,
CommandOutputStubComponent,
ResultDetailComponent,
],
imports: [
NoopAnimationsModule,
FormsModule,
MaterialsModule,
],
providers: [
{ provide: ApiService, useClass: ApiServiceStub },
{ provide: JobStateService, useClass: JobStateServiceStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useValue: routerStub },
{ provide: TableService, useClass: TableServiceStub }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', fakeAsync(() => {
flush();
expect(component).toBeTruthy();
let text = fixture.nativeElement.querySelector('.job-state .name').textContent;
expect(text).toContain(ApiServiceStub.job.commandLine);
text = fixture.nativeElement.querySelector('.state-text').textContent;
expect(text).toContain('Finished');
expect(component.currentOutput.content).toEqual(ApiServiceStub.outputContent);
let selectedNode = component.selectedNode;
expect(selectedNode.name).toEqual(ApiServiceStub.tasks[0].name);
expect(selectedNode.state).toEqual(ApiServiceStub.tasks[0].state);
}));
});

Просмотреть файл

@ -1,623 +0,0 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { ApiService, Loop } from '../../services/api.service';
import { NodeSelectorComponent } from '../node-selector/node-selector.component';
import { CommandOutputComponent } from '../command-output/command-output.component';
import { CommandInputComponent } from '../command-input/command-input.component';
import { ConfirmDialogComponent } from '../../widgets/confirm-dialog/confirm-dialog.component';
import { JobStateService } from '../../services/job-state/job-state.service';
import { TableService } from '../../services/table/table.service';
@Component({
selector: 'app-result-detail',
templateUrl: './result-detail.component.html',
styleUrls: ['./result-detail.component.scss']
})
export class ResultDetailComponent implements OnInit {
@ViewChild('output')
private output: CommandOutputComponent;
@ViewChild('selector')
private selector: NodeSelectorComponent;
public id: string;
public result: any;
private gotTasks: boolean = false;
private subcription: Subscription;
private jobLoop: object;
private nodesLoop: object;
private nodeLoop: object;
private errorMsg: string;
private nodeOutputs = {};
private autoload = true;
private outputInitOffset = -8192;
private outputPageSize = 8192;
public canceling = false;
private lastId = 0;
public maxPageSize = 100;
public scrolled = false;
public loadFinished = false;
private reverse = true;
private selectedNodes = [];
pivot = Math.round(this.maxPageSize / 2) - 1;
startIndex = 0;
lastScrolled = 0;
public listLoading = false;
public empty = true;
private endId = -1;
public scriptBlock: boolean = false;
constructor(
private route: ActivatedRoute,
private router: Router,
private api: ApiService,
private jobStateService: JobStateService,
private dialog: MatDialog,
private tableService: TableService
) { }
ngOnInit() {
this.subcription = this.route.paramMap.subscribe(map => {
this.result = { state: 'unknown', command: '', nodes: [], timeout: 1800 };
this.nodeOutputs = {};
this.id = map.get('id');
this.lastId = 0;
this.loadFinished = false;
this.empty = true;
this.updateJob(this.id);
this.updateNodes(this.id);
});
}
get initializing() {
return this.result.state == 'unknown';
}
get isLoaded(): boolean {
return this.gotTasks;
}
public stateClass(state) {
return this.jobStateService.stateClass(state);
}
get isSingleCmd() {
let match = /\r|\n/.exec(this.result.command);
return match ? false : true;
}
public toggleScriptBlock() {
this.scriptBlock = !this.scriptBlock;
}
updateJob(id) {
this.jobLoop = Loop.start(
//observable:
this.api.command.get(id),
//observer:
{
next: (job: any) => {
if (id != this.id) {
return;
}
this.result.state = job.state;
this.result.command = job.commandLine;
this.result.progress = job.progress;
this.result.timeout = job.maximumRuntimeSeconds;
return true;
},
error: (err) => {
this.errorMsg = err;
}
}
);
}
private getTasksRequest() {
return this.api.command.getTasksByPage(this.id, this.lastId, this.maxPageSize);
}
updateNodes(id) {
this.nodesLoop = Loop.start(
//observable:
this.getTasksRequest(),
//observer:
{
next: (tasks) => {
if (id != this.id) {
return;
}
this.empty = false;
if (tasks.length > 0) {
this.gotTasks = true;
this.result.nodes = this.tableService.updateData(tasks, this.result.nodes, 'id');
if (this.endId != -1 && tasks[tasks.length - 1].id != this.endId) {
this.listLoading = false;
}
}
if (this.reverse && tasks.length < this.maxPageSize) {
this.loadFinished = true;
}
return this.getTasksRequest();
},
error: (err) => {
this.errorMsg = err;
}
}
);
}
onUpdateLastIdEvent(data) {
this.lastId = data.lastId;
this.endId = data.endId;
}
ngOnDestroy() {
if (this.subcription) {
this.subcription.unsubscribe();
}
if (this.jobLoop) {
Loop.stop(this.jobLoop);
}
if (this.nodesLoop) {
Loop.stop(this.nodesLoop);
}
if (this.nodeLoop) {
Loop.stop(this.nodeLoop);
}
for (let key in this.nodeOutputs) {
let loop = this.nodeOutputs[key].keyLoop;
if (loop)
Loop.stop(loop);
}
}
//This should work but not in fact! Because this.selector is set later than
//selectNode is called. That seems the NodeSelectorComponent can fire events
//before Angular captures it in this.selector. A surprise!
//
//get selectedNode(): any {
// return this.selector ? this.selector.selectedNode : null;
//}
selectedNode: any;
selectNode({ node, prevNode }) {
this.selectedNode = node;
if (prevNode) {
this.stopNodeOutputKeyLoop(prevNode);
this.stopAutoload(prevNode);
}
if (!node) {
return;
}
if (this.autoload) {
this.startAutoload(node);
}
else {
this.loadOnce(node);
}
}
isSelected(node) {
return node && this.selectedNode && node.name === this.selectedNode.name;
}
getNodeOutputKey(node, onGot) {
return Loop.start(
//observable:
Observable.create((observer) => {
this.api.command.getTaskResult(this.id, node.id).subscribe(
result => {
observer.next(result.resultKey);
observer.complete();
},
error => {
observer.error(error);
}
);
}),
//observer:
{
next: (key) => {
if (key) {
onGot(key);
return false;
}
//TODO: When is it impossible to get a key?
//if (this.isNodeOver(node)) {
// onGot(null);
// return false;
//}
return true;
},
error: (err) => {
if (err.status == 404 && !this.isNodeOver(node)) {
// return value is assigned to looper.ended in observer.err
// can't tell when to stop in 404. Job state and node state both can't indentify
return false;
}
else if (err.status == 404 && node.state == 'Finished') {
return false;
}
else {
onGot(err);
return true;
}
}
},
//interval(in ms):
1000,
);
}
getNodeOutput(node): any {
let output = this.nodeOutputs[node.name];
if (!output) {
output = this.nodeOutputs[node.name] = {
content: '',
next: this.outputInitOffset,
start: undefined,
end: undefined,
loading: false,
key: null,
error: ''
};
let onKeyReady = (callback) => {
if (output.key === null) {
if (!output.keyLoop) {
output.loading = 'key';
output.keyLoop = this.getNodeOutputKey(node, (key) => {
let keyType = typeof (key);
output.loading = false;
if (key && keyType == 'string') {
output.key = key;
callback(true);
}
else if (key && keyType == 'object') {
output.error = key;
callback(false);
}
else {
output.key = false;
callback(false);
}
});
}
}
else if (output.key === false) { //No key, for no output
callback(false);
}
else {
callback(true);
}
}
(output as any).onKeyReady = onKeyReady;
}
return output;
}
stopNodeOutputKeyLoop(node) {
let output = this.nodeOutputs[node.name];
if (output && output.keyLoop) {
Loop.stop(output.keyLoop);
output.keyLoop = null;
if (output.loading === 'key') {
output.loading = false;
}
}
}
updateNodeOutput(output, result): boolean {
//NOTE: There may be two inflight updates for the same piece of output, one
//by autoload and the other one by manual trigger. Drop the one arrives later.
if (output.next > result.offset) {
return false;
}
//Update start field when and only when it's not updated yet.
if (typeof (output.start) === 'undefined' && result.offset >= 0) {
output.start = result.offset;
}
//NOTE: result.end depends on passing an opt.over parameter to API getOutput.
output.end = result.end;
if (result.content) {
output.content += result.content;
}
output.next = result.offset + result.size;
return result.content ? true : false;
}
updateNodeOutputBackward(output, result): boolean {
//Update next field when and only when it's not updated yet.
if (output.next === this.outputInitOffset) {
output.next = result.offset + result.size;
}
if (result.content) {
output.content = result.content + output.content;
}
output.start = result.offset;
return result.content ? true : false;
}
stopAutoload(node): void {
let output = this.getNodeOutput(node)
output.loading = false;
if (this.nodeLoop) {
Loop.stop(this.nodeLoop);
this.nodeLoop = null;
}
}
startAutoload(node): void {
let output = this.getNodeOutput(node)
if (output.end || output.loading) {
return;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'auto';
this.nodeLoop = Loop.start(
//observable:
this.api.command.getOutput(output.key, output.next, this.outputPageSize),
//observer:
{
next: (result) => {
if (this.updateNodeOutput(output, result)) {
setTimeout(() => this.scrollOutputToBottom(), 0);
}
let over = output.end || !this.autoload;
if (over) {
output.loading = false;
}
return over ? false :
this.api.command.getOutput(output.key, output.next, this.outputPageSize);
},
error: (err) => {
output.loading = false;
output.error = err;
return true;
}
},
//interval(in ms):
0,
);
});
}
loadOnce(node) {
let output = this.getNodeOutput(node)
if (output.content || output.end || output.loading) {
return;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'once';
let opt = { fulfill: true, timeout: 2000 };
this.api.command.getOutput(output.key, output.next, this.outputPageSize, opt as any).subscribe(
result => {
output.loading = false;
this.updateNodeOutput(output, result);
},
error => {
output.loading = false;
output.error = error;
}
);
});
}
get loading(): boolean {
let output = this.currentOutput;
return output && output.loading;
}
get currentOutput(): any {
if (!this.selectedNode)
return;
return this.getNodeOutput(this.selectedNode);
}
get isOutputDisabled(): boolean {
return !this.selectedNode || !this.currentOutput.key;
}
get currentOutputUrl(): string {
return this.isOutputDisabled ? '' : this.api.command.getDownloadUrl(this.currentOutput.key);
}
scrollOutputToBottom(): void {
this.output.scrollToBottom();
}
isJobOver(state): boolean {
return state == 'Finished' || state == 'Failed' || state == 'Canceled';
}
get isOver(): boolean {
let state = this.result.state;
return this.isJobOver(state);
}
isNodeOver(node): boolean {
let state = node.state;
return this.isJobOver(state);
}
toggleAutoload(enabled) {
this.autoload = enabled;
if (enabled) {
this.startAutoload(this.selectedNode);
}
else {
this.stopAutoload(this.selectedNode);
}
}
private scrollTop;
private scrollHeight;
loadPrevAndScroll(node, elem) {
this.scrollTop = elem.scrollTop;
this.scrollHeight = elem.scrollHeight;
this.loadPrev(node,
() => elem.scrollTop = elem.scrollHeight - this.scrollHeight + this.scrollTop);
}
loadPrev(node, onload = undefined) {
let output = this.getNodeOutput(node);
if (output.start === 0 || output.loading) {
return;
}
let prev;
let pageSize = this.outputPageSize;
if (output.start) {
prev = output.start - this.outputPageSize;
if (prev < 0) {
prev = 0;
pageSize = output.start;
}
}
else {
prev = this.outputInitOffset;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'prev';
let opt = { fulfill: true };
this.api.command.getOutput(output.key, prev, pageSize, opt as any)
.subscribe(
result => {
output.loading = false;
if (this.updateNodeOutputBackward(output, result) && onload
&& this.selectedNode && this.selectedNode.name == node.name) {
setTimeout(onload, 0);
}
},
error => {
output.loading = false;
output.error = error;
});
});
}
loadNext(node) {
let output = this.getNodeOutput(node)
if (output.end || output.loading) {
return;
}
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'next';
let opt = { fulfill: true, timeout: 2000 };
this.api.command.getOutput(output.key, output.next, this.outputPageSize, opt as any)
.subscribe(
result => {
output.loading = false;
this.updateNodeOutput(output, result);
},
error => {
output.loading = false;
output.error = error;
});
});
}
loadFromBeginAndScroll(node, elem) {
this.loadFromBegin(node, () => elem.scrollTop = 0);
}
loadFromBegin(node, onload) {
let output = this.getNodeOutput(node)
if (output.loading) {
return;
}
if (output.start === 0 && onload) {
setTimeout(onload, 0);
return;
}
//Reset output for loading from the begin
output.content = '';
output.start = undefined;
output.end = undefined;
output.next = 0;
output.error = '';
output.onKeyReady((hasKey) => {
if (!hasKey) {
return;
}
output.loading = 'top';
let opt = { fulfill: true, timeout: 2000 };
this.api.command.getOutput(output.key, output.next, this.outputPageSize, opt as any).subscribe(
result => {
output.loading = false;
this.updateNodeOutput(output, result);
setTimeout(onload, 0);
},
error => {
output.loading = false;
output.error = error;
});
});
}
newCommand() {
let dialogRef = this.dialog.open(CommandInputComponent, {
width: '98%',
data: { command: this.result.command, timeout: this.result.timeout, isSingleCmd: this.isSingleCmd }
});
dialogRef.afterClosed().subscribe(params => {
if (params && params.command) {
let names = this.result.nodes.map(node => node.name);
this.api.command.create(params.command, names, params.timeout).subscribe(obj => {
this.router.navigate([`/command/results/${obj.id}`]);
this.selector.scrolled = false;
});
}
});
}
cancelCommand() {
let dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '45%',
data: {
title: 'Cancel',
message: 'Are you sure to cancel the current run of command?'
}
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.canceling = true;
this.api.command.cancel(this.id).subscribe();
}
});
}
}

Просмотреть файл

@ -1,94 +0,0 @@
<div class="actions">
<button mat-raised-button (click)="customizeTable()">
<div class="action-btn">
<i class="material-icons btn-icon">settings</i>
<div>Customize Columns...</div>
</div>
</button>
</div>
<div class="list-container">
<div class="list-header" [ngClass]="{'list-header-scrolled': showScrollBar}">
<div class="header job-id" [ngStyle]="getColumnOrder('id')">
ID
</div>
<div class="header job-created" [ngStyle]="getColumnOrder('createdAt')">
Created
</div>
<div class="header job-command" [ngStyle]="getColumnOrder('command')">
Command
</div>
<div class="header job-state" [ngStyle]="getColumnOrder('state')">
State
</div>
<div class="header job-progress" [ngStyle]="getColumnOrder('progress')">
Progress
</div>
<div class="header job-updated" [ngStyle]="getColumnOrder('updatedAt')">
Last Changed
</div>
<div class="header job-nodes" [ngStyle]="getColumnOrder('nodes')">
Nodes Number
</div>
</div>
<div class="list-content">
<cdk-virtual-scroll-viewport itemSize="40" #content class="list-content" (scrolledIndexChange)="indexChanged($event)">
<div *cdkVirtualFor="let job of dataSource; templateCacheSize: 0; trackBy: trackByFn.bind(this)" class="list-item">
<div class="icon-cell job-id" [ngStyle]="getColumnOrder('id')">
<i class="material-icons cell-icon color-icon">call_to_action</i>
<a [routerLink]="job.id" class="cell-text">{{job.id}}</a>
</div>
<div class="icon-cell job-created" [ngStyle]="getColumnOrder('createdAt')">
<i class="material-icons cell-icon">access_time</i>
<div class="cell-text">{{job.createdAt | date:'yyyy-MM-dd HH:mm:ss'}}</div>
</div>
<div class="icon-cell job-command" [ngStyle]="getColumnOrder('command')">
<i class="material-icons cell-icon">code</i>
<div class="cell-text">{{job.command}}</div>
</div>
<div class="icon-cell job-state" [ngStyle]="getColumnOrder('state')">
<i class="material-icons cell-icon" [ngClass]="stateClass(job.state)">{{stateIcon(job.state)}}</i>
<div class="cell-text">{{job.state | titlecase}}</div>
</div>
<div class="table-progress job-progress" [ngStyle]="getColumnOrder('progress')">
<mat-progress-bar mode="determinate" [value]="job.progress * 100"></mat-progress-bar>
<div class="progress-num"> {{job.progress | percent}} </div>
</div>
<div class="icon-cell job-updated" [ngStyle]="getColumnOrder('updatedAt')">
<i class="material-icons cell-icon">access_alarm</i>
<div class="cell-text"> {{job.updatedAt | date:'yyyy-MM-dd HH:mm:ss'}} </div>
</div>
<div class="icon-cell job-nodes" [ngStyle]="getColumnOrder('nodes')" (click)="getTargetNodes(job.id,job.targetNodes)"
[ngClass]="{'active-job': selectedJobId == job.id}">
<i class="material-icons cell-icon">devices</i>
<div class="cell-text"> {{job.targetNodes.length}} </div>
</div>
</div>
</cdk-virtual-scroll-viewport>
<app-scroll-to-top [scrolled]="scrolled" [targetEle]="content"></app-scroll-to-top>
<div class="list-loading" *ngIf="empty">
<mat-spinner></mat-spinner>
</div>
</div>
</div>
<app-loading-progress-bar [loadFinished]="loadFinished" [hidden]="!loading || !scrolled" class="virtual-scroll-loading"></app-loading-progress-bar>
<app-content-window side="right" [title]="windowTitle" width="20" *ngIf="showTargetNodes" (showWnd)="onShowWnd($event)">
<ng-template #wndContent>
<div class="nodes" #nodes>
<div class="node icon-cell" *ngFor="let node of targetNodes">
<i class="material-icons cell-icon">desktop_windows</i>
<div class="cell-text">{{node}}</div>
</div>
</div>
</ng-template>
</app-content-window>

Просмотреть файл

@ -1,77 +0,0 @@
@import "../../stylesheets/mixin.scss";
@import "../../stylesheets/table.scss";
.actions {
@include display-flex(center, space-between);
}
.filter-area {
@include display-flex(center, flex-end);
mat-form-field {
margin: 0 10px;
input {
font-size: 14px;
}
}
}
.job-id {
flex: 0.2;
}
.job-command {
flex: 1;
@include ellipsis-text;
}
.job-progress {
flex: 0.8; // text-align: right;
}
.job-updated {
flex: 0.4;
}
.job-created,
.job-state {
flex: 0.4;
}
.job-nodes {
flex: 0.3;
&:hover {
cursor: pointer;
}
.cell-text {
text-decoration-color: $color;
text-decoration-line: underline;
text-decoration-style: solid;
}
}
.active-job {
color: $color;
}
.nodes {
overflow-y: auto;
height: 85vh;
.node {
@include display-flex(center);
height: 25px;
font-size: 14px;
.cell-icon {
font-size: 1em;
}
&:hover {
color: $color;
}
}
}

Просмотреть файл

@ -1,139 +0,0 @@
import { async, ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
import { Component, Directive, Input, TrackByFunction, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { of } from 'rxjs/observable/of';
import { FormsModule } from '@angular/forms'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialsModule } from '../../materials.module';
import { ApiService } from '../../services/api.service';
import { JobStateService } from '../../services/job-state/job-state.service';
import { ResultListComponent } from './result-list.component';
import { TableService } from '../../services/table/table.service';
import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { animationFrameScheduler } from 'rxjs';
import { Router, ActivatedRoute } from '@angular/router';
@Directive({
selector: '[routerLink]',
host: { '(click)': 'onClick()' }
})
class RouterLinkDirectiveStub {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
const activatedRouteSpy = jasmine.createSpyObj('ActivatedRoute', ['']);
class ApiServiceStub {
static results = [
{ id: 1, createdAt: '2018-08-01', command: 'a command', state: 'Finished', progress: 100, updatedAt: '2018-08-01' },
{ id: 2, createdAt: '2018-08-01', command: 'a command', state: 'Finished', progress: 100, updatedAt: '2018-08-01' },
{ id: 3, createdAt: '2018-08-01', command: 'a command', state: 'Finished', progress: 100, updatedAt: '2018-08-01' },
{ id: 4, createdAt: '2018-08-01', command: 'a command', state: 'Finished', progress: 100, updatedAt: '2018-08-01' },
]
command = {
getJobsByPage: () => of(ApiServiceStub.results),
}
}
class JobStateServiceStub {
stateClass(state) {
return 'finished';
}
stateIcon(state) {
return 'done';
}
}
const TableServiceStub = {
updateData: (newData, dataSource, propertyName) => newData,
loadSetting: (key, initVal) => initVal,
saveSetting: (key, val) => undefined,
isContentScrolled: () => false
}
function finishInit(fixture: ComponentFixture<any>) {
// On the first cycle we render and measure the viewport.
fixture.detectChanges();
flush();
// On the second cycle we render the items.
fixture.detectChanges();
flush();
// Flush the initial fake scroll event.
animationFrameScheduler.flush(null);
flush();
fixture.detectChanges();
}
fdescribe('ClusrunResultListComponent', () => {
let component: ResultListComponent;
let fixture: ComponentFixture<ResultListComponent>;
let viewport: CdkVirtualScrollViewport;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
RouterLinkDirectiveStub,
ResultListComponent,
],
imports: [
BrowserAnimationsModule,
FormsModule,
MaterialsModule,
ScrollingModule
],
providers: [
{ provide: ApiService, useClass: ApiServiceStub },
{ provide: JobStateService, useClass: JobStateServiceStub },
{ provide: TableService, useValue: TableServiceStub },
{ provide: Router, useValue: routerSpy },
{ provide: ActivatedRoute, useValue: activatedRouteSpy }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultListComponent);
component = fixture.componentInstance;
viewport = component.cdkVirtualScrollViewport;
fixture.detectChanges();
});
it('should create', fakeAsync(() => {
expect(component).toBeTruthy();
expect(viewport.getDataLength()).toEqual(4);
// finishInit(fixture);
// flush();
// fixture.detectChanges();
// flush();
// console.log(viewport.getDataLength());
// viewport.setRenderedRange({ start: 0, end: 3 });
// viewport.checkViewportSize();
// fixture.detectChanges();
// flush();
// animationFrameScheduler.flush(null);
// flush();
// fixture.detectChanges();
// console.log(viewport.getRenderedRange());
// console.log(component.dataSource);
// const contentWrapper =
// viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper');
// console.log(contentWrapper.children.length);
// console.log(fixture.nativeElement.querySelectorAll('.list-item'));
// let text = fixture.nativeElement.querySelector('.list-item .job-command').textContent;
// expect(text).toContain(ApiServiceStub.results[0].command);
}));
});

Просмотреть файл

@ -1,192 +0,0 @@
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { MatDialog } from '@angular/material';
import { TableOptionComponent } from '../../widgets/table-option/table-option.component';
import { ApiService, Loop } from '../../services/api.service';
import { JobStateService } from '../../services/job-state/job-state.service';
import { TableService } from '../../services/table/table.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { VirtualScrollService } from '../../services/virtual-scroll/virtual-scroll.service';
import { Router, ActivatedRoute } from '@angular/router';
import { ListJob } from '../../models/diagnostics/list-job';
@Component({
selector: 'app-result-list',
templateUrl: './result-list.component.html',
styleUrls: ['./result-list.component.scss']
})
export class ResultListComponent implements OnInit {
@ViewChild('content') cdkVirtualScrollViewport: CdkVirtualScrollViewport;
@ViewChild('nodes') nodes: ElementRef;
public dataSource = [];
static customizableColumns = [
{ name: 'createdAt', displayed: true, displayName: 'Created' },
{ name: 'nodes', display: true, displayName: 'Nodes' },
{ name: 'command', displayed: true, displayName: 'Command' },
{ name: 'state', displayed: true, displayName: 'State' },
{ name: 'progress', displayed: true, displayName: 'Progress' },
{ name: 'updatedAt', displayed: true, displayName: 'Last Changed' },
];
private availableColumns;
public displayedColumns;
private lastId = 0;
private commandLoop: object;
public maxPageSize = 300;
private reverse = true;
public scrolled = false;
public loadFinished = false;
private interval = 2000;
pivot = Math.round(this.maxPageSize / 2) - 1;
startIndex = 0;
lastScrolled = 0;
public loading = false;
public empty = true;
private endId = -1;
public targetNodes: Array<ListJob>;
public showTargetNodes = false;
public selectedJobId = -1;
public windowTitle: string;
constructor(
private api: ApiService,
private router: Router,
private route: ActivatedRoute,
private jobStateService: JobStateService,
private tableService: TableService,
private dialog: MatDialog,
private virtualScrollService: VirtualScrollService
) { }
ngOnInit() {
this.loadSettings();
this.getDisplayedColumns();
this.commandLoop = Loop.start(
this.getCommandRequest(),
{
next: (result) => {
this.empty = false;
if (result.length > 0) {
this.dataSource = this.tableService.updateData(result, this.dataSource, 'id');
if (this.endId != -1 && result[result.length - 1].id != this.endId) {
this.loading = false;
}
}
if (this.reverse && result.length < this.maxPageSize) {
this.loadFinished = true;
}
return this.getCommandRequest();
}
},
this.interval
);
}
ngOnDestroy() {
if (this.commandLoop) {
Loop.stop(this.commandLoop);
}
}
private stateIcon(state) {
return this.jobStateService.stateIcon(state);
}
private stateClass(state) {
return this.jobStateService.stateClass(state);
}
private getCommandRequest() {
return this.api.command.getJobsByPage({ lastId: this.lastId, count: this.maxPageSize, reverse: this.reverse });
}
getDisplayedColumns(): void {
let columns = this.availableColumns.filter(e => e.displayed).map(e => e.name);
// columns.push('actions');
this.displayedColumns = ['id'].concat(columns);
}
customizeTable(): void {
let dialogRef = this.dialog.open(TableOptionComponent, {
width: '60%',
data: { columns: this.availableColumns }
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.availableColumns = res.columns;
this.getDisplayedColumns();
this.saveSettings();
}
});
}
saveSettings(): void {
this.tableService.saveSetting('CommandList', this.availableColumns);
}
loadSettings(): void {
this.availableColumns = this.tableService.loadSetting('CommandList', ResultListComponent.customizableColumns);
}
trackByFn(index, item) {
return this.tableService.trackByFn(item, this.displayedColumns);
}
getColumnOrder(col) {
let index = this.displayedColumns.findIndex(item => {
return item == col;
});
let order = index + 1;
if (order) {
return { 'order': index + 1 };
}
else {
return { 'display': 'none' };
}
}
goDetailPage(id) {
this.router.navigate(['.', `${id}`], { relativeTo: this.route });
}
getTargetNodes(id, nodes) {
this.showTargetNodes = true;
if (this.nodes) {
this.nodes.nativeElement.scrollTop = 0;
}
this.selectedJobId = id;
this.windowTitle = `${nodes.length} Nodes`;
this.targetNodes = nodes;
}
onShowWnd(condition: boolean) {
this.showTargetNodes = condition;
if (!condition) {
this.selectedJobId = -1;
}
}
indexChanged($event) {
let result = this.virtualScrollService.indexChangedCalc(this.maxPageSize, this.pivot, this.cdkVirtualScrollViewport, this.dataSource, this.lastScrolled, this.startIndex);
this.pivot = result.pivot;
this.lastScrolled = result.lastScrolled;
this.lastId = result.lastId == undefined ? this.lastId : result.lastId;
this.endId = result.endId == undefined ? this.endId : result.endId;
this.loading = result.loading;
this.startIndex = result.startIndex;
this.scrolled = result.scrolled;
}
get showScrollBar() {
return this.tableService.isContentScrolled(this.cdkVirtualScrollViewport.elementRef.nativeElement);
}
}

Просмотреть файл

@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
const routes: Routes = [
{
path: '',
component: DashboardComponent
},
];
@NgModule({
imports: [ RouterModule.forChild(routes) ],
exports: [ RouterModule ],
})
export class DashboardRoutingModule { }

Просмотреть файл

@ -1,3 +0,0 @@
.row > * {
margin-bottom: 1em;
}

Просмотреть файл

@ -1,21 +0,0 @@
<div class="container-fluid">
<div class="row">
<div class="col-lg-4">
<dashboard-node-state state="OK" [total]="totalNodes" stateIcon="lightbulb_outline" [stateNum]="nodes?.OK"></dashboard-node-state>
</div>
<div class="col-lg-4">
<dashboard-node-state state="Warning" [total]="totalNodes" stateIcon="alarm" [stateNum]="nodes?.Warning"></dashboard-node-state>
</div>
<div class="col-lg-4">
<dashboard-node-state state="Error" [total]="totalNodes" stateIcon="do_not_disturb" [stateNum]="nodes?.Error"></dashboard-node-state>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<dashboard-job-overview jobCategory="Diagnostics" [jobs]="diags" icon="local_hospital"></dashboard-job-overview>
</div>
<div class="col-lg-6">
<dashboard-job-overview jobCategory="ClusRun" [jobs]="clusrun" icon="call_to_action"></dashboard-job-overview>
</div>
</div>
</div>

Просмотреть файл

@ -1,72 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { NodeStateComponent } from './node-state/node-state.component';
import { JobOverviewComponent } from './job-overview/job-overview.component';
import { ChartModule } from 'angular2-chartjs';
import { of } from 'rxjs';
import { ApiService } from '../services/api.service';
import { Router, ActivatedRoute } from '@angular/router';
class ApiServiceStub {
nodes = { data: { Error: 1, OK: 97, Warning: 0 } };
diags = {
data: {
Canceled: 55,
Canceling: 0,
Failed: 106,
Finished: 102,
Finishing: 0,
Queued: 0,
Running: 1
}
};
clusrun = {
data: {
Canceled: 64,
Canceling: 0,
Failed: 16,
Finished: 12,
Finishing: 0,
Queued: 10,
Running: 1
}
};
dashboard = {
getNodes: () => of(this.nodes),
getDiags: () => of(this.diags),
getClusrun: () => of(this.clusrun)
}
}
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
const activatedRouteSpy = jasmine.createSpyObj('ActivatedRoute', ['']);
fdescribe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardComponent, NodeStateComponent, JobOverviewComponent],
imports: [ChartModule],
providers: [
{ provide: ApiService, useClass: ApiServiceStub },
{ provide: Router, useValue: routerSpy },
{ provide: ActivatedRoute, useValue: activatedRouteSpy }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -1,79 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService, Loop } from "../services/api.service";
import { DashboardNodes } from '../models/dashboard/dashboard-nodes';
import { DashboardJobs } from '../models/dashboard/dashboard-jobs';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit, OnDestroy {
constructor(
private api: ApiService
) { }
public nodes: DashboardNodes;
public totalNodes = 0;
public diags: DashboardJobs;
public clusrun: DashboardJobs;
private nodesLoop: object;
private diagsLoop: object;
private clusrunLoop: object;
private interval = 3000;
ngOnInit() {
this.nodesLoop = Loop.start(
this.api.dashboard.getNodes(),
{
next: (res) => {
this.totalNodes = 0;
this.nodes = res.data;
let states = Object.keys(this.nodes);
for (let i = 0; i < states.length; i++) {
this.totalNodes += this.nodes[states[i]];
}
return true;
}
},
this.interval
);
this.diagsLoop = Loop.start(
this.api.dashboard.getDiags(),
{
next: (res) => {
this.diags = res.data;
return true;
}
},
this.interval
);
this.clusrunLoop = Loop.start(
this.api.dashboard.getClusrun(),
{
next: (res) => {
this.clusrun = res.data;
return true;
}
},
this.interval
);
}
ngOnDestroy() {
if (this.nodesLoop) {
Loop.stop(this.nodesLoop);
}
if (this.diagsLoop) {
Loop.stop(this.diagsLoop);
}
if (this.clusrunLoop) {
Loop.stop(this.clusrunLoop);
}
}
}

Просмотреть файл

@ -1,20 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChartModule } from 'angular2-chartjs';
import { MaterialsModule } from '../materials.module';
import { DashboardRoutingModule } from './dashboard-routing.module';
import { DashboardComponent } from './dashboard.component';
import { NodeStateComponent } from './node-state/node-state.component';
import { JobOverviewComponent } from './job-overview/job-overview.component';
@NgModule({
imports: [
CommonModule,
DashboardRoutingModule,
ChartModule,
MaterialsModule,
],
declarations: [DashboardComponent, NodeStateComponent, JobOverviewComponent],
//bootstrap: [HomeComponent]
})
export class DashboardModule { }

Просмотреть файл

@ -1,71 +0,0 @@
<div class="card">
<div class="headline">
<div class="name">{{jobCategory | uppercase}}</div>
<div class="operation">
<div class="detail" (click)="jobInfo()">
<i class="material-icons">more_horiz</i>
<span class="info-title">DETAIL</span>
</div>
</div>
</div>
<div class="card-content">
<div class="job-overview">
<div class="total-info">
<div class="outline">
<div class="icon job">
<i class="material-icons">{{icon}}</i>
</div>
<div class="info">
<div class="info-title">
<span class="active">ACTIVE JOBS</span>
/ TOTAL
</div>
<div class="info-content-total">
<span class="active">{{activeJobs}}</span>
/
<span class="total">{{totalJobs}}</span>
</div>
</div>
</div>
</div>
</div>
<div class="info-content">
<div class="job-item">
<div class="item-title Queued">Queued</div>
<div class="item-value">{{jobs?.Queued}}</div>
</div>
<div class="job-item ">
<div class="item-title Running">Running</div>
<div class="item-value">{{jobs?.Running}}</div>
</div>
<div class="job-item">
<div class="item-title Finishing">Finishing</div>
<div class="item-value">{{jobs?.Finishing}}</div>
</div>
<div class="job-item">
<div class="item-title Finished">Finished</div>
<div class="item-value">{{jobs?.Finished}}</div>
</div>
<div class="job-item">
<div class="item-title Canceling">Canceling</div>
<div class="item-value">{{jobs?.Canceling}}</div>
</div>
<div class="job-item">
<div class="item-title Canceled">Canceled</div>
<div class="item-value">{{jobs?.Canceled}}</div>
</div>
<div class="job-item">
<div class="item-title Failed">Failed</div>
<div class="item-value">{{jobs?.Failed}}</div>
</div>
</div>
<div class="job-chart">
<chart type="bar" [data]="chartData" [options]="chartOption"></chart>
</div>
<div class="shade" *ngIf="loading">
<div class="progress">
</div>
</div>
</div>
</div>

Просмотреть файл

@ -1,87 +0,0 @@
@import "../../stylesheets/dashboard.scss";
chart {
height: 360px;
}
.card {
@include display-flex($flex-direction: column);
.job-overview {
@include display-flex($justify-content: space-between);
}
padding-bottom: .5em;
}
.mat-button-toggle-checked {
color: #3f51b5;
background: #FFF;
border-left: 3px solid #3f51b5;
}
.total-info {
display: flex;
justify-content: space-between;
}
.info-content-total {
font-size: 1.2em;
font-weight: 500;
}
.info-content {
@include display-flex($align-items: center, $justify-content: space-between);
font-size: .9em;
margin-top: .7em;
}
.job-item {
padding: 0em .8em 0em .5em;
margin: .3em 0;
display: inline-block;
min-width: 3em;
}
.item-title {
border-left: 3px solid rgba(28, 86, 188, .9);
padding-left: .5em;
font-size: .75em;
}
.info-content-total .active {
color: rgba(28, 86, 188, .9);
}
.Queued {
border-left-color: rgba(66, 134, 244, .8);
}
.Finishing {
border-left-color: rgba(153, 229, 100, .8);
}
.Finished {
border-left-color: rgba(47, 196, 134, .8);
}
.Running {
border-left-color: rgba(63, 81, 181, .8);
}
.Canceling {
border-left-color: rgba(232, 199, 37, .8);
}
.Canceled {
border-left-color: rgba(206, 206, 206, .8);
}
.Failed {
border-left-color: rgba(229, 83, 57, .8);
}
.job-chart {
background: #FFF;
}
.operation {
@include display-flex;
}

Просмотреть файл

@ -1,53 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChartModule } from 'angular2-chartjs';
import { JobOverviewComponent } from './job-overview.component';
import { Router, ActivatedRoute } from '@angular/router';
fdescribe('JobOverviewComponent', () => {
let component: JobOverviewComponent;
let fixture: ComponentFixture<JobOverviewComponent>;
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
const activatedRouteSpy = jasmine.createSpyObj('ActivatedRoute', ['']);
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
JobOverviewComponent
],
imports: [
ChartModule
],
providers: [
{ provide: Router, useValue: routerSpy },
{ provide: ActivatedRoute, useValue: activatedRouteSpy }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(JobOverviewComponent);
component = fixture.componentInstance;
component.icon = "Test";
component.jobs = {
Queued: 10,
Running: 1000,
Finished: 10000,
Canceling: 0,
Canceled: 0,
Failed: 10
};
component.jobCategory = "test";
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
let category = fixture.nativeElement.querySelector('.headline .name').textContent;
expect(category).toEqual('TEST');
let icon = fixture.nativeElement.querySelector('.job').textContent;
expect(icon).toEqual('Test');
let runningNum = fixture.nativeElement.querySelectorAll('.item-value')[1].textContent;
expect(runningNum).toEqual('1000');
});
});

Просмотреть файл

@ -1,118 +0,0 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'dashboard-job-overview',
templateUrl: './job-overview.component.html',
styleUrls: ['./job-overview.component.scss']
})
export class JobOverviewComponent implements OnInit, OnChanges {
@Input() icon: string;
@Input() jobs: any;
@Input() jobCategory: string;
public totalJobs = 0;
public activeJobs = 0;
public loading = true;
private labels = [
'Queued',
'Running',
'Finishing',
'Finished',
'Canceling',
'Canceled',
'Failed'
];
private colors = [
'rgba(66, 134, 244, .8)',
'rgba(63, 81, 181, .8)',
'rgba(153, 229, 100, .8)',
'rgba(47, 196, 134, .8)',
'rgba(232, 199, 37, .8)',
'rgba(206, 206, 206, .8)',
'rgba(229, 83, 57, .8)'
];
chartData = {};
chartOption = {
responsive: true,
animation: {
duration: 0
},
maintainAspectRatio: false,
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
min: 0,
callback: function (value, index, values) {
if (Math.floor(value) === value) {
return value;
}
}
}
}]
}
};
constructor(
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
this.totalJobs = 0;
this.activeJobs = 0;
if (this.jobs) {
let states = Object.keys(this.jobs);
if (states.length > 0) {
this.loading = false;
for (let i = 0; i < states.length; i++) {
this.totalJobs += this.jobs[states[i]];
if (states[i] !== 'Finished' && states[i] !== 'Failed' && states[i] !== 'Canceled') {
this.activeJobs += this.jobs[states[i]];
}
}
this.generateChartData(this.jobs);
}
}
}
generateChartData(jobs) {
let data = [];
for (let i = 0; i < this.labels.length; i++) {
data.push(jobs[this.labels[i]]);
}
this.chartData = {
labels: this.labels,
datasets: [{
data: data,
backgroundColor: this.colors
}]
};
}
jobInfo() {
let category = ['..'];
if (this.jobCategory == 'Diagnostics') {
category.push('diagnostics');
}
else if (this.jobCategory == 'ClusRun') {
category.push('command');
}
category.push('results');
this.router.navigate(category, { relativeTo: this.route });
}
}

Просмотреть файл

@ -1,27 +0,0 @@
<div class="card">
<div class="headline">
<div class="name" [ngClass]="state">NODES</div>
<div class="detail" (click)="nodesInfo()">
<i class="material-icons detail-icon">more_horiz</i>
<span class="info-title">DETAIL</span>
</div>
</div>
<div class="outline card-content">
<div class="icon" [ngClass]="state">
<i class="material-icons">{{stateIcon}}</i>
</div>
<div class="info">
<div class="info-title">
<span [ngClass]="state"> {{state | uppercase}} </span>
/ TOTAL
</div>
<div class="info-content">
<span [ngClass]="state">{{stateNum}}</span>
/
<span class="total">{{total}}</span>
</div>
</div>
<div class="shade" *ngIf="loading">
</div>
</div>
</div>

Просмотреть файл

@ -1 +0,0 @@
@import "../../stylesheets/dashboard.scss";

Просмотреть файл

@ -1,45 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NodeStateComponent } from './node-state.component';
import { Router, ActivatedRoute } from '@angular/router';
fdescribe('NodeStateComponent', () => {
let component: NodeStateComponent;
let fixture: ComponentFixture<NodeStateComponent>;
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
const activatedRouteSpy = jasmine.createSpyObj('ActivatedRoute', ['']);
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NodeStateComponent],
providers: [
{ provide: Router, useValue: routerSpy },
{ provide: ActivatedRoute, useValue: activatedRouteSpy }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NodeStateComponent);
component = fixture.componentInstance;
component.state = "OK",
component.stateNum = 1000;
component.stateIcon = "lightbulb_outline";
component.total = 1001;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
let icon = fixture.nativeElement.querySelector('.icon .material-icons').textContent;
expect(icon).toEqual('lightbulb_outline');
let state = fixture.nativeElement.querySelector('.info-title .OK').textContent;
expect(state).toEqual(' OK ');
let num = fixture.nativeElement.querySelector('.info-content .OK').textContent;
expect(num).toEqual('1000');
let total = fixture.nativeElement.querySelector('.info-content .total').textContent;
expect(total).toEqual('1001');
});
});

Просмотреть файл

@ -1,34 +0,0 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'dashboard-node-state',
templateUrl: './node-state.component.html',
styleUrls: ['./node-state.component.scss']
})
export class NodeStateComponent implements OnInit, OnChanges {
@Input() state: string;
@Input() stateNum: number;
@Input() stateIcon: string;
@Input() total: number;
public loading = true;
nodesInfo() {
this.router.navigate(['..', 'resource'], { relativeTo: this.route, queryParams: { filter: this.state } });
}
constructor(
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
if (this.stateNum !== undefined) {
this.loading = false;
}
}
}

Просмотреть файл

@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DiagnosticsComponent } from './diagnostics.component';
import { ResultListComponent } from './result-list/result-list.component';
import { ResultDetailComponent } from './result-detail/result-detail.component';
const routes: Routes = [{
path: '',
component: DiagnosticsComponent,
children: [
{ path: 'results', component: ResultListComponent, data: { breadcrumb: "Results" }},
{ path: 'results/:id', component: ResultDetailComponent, data: { breadcrumb: "Result" }},
{ path: '', redirectTo: 'results', pathMatch: 'full' },
],
}];
@NgModule({
imports: [ RouterModule.forChild(routes) ],
exports: [ RouterModule ],
})
export class DiagnosticsRoutingModule { }

Просмотреть файл

@ -1,3 +0,0 @@
nav {
margin-bottom: 1em;
}

Просмотреть файл

@ -1 +0,0 @@
<router-outlet></router-outlet>

Просмотреть файл

@ -1,28 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { DiagnosticsComponent } from './diagnostics.component';
@Component({ selector: 'router-outlet', template: '' })
class RouterOutletStubComponent { }
fdescribe('DiagnosticsComponent', () => {
let component: DiagnosticsComponent;
let fixture: ComponentFixture<DiagnosticsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DiagnosticsComponent, RouterOutletStubComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DiagnosticsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -1,13 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-diagnostics',
templateUrl: './diagnostics.component.html',
styleUrls: ['./diagnostics.component.css']
})
export class DiagnosticsComponent implements OnInit {
constructor() {}
ngOnInit() {
}
}

Просмотреть файл

@ -1,58 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'
import { ChartModule } from 'angular2-chartjs';
import { MaterialsModule } from '../materials.module';
import { WidgetsModule } from '../widgets/widgets.module';
import { DiagnosticsRoutingModule } from './diagnostics-routing.module';
import { DiagnosticsComponent } from './diagnostics.component';
import { ResultListComponent } from './result-list/result-list.component';
import { ResultDetailComponent } from './result-detail/result-detail.component';
import { PingPongReportComponent } from './result-detail/diags/mpi/pingpong/pingpong-report/pingpong-report.component';
import { TaskDetailComponent } from './result-detail/task/task-detail/task-detail.component';
import { PingPongOverviewResultComponent } from './result-detail/diags/mpi/pingpong/overview-result/overview-result.component';
import { TaskTableComponent } from './result-detail/task/task-table/task-table.component';
import { ResultLayoutComponent } from './result-detail/result-layout/result-layout.component';
import { RingReportComponent } from './result-detail/diags/mpi/ring/ring-report/ring-report.component';
import { NodesInfoComponent } from './result-detail/diags/nodes-info/nodes-info.component';
import { SharedModule } from '../shared.module';
import { OverviewResultComponent } from './result-detail/diags/general-template/overview-result/overview-result.component';
import { GeneralReportComponent } from './result-detail/diags/general-template/general-report/general-report.component';
import { FailedReasonsComponent } from './result-detail/diags/mpi/pingpong/failed-reasons/failed-reasons.component';
import { GoodNodesComponent } from './result-detail/diags/mpi/pingpong/good-nodes/good-nodes.component';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { PerformanceComponent } from './result-detail/diags/mpi/performance/performance.component';
import { ConnectivityComponent } from './result-detail/diags/mpi/pingpong/connectivity/connectivity.component';
@NgModule({
imports: [
CommonModule,
DiagnosticsRoutingModule,
MaterialsModule,
WidgetsModule,
ChartModule,
FormsModule,
SharedModule,
ScrollingModule
],
declarations: [
DiagnosticsComponent,
ResultListComponent,
ResultDetailComponent,
PingPongReportComponent,
TaskDetailComponent,
PingPongOverviewResultComponent,
TaskTableComponent,
ResultLayoutComponent,
RingReportComponent,
NodesInfoComponent,
OverviewResultComponent,
GeneralReportComponent,
FailedReasonsComponent,
GoodNodesComponent,
PerformanceComponent,
ConnectivityComponent
],
entryComponents: [RingReportComponent, PingPongReportComponent, TaskDetailComponent, GeneralReportComponent]
})
export class DiagnosticsModule { }

Просмотреть файл

@ -1,28 +0,0 @@
<app-result-layout [result]="result">
<ng-template #overview>
<mat-tab-group>
<mat-tab label="Result" *ngIf="!hasError">
<general-overview-result [result]="aggregationResult" *ngIf="aggregationResult"></general-overview-result>
<div class="overview-progress" *ngIf="!aggregationResult">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</mat-tab>
<mat-tab label="Error" *ngIf="hasError">
<div class="error-message">
{{aggregationResult.Error}}
</div>
</mat-tab>
<mat-tab label="Nodes">
<app-nodes-info [nodes]="nodes" [badNodes]="undefined"></app-nodes-info>
</mat-tab>
<mat-tab label="Events">
<app-event-list [events]="events"></app-event-list>
</mat-tab>
</mat-tab-group>
</ng-template>
<ng-template #task>
<diag-task-table [dataSource]="dataSource" [loadFinished]='loadFinished' [customizableColumns]="customizableColumns"
[maxPageSize]="pageSize" [tableName]="componentName" (updateLastIdEvent)="onUpdateLastIdEvent($event)" [empty]="empty"></diag-task-table>
</ng-template>
</app-result-layout>

Просмотреть файл

@ -1,4 +0,0 @@
@import "../../nodes-info/nodes-info.component.scss";
.overview-progress {
margin-top: 1em;
}

Просмотреть файл

@ -1,170 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input, ViewChild, Output, EventEmitter } from '@angular/core';
import { GeneralReportComponent } from './general-report.component';
import { of } from 'rxjs/observable/of';
import { OverviewResultComponent } from '../overview-result/overview-result.component';
import { MaterialsModule } from '../../../../../materials.module';
import { ApiService } from '../../../../../services/api.service';
import { TableService } from '../../../../../services/table/table.service';
import { DiagReportService } from '../../../../../services/diag-report/diag-report.service';
@Component({ selector: 'app-result-layout', template: '' })
class ResultLayoutComponent {
@Input()
result: any;
@Input()
aggregationResult: any;
}
@Component({ selector: 'pingpong-overview-result', template: '' })
class PingPongOverviewResultComponent {
@Input()
result: any;
}
@Component({ selector: 'app-event-list', template: '' })
class EventListComponent {
@Input()
events: any;
}
@Component({ selector: 'app-nodes-info', template: '' })
class NodesInfoComponent {
@Input()
nodes: Array<any>;
@Input()
badNodes: Array<any>;
}
@Component({ selector: 'diag-task-table', template: '' })
class DiagTaskTableComponent {
@Input()
dataSource: any;
@Input()
currentData: any;
@Input()
customizableColumns: any;
@Input()
tableName: any;
@Input()
loadFinished: boolean;
@Input()
maxPageSize: number;
@Output()
updateLastIdEvent = new EventEmitter();
@Input()
public empty: boolean;
}
@Component({
template: `
<div class="error-message" *ngIf="result.aggregationResult != undefined && result.aggregationResult.Error != undefined">{{result.aggregationResult.Error}}</div>
`
})
class WrapperComponent {
public result = { aggregationResult: { Error: "error message" } };
}
class ApiServiceStub {
static taskResult = [{
customizedData: "Standard_H16r",
jobId: 301,
nodeName: "EVANCVMSS000002",
state: "Finished"
}];
static jobResult = {
id: 302,
name: "cpu",
aggregationResult: { Error: "error message" }
};
diag = {
getDiagTasksByPage: (id: any, lastId, count) => of(ApiServiceStub.taskResult),
getDiagJob: (id: any) => of(ApiServiceStub.jobResult),
getJobAggregationResult: (id: any) => of({ Error: "error message" }),
getJobEvents: (id: any) => of([])
}
}
const TableServiceStub = {
updateData: (newData, dataSource, propertyName) => newData,
loadSetting: (key, initVal) => initVal,
saveSetting: (key, val) => undefined
}
class DiagReportServiceStub {
hasError(result) {
return true;
}
jobFinished(state) {
return true;
}
getErrorMsg(err) {
return { Error: err };
}
}
fdescribe('GeneralReportComponent', () => {
let component: GeneralReportComponent;
let fixture: ComponentFixture<GeneralReportComponent>;
let wrapperComponent: WrapperComponent;
let wrapperFixture: ComponentFixture<WrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
GeneralReportComponent,
ResultLayoutComponent,
OverviewResultComponent,
DiagTaskTableComponent,
EventListComponent,
NodesInfoComponent,
WrapperComponent
],
imports: [MaterialsModule],
providers: [
{ provide: ApiService, useClass: ApiServiceStub },
{ provide: TableService, useValue: TableServiceStub },
{ provide: DiagReportService, useClass: DiagReportServiceStub }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GeneralReportComponent);
component = fixture.componentInstance;
wrapperFixture = TestBed.createComponent(WrapperComponent);
wrapperComponent = wrapperFixture.componentInstance;
component.result = { aggregationResult: { Error: "error message" } };
fixture.detectChanges();
wrapperFixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show error message', () => {
let wrapperComponent = wrapperFixture.componentInstance;
let itemElement = wrapperFixture.debugElement.nativeElement;
let text = itemElement.querySelector(".error-message").textContent;
expect(text).toEqual('error message');
});
});

Просмотреть файл

@ -1,137 +0,0 @@
import { Component, OnInit, Input, OnDestroy } from '@angular/core';
import { ApiService, Loop } from '../../../../../services/api.service';
import { TableService } from '../../../../../services/table/table.service';
import { DiagReportService } from '../../../../../services/diag-report/diag-report.service';
@Component({
selector: 'app-general-report',
templateUrl: './general-report.component.html',
styleUrls: ['./general-report.component.scss']
})
export class GeneralReportComponent implements OnInit {
@Input() result: any;
private dataSource = [];
private lastId = 0;
private pageSize = 300;
public loadFinished = false;
private jobId: string;
private interval: number;
private tasksLoop: Object;
private jobState: string;
public tasks = [];
public events = [];
public nodes = [];
public aggregationResult: any;
public loading = false;
public empty = true;
private endId = -1;
public customizableColumns = [
{ name: 'node', displayed: true },
{ name: 'state', displayed: true },
{ name: 'remark', displayed: true },
{ name: 'detail', displayed: true }
];
constructor(
private api: ApiService,
private tableService: TableService,
private diagReportService: DiagReportService
) {
this.interval = 5000;
}
ngOnInit() {
this.jobId = this.result.id;
this.jobState = this.result.state;
if (this.jobFinished) {
this.getAggregationResult();
}
this.tasksLoop = this.getTasksInfo();
}
get hasError() {
return this.diagReportService.hasError(this.aggregationResult);
}
get jobFinished() {
return this.diagReportService.jobFinished(this.jobState);
}
getTasksRequest() {
return this.api.diag.getDiagTasksByPage(this.jobId, this.lastId, this.pageSize);
}
getTasksInfo(): any {
return Loop.start(
this.getTasksRequest(),
{
next: (result) => {
this.empty = false;
if (this.endId != -1 && result[result.length - 1].id != this.endId) {
this.loading = false;
}
if (result.length < this.pageSize) {
this.loadFinished = true;
}
if (result.length > 0) {
this.dataSource = this.tableService.updateData(result, this.dataSource, 'id');
}
if (this.jobFinished) {
this.getAggregationResult();
}
this.getJobInfo();
this.getEvents();
return this.getTasksRequest();
}
},
this.interval
);
}
onUpdateLastIdEvent(data) {
this.lastId = data.lastId;
this.endId = data.endId;
}
ngOnDestroy() {
if (this.tasksLoop) {
Loop.stop(this.tasksLoop);
}
}
getJobInfo() {
this.api.diag.getDiagJob(this.result.id).subscribe(res => {
this.jobState = res.state;
this.result = res;
this.nodes = res.targetNodes;
});
}
getAggregationResult() {
this.api.diag.getJobAggregationResult(this.result.id).subscribe(
res => {
this.aggregationResult = res;
},
err => {
this.aggregationResult = this.diagReportService.getErrorMsg(err);
});
}
getEvents() {
this.api.diag.getJobEvents(this.result.id).subscribe(res => {
this.events = res;
});
}
getLink(node) {
let path = [];
path.push('/resource');
path.push(node);
return path;
}
}

Просмотреть файл

@ -1,2 +0,0 @@
<div [innerHTML]="res" class="overview-res">
</div>

Просмотреть файл

@ -1,6 +0,0 @@
@import "../../../../../stylesheets/mixin.scss";
@import "../../../../../stylesheets/info.scss";
.overview-res {
font-size: 13px;
}

Просмотреть файл

@ -1,31 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OverviewResultComponent } from './overview-result.component';
fdescribe('OverviewResultComponent', () => {
let component: OverviewResultComponent;
let fixture: ComponentFixture<OverviewResultComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OverviewResultComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OverviewResultComponent);
component = fixture.componentInstance;
component.result = {
Html: "<h1>Test</h1>"
};
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
let h = fixture.nativeElement.querySelector('h1');
let text = h.textContent;
expect(text).toEqual("Test");
});
});

Просмотреть файл

@ -1,33 +0,0 @@
import { Component, OnInit, Input, OnChanges, SimpleChange } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'general-overview-result',
templateUrl: './overview-result.component.html',
styleUrls: ['./overview-result.component.scss']
})
export class OverviewResultComponent implements OnInit {
@Input()
result: any;
constructor(private sanitizer: DomSanitizer) {
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
this.updateOverviewData();
}
ngOnInit() {
this.updateOverviewData();
}
res: any;
description: string;
title: string;
updateOverviewData() {
if (this.result !== undefined) {
this.res = this.sanitizer.bypassSecurityTrustHtml(this.result.Html);
}
}
}

Просмотреть файл

@ -1,19 +0,0 @@
<div class="performance">
<div class="description">
<div class="title">
Description
</div>
<div class="content">
{{result.Description}}
</div>
</div>
<div class="charts row">
<div class="col-lg-6 chart">
<chart type="line" [data]="latency" [options]="latencyOption" #latencyChart>
</chart>
</div>
<div class="col-lg-6 chart">
<chart type="line" [data]="throughput" [options]="throughputOption" #throughputChart>
</chart>
</div>
</div>

Просмотреть файл

@ -1,29 +0,0 @@
@import "../../../../../stylesheets/mixin.scss";
.performance {
font-family: $font-stack;
padding: 1em;
padding-bottom: 0;
}
.description {
font-size: .9em;
margin-bottom: 20px;
.title {
color: $color;
font-size: 1.1em;
padding-bottom: 10px;
}
.content {
white-space: pre-line;
line-height: 25px;
}
}
.charts {
.chart {
text-align: center;
}
}

Просмотреть файл

@ -1,47 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PerformanceComponent } from './performance.component';
import { ChartModule } from 'angular2-chartjs';
fdescribe('PerformanceComponent', () => {
let component: PerformanceComponent;
let fixture: ComponentFixture<PerformanceComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PerformanceComponent],
imports: [
ChartModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PerformanceComponent);
component = fixture.componentInstance;
component.result = {
Result: [
{
Latency: {
unit: 'usec',
value: '244.46'
},
Message_Size: {
unit: 'Bytes',
value: '0'
},
Throughput: {
unit: 'Mbytes/sec',
value: '0.00'
}
}
]
};
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -1,171 +0,0 @@
import { Component, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core';
@Component({
selector: 'mpi-performance',
templateUrl: './performance.component.html',
styleUrls: ['./performance.component.scss']
})
export class PerformanceComponent implements OnInit {
@Input()
result: any;
@ViewChild('throughputChart')
throughputChart: any;
@ViewChild('latencyChart')
latencyChart: any;
latency;
latencyData: Array<string> = [];
latencyOption: any;
throughput;
throughputData: Array<string> = [];
throughputOption: any;
packageSize: Array<string> = [];
constructor(private cd: ChangeDetectorRef) {
}
ngOnInit() { }
ngAfterViewInit() {
this.getChartData(this.result.Result);
this.cd.detectChanges();
}
getChartData(res) {
let throughput_unit;
let latency_unit;
let packageSize_unit;
for (let i = 0; i < res.length; i++) {
this.latencyData.push(res[i].Latency.value);
this.throughputData.push(res[i].Throughput.value);
this.packageSize.push(res[i].Message_Size.value);
if (!latency_unit) {
latency_unit = res[i].Latency.unit;
}
if (!throughput_unit) {
throughput_unit = res[i].Throughput.unit;
}
if (!packageSize_unit) {
packageSize_unit = res[i].Message_Size.unit;
}
}
this.latency = {
labels: this.packageSize,
datasets: [{
borderColor: 'rgb(63, 81, 181)',
borderWidth: 1,
data: this.latencyData,
fill: false
}]
};
this.throughput = {
labels: this.packageSize,
datasets: [{
borderColor: 'rgb(63, 81, 181)',
borderWidth: 1,
data: this.throughputData,
fill: false
}]
};
this.throughputChart.canvas.parentNode.style.height = `35vh`;
this.latencyChart.canvas.parentNode.style.height = `35vh`;
this.throughputOption = {
maintainAspectRatio: false,
responsive: true,
legend: {
display: false,
},
scales: {
xAxes: [{
display: true,
ticks: {
min: 0,
beginAtZero: true,
callback: function (value, index, values) {
return `${value}`;
}
},
scaleLabel: {
display: true,
labelString: `Package Size ( ${packageSize_unit} )`
}
}],
yAxes: [{
display: true,
ticks: {
callback: (value, index, values) => {
return `${value}`;
}
},
scaleLabel: {
display: true,
labelString: `Throughput ( ${throughput_unit} )`
}
}]
},
tooltips: {
callbacks: {
label: function (tooltipItem, data) {
return `${tooltipItem.yLabel} ${throughput_unit}`;
},
title: function (tooltipItem, data) {
return `${tooltipItem[0].xLabel} ${packageSize_unit}`;
}
}
}
};
this.latencyOption = {
maintainAspectRatio: false,
responsive: true,
legend: {
display: false,
},
scales: {
xAxes: [{
display: true,
ticks: {
min: 0,
beginAtZero: true,
callback: function (value, index, values) {
return `${value}`;
}
},
scaleLabel: {
display: true,
labelString: `Package Size ( ${packageSize_unit} )`
}
}],
yAxes: [{
display: true,
ticks: {
callback: (value, index, values) => {
return `${value}`;
}
},
scaleLabel: {
display: true,
labelString: `Latency ( ${latency_unit} )`
}
}]
},
tooltips: {
callbacks: {
label: function (tooltipItem, data) {
return `${tooltipItem.yLabel} ${latency_unit}`;
},
title: function (tooltipItem, data) {
return `${tooltipItem[0].xLabel} ${packageSize_unit}`;
}
}
}
};
}
}

Просмотреть файл

@ -1,44 +0,0 @@
<div class="selected-connectivity">
<div class="connectivity-item fixed-width">
<div class="name">
Latency:
</div>
<div class="connectivity-value">
{{selectedConnectivity.latency}}
</div>
</div>
<div class="connectivity-item fixed-width">
<div class="name">
Throughput:
</div>
<div class="connectivity-value">
{{selectedConnectivity.throughput}}
</div>
</div>
<div class="connectivity-item fixed-width">
<div class="name">
Runtime:
</div>
<div class="connectivity-value">
{{selectedConnectivity.runtime}}
</div>
</div>
<div class="connectivity-item">
<div class="name">
Selected Pair:
</div>
<div class="connectivity-value">
{{selectedConnectivity.nodes}}
</div>
</div>
</div>
<div class="connectivity">
<div *ngFor="let node of nodes; trackBy: trackByNodeFn" class="connectivity-content">
<div class="node-name">{{getNodeName(node)}}</div>
<div class="tile-area">
<div class="tile" [matTooltip]="connectivityTip(node, connectivity)" *ngFor="let connectivity of getConnectivity(node); index as i; trackBy: trackByConnectivityFn"
[ngClass]="connectivityClass(connectivity)" (click)="getConnectivityInfo(node,connectivity)" [tabindex]="i">
</div>
</div>
</div>
</div>

Просмотреть файл

@ -1,88 +0,0 @@
@import "../../../../../../stylesheets/mixin.scss";
.selected-connectivity {
padding: 10px;
@include display-flex(center);
border-bottom: 1px solid rgba(0, 0, 0, .12);
.connectivity-item {
font-size: .9em;
.name {
color: $color;
}
.connectivity-value {
font-size: 13px;
}
}
.fixed-width {
width: 150px;
}
}
.connectivity {
padding: 10px;
width: 100%;
max-height: 70vh;
padding-bottom: 0;
overflow: auto;
}
.connectivity-content {
@include display-flex(center);
// flex-wrap: nowrap;
// display: inline-block;
min-width: 100%;
.tile-area {
@include display-flex;
}
.tile {
min-width: 15px;
height: 15px;
margin: 2px;
cursor: pointer;
}
.tile:hover {
border: 1px solid #fff;
}
.tile:active {
border: 1px dashed #fff;
}
.tile:focus {
border: 1px dashed #fff;
}
.node-name {
min-width: 120px;
font-size: 13px;
@include ellipsis-text;
margin-right: 5px;
height: 15px
}
}
.good-connectivity {
background-color: #49a067;
}
.warning-connectivity {
background-color: #FF8F00;
}
.bad-connectivity {
background-color: gray;
}
::ng-deep .mat-tooltip {
white-space: pre;
font-size: 12px;
line-height: 20px;
}

Просмотреть файл

@ -1,92 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ConnectivityComponent } from './connectivity.component';
import { MaterialsModule } from '../../../../../../materials.module';
fdescribe('ConnectivityComponent', () => {
let component: ConnectivityComponent;
let fixture: ComponentFixture<ConnectivityComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ConnectivityComponent],
imports: [MaterialsModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConnectivityComponent);
component = fixture.componentInstance;
component.nodes = [
{
testNode1: [
{
testNode3: {
Connectivity: "Good",
Latency: "256.35 us",
Runtime: "4.375 s",
Throughput: "90.49 MB/s"
}
},
{
testNode2: {
Connectivity: "Good",
Latency: "169.1 us",
Runtime: "4.335 s",
Throughput: "91.33 MB/s"
}
},
{
testNode1: {
Connectivity: "Good",
Latency: "1.65 us",
Runtime: "0.249 s",
Throughput: "4178.42 MB/s"
}
}
]
},
{
testNode2: [
{
testNode3: {
Connectivity: "Good",
Latency: "607.2 us",
Runtime: "4.532 s",
Throughput: "91.98 MB/s"
}
},
{
testNode2: {
Connectivity: "Good",
Latency: "3.45 us",
Runtime: "0.294 s",
Throughput: "4007.74 MB/s"
}
}
]
},
{
testNode3: [
{
testNode3: {
Connectivity: "Good",
Latency: "2.15 us",
Runtime: "0.231 s",
Throughput: "4910.52 MB/s"
}
}
]
}
];
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
let tiles = fixture.nativeElement.querySelectorAll('.tile');
expect(tiles.length).toEqual(6);
});
});

Просмотреть файл

@ -1,74 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'pingpong-connectivity',
templateUrl: './connectivity.component.html',
styleUrls: ['./connectivity.component.scss']
})
export class ConnectivityComponent implements OnInit {
@Input()
nodes: any;
public selectedConnectivity = {
'nodes': ' - ',
'latency': ' - ',
'throughput': ' - ',
'runtime': ' - '
};
constructor() { }
ngOnInit() {
}
getConnectivityInfo(node, connectivity) {
let connectivityInfo = connectivity[Object.keys(connectivity)[0]];
let connectNodes = `${this.getNodeName(node)} <---> ${Object.keys(connectivity)[0]}`;
let latency = connectivityInfo['Latency'] == undefined ? ' - ' : connectivityInfo['Latency'];
let throughput = connectivityInfo['Throughput'] == undefined ? ' - ' : connectivityInfo['Throughput'];
let runtime = connectivityInfo['Runtime'] == undefined ? ' - ' : connectivityInfo['Runtime'];
this.selectedConnectivity = {
'nodes': connectNodes,
'latency': latency,
'throughput': throughput,
'runtime': runtime
};
}
connectivityTip(node, connectivity) {
let connectivityInfo = connectivity[Object.keys(connectivity)[0]];
let connectNodes = `${this.getNodeName(node)} <---> ${Object.keys(connectivity)[0]}`;
let latency = connectivityInfo['Latency'] == undefined ? ' - ' : connectivityInfo['Latency'];
let throughput = connectivityInfo['Throughput'] == undefined ? ' - ' : connectivityInfo['Throughput'];
let runtime = connectivityInfo['Runtime'] == undefined ? ' - ' : connectivityInfo['Runtime'];
return `${connectNodes}\r\nLatency: ${latency}\r\nThroughput: ${throughput}\r\nRuntime: ${runtime}`;
}
getConnectivity(node) {
return node[Object.keys(node)[0]];
}
public colorMap = {
'Bad': 'bad-connectivity',
'Warning': 'warning-connectivity',
'Good': 'good-connectivity'
};
connectivityClass(connectivity) {
let color = connectivity[Object.keys(connectivity)[0]]['Connectivity'];
return this.colorMap[color] ? this.colorMap[color] : 'bad-connectivity';
}
getNodeName(node) {
return Object.keys(node)[0];
}
trackByNodeFn(index, item) {
return index;
}
trackByConnectivityFn(index, item) {
return index;
}
}

Просмотреть файл

@ -1,59 +0,0 @@
<div class="overview-header">
<div class="chart-type">
<div class="chart-btn" [class.active]="activeMode == 'total'" (click)="setActiveMode('total')">Overview</div>
<div class="chart-btn" [class.active]="activeMode == 'node'" (click)="setActiveMode('node')">By Failed Node</div>
</div>
<div class="select-node" *ngIf="activeMode == 'node'">
<mat-form-field>
<mat-select placeholder="Select Failed Node" [(ngModel)]="selectedNode" (selectionChange)="changeNode()">
<mat-option *ngFor="let node of nodes" [value]="node" style="font-size: .9em;">
{{ node }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="overview-result">
<mat-accordion multi=true class="result-pair" *ngIf="activeMode == 'node'">
<mat-expansion-panel *ngFor="let f of failure">
<mat-expansion-panel-header>
<mat-panel-title>
<div> {{f}} </div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="pair-area">
<div class="failed-pair" *ngFor="let pr of failedNodes[selectedNode][f]">
<i class="material-icons node-icon">swap_horiz</i>
<div class="node-name" *ngFor="let node of pr">
<a [routerLink]="getLink(node)"> {{node}} </a>
</div>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
<mat-accordion multi=true class="result-pair" *ngIf="activeMode == 'total'">
<mat-expansion-panel *ngFor="let r of reasons">
<mat-expansion-panel-header>
<mat-panel-title>
<div> {{r.Reason}} </div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="node-info solution" *ngIf="r.Solution">
<i class="material-icons node-icon">lightbulb_outline</i>
<div class="node-name solution-text">{{r.Solution}}</div>
</div>
<div *ngIf="r.NodePairs" class="pair-area">
<div *ngFor="let pr of r.NodePairs" class="failed-pair">
<i class="material-icons node-icon">swap_horiz</i>
<div class="node-name" *ngFor="let node of pr">
<a [routerLink]="getLink(node)"> {{node}} </a>
</div>
</div>
</div>
<div *ngIf="r.Nodes" class="pair-area">
<app-nodes-info [nodes]="r.Nodes"></app-nodes-info>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>

Просмотреть файл

@ -1,112 +0,0 @@
@import "../../../../../../stylesheets/mixin.scss";
@import "../../../../../../stylesheets/info.scss";
.overview-result {
@include display-flex($justify-content: space-between);
margin-top: 10px;
}
.chart-type {
@include display-flex($align-items: center);
margin-top: 10px;
}
.chart-btn {
height: 2em;
font-size: .8em;
line-height: 2em;
padding: 0 1em;
color: #FFF;
background-color: rgba(63, 81, 181, .2);
text-align: center;
}
.chart-btn:hover {
cursor: pointer;
}
.chart-btn.active {
color: #FFF;
background-color: rgba(63, 81, 181, .9);
}
.overview-header {
@include display-flex($align-items: flex-start);
}
.select-node {
margin-top: .7em;
margin-left: 2em;
}
.mat-select {
font-size: .9em;
}
.pair-name {
padding-right: .5em;
}
.result-pair {
flex: 2;
}
.mat-expansion-panel-header {
height: auto !important;
padding: 10px !important;
}
.mat-expansion-panel-header-title {
font-size: 14px;
}
.result-pair {
overflow: hidden !important;
}
::ng-deep .result-pair .mat-expansion-panel-body {
padding: 0 0.8em 0.8em;
font-size: 16px; // overflow: hidden !important;
.pair-area {
max-height: 400px;
overflow: auto;
padding-right: 10px;
font-size: 13px;
}
}
mat-accordion {
height: 100%;
}
.solution {
font-size: 0.85em;
color: $color;
height: auto !important;
align-items: flex-start !important;
}
.solution-text {
overflow: visible !important;
white-space: pre-wrap !important;
}
.overview-result .node-name {
text-decoration: none;
}
.failed-pair {
@include display-flex($align-items: center);
height: 2.5em;
padding: 0 5px;
&:hover {
background-color: #eeeeee;
}
.node-name {
margin-right: 5px;
text-decoration: underline;
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше