added app project files
This commit is contained in:
		
							parent
							
								
									991f5cabdc
								
							
						
					
					
						commit
						20ee57102e
					
				
							
								
								
									
										13
									
								
								app/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/.editorconfig
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					# Editor configuration, see http://editorconfig.org
 | 
				
			||||||
 | 
					root = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*]
 | 
				
			||||||
 | 
					charset = utf-8
 | 
				
			||||||
 | 
					indent_style = space
 | 
				
			||||||
 | 
					indent_size = 2
 | 
				
			||||||
 | 
					insert_final_newline = true
 | 
				
			||||||
 | 
					trim_trailing_whitespace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*.md]
 | 
				
			||||||
 | 
					max_line_length = off
 | 
				
			||||||
 | 
					trim_trailing_whitespace = false
 | 
				
			||||||
							
								
								
									
										42
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					# See http://help.github.com/ignore-files/ for more about ignoring files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# compiled output
 | 
				
			||||||
 | 
					/dist
 | 
				
			||||||
 | 
					/dist-server
 | 
				
			||||||
 | 
					/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
 | 
				
			||||||
 | 
					yarn-error.log
 | 
				
			||||||
 | 
					testem.log
 | 
				
			||||||
 | 
					/typings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# System Files
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					Thumbs.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server/node_modules/
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					https://medium.com/bb-tutorials-and-thoughts/how-to-develop-and-build-angular-app-with-nodejs-e24c40444421
 | 
				
			||||||
							
								
								
									
										196
									
								
								app/angular.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								app/angular.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
 | 
				
			||||||
 | 
					  "version": 1,
 | 
				
			||||||
 | 
					  "newProjectRoot": "projects",
 | 
				
			||||||
 | 
					  "projects":
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "manta":
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "root": "",
 | 
				
			||||||
 | 
					      "sourceRoot": "src",
 | 
				
			||||||
 | 
					      "projectType": "application",
 | 
				
			||||||
 | 
					      "prefix": "app",
 | 
				
			||||||
 | 
					      "schematics":
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "architect":
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "build":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:browser",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "progress": false,
 | 
				
			||||||
 | 
					            "outputPath": "dist",
 | 
				
			||||||
 | 
					            "index": "src/index.html",
 | 
				
			||||||
 | 
					            "main": "src/main.ts",
 | 
				
			||||||
 | 
					            "polyfills": "src/polyfills.ts",
 | 
				
			||||||
 | 
					            "tsConfig": "src/tsconfig.app.json",
 | 
				
			||||||
 | 
					            "assets":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "src/assets",
 | 
				
			||||||
 | 
					              "src/manifest.webmanifest",
 | 
				
			||||||
 | 
					              "src/env.js"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "styles":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "./node_modules/bootstrap/dist/css/bootstrap.min.css",
 | 
				
			||||||
 | 
					              "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",
 | 
				
			||||||
 | 
					              "./node_modules/ngx-toastr/toastr.css",
 | 
				
			||||||
 | 
					              "src/styles/styles.scss",
 | 
				
			||||||
 | 
					              "src/styles/icons.scss"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "scripts":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "configurations":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "production":
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "fileReplacements":
 | 
				
			||||||
 | 
					              [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                  "replace": "src/environments/environment.ts",
 | 
				
			||||||
 | 
					                  "with": "src/environments/environment.prod.ts"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					              "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": "manta:build",
 | 
				
			||||||
 | 
					            "proxyConfig": "proxy.conf.js"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "configurations":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "production":
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "browserTarget": "manta:build:production"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "extract-i18n":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:extract-i18n",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "browserTarget": "manta:build"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "test":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:karma",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "main": "src/test.ts",
 | 
				
			||||||
 | 
					            "polyfills": "src/polyfills.ts",
 | 
				
			||||||
 | 
					            "tsConfig": "src/tsconfig.spec.json",
 | 
				
			||||||
 | 
					            "karmaConfig": "src/karma.conf.js",
 | 
				
			||||||
 | 
					            "styles":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "src/styles.css"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "scripts":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "assets":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "src/assets"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "lint":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:tslint",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "tsConfig":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "src/tsconfig.app.json",
 | 
				
			||||||
 | 
					              "src/tsconfig.spec.json"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "exclude":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "**/node_modules/**"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "server":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:server",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "outputPath": "dist-server",
 | 
				
			||||||
 | 
					            "main": "src/main.ts",
 | 
				
			||||||
 | 
					            "tsConfig": "src/tsconfig.server.json"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "configurations":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "dev":
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "optimization": true,
 | 
				
			||||||
 | 
					              "outputHashing": "all",
 | 
				
			||||||
 | 
					              "sourceMap": false,
 | 
				
			||||||
 | 
					              "namedChunks": false,
 | 
				
			||||||
 | 
					              "extractLicenses": true,
 | 
				
			||||||
 | 
					              "vendorChunk": true
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "production":
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              "optimization": true,
 | 
				
			||||||
 | 
					              "outputHashing": "all",
 | 
				
			||||||
 | 
					              "sourceMap": false,
 | 
				
			||||||
 | 
					              "namedChunks": false,
 | 
				
			||||||
 | 
					              "extractLicenses": true,
 | 
				
			||||||
 | 
					              "vendorChunk": false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "manta-e2e":
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "root": "e2e/",
 | 
				
			||||||
 | 
					      "projectType": "application",
 | 
				
			||||||
 | 
					      "architect":
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "e2e":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:protractor",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "protractorConfig": "e2e/protractor.conf.js",
 | 
				
			||||||
 | 
					            "devServerTarget": "manta:serve"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "lint":
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "builder": "@angular-devkit/build-angular:tslint",
 | 
				
			||||||
 | 
					          "options":
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "tsConfig": "e2e/tsconfig.e2e.json",
 | 
				
			||||||
 | 
					            "exclude":
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					              "**/node_modules/**"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "defaultProject": "manta"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/browserslist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/browserslist
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
 | 
				
			||||||
 | 
					# For additional information regarding the format and rule options, please see:
 | 
				
			||||||
 | 
					# https://github.com/browserslist/browserslist#queries
 | 
				
			||||||
 | 
					# For IE 9-11 support, please uncomment the last line of the file and adjust as needed
 | 
				
			||||||
 | 
					> 0.5%
 | 
				
			||||||
 | 
					last 2 versions
 | 
				
			||||||
 | 
					Firefox ESR
 | 
				
			||||||
 | 
					not dead
 | 
				
			||||||
 | 
					# IE 9-11
 | 
				
			||||||
							
								
								
									
										28
									
								
								app/e2e/protractor.conf.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/e2e/protractor.conf.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					// 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: ["./src/**/*.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: require("path").join(__dirname, "./tsconfig.e2e.json")
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    jasmine
 | 
				
			||||||
 | 
					      .getEnv()
 | 
				
			||||||
 | 
					      .addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										14
									
								
								app/e2e/src/app.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/e2e/src/app.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { AppPage } from './app.po';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('App', () => {
 | 
				
			||||||
 | 
					  let page: AppPage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    page = new AppPage();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should display welcome message', () => {
 | 
				
			||||||
 | 
					    page.navigateTo();
 | 
				
			||||||
 | 
					    expect(page.getMainHeading()).toEqual('Hello, world!');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										11
									
								
								app/e2e/src/app.po.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/e2e/src/app.po.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { browser, by, element } from 'protractor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AppPage {
 | 
				
			||||||
 | 
					  navigateTo() {
 | 
				
			||||||
 | 
					    return browser.get('/');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getMainHeading() {
 | 
				
			||||||
 | 
					    return element(by.css('app-root h1')).getText();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								app/e2e/tsconfig.e2e.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/e2e/tsconfig.e2e.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "../tsconfig.json",
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "outDir": "../out-tsc/app",
 | 
				
			||||||
 | 
					    "module": "commonjs",
 | 
				
			||||||
 | 
					    "target": "es5",
 | 
				
			||||||
 | 
					    "types": [
 | 
				
			||||||
 | 
					      "jasmine",
 | 
				
			||||||
 | 
					      "jasminewd2",
 | 
				
			||||||
 | 
					      "node"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										29
									
								
								app/ngsw-config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/ngsw-config.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
 | 
				
			||||||
 | 
					  "index": "/index.html",
 | 
				
			||||||
 | 
					  "assetGroups": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "app",
 | 
				
			||||||
 | 
					      "installMode": "prefetch",
 | 
				
			||||||
 | 
					      "resources": {
 | 
				
			||||||
 | 
					        "files": [
 | 
				
			||||||
 | 
					          "/favicon.ico",
 | 
				
			||||||
 | 
					          "/index.html",
 | 
				
			||||||
 | 
					          "/*.css",
 | 
				
			||||||
 | 
					          "/*.js",
 | 
				
			||||||
 | 
					          "/manifest.webmanifest"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, {
 | 
				
			||||||
 | 
					      "name": "assets",
 | 
				
			||||||
 | 
					      "installMode": "lazy",
 | 
				
			||||||
 | 
					      "updateMode": "prefetch",
 | 
				
			||||||
 | 
					      "resources": {
 | 
				
			||||||
 | 
					        "files": [
 | 
				
			||||||
 | 
					          "/assets/**",
 | 
				
			||||||
 | 
					          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16038
									
								
								app/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16038
									
								
								app/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										79
									
								
								app/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								app/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "manta",
 | 
				
			||||||
 | 
					  "version": "0.0.0",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "ng": "ng",
 | 
				
			||||||
 | 
					    "start": "ng serve",
 | 
				
			||||||
 | 
					    "build": "ng build",
 | 
				
			||||||
 | 
					    "build:ssr": "ng run manta:server:dev",
 | 
				
			||||||
 | 
					    "test": "ng test",
 | 
				
			||||||
 | 
					    "lint": "ng lint",
 | 
				
			||||||
 | 
					    "e2e": "ng e2e"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@angular-slider/ngx-slider": "^2.0.3",
 | 
				
			||||||
 | 
					    "@angular/animations": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/cdk": "^11.0.3",
 | 
				
			||||||
 | 
					    "@angular/common": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/compiler": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/core": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/forms": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/http": "^7.2.16",
 | 
				
			||||||
 | 
					    "@angular/platform-browser": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/platform-browser-dynamic": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/platform-server": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/router": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/service-worker": "^11.0.5",
 | 
				
			||||||
 | 
					    "@fortawesome/angular-fontawesome": "^0.4.0",
 | 
				
			||||||
 | 
					    "@fortawesome/fontawesome-svg-core": "^1.2.27",
 | 
				
			||||||
 | 
					    "@fortawesome/free-brands-svg-icons": "^5.12.1",
 | 
				
			||||||
 | 
					    "@fortawesome/free-solid-svg-icons": "^5.12.1",
 | 
				
			||||||
 | 
					    "@nguniversal/module-map-ngfactory-loader": "^8.1.1",
 | 
				
			||||||
 | 
					    "@ngx-translate/core": "^12.1.2",
 | 
				
			||||||
 | 
					    "@swimlane/ngx-datatable": "^19.0.0",
 | 
				
			||||||
 | 
					    "aspnet-prerendering": "^3.0.1",
 | 
				
			||||||
 | 
					    "backoff-rxjs": "^6.5.7",
 | 
				
			||||||
 | 
					    "bootstrap": "5.0.0-beta2",
 | 
				
			||||||
 | 
					    "core-js": "^3.3.3",
 | 
				
			||||||
 | 
					    "crypto-js": "^4.0.0",
 | 
				
			||||||
 | 
					    "fuse.js": "^5.1.0",
 | 
				
			||||||
 | 
					    "messageformat": "^2.3.0",
 | 
				
			||||||
 | 
					    "ngx-autosize": "^1.7.4",
 | 
				
			||||||
 | 
					    "ngx-bootstrap": "^6.2.0",
 | 
				
			||||||
 | 
					    "ngx-clipboard": "^14.0.1",
 | 
				
			||||||
 | 
					    "ngx-timeago": "^2.0.0",
 | 
				
			||||||
 | 
					    "ngx-toastr": "^11.3.3",
 | 
				
			||||||
 | 
					    "ngx-translate-messageformat-compiler": "^4.5.0",
 | 
				
			||||||
 | 
					    "ngx-virtual-scroller": "^4.0.3",
 | 
				
			||||||
 | 
					    "oidc-client": "^1.9.1",
 | 
				
			||||||
 | 
					    "rxjs": "^6.5.3",
 | 
				
			||||||
 | 
					    "ts-cacheable": "^1.0.4",
 | 
				
			||||||
 | 
					    "zone.js": "0.9.1"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@angular-devkit/build-angular": "^0.1100.5",
 | 
				
			||||||
 | 
					    "@angular/cli": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/compiler-cli": "^11.0.5",
 | 
				
			||||||
 | 
					    "@angular/language-service": "^11.0.5",
 | 
				
			||||||
 | 
					    "@types/crypto-js": "^4.0.1",
 | 
				
			||||||
 | 
					    "@types/jasmine": "~3.4.4",
 | 
				
			||||||
 | 
					    "@types/jasminewd2": "~2.0.8",
 | 
				
			||||||
 | 
					    "@types/node": "~12.11.6",
 | 
				
			||||||
 | 
					    "codelyzer": "^5.2.0",
 | 
				
			||||||
 | 
					    "jasmine-core": "~3.5.0",
 | 
				
			||||||
 | 
					    "jasmine-spec-reporter": "~4.2.1",
 | 
				
			||||||
 | 
					    "karma": "^5.0.2",
 | 
				
			||||||
 | 
					    "karma-chrome-launcher": "~3.1.0",
 | 
				
			||||||
 | 
					    "karma-coverage-istanbul-reporter": "~2.1.0",
 | 
				
			||||||
 | 
					    "karma-jasmine": "~2.0.1",
 | 
				
			||||||
 | 
					    "karma-jasmine-html-reporter": "^1.4.2",
 | 
				
			||||||
 | 
					    "typescript": "4.0.5",
 | 
				
			||||||
 | 
					    "webpack-dev-server": "^3.10.3"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "optionalDependencies": {
 | 
				
			||||||
 | 
					    "protractor": "~5.4.2",
 | 
				
			||||||
 | 
					    "ts-node": "~8.4.1",
 | 
				
			||||||
 | 
					    "tslint": "~5.20.0"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								app/proxy.conf.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/proxy.conf.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  '/api': {
 | 
				
			||||||
 | 
					    target: 'https://localhost:8443',
 | 
				
			||||||
 | 
					    secure: false,
 | 
				
			||||||
 | 
					    pathRewrite: {'^/api': ''},
 | 
				
			||||||
 | 
					    logLevel: 'debug'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					<form novalidate>
 | 
				
			||||||
 | 
					  <fieldset [formGroup]="editorForm" [disabled]="working">
 | 
				
			||||||
 | 
					    <button type="button" class="close" [attr.aria-label]="'general.closeWithoutSaving' | translate" (click)="close()">
 | 
				
			||||||
 | 
					      <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content">
 | 
				
			||||||
 | 
					      <h4>Update your profile</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="row g-3 mt-3">
 | 
				
			||||||
 | 
					        <div class="col-sm-4">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="firstName" formControlName="firstName" placeholder="First name" [appAutofocus]="true" [appAutofocusDelay]="600">
 | 
				
			||||||
 | 
					            <label for="firstName">First name</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-sm-4">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="lastName" formControlName="lastName" placeholder="Last name">
 | 
				
			||||||
 | 
					            <label for="lastName">Last name</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-sm-4">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="companyName" formControlName="companyName" placeholder="Company name">
 | 
				
			||||||
 | 
					            <label for="companyName">Company name</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="col-sm-10">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="address" formControlName="address" placeholder="Address">
 | 
				
			||||||
 | 
					            <label for="address">Address</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-sm-2">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="postalCode" formControlName="postalCode" placeholder="Postal code">
 | 
				
			||||||
 | 
					            <label for="postalCode">Postal code</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="col-sm-4">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="city" formControlName="city" placeholder="City">
 | 
				
			||||||
 | 
					            <label for="city">City</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-sm-4">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="state" formControlName="state" placeholder="State">
 | 
				
			||||||
 | 
					            <label for="state">State</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-sm-4">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
 | 
				
			||||||
 | 
					            <label for="country">Country</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="col-sm-6">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="tel" class="form-control" id="phone" formControlName="phone" placeholder="Phone">
 | 
				
			||||||
 | 
					            <label for="phone">Phone</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-sm-6">
 | 
				
			||||||
 | 
					          <div class="form-floating">
 | 
				
			||||||
 | 
					            <input type="email" class="form-control" id="email" formControlName="email" placeholder="Email">
 | 
				
			||||||
 | 
					            <label for="email">Email</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-12 mt-3">
 | 
				
			||||||
 | 
					          <span class="form-check">
 | 
				
			||||||
 | 
					            <input class="form-check-input" type="checkbox" id="cns" formControlName="cns">
 | 
				
			||||||
 | 
					            <label class="form-check-label" for="cns">Use Container Name Service</label>
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="d-flex justify-content-end mt-5">
 | 
				
			||||||
 | 
					        <button class="btn btn-link text-info me-3" (click)="close()">Close without saving</button>
 | 
				
			||||||
 | 
					        <button class="btn btn-lg btn-info" (click)="saveChanges()">
 | 
				
			||||||
 | 
					          <fa-icon icon="spinner" [pulse]="true" class="me-1" *ngIf="working"></fa-icon>
 | 
				
			||||||
 | 
					          Save changes
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </fieldset>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					input
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  padding: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-check
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0 0 0 1.5rem;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .form-check-input
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    margin-right: .5rem;
 | 
				
			||||||
 | 
					    float: none;
 | 
				
			||||||
 | 
					    width: 1.4em;
 | 
				
			||||||
 | 
					    max-width: 1rem;
 | 
				
			||||||
 | 
					    margin-bottom: .25rem;
 | 
				
			||||||
 | 
					    cursor: inherit;
 | 
				
			||||||
 | 
					    background-color: #0dc3e9;
 | 
				
			||||||
 | 
					    border-color: #0dc3e9;
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 1px rgb(12, 19, 33, .5) inset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:checked
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      background-color: #ff9c07;
 | 
				
			||||||
 | 
					      border-color: #ff9c07;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:focus)
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        box-shadow: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:checked[type=radio]
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%230c1321'/%3e%3c/svg%3e");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:checked[type=checkbox]
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%230c1321' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      box-shadow: 0 0 0 0.25rem rgba(13, 195, 233, .5);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:checked:focus
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      box-shadow: 0 0 0 0.25rem rgba(255, 156, 7, .25);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .form-check-label
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    cursor: inherit;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: .75rem 0;
 | 
				
			||||||
 | 
					    font-family: "Bebas Neue", sans-serif;
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-check-input:checked + .form-check-label,
 | 
				
			||||||
 | 
					.form-check-input:checked + .form-check-label .package-specs,
 | 
				
			||||||
 | 
					.form-check-input:checked + .form-check-label .h3
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AccountEditorComponent } from './account-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AccountEditorComponent', () => {
 | 
				
			||||||
 | 
					  let component: AccountEditorComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<AccountEditorComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ AccountEditorComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(AccountEditorComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										101
									
								
								app/src/app/account/account-editor/account-editor.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								app/src/app/account/account-editor/account-editor.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,101 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					import { BsModalRef } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
 | 
				
			||||||
 | 
					import { NavigationStart, Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { filter, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { AuthService } from '../../helpers/auth.service';
 | 
				
			||||||
 | 
					import { UserInfo } from '../models/user-info';
 | 
				
			||||||
 | 
					import { AccountService } from '../helpers/account.service';
 | 
				
			||||||
 | 
					import { ToastrService } from 'ngx-toastr';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-account-editor',
 | 
				
			||||||
 | 
					  templateUrl: './account-editor.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./account-editor.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AccountEditorComponent implements OnInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  save = new Subject<any>();
 | 
				
			||||||
 | 
					  loading: boolean;
 | 
				
			||||||
 | 
					  working: boolean;
 | 
				
			||||||
 | 
					  editorForm: FormGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly modalRef: BsModalRef,
 | 
				
			||||||
 | 
					    private readonly router: Router,
 | 
				
			||||||
 | 
					    private readonly fb: FormBuilder,
 | 
				
			||||||
 | 
					    private readonly authService: AuthService,
 | 
				
			||||||
 | 
					    private readonly accountService: AccountService,
 | 
				
			||||||
 | 
					    private readonly toastr: ToastrService)
 | 
				
			||||||
 | 
					  {    // When the user navigates away from this route, hide the modal
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$),
 | 
				
			||||||
 | 
					        filter(e => e instanceof NavigationStart)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.modalRef.hide());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    authService.userInfoUpdated$.subscribe(this.createForm.bind(this));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private createForm(userInfo: UserInfo)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm = this.fb.group(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: [userInfo.id],
 | 
				
			||||||
 | 
					        firstName: [userInfo.firstName],
 | 
				
			||||||
 | 
					        lastName: [userInfo.lastName],
 | 
				
			||||||
 | 
					        companyName: [userInfo.companyName],
 | 
				
			||||||
 | 
					        address: [userInfo.address],
 | 
				
			||||||
 | 
					        postalCode: [userInfo.postalCode],
 | 
				
			||||||
 | 
					        city: [userInfo.city],
 | 
				
			||||||
 | 
					        state: [userInfo.state],
 | 
				
			||||||
 | 
					        country: [userInfo.country],
 | 
				
			||||||
 | 
					        email: [userInfo.email],
 | 
				
			||||||
 | 
					        phone: [userInfo.phone],
 | 
				
			||||||
 | 
					        cns: [userInfo.triton_cns_enabled]
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  close(response?: any)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.save.next(response);
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  saveChanges()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.working = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const changes = this.editorForm.getRawValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userInfo = new UserInfo();
 | 
				
			||||||
 | 
					    Object.assign(userInfo, changes);
 | 
				
			||||||
 | 
					    userInfo.triton_cns_enabled = changes.cns;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.accountService.updateAccount(userInfo)
 | 
				
			||||||
 | 
					      .subscribe(response =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.authService.userInfo = response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.close(response);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.working = false;
 | 
				
			||||||
 | 
					      }, err =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          this.toastr.error(err.error.message);
 | 
				
			||||||
 | 
					          this.working = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								app/src/app/account/account.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								app/src/app/account/account.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					<div *ngIf="userInfo" class="pt-3">
 | 
				
			||||||
 | 
					  <div class="row">
 | 
				
			||||||
 | 
					    <div class="col-sm-6">
 | 
				
			||||||
 | 
					      <div class="card">
 | 
				
			||||||
 | 
					        <div class="card-header d-flex justify-content-between align-items-center">
 | 
				
			||||||
 | 
					          <h4>My profile</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <button class="btn btn-sm btn-outline-info" (click)="showEditor()">Update profile</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <ul class="list-group list-group-flush">
 | 
				
			||||||
 | 
					            <li class="list-group-item">
 | 
				
			||||||
 | 
					              Name: <b>{{ userInfo.firstName }} {{ userInfo.lastName }}</b>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li class="list-group-item">
 | 
				
			||||||
 | 
					              Username: <b>{{ userInfo.login }}</b>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li class="list-group-item">
 | 
				
			||||||
 | 
					              Email: <b>{{ userInfo.email }}</b>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li class="list-group-item">
 | 
				
			||||||
 | 
					              Phone: <b>{{ userInfo.phone }}</b>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li class="list-group-item">
 | 
				
			||||||
 | 
					              Container Name Service:
 | 
				
			||||||
 | 
					              <span class="badge border ms-1 text-uppercase"
 | 
				
			||||||
 | 
					                    [ngClass]="userInfo.triton_cns_enabled ? 'border-success text-success' : 'danger-success text-danger'">
 | 
				
			||||||
 | 
					                {{ userInfo.triton_cns_enabled ? 'enabled' : 'disabled' }}
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					          </ul>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="col-sm-6">
 | 
				
			||||||
 | 
					      <div class="card">
 | 
				
			||||||
 | 
					        <div class="card-header d-flex justify-content-between align-items-center">
 | 
				
			||||||
 | 
					          <h4>My SSH keys</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <button class="btn btn-sm btn-outline-info" (click)="addSshKey()">Add SSH key</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <ol class="list-group list-group-flush">
 | 
				
			||||||
 | 
					            <li class="list-group-item" *ngFor="let userKey of userKeys">
 | 
				
			||||||
 | 
					              {{ userKey.name }}: <b class="text-uppercase">{{ userKey.fingerprint }}</b>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					          </ol>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										42
									
								
								app/src/app/account/account.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/src/app/account/account.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					ul, ol
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  background-color: rgba(16, 21, 39, .75);
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h4
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.list-group-item
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					  border-color: rgb(61, 94, 142, .25);
 | 
				
			||||||
 | 
					  color: #5a8cd8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  b
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    color: #ff9c07;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  border: 1px solid rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					  background-color: rgba(16, 21, 39, 0.5);
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
 | 
				
			||||||
 | 
					  transition: box-shadow 0.15s ease-out;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgba(18, 203, 240, .4), 0 0 10px 3px #0e162a;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .card-body
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								app/src/app/account/account.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/app/account/account.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AccountComponent } from './account.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AccountComponent', () => {
 | 
				
			||||||
 | 
					  let component: AccountComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<AccountComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ AccountComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(AccountComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										90
									
								
								app/src/app/account/account.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								app/src/app/account/account.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
				
			||||||
 | 
					import { AccountService } from './helpers/account.service';
 | 
				
			||||||
 | 
					import { AuthService } from '../helpers/auth.service';
 | 
				
			||||||
 | 
					import { first, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { UserInfo } from './models/user-info';
 | 
				
			||||||
 | 
					import { UserKey } from './models/user-key';
 | 
				
			||||||
 | 
					import { BsModalService } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { AccountEditorComponent } from './account-editor/account-editor.component';
 | 
				
			||||||
 | 
					import { ToastrService } from 'ngx-toastr';
 | 
				
			||||||
 | 
					import { SshKeyEditorComponent } from './ssh-key-editor/ssh-key-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-account',
 | 
				
			||||||
 | 
					  templateUrl: './account.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./account.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AccountComponent implements OnInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  userInfo: UserInfo;
 | 
				
			||||||
 | 
					  userKeys: UserKey[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly accountService: AccountService,
 | 
				
			||||||
 | 
					    private readonly authService: AuthService,
 | 
				
			||||||
 | 
					    private readonly modalService: BsModalService,
 | 
				
			||||||
 | 
					    private readonly toastr: ToastrService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    //accountService.getUsers().subscribe(x => console.log(x));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    accountService.getUserLimits().subscribe(x => console.log(x));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    authService.userInfoUpdated$
 | 
				
			||||||
 | 
					      .pipe(takeUntil(this.destroy$))
 | 
				
			||||||
 | 
					      .subscribe(x => this.userInfo = x);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    accountService.getKeys().subscribe(x => this.userKeys = x);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  showEditor()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    const modalConfig = {
 | 
				
			||||||
 | 
					      ignoreBackdropClick: true,
 | 
				
			||||||
 | 
					      keyboard: false,
 | 
				
			||||||
 | 
					      animated: true,
 | 
				
			||||||
 | 
					      initialState: {}
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modalRef = this.modalService.show(AccountEditorComponent, modalConfig);
 | 
				
			||||||
 | 
					    modalRef.setClass('modal-lg');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  addSshKey()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    const modalConfig = {
 | 
				
			||||||
 | 
					      ignoreBackdropClick: true,
 | 
				
			||||||
 | 
					      keyboard: false,
 | 
				
			||||||
 | 
					      animated: true,
 | 
				
			||||||
 | 
					      initialState: {}
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modalRef = this.modalService.show(SshKeyEditorComponent, modalConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    modalRef.content.save.pipe(first()).subscribe(x => this.userKeys = [...this.userKeys, x]);
 | 
				
			||||||
 | 
					    //  this.accountService.addKey('test',
 | 
				
			||||||
 | 
					    //    'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAzf7Cbu8tPvxgwG3MhXK959F7TtsSCQQXb3jSPAJtQT+CltA+OYLod/ojclfQfnutIHUpqq6PsCD/nhxiF2JYkKWve7olJV6akvXQOGNLqRdXTcEouUhevLAQV3sB+YNvjr5FRpspNK8prAn7UU4vyZhCKBT8VAgwkio3u8eR/26XDNow1C9NXC6P+2BYWjjKbJCI41XpLFIzsmHBw+XZox+IbVg8mcVsWfdhEHRDyxM1HgvOKU9vkCwigmww9nsIatSQuM0jCtohQRkddc2DlfKieBmpeC/VqNoWE77iei/nVOcgIaLjwwevdCGHhwtSBmkE+W14JCwFbzl0yThL2w== rsa-key-20210314',
 | 
				
			||||||
 | 
					    //    'ba:04:55:94:64:24:75:a4:b2:60:e5:bf:77:19:df:34')
 | 
				
			||||||
 | 
					    //    .subscribe(response => this.userKeys = [...this.userKeys, response],
 | 
				
			||||||
 | 
					    //      err =>
 | 
				
			||||||
 | 
					    //      {
 | 
				
			||||||
 | 
					    //        this.toastr.error(err.error.message)
 | 
				
			||||||
 | 
					    //      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.destroy$.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								app/src/app/account/account.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/src/app/account/account.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import { NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SharedModule } from '../shared.module';
 | 
				
			||||||
 | 
					import { RouterModule } from '@angular/router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TranslateModule, TranslateService, LangChangeEvent } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { TranslateLoader } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { WebpackTranslateLoader } from '../helpers/webpack-translate-loader.service';
 | 
				
			||||||
 | 
					import { TranslateCompiler } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AccountComponent } from './account.component';
 | 
				
			||||||
 | 
					import { AccountEditorComponent } from './account-editor/account-editor.component';
 | 
				
			||||||
 | 
					import { SshKeyEditorComponent } from './ssh-key-editor/ssh-key-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					  declarations: [AccountComponent, AccountEditorComponent, SshKeyEditorComponent],
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    SharedModule,
 | 
				
			||||||
 | 
					    RouterModule.forChild([
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        component: AccountComponent
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]),
 | 
				
			||||||
 | 
					    TranslateModule.forChild({
 | 
				
			||||||
 | 
					      loader: {
 | 
				
			||||||
 | 
					        provide: TranslateLoader,
 | 
				
			||||||
 | 
					        //useClass: WebpackTranslateLoader
 | 
				
			||||||
 | 
					        useFactory: () => new WebpackTranslateLoader('account')
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      compiler: {
 | 
				
			||||||
 | 
					        provide: TranslateCompiler,
 | 
				
			||||||
 | 
					        useFactory: () => new TranslateMessageFormatCompiler()
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      isolate: true
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AccountModule
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  constructor(private readonly translate: TranslateService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    translate.use(translate.store.currentLang);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    translate.store.onLangChange.subscribe((event: LangChangeEvent) => translate.use(event.lang));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								app/src/app/account/helpers/account.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/app/account/helpers/account.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AccountService } from './account.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AccountService', () => {
 | 
				
			||||||
 | 
					  let service: AccountService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({});
 | 
				
			||||||
 | 
					    service = TestBed.inject(AccountService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be created', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										50
									
								
								app/src/app/account/helpers/account.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/src/app/account/helpers/account.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
 | 
					import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
 | 
				
			||||||
 | 
					import { UserInfo } from '../models/user-info';
 | 
				
			||||||
 | 
					import { Observable } from 'rxjs';
 | 
				
			||||||
 | 
					import { UserKey } from '../models/user-key';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable({
 | 
				
			||||||
 | 
					  providedIn: 'root'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AccountService
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly httpClient: HttpClient) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  getUserInfo(): Observable<UserInfo>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<UserInfo>(`/api/my`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  updateAccount(userInfo: UserInfo): Observable<UserInfo>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.post<UserInfo>(`/api/my`, userInfo);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  getUserLimits()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get(`/api/my/limits`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  getKeys(): Observable<UserKey[]>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<UserKey[]>(`/api/my/keys`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  addKey(name: string, key: string, fingerprint: string): Observable<UserKey>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.post<UserKey>(`/api/my/keys`, { name, key, fingerprint });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  deleteKey(name: string)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.delete(`/api/my/keys/${name}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/src/app/account/models/user-info.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/src/app/account/models/user-info.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import { User } from '../../security/models/user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UserInfo extends User
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  triton_cns_enabled: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/src/app/account/models/user-key.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/src/app/account/models/user-key.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					export class UserKey
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  fingerprint: string;
 | 
				
			||||||
 | 
					  key: string;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					<form novalidate>
 | 
				
			||||||
 | 
					  <fieldset [formGroup]="editorForm" [disabled]="working" *ngIf="editorForm">
 | 
				
			||||||
 | 
					    <button type="button" class="close" [attr.aria-label]="'general.closeWithoutSaving' | translate" (click)="close()">
 | 
				
			||||||
 | 
					      <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content">
 | 
				
			||||||
 | 
					      <h4 class="mb-3">SSH key editor</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="mt-3">
 | 
				
			||||||
 | 
					        <input type="text" class="form-control" formControlName="name" placeholder="Name" [appAutofocus]="true" [appAutofocusDelay]="600">
 | 
				
			||||||
 | 
					        <textarea rows="4" class="form-control" formControlName="key" placeholder="SSH key"></textarea>
 | 
				
			||||||
 | 
					        <input type="text" class="form-control" formControlName="fingerprint" placeholder="Fingerprint">
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="d-flex justify-content-end align-items-center mt-5">
 | 
				
			||||||
 | 
					        <button class="btn btn-info" (click)="saveChanges()" [disabled]="editorForm.invalid">Save changes</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </fieldset>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					p
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					  font-size: 1.15rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-control
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  + .form-control
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    margin-top: .5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					textarea
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  border-radius: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SshKeyEditorComponent } from './ssh-key-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('SshKeyEditorComponent', () => {
 | 
				
			||||||
 | 
					  let component: SshKeyEditorComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<SshKeyEditorComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ SshKeyEditorComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(SshKeyEditorComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnDestroy, Input } from '@angular/core';
 | 
				
			||||||
 | 
					import { BsModalRef } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
 | 
				
			||||||
 | 
					import { NavigationStart, Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { filter, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { AccountService } from '../helpers/account.service';
 | 
				
			||||||
 | 
					import { ToastrService } from 'ngx-toastr';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-ssh-key-editor',
 | 
				
			||||||
 | 
					  templateUrl: './ssh-key-editor.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./ssh-key-editor.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class SshKeyEditorComponent implements OnInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  save = new Subject<any>();
 | 
				
			||||||
 | 
					  editorForm: FormGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly accountService: AccountService,
 | 
				
			||||||
 | 
					    private readonly modalRef: BsModalRef,
 | 
				
			||||||
 | 
					    private readonly router: Router,
 | 
				
			||||||
 | 
					    private readonly fb: FormBuilder,
 | 
				
			||||||
 | 
					    private readonly toastr: ToastrService)
 | 
				
			||||||
 | 
					  {    // When the user navigates away from this route, hide the modal
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$),
 | 
				
			||||||
 | 
					        filter(e => e instanceof NavigationStart)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.modalRef.hide());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private createForm()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm = this.fb.group(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name: [null, Validators.required],
 | 
				
			||||||
 | 
					        key: [null, Validators.required],
 | 
				
			||||||
 | 
					        fingerprint: [null, Validators.required]
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  close()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  saveChanges()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    const sshKey = this.editorForm.getRawValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.accountService.addKey(sshKey.name, sshKey.key, sshKey.fingerprint)
 | 
				
			||||||
 | 
					      .subscribe(response =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.save.next(response);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.modalRef.hide();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					        err =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          this.toastr.error(err.error.message);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.createForm();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.destroy$.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								app/src/app/app-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								app/src/app/app-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					import { NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					import { Routes, RouterModule } from '@angular/router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { UnauthorizedComponent } from './pages/unauthorized/unauthorized.component';
 | 
				
			||||||
 | 
					import { NotFoundComponent } from './pages/not-found/not-found.component';
 | 
				
			||||||
 | 
					import { AuthGuardService } from './helpers/auth-guard.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appRoutes: Routes = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '',
 | 
				
			||||||
 | 
					    pathMatch: 'full',
 | 
				
			||||||
 | 
					    redirectTo: 'dashboard',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'file-manager',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./file-manager/file-manager.module').then(x => x.FileManagerModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					    data:
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: 'fileManager.title',
 | 
				
			||||||
 | 
					      subTitle: 'fileManager.subTitle',
 | 
				
			||||||
 | 
					      icon: 'folder'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'dashboard',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./instances/instances.module').then(x => x.InstancesModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'catalog',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./catalog/catalog.module').then(x => x.CatalogModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'volumes',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./volumes/volumes.module').then(x => x.VolumesModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'networking',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./networking/networking.module').then(x => x.NetworkingModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'security',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./security/security.module').then(x => x.SecurityModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					    data:
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: 'security.title',
 | 
				
			||||||
 | 
					      subTitle: 'security.subTitle',
 | 
				
			||||||
 | 
					      icon: 'shield-alt'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'account',
 | 
				
			||||||
 | 
					    loadChildren: () => import('./account/account.module').then(x => x.AccountModule),
 | 
				
			||||||
 | 
					    canActivate: [AuthGuardService],
 | 
				
			||||||
 | 
					    canLoad: [AuthGuardService],
 | 
				
			||||||
 | 
					    data:
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: 'account.title',
 | 
				
			||||||
 | 
					      subTitle: 'account.subTitle',
 | 
				
			||||||
 | 
					      icon: 'user-cog'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'unauthorized',
 | 
				
			||||||
 | 
					    component: UnauthorizedComponent
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '**',
 | 
				
			||||||
 | 
					    component: NotFoundComponent
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					  imports: [RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })],
 | 
				
			||||||
 | 
					  exports: [RouterModule]
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AppRoutingModule { }
 | 
				
			||||||
							
								
								
									
										57
									
								
								app/src/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/src/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					<main [class.menu-open]="menuVisibility">
 | 
				
			||||||
 | 
					  <div class="module-loading-spinner" *ngIf="showProgress">
 | 
				
			||||||
 | 
					    <div class="spinner"></div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="backdrop" *ngIf="menuVisibility" (click)="menuVisibility = false"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="menu">
 | 
				
			||||||
 | 
					    <app-nav-menu (navigate)="menuVisibility = false"></app-nav-menu>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="pusher">
 | 
				
			||||||
 | 
					    <div class="content">
 | 
				
			||||||
 | 
					      <div class="content-inner d-flex flex-column h-100">
 | 
				
			||||||
 | 
					        <header class="sticky-top">
 | 
				
			||||||
 | 
					          <nav class="navbar w-100 px-2">
 | 
				
			||||||
 | 
					            <div class="brand d-flex align-items-center w-75">
 | 
				
			||||||
 | 
					              <a href="javascript:void(0)" class="icon" (click)="menuVisibility = !menuVisibility">
 | 
				
			||||||
 | 
					                <fa-icon [fixedWidth]="true" icon="outdent"></fa-icon>
 | 
				
			||||||
 | 
					                {{ title | translate }}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div *ngIf="userInfo" class="btn-group" role="group" dropdown placement="bottom right">
 | 
				
			||||||
 | 
					              <button id="accountMenuDropdown" type="button" class="btn btn-link dropdown-toggle" dropdownToggle aria-expanded="false">
 | 
				
			||||||
 | 
					                <fa-icon icon="user" size="sm" class="me-1 align-middle"></fa-icon>
 | 
				
			||||||
 | 
					                <span class="d-none d-sm-inline-block">{{ userInfo.firstName }} {{ userInfo.lastName }}</span>
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					              <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" aria-labelledby="accountMenuDropdown">
 | 
				
			||||||
 | 
					                <li>
 | 
				
			||||||
 | 
					                  <a class="dropdown-item" [routerLink]="['./account']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					                    <fa-icon icon="user-cog"></fa-icon>
 | 
				
			||||||
 | 
					                    Account details
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                </li>
 | 
				
			||||||
 | 
					                <li class="dropdown-divider"></li>
 | 
				
			||||||
 | 
					                <li>
 | 
				
			||||||
 | 
					                  <button class="dropdown-item" (click)="logOff()">
 | 
				
			||||||
 | 
					                    <fa-icon icon="power-off"></fa-icon>
 | 
				
			||||||
 | 
					                    Log off
 | 
				
			||||||
 | 
					                  </button>
 | 
				
			||||||
 | 
					                </li>
 | 
				
			||||||
 | 
					              </ul>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          </nav>
 | 
				
			||||||
 | 
					        </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="no-overflow flex-grow-1">
 | 
				
			||||||
 | 
					          <div class="h-100">
 | 
				
			||||||
 | 
					            <router-outlet></router-outlet>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
							
								
								
									
										200
									
								
								app/src/app/app.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								app/src/app/app.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,200 @@
 | 
				
			|||||||
 | 
					:host
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.module-loading-spinner
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  background-color: rgb(9, 11, 23);
 | 
				
			||||||
 | 
					  opacity: .5;
 | 
				
			||||||
 | 
					  z-index: 1030;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main,
 | 
				
			||||||
 | 
					.pusher,
 | 
				
			||||||
 | 
					.content
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  border-radius: .5rem;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 6px rgba(38, 43, 80, .32), 0 0 0 11px rgba(26, 31, 60, .52), 0 0 0 15px rgba(17, 21, 48, .35);
 | 
				
			||||||
 | 
					  perspective: 1500px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:after
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    content: '';
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    right: 0;
 | 
				
			||||||
 | 
					    background: url('../assets/images/bg.jpg') #111530 no-repeat;
 | 
				
			||||||
 | 
					    background-size: cover;
 | 
				
			||||||
 | 
					    filter: blur(40px);
 | 
				
			||||||
 | 
					    -webkit-filter: blur(40px);
 | 
				
			||||||
 | 
					    z-index: -1;
 | 
				
			||||||
 | 
					    opacity: .7;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  border-radius: .5rem;
 | 
				
			||||||
 | 
					  background-color: rgba(17, 21, 48, .5);
 | 
				
			||||||
 | 
					  border: 1px solid rgba(0, 0, 0, .3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content,
 | 
				
			||||||
 | 
					.content-inner
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.backdrop
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pusher
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  perspective: 1000px;
 | 
				
			||||||
 | 
					  transition: transform .5s;
 | 
				
			||||||
 | 
					  transform-style: preserve-3d;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 4px -2px #FFF, 3rem 1rem 3rem -1rem rgba(0, 0, 0, 0.3), 3px 2px 0 2px rgba(13, 16, 37, .4), 4px 3px 1px 2px rgba(0, 0, 0, 0.2);
 | 
				
			||||||
 | 
					  border-radius: .5rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:before
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    right: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    background: linear-gradient(208deg, rgba(255, 255, 255, 0) 25%, rgba(255, 255, 255, 0.1) 50%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.3) 100%);
 | 
				
			||||||
 | 
					    border-radius: .5rem;
 | 
				
			||||||
 | 
					    content: '';
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: opacity .5s;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					    z-index: 1030;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  visibility: hidden;
 | 
				
			||||||
 | 
					  min-width: 275px;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  background-color: rgba(16,21,39, .8);
 | 
				
			||||||
 | 
					  transition: all .5s;
 | 
				
			||||||
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					  transform: translate3d(-100%, 0, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:after
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    right: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    background: rgba(0,0,0,.2);
 | 
				
			||||||
 | 
					    content: '';
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    transition: all .5s;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu-open
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  .menu
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    visibility: visible;
 | 
				
			||||||
 | 
					    transform: translate3d(0, 0, 0);
 | 
				
			||||||
 | 
					    box-shadow: 2rem 0 2rem -1rem rgba(0, 0, 0, .3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:after
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      width: 0;
 | 
				
			||||||
 | 
					      height: 0;
 | 
				
			||||||
 | 
					      opacity: 0;
 | 
				
			||||||
 | 
					      transition: opacity .5s, width .1s .5s, height .1s .5s;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .pusher
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    transform: translate3d(30px, 0, -600px) rotateY(-20deg);
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:before
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					      opacity: 1;
 | 
				
			||||||
 | 
					      transform: rotate(0deg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					header nav
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.brand
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  font-family: 'Bebas Neue', sans-serif;
 | 
				
			||||||
 | 
					  font-size: 2rem;
 | 
				
			||||||
 | 
					  line-height: 1;
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .icon
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    padding: .5rem .25rem;
 | 
				
			||||||
 | 
					    margin: 0 .5rem .15rem 0;
 | 
				
			||||||
 | 
					    line-height: 0;
 | 
				
			||||||
 | 
					    color: #ff9c07;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  p
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					    font-variant: normal;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    opacity: .75;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#accountMenuDropdown
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #FFF;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										82
									
								
								app/src/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								app/src/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import { Component } from '@angular/core';
 | 
				
			||||||
 | 
					import { Router, RouteConfigLoadStart, RouteConfigLoadEnd, NavigationStart, NavigationEnd, ActivatedRoute } from '@angular/router';
 | 
				
			||||||
 | 
					import { TranslateService } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { BsLocaleService } from 'ngx-bootstrap/datepicker';
 | 
				
			||||||
 | 
					import { filter, map, mergeMap } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { TokenService } from './helpers/token.service';
 | 
				
			||||||
 | 
					import { AuthService } from './helpers/auth.service';
 | 
				
			||||||
 | 
					import { UserInfo } from './account/models/user-info';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-root',
 | 
				
			||||||
 | 
					  styleUrls: ['./app.component.scss'],
 | 
				
			||||||
 | 
					  templateUrl: './app.component.html'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AppComponent
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  showProgress = false;
 | 
				
			||||||
 | 
					  menuVisibility = false;
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  subTitle: string;
 | 
				
			||||||
 | 
					  icon: string | string[];
 | 
				
			||||||
 | 
					  userInfo: UserInfo;
 | 
				
			||||||
 | 
					  routeChanged: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly router: Router,
 | 
				
			||||||
 | 
					    private readonly activatedRoute: ActivatedRoute,
 | 
				
			||||||
 | 
					    private readonly translate: TranslateService,
 | 
				
			||||||
 | 
					    private readonly localeService: BsLocaleService,
 | 
				
			||||||
 | 
					    private readonly authService: AuthService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // This language will be used as a fallback when a translation isn't found in the current language
 | 
				
			||||||
 | 
					    translate.setDefaultLang('en');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    translate.use('en');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // NgxBootstrap locale
 | 
				
			||||||
 | 
					    this.localeService.use('en');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(filter(x => x instanceof RouteConfigLoadStart || x instanceof RouteConfigLoadEnd))
 | 
				
			||||||
 | 
					      .subscribe(x =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.showProgress = x instanceof RouteConfigLoadStart;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(filter(x => x instanceof NavigationStart))
 | 
				
			||||||
 | 
					      .subscribe(x => this.routeChanged = false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        filter(x => x instanceof NavigationEnd),
 | 
				
			||||||
 | 
					        map(() => activatedRoute),
 | 
				
			||||||
 | 
					        map(route =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          while (route.firstChild)
 | 
				
			||||||
 | 
					            route = route.firstChild;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return route;
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        filter(route => route.outlet === 'primary'),
 | 
				
			||||||
 | 
					        mergeMap(route => route.data)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(x =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.title = x.title;
 | 
				
			||||||
 | 
					        this.subTitle = x.subTitle;
 | 
				
			||||||
 | 
					        this.icon = x.icon;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.routeChanged = true;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    authService.userInfoUpdated$.subscribe(userInfo => this.userInfo = userInfo);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  logOff()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.authService.logout();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										69
									
								
								app/src/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								app/src/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					import { BrowserModule } from '@angular/platform-browser';
 | 
				
			||||||
 | 
					import { NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
 | 
				
			||||||
 | 
					import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { registerLocaleData } from '@angular/common';
 | 
				
			||||||
 | 
					import localeEn from '@angular/common/locales/en';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TranslateModule, TranslateCompiler, TranslateLoader } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { MESSAGE_FORMAT_CONFIG, TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
 | 
				
			||||||
 | 
					import { WebpackTranslateLoader } from './helpers/webpack-translate-loader.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SharedModule } from './shared.module';
 | 
				
			||||||
 | 
					import { AppRoutingModule } from './app-routing.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AppComponent } from './app.component';
 | 
				
			||||||
 | 
					import { DashboardComponent } from './pages/dashboard/dashboard.component';
 | 
				
			||||||
 | 
					import { UnauthorizedComponent } from './pages/unauthorized/unauthorized.component';
 | 
				
			||||||
 | 
					import { NotFoundComponent } from './pages/not-found/not-found.component';
 | 
				
			||||||
 | 
					import { NavMenuComponent } from './components/nav-menu/nav-menu.component';
 | 
				
			||||||
 | 
					import { AuthInterceptorService } from './helpers/auth-interceptor.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					  declarations: [
 | 
				
			||||||
 | 
					    AppComponent,
 | 
				
			||||||
 | 
					    DashboardComponent,
 | 
				
			||||||
 | 
					    UnauthorizedComponent,
 | 
				
			||||||
 | 
					    NotFoundComponent,
 | 
				
			||||||
 | 
					    NavMenuComponent
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    BrowserAnimationsModule,
 | 
				
			||||||
 | 
					    BrowserModule.withServerTransition({ appId: 'manta-portal' }),
 | 
				
			||||||
 | 
					    HttpClientModule,
 | 
				
			||||||
 | 
					    AppRoutingModule,
 | 
				
			||||||
 | 
					    SharedModule,
 | 
				
			||||||
 | 
					    TranslateModule.forRoot({
 | 
				
			||||||
 | 
					      loader: {
 | 
				
			||||||
 | 
					        provide: TranslateLoader,
 | 
				
			||||||
 | 
					        useFactory: () => new WebpackTranslateLoader()
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      compiler: {
 | 
				
			||||||
 | 
					        provide: TranslateCompiler,
 | 
				
			||||||
 | 
					        useFactory: () => new TranslateMessageFormatCompiler()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  providers: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      provide: MESSAGE_FORMAT_CONFIG,
 | 
				
			||||||
 | 
					      useValue: {
 | 
				
			||||||
 | 
					        locales: ['en', 'ro']
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      provide: HTTP_INTERCEPTORS,
 | 
				
			||||||
 | 
					      useClass: AuthInterceptorService,
 | 
				
			||||||
 | 
					      multi: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  bootstrap: [AppComponent]
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AppModule
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  constructor()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    registerLocaleData(localeEn, 'en');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								app/src/app/app.server.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/src/app/app.server.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					import { ServerModule } from '@angular/platform-server';
 | 
				
			||||||
 | 
					import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
 | 
				
			||||||
 | 
					import { AppComponent } from './app.component';
 | 
				
			||||||
 | 
					import { AppModule } from './app.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					    imports: [AppModule, ServerModule, ModuleMapLoaderModule],
 | 
				
			||||||
 | 
					    bootstrap: [AppComponent]
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AppServerModule { }
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/src/app/catalog/catalog.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/src/app/catalog/catalog.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<router-outlet></router-outlet>
 | 
				
			||||||
							
								
								
									
										0
									
								
								app/src/app/catalog/catalog.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/src/app/catalog/catalog.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										25
									
								
								app/src/app/catalog/catalog.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/app/catalog/catalog.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CatalogComponent } from './catalog.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('CatalogComponent', () => {
 | 
				
			||||||
 | 
					  let component: CatalogComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<CatalogComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ CatalogComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(CatalogComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										21
									
								
								app/src/app/catalog/catalog.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/src/app/catalog/catalog.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-catalog',
 | 
				
			||||||
 | 
					  templateUrl: './catalog.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./catalog.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CatalogComponent implements OnInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  images: any[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								app/src/app/catalog/catalog.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/src/app/catalog/catalog.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					import { NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SharedModule } from '../shared.module';
 | 
				
			||||||
 | 
					import { RouterModule } from '@angular/router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TranslateModule, TranslateService, LangChangeEvent } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { TranslateLoader } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { WebpackTranslateLoader } from '../helpers/webpack-translate-loader.service';
 | 
				
			||||||
 | 
					import { TranslateCompiler } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CatalogComponent } from './catalog.component';
 | 
				
			||||||
 | 
					import { CustomImagesComponent } from './custom-images/custom-images.component';
 | 
				
			||||||
 | 
					import { DockerImagesComponent } from './docker-images/docker-images.component';
 | 
				
			||||||
 | 
					import { DockerRegistryComponent } from './docker-registry/docker-registry.component';
 | 
				
			||||||
 | 
					import { DockerImageEditorComponent } from './docker-image-editor/docker-image-editor.component';
 | 
				
			||||||
 | 
					import { DockerRegistryEditorComponent } from './docker-registry-editor/docker-registry-editor.component';
 | 
				
			||||||
 | 
					import { CustomImageEditorComponent } from './custom-image-editor/custom-image-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					  declarations: [
 | 
				
			||||||
 | 
					    CatalogComponent,
 | 
				
			||||||
 | 
					    CustomImagesComponent,
 | 
				
			||||||
 | 
					    DockerImagesComponent,
 | 
				
			||||||
 | 
					    DockerRegistryComponent,
 | 
				
			||||||
 | 
					    DockerImageEditorComponent,
 | 
				
			||||||
 | 
					    DockerRegistryEditorComponent
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    SharedModule,
 | 
				
			||||||
 | 
					    RouterModule.forChild([
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        component: CatalogComponent,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            path: '',
 | 
				
			||||||
 | 
					            redirectTo: 'custom-images'
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            path: 'custom-images',
 | 
				
			||||||
 | 
					            component: CustomImagesComponent,
 | 
				
			||||||
 | 
					            data:
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              title: 'catalog.customImages.title',
 | 
				
			||||||
 | 
					              subTitle: 'catalog.customImages.subTitle',
 | 
				
			||||||
 | 
					              icon: 'layer-group'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            path: 'docker-images',
 | 
				
			||||||
 | 
					            component: DockerImagesComponent,
 | 
				
			||||||
 | 
					            data:
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              title: 'catalog.dockerImages.title',
 | 
				
			||||||
 | 
					              subTitle: 'catalog.dockerImages.subTitle',
 | 
				
			||||||
 | 
					              icon: ['fab', 'docker']
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            path: 'docker-registry',
 | 
				
			||||||
 | 
					            component: DockerRegistryComponent,
 | 
				
			||||||
 | 
					            data:
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              title: 'catalog.dockerRegistry.title',
 | 
				
			||||||
 | 
					              subTitle: 'catalog.dockerRegistry.subTitle',
 | 
				
			||||||
 | 
					              icon: ['fab', 'docker']
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]),
 | 
				
			||||||
 | 
					    TranslateModule.forChild({
 | 
				
			||||||
 | 
					      loader: {
 | 
				
			||||||
 | 
					        provide: TranslateLoader,
 | 
				
			||||||
 | 
					        //useClass: WebpackTranslateLoader
 | 
				
			||||||
 | 
					        useFactory: () => new WebpackTranslateLoader('catalog')
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      compiler: {
 | 
				
			||||||
 | 
					        provide: TranslateCompiler,
 | 
				
			||||||
 | 
					        useFactory: () => new TranslateMessageFormatCompiler()
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      isolate: true
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  entryComponents: [
 | 
				
			||||||
 | 
					    DockerImageEditorComponent,
 | 
				
			||||||
 | 
					    DockerRegistryEditorComponent,
 | 
				
			||||||
 | 
					    CustomImageEditorComponent
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CatalogModule
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  constructor(private readonly translate: TranslateService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    translate.use(translate.store.currentLang);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    translate.store.onLangChange.subscribe((event: LangChangeEvent) => translate.use(event.lang));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					<form novalidate>
 | 
				
			||||||
 | 
					  <fieldset [formGroup]="editorForm">
 | 
				
			||||||
 | 
					    <button type="button" class="close" [attr.aria-label]="'general.closeWithoutSaving' | translate" (click)="close()">
 | 
				
			||||||
 | 
					      <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content">
 | 
				
			||||||
 | 
					      <h4 class="mb-3">Create image from machine</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <p class="my-2">Fill in the name and version for a new image based on the "{{ instance.name }}" machine</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <input type="text" class="form-control mb-3" formControlName="name" placeholder="Image name" [appAutofocus]="true" [appAutofocusDelay]="600">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <input type="text" class="form-control mb-3" formControlName="version" placeholder="Image version">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <textarea class="form-control" formControlName="description" placeholder="Description (optional)">
 | 
				
			||||||
 | 
					      </textarea>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="d-flex justify-content-end align-items-center mt-5">
 | 
				
			||||||
 | 
					        <button class="btn btn-info" (click)="saveChanges()" [disabled]="editorForm.invalid">Create image</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </fieldset>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					h4
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #00dcff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					p
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					  font-size: 1.15rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input, textarea
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  padding: .5rem 1rem;
 | 
				
			||||||
 | 
					  border-radius: 3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input, input:focus, input:active,
 | 
				
			||||||
 | 
					textarea, textarea:focus, textarea:active
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  background: transparent;
 | 
				
			||||||
 | 
					  border-color: #00e7ff;
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					textarea
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  resize: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CustomImageEditorComponent } from './custom-image-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('CustomImageEditorComponent', () => {
 | 
				
			||||||
 | 
					  let component: CustomImageEditorComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<CustomImageEditorComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ CustomImageEditorComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(CustomImageEditorComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, Input } from '@angular/core';
 | 
				
			||||||
 | 
					import { BsModalRef } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
 | 
				
			||||||
 | 
					import { NavigationStart, Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { filter, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-custom-image-editor',
 | 
				
			||||||
 | 
					  templateUrl: './custom-image-editor.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./custom-image-editor.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CustomImageEditorComponent implements OnInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  instance: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  save = new Subject<any>();
 | 
				
			||||||
 | 
					  editorForm: FormGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly modalRef: BsModalRef,
 | 
				
			||||||
 | 
					    private readonly router: Router,
 | 
				
			||||||
 | 
					    private readonly fb: FormBuilder)
 | 
				
			||||||
 | 
					  {    // When the user navigates away from this route, hide the modal
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$),
 | 
				
			||||||
 | 
					        filter(e => e instanceof NavigationStart)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.modalRef.hide());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private createForm()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm = this.fb.group(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name: [null, Validators.required],
 | 
				
			||||||
 | 
					        version: [null, Validators.required],
 | 
				
			||||||
 | 
					        description: [null]
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  close()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  saveChanges()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.save.next(this.editorForm.getRawValue());
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.createForm();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								app/src/app/catalog/custom-images/custom-images.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/src/app/catalog/custom-images/custom-images.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					<div class="d-flex flex-column h-100 pb-3">
 | 
				
			||||||
 | 
					  <div class="container text-center mt-1" [formGroup]="editorForm">
 | 
				
			||||||
 | 
					    <div class="btn-toolbar pt-2">
 | 
				
			||||||
 | 
					      <span class="d-none d-sm-block flex-grow-1"></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ng-container *ngIf="images && images.length">
 | 
				
			||||||
 | 
					        <div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
 | 
				
			||||||
 | 
					          <input type="text" class="form-control" placeholder="Search by name..." formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$">
 | 
				
			||||||
 | 
					          <button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
 | 
				
			||||||
 | 
					                  tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
 | 
				
			||||||
 | 
					            <fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom left">
 | 
				
			||||||
 | 
					          <button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
 | 
				
			||||||
 | 
					            Sort by
 | 
				
			||||||
 | 
					            <b *ngIf="editorForm.get('sortProperty').value === 'name'">name</b>
 | 
				
			||||||
 | 
					            <b *ngIf="editorForm.get('sortProperty').value === 'description'">description</b>
 | 
				
			||||||
 | 
					            <b *ngIf="editorForm.get('sortProperty').value === 'os'">operating system</b>
 | 
				
			||||||
 | 
					            <b *ngIf="editorForm.get('sortProperty').value === 'type'">type</b>
 | 
				
			||||||
 | 
					            <b *ngIf="editorForm.get('sortProperty').value === 'state'">status</b>
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
 | 
				
			||||||
 | 
					            <li role="menuitem">
 | 
				
			||||||
 | 
					              <button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'" (click)="setSortProperty('name')">
 | 
				
			||||||
 | 
					                Name
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li role="menuitem">
 | 
				
			||||||
 | 
					              <button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'description'" (click)="setSortProperty('description')">
 | 
				
			||||||
 | 
					                Description
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li role="menuitem">
 | 
				
			||||||
 | 
					              <button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'" (click)="setSortProperty('os')">
 | 
				
			||||||
 | 
					                Operating system
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li role="menuitem">
 | 
				
			||||||
 | 
					              <button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'type'" (click)="setSortProperty('type')">
 | 
				
			||||||
 | 
					                Type
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					            <li role="menuitem">
 | 
				
			||||||
 | 
					              <button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'status'" (click)="setSortProperty('status')">
 | 
				
			||||||
 | 
					                Status
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					          </ul>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </ng-container>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="spinner-border text-center text-info text-faded" role="status" *ngIf="loadingIndicator">
 | 
				
			||||||
 | 
					      <span class="visually-hidden">Loading...</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="overflow-auto">
 | 
				
			||||||
 | 
					    <div class="container my-4">
 | 
				
			||||||
 | 
					      <div class="table-responsive" *ngIf="!loadingIndicator">
 | 
				
			||||||
 | 
					        <p *ngIf="!images.length" class="text-center text-info text-faded p-3 mb-0">
 | 
				
			||||||
 | 
					          You don't have any custom images yet
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <table class="table table-hover" *ngIf="images.length">
 | 
				
			||||||
 | 
					          <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <th>Name</th>
 | 
				
			||||||
 | 
					              <th>Description</th>
 | 
				
			||||||
 | 
					              <th>OS</th>
 | 
				
			||||||
 | 
					              <th>Type</th>
 | 
				
			||||||
 | 
					              <th>Publish date</th>
 | 
				
			||||||
 | 
					              <th>Status</th>
 | 
				
			||||||
 | 
					              <th></th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </thead>
 | 
				
			||||||
 | 
					          <tbody>
 | 
				
			||||||
 | 
					            <tr *ngFor="let image of listItems">
 | 
				
			||||||
 | 
					              <td>
 | 
				
			||||||
 | 
					                {{ image.name }}
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td>
 | 
				
			||||||
 | 
					                <div class="text-truncate">{{ image.description }}</div>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td class="text-uppercase">
 | 
				
			||||||
 | 
					                {{ image.os }}
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td class="text-uppercase">
 | 
				
			||||||
 | 
					                {{ image.type }}
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td>
 | 
				
			||||||
 | 
					                {{ image.published_at ? (image.published_at | timeago) : '' }}
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td>
 | 
				
			||||||
 | 
					                <span class="badge" [ngClass]="image.state === 'active' ? 'bg-success' : 'bg-warning text-dark'">{{ image.state }}</span>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td class="text-end">
 | 
				
			||||||
 | 
					                <div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body" [isDisabled]="image.working">
 | 
				
			||||||
 | 
					                  <button class="btn btn-link text-info" dropdownToggle
 | 
				
			||||||
 | 
					                          tooltip="More options" container="body" placement="top" [adaptivePosition]="false">
 | 
				
			||||||
 | 
					                    <fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
 | 
				
			||||||
 | 
					                  </button>
 | 
				
			||||||
 | 
					                  <ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="button-split">
 | 
				
			||||||
 | 
					                    <li role="menuitem">
 | 
				
			||||||
 | 
					                      <button class="dropdown-item" (click)="deleteCustomImage(image)">
 | 
				
			||||||
 | 
					                        <fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
 | 
				
			||||||
 | 
					                        Delete this image
 | 
				
			||||||
 | 
					                      </button>
 | 
				
			||||||
 | 
					                    </li>
 | 
				
			||||||
 | 
					                  </ul>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					          <tfoot *ngIf="!listItems.length">
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td colspan="7" class="text-uppercase">
 | 
				
			||||||
 | 
					                No images match your search criteria
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tfoot>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					.table-responsive
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  background-color: rgba(16, 21, 39, 0.75);
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
 | 
				
			||||||
 | 
					  transition: box-shadow 0.15s ease-out;
 | 
				
			||||||
 | 
					  border-radius: .25rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgb(18 203 240 / 40%), 0 0 10px 3px #0e162a;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .rule
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    text-transform: uppercase;
 | 
				
			||||||
 | 
					    color: #3d5e8e;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .highlight
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    color: #8881ff;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  b, .strong
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    color: #ff9c07;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .text-truncate
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    max-width: 350px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .inline-list-item + .inline-list-item
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    padding-left: .25rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:before
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      content: attr(text);
 | 
				
			||||||
 | 
					      color: #3d5e8e;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CustomImagesComponent } from './custom-images.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('CustomImagesComponent', () => {
 | 
				
			||||||
 | 
					  let component: CustomImagesComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<CustomImagesComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ CustomImagesComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(CustomImagesComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										191
									
								
								app/src/app/catalog/custom-images/custom-images.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								app/src/app/catalog/custom-images/custom-images.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
				
			||||||
 | 
					import { ColumnMode, SelectionType } from '@swimlane/ngx-datatable';
 | 
				
			||||||
 | 
					import { CatalogService } from '../helpers/catalog.service';
 | 
				
			||||||
 | 
					import { AuthService } from '../../helpers/auth.service';
 | 
				
			||||||
 | 
					import { debounceTime, distinctUntilChanged, filter, first, map, switchMap, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { ToastrService } from 'ngx-toastr';
 | 
				
			||||||
 | 
					import { CatalogImage } from '../models/image';
 | 
				
			||||||
 | 
					import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
 | 
				
			||||||
 | 
					import Fuse from 'fuse.js';
 | 
				
			||||||
 | 
					import { sortArray } from '../../helpers/utils.service';
 | 
				
			||||||
 | 
					import { BsModalService } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { ConfirmationDialogComponent } from '../../components/confirmation-dialog/confirmation-dialog.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-custom-images',
 | 
				
			||||||
 | 
					  templateUrl: './custom-images.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./custom-images.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CustomImagesComponent implements OnInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  images: CatalogImage[] = [];
 | 
				
			||||||
 | 
					  listItems: CatalogImage[] = [];
 | 
				
			||||||
 | 
					  editorForm: FormGroup;
 | 
				
			||||||
 | 
					  loadingIndicator = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					  private readonly fuseJsOptions: {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly catalogService: CatalogService,
 | 
				
			||||||
 | 
					    private readonly modalService: BsModalService,
 | 
				
			||||||
 | 
					    private readonly authService: AuthService,
 | 
				
			||||||
 | 
					    private readonly toastr: ToastrService,
 | 
				
			||||||
 | 
					    private readonly fb: FormBuilder)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // Configure FuseJs
 | 
				
			||||||
 | 
					    this.fuseJsOptions = {
 | 
				
			||||||
 | 
					      includeScore: false,
 | 
				
			||||||
 | 
					      minMatchCharLength: 2,
 | 
				
			||||||
 | 
					      includeMatches: true,
 | 
				
			||||||
 | 
					      shouldSort: false,
 | 
				
			||||||
 | 
					      threshold: .3, // Lower value means a more exact search
 | 
				
			||||||
 | 
					      keys: [
 | 
				
			||||||
 | 
					        { name: 'name', weight: .9 },
 | 
				
			||||||
 | 
					        { name: 'description', weight: .8 },
 | 
				
			||||||
 | 
					        { name: 'os', weight: .7 },
 | 
				
			||||||
 | 
					        { name: 'type', weight: .7 }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.createForm();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private createForm()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm = this.fb.group(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        searchTerm: [''],
 | 
				
			||||||
 | 
					        sortProperty: ['name']
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.editorForm.get('searchTerm').valueChanges
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        debounceTime(300),
 | 
				
			||||||
 | 
					        distinctUntilChanged(),
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.applyFiltersAndSort());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.editorForm.get('sortProperty').valueChanges
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        distinctUntilChanged(),
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.applyFiltersAndSort());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private applyFiltersAndSort()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    let listItems: CatalogImage[] = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const searchTerm = this.editorForm.get('searchTerm').value;
 | 
				
			||||||
 | 
					    if (searchTerm.length >= 2)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      const fuse = new Fuse(this.images, this.fuseJsOptions);
 | 
				
			||||||
 | 
					      const fuseResults = fuse.search(searchTerm);
 | 
				
			||||||
 | 
					      listItems = fuseResults.map(x => x.item as CatalogImage);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!listItems)
 | 
				
			||||||
 | 
					      listItems = [...this.images];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.listItems = sortArray(listItems, this.editorForm.get('sortProperty').value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  setSortProperty(propertyName: string)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm.get('sortProperty').setValue(propertyName);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  clearSearch()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm.get('searchTerm').setValue('');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private getCustomImages()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.loadingIndicator = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.authService.userInfoUpdated$
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$),
 | 
				
			||||||
 | 
					        filter(userInfo => userInfo != null),
 | 
				
			||||||
 | 
					        switchMap(userInfo => this.catalogService.getCustomImages(userInfo.id))
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(images =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.images = images;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.applyFiltersAndSort();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.loadingIndicator = false
 | 
				
			||||||
 | 
					      }, err =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        const errorDetails = err.error?.message ? `(${err.error.message})` : '';
 | 
				
			||||||
 | 
					        this.toastr.error(`Failed to retrieve the list of custom images ${errorDetails}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.loadingIndicator = false;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  deleteCustomImage(image: CatalogImage)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    const modalConfig = {
 | 
				
			||||||
 | 
					      ignoreBackdropClick: true,
 | 
				
			||||||
 | 
					      keyboard: false,
 | 
				
			||||||
 | 
					      animated: true,
 | 
				
			||||||
 | 
					      initialState: {
 | 
				
			||||||
 | 
					        prompt: `Are you sure you wish to permanently delete the "${image.name}" image?`,
 | 
				
			||||||
 | 
					        confirmButtonText: 'Yes, delete this image',
 | 
				
			||||||
 | 
					        declineButtonText: 'No, keep it',
 | 
				
			||||||
 | 
					        confirmByDefault: false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    modalRef.content.confirm.pipe(first()).subscribe(() =>
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      this.toastr.info(`Removing machine "${image.name}"...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.catalogService.deleteImage(image.id)
 | 
				
			||||||
 | 
					        .subscribe(() =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          const index = this.images.findIndex(i => i.id === image.id);
 | 
				
			||||||
 | 
					          if (index >= 0)
 | 
				
			||||||
 | 
					            this.images.splice(index, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.applyFiltersAndSort();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.toastr.info(`The image "${image.name}" has been removed`);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					          err =>
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            this.toastr.error(`Failed to delete the "${image.name}" image ${err.error.message}`);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.getCustomImages();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.destroy$.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<p>docker-image-editor works!</p>
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DockerImageEditorComponent } from './docker-image-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('DockerImageEditorComponent', () => {
 | 
				
			||||||
 | 
					  let component: DockerImageEditorComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<DockerImageEditorComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ DockerImageEditorComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(DockerImageEditorComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-docker-image-editor',
 | 
				
			||||||
 | 
					  templateUrl: './docker-image-editor.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./docker-image-editor.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class DockerImageEditorComponent implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnInit(): void {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					<ngx-datatable [rows]="rows" [headerHeight]="48" [footerHeight]="46" [rowHeight]="40"
 | 
				
			||||||
 | 
					               [scrollbarV]="true" [scrollbarH]="false" [selectionType]="selectionType.checkbox" [columnMode]="columnMode.flex"
 | 
				
			||||||
 | 
					               [loadingIndicator]="loadingIndicator">
 | 
				
			||||||
 | 
					  <ngx-datatable-column [width]="30"
 | 
				
			||||||
 | 
					                        [sortable]="false"
 | 
				
			||||||
 | 
					                        [canAutoResize]="false"
 | 
				
			||||||
 | 
					                        [draggable]="false"
 | 
				
			||||||
 | 
					                        [resizeable]="false"
 | 
				
			||||||
 | 
					                        [headerCheckboxable]="true"
 | 
				
			||||||
 | 
					                        [checkboxable]="true">
 | 
				
			||||||
 | 
					  </ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column name="Name" [canAutoResize]="true" [flexGrow]="1"></ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column name="Description" [flexGrow]="2">
 | 
				
			||||||
 | 
					    <ng-template let-value="value" ngx-datatable-cell-template>
 | 
				
			||||||
 | 
					      <div class="text-truncate">{{ value }}</div>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column name="Status" prop="state" [width]="70" [canAutoResize]="false">
 | 
				
			||||||
 | 
					    <ng-template let-value="value" ngx-datatable-cell-template>
 | 
				
			||||||
 | 
					      <span class="badge" [ngClass]="value === 'active' ? 'bg-success' : 'bg-warning text-dark'">{{ value }}</span>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column name="OS" [canAutoResize]="false" [width]="100">
 | 
				
			||||||
 | 
					    <ng-template let-value="value" ngx-datatable-cell-template>
 | 
				
			||||||
 | 
					      <div class="os" [class.smartos]="value === 'smartos'" [class.bsd]="value === 'bsd'"
 | 
				
			||||||
 | 
					           [class.windows]="value === 'windows'" [class.linux]="value === 'linux'">
 | 
				
			||||||
 | 
					        <span>{{ value }}</span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column name="Type" [width]="120" [canAutoResize]="false"></ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column name="Publish date" prop="published_at" [width]="100" [canAutoResize]="false">
 | 
				
			||||||
 | 
					    <ng-template let-value="value" ngx-datatable-cell-template>
 | 
				
			||||||
 | 
					      {{ value | date | timeago }}
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ngx-datatable-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!--<ngx-datatable-column name="Tags">
 | 
				
			||||||
 | 
					    <ng-template let-value="value" ngx-datatable-cell-template>
 | 
				
			||||||
 | 
					      <ng-container *ngFor="let tag of value | keyvalue">
 | 
				
			||||||
 | 
					        <span class="badge badge-dark mr-1">{{ tag.key }} <span class="text-primary">{{ tag.value }}</span></span>
 | 
				
			||||||
 | 
					      </ng-container>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ngx-datatable-column>-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <ngx-datatable-column [flexGrow]="1" cellClass="text-right" headerClass="text-right" [sortable]="false">
 | 
				
			||||||
 | 
					    <ng-template let-value="value" ngx-datatable-cell-template>
 | 
				
			||||||
 | 
					      <button class="btn btn-sm btn-primary m-1">Instances</button>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ngx-datatable-column>
 | 
				
			||||||
 | 
					</ngx-datatable>
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DockerImagesComponent } from './docker-images.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('DockerImagesComponent', () => {
 | 
				
			||||||
 | 
					  let component: DockerImagesComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<DockerImagesComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ DockerImagesComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(DockerImagesComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										28
									
								
								app/src/app/catalog/docker-images/docker-images.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/src/app/catalog/docker-images/docker-images.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					import { ColumnMode, SelectionType } from '@swimlane/ngx-datatable';
 | 
				
			||||||
 | 
					import { CatalogService } from '../helpers/catalog.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-docker-images',
 | 
				
			||||||
 | 
					  templateUrl: './docker-images.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./docker-images.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class DockerImagesComponent implements OnInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  rows: any[] = [];
 | 
				
			||||||
 | 
					  loadingIndicator = true;
 | 
				
			||||||
 | 
					  selectionType = SelectionType;
 | 
				
			||||||
 | 
					  columnMode = ColumnMode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly catalogService: CatalogService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<p>docker-registry-editor works!</p>
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DockerRegistryEditorComponent } from './docker-registry-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('DockerRegistryEditorComponent', () => {
 | 
				
			||||||
 | 
					  let component: DockerRegistryEditorComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<DockerRegistryEditorComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ DockerRegistryEditorComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(DockerRegistryEditorComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-docker-registry-editor',
 | 
				
			||||||
 | 
					  templateUrl: './docker-registry-editor.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./docker-registry-editor.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class DockerRegistryEditorComponent implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnInit(): void {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<p>docker-registry works!</p>
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DockerRegistryComponent } from './docker-registry.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('DockerRegistryComponent', () => {
 | 
				
			||||||
 | 
					  let component: DockerRegistryComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<DockerRegistryComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ DockerRegistryComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(DockerRegistryComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-docker-registry',
 | 
				
			||||||
 | 
					  templateUrl: './docker-registry.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./docker-registry.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class DockerRegistryComponent implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnInit(): void {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								app/src/app/catalog/helpers/catalog.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/app/catalog/helpers/catalog.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CatalogService } from './catalog.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('CatalogService', () => {
 | 
				
			||||||
 | 
					  let service: CatalogService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({});
 | 
				
			||||||
 | 
					    service = TestBed.inject(CatalogService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be created', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										165
									
								
								app/src/app/catalog/helpers/catalog.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								app/src/app/catalog/helpers/catalog.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
 | 
					import { Observable, Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { HttpClient } from '@angular/common/http';
 | 
				
			||||||
 | 
					import { Cacheable } from 'ts-cacheable';
 | 
				
			||||||
 | 
					import { delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { CatalogPackage } from '../models/package';
 | 
				
			||||||
 | 
					import { CatalogImage } from '../models/image';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const cacheBuster$ = new Subject<void>();
 | 
				
			||||||
 | 
					const imagesCacheBuster$ = new Subject<void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable({
 | 
				
			||||||
 | 
					  providedIn: 'root'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CatalogService
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly httpClient: HttpClient) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @Cacheable({
 | 
				
			||||||
 | 
					    cacheBusterObserver: cacheBuster$
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  getDataCenters(): Observable<any[]>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<any[]>(`/api/my/datacenters`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  getServices(): Observable<any>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<any>(`/api/my/services`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @Cacheable({
 | 
				
			||||||
 | 
					    cacheBusterObserver: cacheBuster$
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  getPackages(): Observable<CatalogPackage[]>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<CatalogPackage[]>(`/api/my/packages`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @Cacheable({
 | 
				
			||||||
 | 
					    cacheBusterObserver: cacheBuster$
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  getPackage(packageId: string): Observable<CatalogPackage>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<CatalogPackage>(`/api/my/packages/${packageId}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @Cacheable({
 | 
				
			||||||
 | 
					    cacheBusterObserver: imagesCacheBuster$
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  getImages(allStates = false): Observable<CatalogImage[]>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<CatalogImage[]>(`/api/my/images?${allStates ? 'state=all' : ''}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @Cacheable({
 | 
				
			||||||
 | 
					    cacheBusterObserver: imagesCacheBuster$
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  getCustomImages(ownerId: string): Observable<CatalogImage[]>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<CatalogImage[]>(`/api/my/images?$state=all&owner=${ownerId}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @Cacheable({
 | 
				
			||||||
 | 
					    cacheBusterObserver: imagesCacheBuster$
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  getImage(id: string): Observable<CatalogImage>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.get<CatalogImage>(`/api/my/images/${id}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  getImageUntilExpectedState(image: CatalogImage, expectedStates: string[], maxRetries = 10): Observable<CatalogImage>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // Keep polling the image until it reaches the expected state
 | 
				
			||||||
 | 
					    return this.httpClient.get<CatalogImage>(`/api/my/images/${image.id}`)
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        tap(x => image.state = x.state),
 | 
				
			||||||
 | 
					        repeatWhen(x =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          let retries = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return x.pipe(delay(3000), map(y =>
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            if (retries++ === maxRetries)
 | 
				
			||||||
 | 
					              throw { error: `Failed to retrieve the current status for image "${image.name}"` };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return y;
 | 
				
			||||||
 | 
					          }));
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        filter(x => expectedStates.includes(x.state)),
 | 
				
			||||||
 | 
					        take(1) //  needed to stop the repeatWhen loop
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  createImage(instanceId: string, name: string, version: string,
 | 
				
			||||||
 | 
					    description?: string, homepage?: string, eula?: string, acl?: string, tags?: string): Observable<CatalogImage>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.post<any>(`/api/my/images`,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        machine: instanceId,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        version,
 | 
				
			||||||
 | 
					        description,
 | 
				
			||||||
 | 
					        homepage,
 | 
				
			||||||
 | 
					        eula,
 | 
				
			||||||
 | 
					        acl,
 | 
				
			||||||
 | 
					        tags
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .pipe(tap(() => imagesCacheBuster$.next()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  importImage(sourceDataCenterId: string, imageId: string): Observable<CatalogImage>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // Copy the image with id from the source datacenter into this datacenter
 | 
				
			||||||
 | 
					    return this.httpClient.post<CatalogImage>(`/api/my/images?action=import-from-datacenter`,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        datacenter: sourceDataCenterId,
 | 
				
			||||||
 | 
					        image: imageId
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .pipe(tap(() => imagesCacheBuster$.next()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  editImage(imageId: string, name: string, version: string, description?: string, homepage?: string, eula?: string, acl?: string, tags?: string): Observable<any>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.httpClient.post<any>(`/api/my/images/${imageId}?action=update`,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        version,
 | 
				
			||||||
 | 
					        description,
 | 
				
			||||||
 | 
					        homepage,
 | 
				
			||||||
 | 
					        eula,
 | 
				
			||||||
 | 
					        acl,
 | 
				
			||||||
 | 
					        tags
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .pipe(tap(() => imagesCacheBuster$.next()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  cloneImage(imageId: string): Observable<any>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // https://apidocs.joyent.com/cloudapi/#CloneImage
 | 
				
			||||||
 | 
					    return this.httpClient.post<any>(`/api/my/images/${imageId}?action=clone`, {})
 | 
				
			||||||
 | 
					      .pipe(tap(() => imagesCacheBuster$.next()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  deleteImage(id: string): Observable<any>
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // Note: Caller must be the owner of the image
 | 
				
			||||||
 | 
					    return this.httpClient.delete(`/api/my/images/${id}`)
 | 
				
			||||||
 | 
					      .pipe(tap(() => imagesCacheBuster$.next()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								app/src/app/catalog/images/images.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/src/app/catalog/images/images.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					<table class="table">
 | 
				
			||||||
 | 
					  <thead>
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					      <th>Name</th>
 | 
				
			||||||
 | 
					      <th>Description</th>
 | 
				
			||||||
 | 
					      <th>OS</th>
 | 
				
			||||||
 | 
					      <th></th>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					  </thead>
 | 
				
			||||||
 | 
					  <tbody>
 | 
				
			||||||
 | 
					    <tr *ngFor="let image of images">
 | 
				
			||||||
 | 
					      <td>{{ image.name }}</td>
 | 
				
			||||||
 | 
					      <td>{{ image.description }}</td>
 | 
				
			||||||
 | 
					      <td>{{ image.os }}</td>
 | 
				
			||||||
 | 
					      <td>
 | 
				
			||||||
 | 
					        <button class="btn btn-sm btn-secondary" (click)="select.emit(image)">select</button>
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					  </tbody>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
							
								
								
									
										0
									
								
								app/src/app/catalog/images/images.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/src/app/catalog/images/images.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										25
									
								
								app/src/app/catalog/images/images.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/app/catalog/images/images.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ImagesComponent } from './images.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('ImagesComponent', () => {
 | 
				
			||||||
 | 
					  let component: ImagesComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<ImagesComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ ImagesComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(ImagesComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										37
									
								
								app/src/app/catalog/images/images.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/src/app/catalog/images/images.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, Output, EventEmitter } from '@angular/core';
 | 
				
			||||||
 | 
					import { ColumnMode, SelectionType } from '@swimlane/ngx-datatable';
 | 
				
			||||||
 | 
					import { CatalogService } from '../helpers/catalog.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-images',
 | 
				
			||||||
 | 
					  templateUrl: './images.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./images.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ImagesComponent implements OnInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  select = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  images: any[];
 | 
				
			||||||
 | 
					  loadingIndicator = true;
 | 
				
			||||||
 | 
					  selectionType = SelectionType;
 | 
				
			||||||
 | 
					  columnMode = ColumnMode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly catalogService: CatalogService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    catalogService.getImages().subscribe(x =>
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      this.images = x;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.loadingIndicator = false;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    catalogService.getDataCenters().subscribe(console.log);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								app/src/app/catalog/models/image.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/app/catalog/models/image.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					export class CatalogImage
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  version: string;
 | 
				
			||||||
 | 
					  description: string;
 | 
				
			||||||
 | 
					  owner: string;
 | 
				
			||||||
 | 
					  public: boolean;
 | 
				
			||||||
 | 
					  published_at: Date;
 | 
				
			||||||
 | 
					  state: string;
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  os: string;
 | 
				
			||||||
 | 
					  tags: { key: string; value: string };
 | 
				
			||||||
 | 
					  requirements: any;
 | 
				
			||||||
 | 
					  homepage: string;
 | 
				
			||||||
 | 
					  image_size: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								app/src/app/catalog/models/package.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/src/app/catalog/models/package.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					export class CatalogPackage
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  brand: string;
 | 
				
			||||||
 | 
					  memory: number;
 | 
				
			||||||
 | 
					  memorySize: string;
 | 
				
			||||||
 | 
					  memorySizeLabel: string;
 | 
				
			||||||
 | 
					  disk: number;
 | 
				
			||||||
 | 
					  diskSize: string;
 | 
				
			||||||
 | 
					  diskSizeLabel: string;
 | 
				
			||||||
 | 
					  swap: number;
 | 
				
			||||||
 | 
					  lwps: number;
 | 
				
			||||||
 | 
					  vcpus: number;
 | 
				
			||||||
 | 
					  version: string;
 | 
				
			||||||
 | 
					  group: string;
 | 
				
			||||||
 | 
					  description: string;
 | 
				
			||||||
 | 
					  disks: any[];
 | 
				
			||||||
 | 
					  flexible_disk: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								app/src/app/catalog/packages/packages.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/src/app/catalog/packages/packages.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					<div class="text-center mt-3" *ngIf="loadingIndicator">
 | 
				
			||||||
 | 
					  <div class="spinner-border text-info" role="status">
 | 
				
			||||||
 | 
					    <span class="visually-hidden">Loading...</span>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ng-container *ngIf="!loadingIndicator">
 | 
				
			||||||
 | 
					  <div class="btn-group w-100" btnRadioGroup>
 | 
				
			||||||
 | 
					    <label [btnRadio]="group" class="btn" [class.active]="group === selectedPackageGroup" *ngFor="let group of packageGroups"
 | 
				
			||||||
 | 
					           (click)="setPackageGroup($event, group)">
 | 
				
			||||||
 | 
					      {{ group }}
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="list-group list-group-flush flex-grow-1" *ngIf="packages">
 | 
				
			||||||
 | 
					    <ng-container *ngFor="let pkg of packages[selectedPackageGroup]">
 | 
				
			||||||
 | 
					      <a *ngIf="pkg.visible" class="list-group-item list-group-item-action d-flex align-items-center justify-content-between">
 | 
				
			||||||
 | 
					        <div class="form-check">
 | 
				
			||||||
 | 
					          <input class="form-check-input" type="radio" id="pkg-{{ pkg.id }}" name="pkg" [value]="pkg" [(ngModel)]="selectedPackage">
 | 
				
			||||||
 | 
					          <label class="form-check-label d-flex justify-content-between align-items-center pb-2" for="pkg-{{ pkg.id }}">
 | 
				
			||||||
 | 
					            <span class="d-block flex-grow-1">
 | 
				
			||||||
 | 
					              <span class="d-block">
 | 
				
			||||||
 | 
					                <span class="h3 text-uppercase">
 | 
				
			||||||
 | 
					                  {{ pkg.name }}
 | 
				
			||||||
 | 
					                  <!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>-->
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                <small class="text-faded pb-1 d-block">v<b>{{ pkg.version }}</b></small>
 | 
				
			||||||
 | 
					                <small class="mb-0 pe-3 text-faded" *ngIf="pkg.description">{{ pkg.description }}</small>
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					            <b class="d-sm-flex flex-nowrap d-none justify-content-between">
 | 
				
			||||||
 | 
					              <span class="package-specs">
 | 
				
			||||||
 | 
					                <span class="title">CPU</span>
 | 
				
			||||||
 | 
					                <span class="h5">{{ pkg.vcpus || 1 }}</span>
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					              <span class="package-specs">
 | 
				
			||||||
 | 
					                <span class="title">Memory</span>
 | 
				
			||||||
 | 
					                <span class="h5">
 | 
				
			||||||
 | 
					                  {{ pkg.memorySize }}
 | 
				
			||||||
 | 
					                  <small>{{ pkg.memorySizeLabel }}</small>
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					              <span class="package-specs">
 | 
				
			||||||
 | 
					                <span class="title">Disk</span>
 | 
				
			||||||
 | 
					                <span class="h5">
 | 
				
			||||||
 | 
					                  {{ pkg.diskSize }}
 | 
				
			||||||
 | 
					                  <small>{{ pkg.diskSizeLabel }}</small>
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </b>
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </ng-container>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</ng-container>
 | 
				
			||||||
							
								
								
									
										151
									
								
								app/src/app/catalog/packages/packages.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								app/src/app/catalog/packages/packages.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					:host
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.list-group:not(.select-list)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					  border-radius: 0;
 | 
				
			||||||
 | 
					  border-top: 1px solid rgba(13, 195, 233, .5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .list-group-item
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    border-radius: 0;
 | 
				
			||||||
 | 
					    background: transparent;
 | 
				
			||||||
 | 
					    color: #5a8cd8;
 | 
				
			||||||
 | 
					    border-color: rgb(61, 94, 142, .25);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-group
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  .btn
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					    border-radius: 0;
 | 
				
			||||||
 | 
					    border-left: none;
 | 
				
			||||||
 | 
					    border-right: none;
 | 
				
			||||||
 | 
					    border-top: none;
 | 
				
			||||||
 | 
					    border-bottom: 2px solid transparent;
 | 
				
			||||||
 | 
					    transition: none;
 | 
				
			||||||
 | 
					    color: #0dc3e9;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.active
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      border-bottom-color: #0dc3e9;
 | 
				
			||||||
 | 
					      box-shadow: 0 -1rem 1.5rem -1.5rem inset;
 | 
				
			||||||
 | 
					      text-shadow: 0 0 .5rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus:not(.active)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      box-shadow: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-check
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0 0 0 2.5rem;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .form-check-input
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    margin-right: .5rem;
 | 
				
			||||||
 | 
					    float: none;
 | 
				
			||||||
 | 
					    width: 1.4em;
 | 
				
			||||||
 | 
					    max-width: 1rem;
 | 
				
			||||||
 | 
					    margin-bottom: .25rem;
 | 
				
			||||||
 | 
					    cursor: inherit;
 | 
				
			||||||
 | 
					    background-color: #0dc3e9;
 | 
				
			||||||
 | 
					    border-color: #0dc3e9;
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 1px rgba(12, 19, 33, .5) inset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:checked[type=radio]
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%230c1321'/%3e%3c/svg%3e");
 | 
				
			||||||
 | 
					      background-color: #ff9c07;
 | 
				
			||||||
 | 
					      border-color: #ff9c07;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:focus)
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        box-shadow: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      box-shadow: 0 0 0 0.25rem rgba(255, 156, 7, .25);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .h3
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    text-transform: uppercase;
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					    line-height: 1;
 | 
				
			||||||
 | 
					    font-size: 1.5rem;
 | 
				
			||||||
 | 
					    color: #8881ff;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .form-check-label
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    cursor: inherit;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: .75rem .25rem;
 | 
				
			||||||
 | 
					    color: #5a8cd8;
 | 
				
			||||||
 | 
					    font-family: "Mukta", sans-serif;
 | 
				
			||||||
 | 
					    text-transform: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-check-input:checked + .form-check-label,
 | 
				
			||||||
 | 
					.form-check-input:checked + .form-check-label .package-specs,
 | 
				
			||||||
 | 
					.form-check-input:checked + .form-check-label .h3
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.list-group-item-action .h5
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  font-size: 2.5rem;
 | 
				
			||||||
 | 
					  float: left;
 | 
				
			||||||
 | 
					  margin: .5rem 0 -.25rem 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  small
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    font-size: .8rem;
 | 
				
			||||||
 | 
					    margin-left: -.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.package-specs
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  padding: 2px 2px 2px 1rem;
 | 
				
			||||||
 | 
					  font-size: .9rem;
 | 
				
			||||||
 | 
					  width: 100px;
 | 
				
			||||||
 | 
					  border-left: 1px solid rgba(61, 94, 142, 0.25);
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  margin: 0 .5rem;
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					  flex-basis: 0;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .title
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: 1rem;
 | 
				
			||||||
 | 
					    opacity: .75;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								app/src/app/catalog/packages/packages.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/app/catalog/packages/packages.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { PackagesComponent } from './packages.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('PackagesComponent', () => {
 | 
				
			||||||
 | 
					  let component: PackagesComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<PackagesComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ PackagesComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(PackagesComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										195
									
								
								app/src/app/catalog/packages/packages.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								app/src/app/catalog/packages/packages.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,195 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
 | 
				
			||||||
 | 
					import { OnDestroy } from '@angular/core/core';
 | 
				
			||||||
 | 
					import { ReplaySubject, Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { FileSizePipe } from '../../pipes/file-size.pipe';
 | 
				
			||||||
 | 
					import { CatalogService } from '../helpers/catalog.service';
 | 
				
			||||||
 | 
					import { CatalogImage } from '../../catalog/models/image';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-packages',
 | 
				
			||||||
 | 
					  templateUrl: './packages.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./packages.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class PackagesComponent implements OnInit, OnDestroy, OnChanges
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  imageType: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  image: CatalogImage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  package: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  select = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  packageGroups: any[];
 | 
				
			||||||
 | 
					  loadingIndicator: boolean;
 | 
				
			||||||
 | 
					  selectedPackageGroup: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private packages: {};
 | 
				
			||||||
 | 
					  private _selectedPackage: {};
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					  private onChanges$ = new ReplaySubject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly catalogService: CatalogService,
 | 
				
			||||||
 | 
					    private readonly fileSizePipe: FileSizePipe)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.getPackages();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  setPackageGroup(event, packageGroup: string)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.selectedPackageGroup = packageGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!packageGroup) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (packageGroup)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      case 'cpu':
 | 
				
			||||||
 | 
					        this.packages[packageGroup].sort((a, b) => (a.vcpus || 1) - (b.vcpus || 1));
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'disk':
 | 
				
			||||||
 | 
					        this.packages[packageGroup].sort((a, b) => a.disk - b.disk);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'memory optimized':
 | 
				
			||||||
 | 
					        this.packages[packageGroup].sort((a, b) => a.memory - b.memory);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        this.packages[packageGroup].sort((a, b) => ((a.vcpus || 1) - (b.vcpus || 1)) || (a.memory - b.memory) || (a.disk - b.disk));
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  set selectedPackage(value)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this._selectedPackage = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.select.next(value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get selectedPackage()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this._selectedPackage;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private getPackages()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.loadingIndicator = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.catalogService.getPackages()
 | 
				
			||||||
 | 
					      .subscribe(response =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        if (this.packages)
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.packages = response.reduce((groups, pkg) =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          let size = this.fileSizePipe.transform(pkg.memory * 1024 * 1024);
 | 
				
			||||||
 | 
					          [pkg.memorySize, pkg.memorySizeLabel] = size.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          size = this.fileSizePipe.transform(pkg.disk * 1024 * 1024);
 | 
				
			||||||
 | 
					          [pkg.diskSize, pkg.diskSizeLabel] = size.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const groupName = pkg.group.toLowerCase() || 'standard';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const group = (groups[groupName] || []);
 | 
				
			||||||
 | 
					          group.push(pkg);
 | 
				
			||||||
 | 
					          groups[groupName] = group;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return groups;
 | 
				
			||||||
 | 
					        }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.setPackageGroups();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private setPackageGroups()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (!this.packages || !this.image || !this.imageType)
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Setup the operating systems array-like object, sorted alphabetically
 | 
				
			||||||
 | 
					    this.packageGroups = Object.keys(this.packages)
 | 
				
			||||||
 | 
					      .filter(x =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.packages[x].forEach(p =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          if (p.name === this.package)
 | 
				
			||||||
 | 
					            this._selectedPackage = p;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!p.brand || !this.image)
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            p.visible = true;
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (this.image.requirements.brand)
 | 
				
			||||||
 | 
					            p.visible = this.image.requirements.brand === p.brand;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (this.image.type === 'zone-dataset')
 | 
				
			||||||
 | 
					            p.visible = ['joyent', 'joyent-minimal'].includes(p.brand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (this.image.type === 'lx-dataset')
 | 
				
			||||||
 | 
					            p.visible = p.brand === 'lx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (this.image.type === 'zvol')
 | 
				
			||||||
 | 
					            p.visible = ['bhyve', 'kvm'].includes(p.brand);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch (this.imageType | 0)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          case 1:
 | 
				
			||||||
 | 
					            return this.packages[x].length && (!x || ['cpu', 'disk', 'memory optimized', 'standard', 'triton'].includes(x));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          case 2:
 | 
				
			||||||
 | 
					            return this.packages[x].length && (!x || ['standard', 'triton'].includes(x));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          default:
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .sort((a, b) => a.localeCompare(b));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Set the pre-selected package group
 | 
				
			||||||
 | 
					    this.selectedPackageGroup = this.packageGroups[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.selectedPackage)
 | 
				
			||||||
 | 
					      this.select.emit(this.selectedPackage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.loadingIndicator = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.onChanges$.pipe(takeUntil(this.destroy$))
 | 
				
			||||||
 | 
					      .subscribe((changes: SimpleChanges) =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        if (changes.image?.currentValue && changes.imageType?.currentValue)
 | 
				
			||||||
 | 
					          this.setPackageGroups();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    // Since we can't control if ngOnChanges is executed before ngOnInit, we need this trick
 | 
				
			||||||
 | 
					    this.onChanges$.next(changes);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.destroy$.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					<fieldset>
 | 
				
			||||||
 | 
					  <button type="button" class="close" [attr.aria-label]="'general.closeWithoutSaving' | translate" (click)="declineAction()">
 | 
				
			||||||
 | 
					    <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					  </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="content">
 | 
				
			||||||
 | 
					    <h4 class="mb-3">{{ title }}</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p class="my-2">{{ prompt }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="d-flex justify-content-end align-items-center mt-5">
 | 
				
			||||||
 | 
					      <button class="btn" [ngClass]="confirmByDefault ? 'btn-link text-info me-3' : 'btn-info order-1'" (click)="declineAction()">
 | 
				
			||||||
 | 
					        {{ declineButtonText }}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <button class="btn" [ngClass]="!confirmByDefault ? 'btn-link text-info me-3' : 'btn-info'" (click)="confirmAction()">
 | 
				
			||||||
 | 
					        {{ confirmButtonText }}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</fieldset>
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					p
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					  font-size: 1.15rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ConfirmationDialogComponent } from './confirmation-dialog.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('ConfirmationDialogComponent', () => {
 | 
				
			||||||
 | 
					  let component: ConfirmationDialogComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<ConfirmationDialogComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ ConfirmationDialogComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(ConfirmationDialogComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnDestroy, Input } from '@angular/core';
 | 
				
			||||||
 | 
					import { BsModalRef } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { NavigationStart, Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { filter, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-confirmation-dialog',
 | 
				
			||||||
 | 
					  templateUrl: './confirmation-dialog.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./confirmation-dialog.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ConfirmationDialogComponent implements OnInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  title = 'Confirmation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  confirmButtonText = 'Yes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  declineButtonText = 'No';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  confirmByDefault = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  confirm = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly modalRef: BsModalRef,
 | 
				
			||||||
 | 
					    private readonly router: Router)
 | 
				
			||||||
 | 
					  {    // When the user navigates away from this route, hide the modal
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$),
 | 
				
			||||||
 | 
					        filter(e => e instanceof NavigationStart)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.modalRef.hide());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  declineAction()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  confirmAction()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.confirm.next();
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.destroy$.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<button class="btn btn-link text-info" (click)="showEditor()" [collapse]="editorVisible" [disabled]="disabled">{{ buttonTitle }}</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div [collapse]="!editorVisible">
 | 
				
			||||||
 | 
					  <div class="row gx-1 my-1 align-items-center" [formGroup]="editorForm">
 | 
				
			||||||
 | 
					    <div class="col align-self-stretch">
 | 
				
			||||||
 | 
					      <input class="form-control h-100" name="key" type="text" [placeholder]="keyLabel" formControlName="key"
 | 
				
			||||||
 | 
					             [appAutofocus]="editorVisible" [appAlphaOnly]="keyAllowedCharacters" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="col-sm-6" *ngIf="showValue">
 | 
				
			||||||
 | 
					      <textarea class="form-control" name="value" formControlName="value" [class.single-line]="singleLine"
 | 
				
			||||||
 | 
					                autosize rows="1" [maxRows]="singleLine ? 1 : 3" [placeholder]="valueLabel" [appAlphaOnly]="valueAllowedCharacters"></textarea>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="col-sm-2 d-flex flex-nowrap justify-content-between align-items-start">
 | 
				
			||||||
 | 
					      <button class="btn px-1 text-success" (click)="saveChanges()" [disabled]="editorForm.invalid">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="check"></fa-icon>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					      <button class="btn px-1 text-danger" (click)="cancelChanges()">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="times"></fa-icon>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					.single-line
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  resize: none;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  overflow-x: auto;
 | 
				
			||||||
 | 
					  overflow-y: hidden !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  padding-top: .4em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-control
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  background: #0c1321;
 | 
				
			||||||
 | 
					  border-color: #00e7ff;
 | 
				
			||||||
 | 
					  border-radius: 3rem;
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { InlineEditorComponent } from './inline-editor.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('InlineEditorComponent', () => {
 | 
				
			||||||
 | 
					  let component: InlineEditorComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<InlineEditorComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ InlineEditorComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(InlineEditorComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										176
									
								
								app/src/app/components/inline-editor/inline-editor.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								app/src/app/components/inline-editor/inline-editor.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,176 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, HostListener } from '@angular/core';
 | 
				
			||||||
 | 
					import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-inline-editor',
 | 
				
			||||||
 | 
					  templateUrl: './inline-editor.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./inline-editor.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class InlineEditorComponent implements OnInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  buttonTitle: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  singleLine: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  key: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  keyLabel = 'Key';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  keyAllowedCharacters: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  keyPattern: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  valueLabel = 'Value';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  valueAllowedCharacters: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  valuePattern: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  showValue = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  disabled: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  saved = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editorVisible: boolean;
 | 
				
			||||||
 | 
					  editorForm: FormGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly elementRef: ElementRef,
 | 
				
			||||||
 | 
					    private readonly fb: FormBuilder) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private createForm()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm = this.fb.group(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        key: [this.key],
 | 
				
			||||||
 | 
					        value: [this.value]
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.keyPattern)
 | 
				
			||||||
 | 
					      this.editorForm.get('key').setValidators([Validators.required, Validators.pattern(this.keyPattern)]);
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      this.editorForm.get('key').setValidators([Validators.required]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.valuePattern)
 | 
				
			||||||
 | 
					      this.editorForm.get('value').setValidators([Validators.required, Validators.pattern(this.valuePattern)]);
 | 
				
			||||||
 | 
					    else if (this.showValue)
 | 
				
			||||||
 | 
					      this.editorForm.get('value').setValidators([Validators.required]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  showEditor()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (this.disabled) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.editorVisible = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    addEventListener('click', this.onDocumentClick.bind(this));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  saveChanges()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    event.preventDefault();
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.editorVisible = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.removeEventListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.showValue)
 | 
				
			||||||
 | 
					      this.saved.emit({
 | 
				
			||||||
 | 
					        key: this.editorForm.get('key').value,
 | 
				
			||||||
 | 
					        value: this.editorForm.get('value').value
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      this.saved.emit(this.editorForm.get('key').value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.editorForm.get('key').setValue(null);
 | 
				
			||||||
 | 
					    this.editorForm.get('value').setValue(null);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  cancelChanges()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorVisible = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.removeEventListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.editorForm.get('key').setValue(null);
 | 
				
			||||||
 | 
					    this.editorForm.get('value').setValue(null);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @HostListener('document:keydown.enter', ['$event'])
 | 
				
			||||||
 | 
					  returnPressed(event)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (event.currentTarget === this.elementRef.nativeElement && this.singleLine)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      event.stopPropagation();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @HostListener('document:keydown.escape', ['$event'])
 | 
				
			||||||
 | 
					  escapePressed(event)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.cancelChanges();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @HostListener('input', ['$event'])
 | 
				
			||||||
 | 
					  textEntered(event)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (event.currentTarget === this.elementRef.nativeElement && this.singleLine && this.value)
 | 
				
			||||||
 | 
					      this.editorForm.get('value').setValue(this.editorForm.get('value').value.replace(/\n/g, ''));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  protected onDocumentClick(event: MouseEvent)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (!this.elementRef.nativeElement.contains(event.target))
 | 
				
			||||||
 | 
					      this.cancelChanges();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private removeEventListeners()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    removeEventListener('click', this.onDocumentClick);
 | 
				
			||||||
 | 
					    removeEventListener('input', this.textEntered);
 | 
				
			||||||
 | 
					    removeEventListener('document:keydown.enter', this.returnPressed);
 | 
				
			||||||
 | 
					    removeEventListener('document:keydown.escape', this.escapePressed);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (!this.buttonTitle)
 | 
				
			||||||
 | 
					      throw 'Specify a button title for the inline editor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.createForm();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.removeEventListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										68
									
								
								app/src/app/components/nav-menu/nav-menu.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								app/src/app/components/nav-menu/nav-menu.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					<nav class="navbar navbar-expand-sm navbar-dark">
 | 
				
			||||||
 | 
					  <ul class="navbar-nav flex-column w-100" *ngIf="isAuthenticated">
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="home"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.dashboard' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./networking/networks']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="network-wired"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.networks' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./networking/firewall-rules']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="fire-alt"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.firewallRules' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./volumes']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="database"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.volumes' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <li class="dropdown-divider"></li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./security']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="shield-alt"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.security' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <!--<li class="dropdown-divider"></li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./file-manager']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="folder"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.fileManager' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>-->
 | 
				
			||||||
 | 
					    <li class="dropdown-divider"></li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./catalog/custom-images']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="layer-group"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.customImages' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <!--<li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./catalog/docker-images']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" [icon]="['fab', 'docker']"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.dockerImages' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>-->
 | 
				
			||||||
 | 
					    <!--<li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./catalog/docker-registry']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" [icon]="['fab', 'docker']"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.dockerRegistry' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>-->
 | 
				
			||||||
 | 
					    <!--<li class="dropdown-divider"></li>
 | 
				
			||||||
 | 
					    <li class="nav-item">
 | 
				
			||||||
 | 
					      <a class="nav-link" [routerLink]="['./account']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
 | 
				
			||||||
 | 
					        <fa-icon [fixedWidth]="true" icon="user-cog"></fa-icon>
 | 
				
			||||||
 | 
					        {{ 'navbar.menu.account' | translate }}
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>-->
 | 
				
			||||||
 | 
					  </ul>
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
							
								
								
									
										17
									
								
								app/src/app/components/nav-menu/nav-menu.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/app/components/nav-menu/nav-menu.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					.dropdown-divider
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  border-color: #222740;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-dark .navbar-nav .nav-link
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #bfb7b1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-dark .navbar-nav .active > .nav-link,
 | 
				
			||||||
 | 
					.navbar-dark .navbar-nav .nav-link.active,
 | 
				
			||||||
 | 
					.navbar-dark .navbar-nav .nav-link.show,
 | 
				
			||||||
 | 
					.navbar-dark .navbar-nav .show > .nav-link
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #f0ad4e;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								app/src/app/components/nav-menu/nav-menu.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/app/components/nav-menu/nav-menu.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { NavMenuComponent } from './nav-menu.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('NavMenuComponent', () => {
 | 
				
			||||||
 | 
					  let component: NavMenuComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<NavMenuComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ NavMenuComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(NavMenuComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										33
									
								
								app/src/app/components/nav-menu/nav-menu.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/src/app/components/nav-menu/nav-menu.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, Output, EventEmitter } from '@angular/core';
 | 
				
			||||||
 | 
					import { Router, NavigationStart } from '@angular/router';
 | 
				
			||||||
 | 
					import { filter } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { TokenService } from '../../helpers/token.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-nav-menu',
 | 
				
			||||||
 | 
					  templateUrl: './nav-menu.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./nav-menu.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class NavMenuComponent implements OnInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  navigate = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isAuthenticated = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly router: Router,
 | 
				
			||||||
 | 
					    private readonly tokenService: TokenService)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(filter(e => e instanceof NavigationStart))
 | 
				
			||||||
 | 
					      .subscribe(e => this.navigate.emit(e));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tokenService.accessTokenUpdated$.subscribe(x => this.isAuthenticated = !!x);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<form novalidate>
 | 
				
			||||||
 | 
					  <fieldset [formGroup]="editorForm" [disabled]="working" *ngIf="editorForm">
 | 
				
			||||||
 | 
					    <button type="button" class="close" [attr.aria-label]="'general.closeWithoutSaving' | translate" (click)="close()">
 | 
				
			||||||
 | 
					      <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content">
 | 
				
			||||||
 | 
					      <h4 class="mb-3">{{ title }}</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <p class="my-2">{{ prompt }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <input type="text" class="form-control" id="value" formControlName="value" [placeholder]="placeholder" [appAutofocus]="true" [appAutofocusDelay]="600">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="d-flex justify-content-end align-items-center mt-5">
 | 
				
			||||||
 | 
					        <button class="btn btn-info" (click)="saveChanges()" [disabled]="editorForm.invalid">{{ saveButtonText }}</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </fieldset>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					p
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  color: #ff9c07;
 | 
				
			||||||
 | 
					  font-size: 1.15rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { PromptDialogComponent } from './prompt-dialog.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('PromptDialogComponent', () => {
 | 
				
			||||||
 | 
					  let component: PromptDialogComponent;
 | 
				
			||||||
 | 
					  let fixture: ComponentFixture<PromptDialogComponent>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await TestBed.configureTestingModule({
 | 
				
			||||||
 | 
					      declarations: [ PromptDialogComponent ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .compileComponents();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    fixture = TestBed.createComponent(PromptDialogComponent);
 | 
				
			||||||
 | 
					    component = fixture.componentInstance;
 | 
				
			||||||
 | 
					    fixture.detectChanges();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should create', () => {
 | 
				
			||||||
 | 
					    expect(component).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					import { Component, OnInit, OnDestroy, Input } from '@angular/core';
 | 
				
			||||||
 | 
					import { BsModalRef } from 'ngx-bootstrap/modal';
 | 
				
			||||||
 | 
					import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
 | 
				
			||||||
 | 
					import { NavigationStart, Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
 | 
					import { filter, takeUntil } from 'rxjs/operators';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-prompt-dialog',
 | 
				
			||||||
 | 
					  templateUrl: './prompt-dialog.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./prompt-dialog.component.scss']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class PromptDialogComponent implements OnInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  saveButtonText = 'Save changes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  placeholder: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  required: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  save = new Subject<string>();
 | 
				
			||||||
 | 
					  editorForm: FormGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private destroy$ = new Subject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly modalRef: BsModalRef,
 | 
				
			||||||
 | 
					    private readonly router: Router,
 | 
				
			||||||
 | 
					    private readonly fb: FormBuilder)
 | 
				
			||||||
 | 
					  {    // When the user navigates away from this route, hide the modal
 | 
				
			||||||
 | 
					    router.events
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.destroy$),
 | 
				
			||||||
 | 
					        filter(e => e instanceof NavigationStart)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe(() => this.modalRef.hide());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private createForm()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.editorForm = this.fb.group(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        value: [this.value]
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.required)
 | 
				
			||||||
 | 
					      this.editorForm.get('value').setValidators(Validators.required);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  close()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  saveChanges()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.save.next(this.editorForm.get('value').value);
 | 
				
			||||||
 | 
					    this.modalRef.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.createForm();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.destroy$.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								app/src/app/directives/alpha-only.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/src/app/directives/alpha-only.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import { AlphaOnlyDirective } from './alpha-only.directive';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AlphaOnlyDirective', () => {
 | 
				
			||||||
 | 
					  it('should create an instance', () => {
 | 
				
			||||||
 | 
					    const directive = new AlphaOnlyDirective();
 | 
				
			||||||
 | 
					    expect(directive).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										53
									
								
								app/src/app/directives/alpha-only.directive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/src/app/directives/alpha-only.directive.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import { Directive, ElementRef, HostListener, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Directive({
 | 
				
			||||||
 | 
					  selector: '[appAlphaOnly]'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AlphaOnlyDirective implements OnInit, OnChanges
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  appAlphaOnly = '^[A-Za-z0-9]+$';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private regex;
 | 
				
			||||||
 | 
					  //private negateRegex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly el: ElementRef) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @HostListener('keypress', ['$event'])
 | 
				
			||||||
 | 
					  onKeyPress(event)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    return this.regex ? this.regex.test(event.key) : true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  @HostListener('paste', ['$event'])
 | 
				
			||||||
 | 
					  onPaste(event)
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (!this.regex) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event.preventDefault();
 | 
				
			||||||
 | 
					    //const value = event.clipboardData.getData('text/plain').replace(this.negateRegex, '');
 | 
				
			||||||
 | 
					    const value = event.clipboardData.getData('text/plain').trim();
 | 
				
			||||||
 | 
					    if (value.match(this.regex))
 | 
				
			||||||
 | 
					      document.execCommand('insertHTML', false, value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnInit()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (!this.appAlphaOnly) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.regex = new RegExp(this.appAlphaOnly);
 | 
				
			||||||
 | 
					    //this.regex = new RegExp(`^[${this.appAlphaOnly}]+$`);
 | 
				
			||||||
 | 
					    //this.negateRegex = new RegExp(`[^${this.appAlphaOnly}]`, 'g');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (changes.appAlphaOnly.currentValue)
 | 
				
			||||||
 | 
					      this.regex = new RegExp(this.appAlphaOnly);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								app/src/app/directives/autofocus.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/src/app/directives/autofocus.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import { AutofocusDirective } from './autofocus.directive';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AutofocusDirective', () => {
 | 
				
			||||||
 | 
					  it('should create an instance', () => {
 | 
				
			||||||
 | 
					    const directive = new AutofocusDirective();
 | 
				
			||||||
 | 
					    expect(directive).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										64
									
								
								app/src/app/directives/autofocus.directive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/src/app/directives/autofocus.directive.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					import { Directive, Input, ElementRef, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Directive({
 | 
				
			||||||
 | 
					  selector: '[appAutofocus]'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AutofocusDirective implements AfterViewInit, OnChanges
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  appAutofocus = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  appAutofocusDelay: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  appFocusAnyElement: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private focusElement: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly element: ElementRef) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngAfterViewInit(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (this.element.nativeElement.nodeName === 'INPUT' ||
 | 
				
			||||||
 | 
					      this.element.nativeElement.nodeName === 'TEXTAREA' ||
 | 
				
			||||||
 | 
					      this.element.nativeElement.nodeName === 'SELECT')
 | 
				
			||||||
 | 
					      this.focusElement = this.element.nativeElement;
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      this.focusElement = this.appFocusAnyElement
 | 
				
			||||||
 | 
					        ? this.element.nativeElement
 | 
				
			||||||
 | 
					        : this.element.nativeElement.querySelector('select, textarea, input');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.setFocus();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.setFocus();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private setFocus()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (!this.appAutofocus) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setTimeout(() =>
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      if (!this.focusElement) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.focusElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.focusElement.select();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      catch (e)
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        this.focusElement.focus();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, (this.appAutofocusDelay | 0));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								app/src/app/directives/lazy-load.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/src/app/directives/lazy-load.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import { LazyLoadDirective } from './lazy-load.directive';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('LazyLoadDirective', () => {
 | 
				
			||||||
 | 
					  it('should create an instance', () => {
 | 
				
			||||||
 | 
					    const directive = new LazyLoadDirective();
 | 
				
			||||||
 | 
					    expect(directive).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										89
									
								
								app/src/app/directives/lazy-load.directive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								app/src/app/directives/lazy-load.directive.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					import { Directive, AfterViewInit, OnDestroy, ElementRef, Output, EventEmitter, Input } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Directive({
 | 
				
			||||||
 | 
					  selector: '[lazyLoad]'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class LazyLoadDirective implements AfterViewInit, OnDestroy
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  container: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  lazyLoadDelay: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  canLoad = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  load = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  unload = new EventEmitter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private observer: any;
 | 
				
			||||||
 | 
					  private delay: any;
 | 
				
			||||||
 | 
					  private options: {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  constructor(private readonly el: ElementRef) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private canLazyLoad = () => window && 'IntersectionObserver' in window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private loadOnIntersection()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.observer = new IntersectionObserver(entries =>
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      entries.forEach(({ isIntersecting }) =>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        if (isIntersecting)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          this.canLoad.emit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.delay = setTimeout(() =>
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            this.load.emit();
 | 
				
			||||||
 | 
					          }, this.lazyLoadDelay);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          clearTimeout(this.delay);
 | 
				
			||||||
 | 
					          this.unload.emit();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.observer.observe(this.el.nativeElement, this.options);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  private loadWithDelay()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.canLoad.emit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.delay = setTimeout(() =>
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      this.load.emit();
 | 
				
			||||||
 | 
					    }, this.lazyLoadDelay);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngAfterViewInit()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    this.options = {
 | 
				
			||||||
 | 
					      threshold: 1.0,
 | 
				
			||||||
 | 
					      rootMargin: '0px',
 | 
				
			||||||
 | 
					      root: this.container
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    this.canLazyLoad() ? this.loadOnIntersection() : this.loadWithDelay();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ----------------------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					  ngOnDestroy()
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    if (this.observer)
 | 
				
			||||||
 | 
					      this.observer.unobserve(this.el.nativeElement);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/src/app/file-manager/file-manager.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/src/app/file-manager/file-manager.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<p>file-manager works!</p>
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user