зеркало из https://github.com/Azure/hpcpack-acm.git
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:
Коммит
b9478985d2
|
@ -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
|
|
@ -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,
|
||||
});
|
||||
};
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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;
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче