Commit 7ae96891 authored by Marcin Byra's avatar Marcin Byra
Browse files

Merge pull request #50 in MEL/melodic-frontend from rc3.0 to master

* commit '6e2175d8':
  Adjust translations
  Adjust translation
  Adjust translation
  Introduce menu items and and minor adjustments
  fix problem with UNKNOWN location for BYON
  info for strong password, fix problem with missing ip by SSH connection
  id for all ByonDefinition elements
  support for byon in ui
  move blocking UI to menu component
  node group validation, block UI during uploading/deleting xmi and saving secure variables
  display proper messages by uploading many files
  add password-based ssh connection
  responsive view for process
parents 9cd6dbb3 6e2175d8
......@@ -6681,6 +6681,14 @@
"integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
"dev": true
},
"ng-block-ui": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/ng-block-ui/-/ng-block-ui-2.1.8.tgz",
"integrity": "sha512-BBcjUn9b/m3+wPlXkYExuy6ko+5oK7pte79gGUVo6a3HqpLnvPQXFgKV1kUpIM97NYfKKtR/+dPj7Xhh/GSV4w==",
"requires": {
"tslib": "^1.9.0"
}
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
......
......@@ -25,6 +25,11 @@ const routes: Routes = [
canActivate: [CommonUserAdminRoleGuard]
},
{
path: 'byon', loadChildren: './byon/byon.module#ByonModule',
canActivate: [CommonUserAdminRoleGuard]
},
{path: '**', loadChildren: './user/user.module#UserModule'},
];
......
......@@ -7,6 +7,7 @@ export enum IpVersion {
}
export class IpAddress {
id: number;
ipAddressType: IpAddressType;
ipVersion: string;
value: string;
......
export class LoginCredential {
id: number;
username: string;
password: string;
privateKey: string;
......
import {OperatingSystem} from '../../process/process-details/offer/model/operating-system';
import {GeoLocation} from '../../process/process-details/offer/model/geo-location';
export class NodeProperties {
id: number;
providerId: string;
numberOfCores: number;
memory: number;
disk: number;
operatingSystem: OperatingSystem;
geoLocation: GeoLocation;
}
import {TestBed} from '@angular/core/testing';
import {WebSshService} from './web-ssh.service';
describe('WebSshService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: WebSshService = TestBed.get(WebSshService);
expect(service).toBeTruthy();
});
});
import {Injectable} from '@angular/core';
import {AppConfigService} from '../../app-config/service/app-config.service';
import {LoginCredential} from '../model/login-credential';
@Injectable({
providedIn: 'root'
})
export class WebSshService {
constructor() {
}
createSshConnection(loginCredential: LoginCredential, publicIp: string, vmName: string) {
console.log(`Create SSH connection for vm: ${vmName}`);
if (loginCredential.privateKey) {
this.connectBySshWithKey(loginCredential, publicIp);
} else {
this.connectBySshWithPassword(loginCredential, publicIp);
}
}
private connectBySshWithKey(loginCredential: LoginCredential, publicIp: string) {
const formattedPrivateKey = this.formatKey(loginCredential.privateKey);
const webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${publicIp}&username=${loginCredential.username}` +
`&privatekey=${formattedPrivateKey}`;
console.log(`Key-based SSH connection under url: ${webSshUrl}`);
window.open(webSshUrl);
}
private connectBySshWithPassword(loginCredential: LoginCredential, publicIp: string) {
const webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${publicIp}&username=${loginCredential.username}` +
`&password=${btoa(loginCredential.password)}`;
console.log(`Password-based SSH connection under address: ${webSshUrl}`);
window.open(webSshUrl);
}
private formatKey(privateKey: string): string {
const lines = privateKey.split('\n');
let result = '';
for (const keyLine of lines) {
result += keyLine + '\\n';
}
return result.substring(0, result.length - 2); // remove last '\n' sign
}
}
......@@ -7,8 +7,8 @@ import {Cloud} from '../../process/process-details/offer/model/cloud';
import {ProcessOfferService} from '../../process/process-details/offer/service/process-offer.service';
import {UserService} from '../../user/service/user.service';
import {UserRole} from '../../user/model/user-role.enum';
import {AppConfigService} from '../../app-config/service/app-config.service';
import {OfferLocation} from '../../process/process-details/offer/model/offer-location';
import {WebSshService} from '../service/web-ssh.service';
@Component({
selector: 'app-vm-list',
......@@ -31,7 +31,8 @@ export class VmListComponent implements OnInit {
constructor(private applicationService: ApplicationService,
private processOfferService: ProcessOfferService,
private snackBar: MatSnackBar,
private userService: UserService) {
private userService: UserService,
private webSshService: WebSshService) {
}
ngOnInit() {
......@@ -107,39 +108,21 @@ export class VmListComponent implements OnInit {
}
getCity(vm: NodeCloudiator): string {
const locationId = vm.originId.split('/')[0];
const location = this.locationList
.find(value => value.id === locationId);
return (location && location.geoLocation) ? location.geoLocation.city : 'UNKNOWN';
if (vm.nodeProperties.geoLocation != null && vm.nodeProperties.geoLocation.city != null) {
return vm.nodeProperties.geoLocation.city;
} else { // find location by locationId
const locationId = vm.originId.split('/')[0];
const location = this.locationList
.find(value => value.id === locationId);
return (location && location.geoLocation) ? location.geoLocation.city : 'UNKNOWN';
}
}
userHasPermissionToSshConnection(vm: NodeCloudiator): boolean {
return (UserRole.ADMIN === UserRole[this.userService.currentUser.userRole]) && vm.loginCredential.privateKey !== null;
return UserRole.ADMIN === UserRole[this.userService.currentUser.userRole];
}
onSshConnectionClick(vm: NodeCloudiator) {
console.log(`SSH connection click for vm: ${vm.name}`);
let webSshUrl;
if (vm.loginCredential.privateKey) {
console.log(`Key-based SSH connection`);
const formattedPrivateKey = this.formatKey(vm.loginCredential.privateKey);
webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${vm.publicIp}&username=${vm.loginCredential.username}` +
`&privatekey=${formattedPrivateKey}`;
window.open(webSshUrl);
} else {
webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${vm.publicIp}&username=${vm.loginCredential.username}` +
`&password=${btoa(vm.loginCredential.password)}`;
console.log(`Password-based SSH connection under address: ${webSshUrl}`);
// todo add window.open(webSshUrl) after webssh fixed
}
}
private formatKey(privateKey: string): string {
const lines = privateKey.split('\n');
let result = '';
for (const keyLine of lines) {
result += keyLine + '\\n';
}
return result.substring(0, result.length - 2); // remove last '\n' sign
this.webSshService.createSshConnection(vm.loginCredential, vm.publicIp, vm.name);
}
}
mat-card {
padding: var(--margin-global);
margin: var(--double-margin);
}
mat-dialog-actions {
margin-bottom: var(--margin-global);
}
mat-form-field {
margin-left: var(--double-margin);
width: 20%;
}
#private-key-field {
width: 55%;
}
.mat-card-with-bigger-bottom {
padding-bottom: var(--quadruple-margin);
}
#provider-id-field {
width: 40%;
}
.node-properties-common-field {
width: 18%;
}
<mat-card>
<form #byonDefinitionFormId="ngForm" [formGroup]="byonDefinitionForm">
<mat-card-title>Byon definition</mat-card-title>
<mat-dialog-content>
<mat-card>
<mat-form-field>
<input matInput formControlName="name" placeholder="name" required>
<mat-error
*ngIf="form.name.hasError('required')">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
</mat-card>
<mat-card>
<mat-card-subtitle><h3>IP addresses</h3></mat-card-subtitle>
<mat-spinner *ngIf="byonEnumsLoadingInProgress"></mat-spinner>
<app-ip-address-list
[ipAddresses]="ipAddresses"
[byonEnums]="byonEnums"
[isReadMode]="isReadMode">
</app-ip-address-list>
</mat-card>
<mat-card class="mat-card-with-bigger-bottom">
<mat-card-subtitle><h3>Login credentials</h3></mat-card-subtitle>
<form #loginCredentialFormId="ngForm" [formGroup]="loginCredentialForm">
<mat-form-field>
<input matInput formControlName="username" placeholder="username" required>
<mat-error
*ngIf="loginCredentialFormControl.username.hasError('required') && (loginCredentialFormControl.username.dirty || loginCredentialFormControl.username.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="password" placeholder="password"
[type]="passwordHide ? 'password' : 'text'">
<mat-icon matSuffix (click)="passwordHide = !passwordHide">{{passwordHide ? 'visibility_off' :
'visibility'}}
</mat-icon>
<mat-error
*ngIf="loginCredentialFormControl.password.hasError('oneOfTwoFieldsRequired') && (loginCredentialFormControl.password.dirty || loginCredentialFormControl.password.touched)">
{{getPasswordOrKeyRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field id="private-key-field">
<mat-label>Private key</mat-label>
<textarea matInput
formControlName="privateKey"
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="10"></textarea>
<mat-error
*ngIf="loginCredentialFormControl.privateKey.hasError('oneOfTwoFieldsRequired') && (loginCredentialFormControl.privateKey.dirty || loginCredentialFormControl.privateKey.touched)">
{{getPasswordOrKeyRequiredMsg()}}
</mat-error>
</mat-form-field>
</form>
</mat-card>
<mat-card>
<mat-card-subtitle><h3>Node properties</h3></mat-card-subtitle>
<form #nodePropertiesFormId="ngForm" [formGroup]="nodePropertiesForm">
<mat-form-field id="provider-id-field">
<input matInput formControlName="providerId" placeholder="provider id" required>
<mat-error
*ngIf="nodePropertiesFormControl.providerId.hasError('required') && (nodePropertiesFormControl.providerId.dirty || nodePropertiesFormControl.providerId.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field class="node-properties-common-field">
<input matInput formControlName="numberOfCores" placeholder="number of cores" required>
<mat-error
*ngIf="nodePropertiesFormControl.numberOfCores.hasError('required') && (nodePropertiesFormControl.numberOfCores.dirty || nodePropertiesFormControl.numberOfCores.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="nodePropertiesFormControl.numberOfCores.hasError('pattern') && (nodePropertiesFormControl.numberOfCores.dirty || nodePropertiesFormControl.numberOfCores.touched)">
{{getNumberOfCoresMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field class="node-properties-common-field">
<input matInput formControlName="memory" placeholder="memory" required>
<mat-error
*ngIf="nodePropertiesFormControl.memory.hasError('required') && (nodePropertiesFormControl.memory.dirty || nodePropertiesFormControl.memory.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="nodePropertiesFormControl.memory.hasError('pattern') && (nodePropertiesFormControl.memory.dirty || nodePropertiesFormControl.memory.touched)">
{{getMemoryMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field class="node-properties-common-field">
<input matInput formControlName="disk" placeholder="disk" required>
<mat-error
*ngIf="nodePropertiesFormControl.disk.hasError('required') && (nodePropertiesFormControl.disk.dirty || nodePropertiesFormControl.disk.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="nodePropertiesFormControl.disk.hasError('pattern') && (nodePropertiesFormControl.disk.dirty || nodePropertiesFormControl.disk.touched)">
{{getDiskMsg()}}
</mat-error>
</mat-form-field>
<mat-card class="mat-card-with-bigger-bottom">
<mat-card-subtitle><h4>Operating system</h4></mat-card-subtitle>
<mat-spinner *ngIf="byonEnumsLoadingInProgress"></mat-spinner>
<form #operatingSystemFormId="ngForm" [formGroup]="operatingSystemForm">
<mat-form-field>
<mat-select formControlName="operatingSystemFamily" placeholder="operating system family"
name="operatingSystemFamily" required>
<mat-option *ngFor="let osFamily of byonEnums.osFamilies" [value]="osFamily">
{{osFamily}}
</mat-option>
</mat-select>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemFamily.hasError('required') && (operatingSystemFormControl.operatingSystemFamily.dirty || operatingSystemFormControl.operatingSystemFamily.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="operatingSystemArchitecture" placeholder="operating system architecture"
name="operatingSystemArchitecture" required>
<mat-option *ngFor="let osArchitecture of byonEnums.osArchitectures" [value]="osArchitecture">
{{osArchitecture}}
</mat-option>
</mat-select>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemArchitecture.hasError('required') && (operatingSystemFormControl.operatingSystemArchitecture.dirty || operatingSystemFormControl.operatingSystemArchitecture.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="operatingSystemVersion" placeholder="operating system version"
required>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemVersion.hasError('required') && (operatingSystemFormControl.operatingSystemVersion.dirty || operatingSystemFormControl.operatingSystemVersion.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemVersion.hasError('pattern') && (operatingSystemFormControl.operatingSystemVersion.dirty || operatingSystemFormControl.operatingSystemVersion.touched)">
{{getOsVersionMsg()}}
</mat-error>
</mat-form-field>
</form>
</mat-card>
<mat-card class="mat-card-with-bigger-bottom">
<mat-card-subtitle><h4>Geolocation</h4></mat-card-subtitle>
<form #geoLocationFormId="ngForm" [formGroup]="geoLocationForm">
<mat-form-field>
<input matInput formControlName="city" placeholder="city" required>
<mat-error
*ngIf="geoLocationFormControl.city.hasError('required') && (geoLocationFormControl.city.dirty || geoLocationFormControl.city.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="country" placeholder="country" required>
<mat-error
*ngIf="geoLocationFormControl.country.hasError('required') && (geoLocationFormControl.country.dirty || geoLocationFormControl.country.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="latitude" placeholder="latitude" required>
<mat-error
*ngIf="geoLocationFormControl.latitude.hasError('required') && (geoLocationFormControl.latitude.dirty || geoLocationFormControl.latitude.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="(geoLocationFormControl.latitude.hasError('pattern') || geoLocationFormControl.latitude.hasError('min') || geoLocationFormControl.latitude.hasError('max')) && (geoLocationFormControl.latitude.dirty || geoLocationFormControl.latitude.touched)">
{{getLatitudeMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="longitude" placeholder="longitude" required>
<mat-error
*ngIf="geoLocationFormControl.longitude.hasError('required') && (geoLocationFormControl.longitude.dirty || geoLocationFormControl.longitude.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="(geoLocationFormControl.longitude.hasError('pattern') || geoLocationFormControl.longitude.hasError('min') || geoLocationFormControl.longitude.hasError('max')) && (geoLocationFormControl.longitude.dirty || geoLocationFormControl.longitude.touched)">
{{getLongitudeMsg()}}
</mat-error>
</mat-form-field>
</form>
</mat-card>
</form>
</mat-card>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button type="button" color="warn" mat-dialog-close>
<mat-icon>undo</mat-icon>
Cancel
</button>
<button mat-raised-button [disabled]="byonDefinitionFormId.invalid || isReadMode"
(click)="saveByonDefinition(byonDefinitionFormId)"
color="primary">
<mat-icon>save</mat-icon>
Save
</button>
</mat-dialog-actions>
</form>
</mat-card>
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ByonDefinitionFormComponent} from './byon-definition-form.component';
describe('ByonDefinitionFormComponent', () => {
let component: ByonDefinitionFormComponent;
let fixture: ComponentFixture<ByonDefinitionFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ByonDefinitionFormComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ByonDefinitionFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {Component, Inject, NgZone, OnInit, Optional, ViewChild} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, NgForm, Validators} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef, MatSnackBar} from '@angular/material';
import {IpAddress} from '../../application/model/ip-address';
import {Byon} from '../model/byon';
import {CdkTextareaAutosize} from '@angular/cdk/text-field';
import {take} from 'rxjs/operators';
import {IpAddressListComponent} from '../ip-address-list/ip-address-list.component';
import {ByonEnums} from '../model/byon-enums';
import {ByonService} from '../service/byon.service';
import {oneOfTwoFieldsRequiredValidator} from '../validator/one-of-two-fields-required.validator';
@Component({
selector: 'app-byon-definition-form',
templateUrl: './byon-definition-form.component.html',
styleUrls: ['./byon-definition-form.component.css']
})
export class ByonDefinitionFormComponent implements OnInit {
@ViewChild(IpAddressListComponent) ipAddressesComponent: IpAddressListComponent;
@ViewChild('autosize', {read: false}) autosize: CdkTextareaAutosize;
byonData: Byon;
isReadMode = false;
passwordHide = true;
byonEnums = new ByonEnums();
byonEnumsLoadingInProgress = false;
ipAddresses = new Array<IpAddress>();
loginCredentialForm: FormGroup;
geoLocationForm: FormGroup;
operatingSystemForm: FormGroup;
byonDefinitionForm: FormGroup;
nodePropertiesForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private dialogRef: MatDialogRef<ByonDefinitionFormComponent>,
private ngZone: NgZone,
private byonService: ByonService,
private snackBar: MatSnackBar,
@Optional() @Inject(MAT_DIALOG_DATA) public dialogData?: any) {
}
triggerResize() {
// Wait for changes to be applied, then trigger textarea resize.
this.ngZone.onStable.pipe(take(1))
.subscribe(() => this.autosize.resizeToFitContent(true));
}
ngOnInit() {
if (this.dialogData) {
this.byonData = this.dialogData ? this.dialogData.byonData : undefined;
this.isReadMode = this.dialogData ? this.dialogData.isReadMode : false;
this.ipAddresses = this.byonData ? this.byonData.ipAddresses : [];
}
this.byonEnumsLoadingInProgress = true;
this.byonService.getByonEnums().subscribe(byonEnumsResponse => {
this.byonEnums = byonEnumsResponse;
this.byonEnumsLoadingInProgress = false;
},
error1 => {
this.byonEnumsLoadingInProgress = false;
this.snackBar.open(`Error by getting available options for byons form: ${error1.error.message}`, 'Close');
}
);
this.loginCredentialForm = this.formBuilder.group({
id: this.byonData ? this.byonData.loginCredential.id : null,
username: this.byonData ? new FormControl({value: this.byonData.loginCredential.username, disabled: this.isReadMode})
: ['', Validators.required],
password: this.byonData ? new FormControl({value: this.byonData.loginCredential.password, disabled: this.isReadMode})
: [''],
privateKey: this.byonData ? new FormControl({value: this.byonData.loginCredential.privateKey, disabled: this.isReadMode})
: ['']
},
{
validators: oneOfTwoFieldsRequiredValidator('password', 'privateKey')
});
this.geoLocationForm = this.formBuilder.group({
id: this.byonData ? this.byonData.nodeProperties.geoLocation.id : null,
city: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.city, disabled: this.isReadMode})
: ['', Validators.required],
country: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.country, disabled: this.isReadMode})
: ['', Validators.required],
latitude: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.latitude, disabled: this.isReadMode})
: ['', [Validators.required, Validators.pattern(/^-*[0-9]*\.*[0-9]+$/),
Validators.min(-90), Validators.max(90)]], // double
longitude: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.longitude, disabled: this.isReadMode})
: ['', [Validators.required, Validators.pattern(/^-*[0-9]*\.*[0-9]+$/),
Validators.min(-180), Validators.max(180)]] // double
});
this.operatingSystemForm = this.formBuilder.group({
id: this.byonData ? this.byonData.nodeProperties.operatingSystem.id