TRIX-26 affinity rule editor

also renamed all occurrences of "instance" to "machine" because there were many inconsistencies
This commit is contained in:
Dragos 2021-06-01 10:55:41 +03:00
parent 4a3f2a0aeb
commit 069afd02b1
85 changed files with 1053 additions and 817 deletions

View File

@ -11,6 +11,6 @@
"\\src\\assets\\i18n",
"\\src\\assets\\i18n\\networking"
],
"SelectedNode": "\\src\\app\\instances\\instances.component.html",
"SelectedNode": "\\src\\app\\instances\\machines.component.html",
"PreviewInSolutionExplorer": false
}

View File

@ -13,7 +13,7 @@ const appRoutes: Routes = [
},
{
path: 'machines',
loadChildren: () => import('./instances/instances.module').then(x => x.InstancesModule),
loadChildren: () => import('./machines/machines.module').then(x => x.MachinesModule),
canActivate: [AuthGuardService],
canLoad: [AuthGuardService],
},

View File

@ -14,7 +14,7 @@ import { SharedModule } from './shared.module';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { DashboardComponent } from './pages/machines/machines.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';

View File

@ -7,7 +7,7 @@
<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>
<p class="my-2">Fill in the name and version for a new image based on the "{{ machine.name }}" machine</p>
<input type="text" class="form-control mb-3" formControlName="name" placeholder="Image name" [appAutofocus]="true" [appAutofocusDelay]="600">

View File

@ -13,7 +13,7 @@ import { filter, takeUntil } from 'rxjs/operators';
export class CustomImageEditorComponent implements OnInit
{
@Input()
instance: any;
machine: any;
save = new Subject<any>();
editorForm: FormGroup;

View File

@ -129,12 +129,12 @@ export class CatalogService
}
// ----------------------------------------------------------------------------------------------------------------
createImage(instanceId: string, name: string, version: string,
createImage(machineId: 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,
machine: machineId,
name,
version,
description,

View File

@ -0,0 +1,43 @@
<button class="btn btn-link text-info" (click)="showEditor()" [collapse]="editorVisible" [disabled]="disabled">Add affinity rule</button>
<div [collapse]="!editorVisible" [formGroup]="editorForm">
<div class="row gx-1 my-1 align-items-center">
<div class="col-sm-2">
<select class="form-select" name="strict" formControlName="strict" [appAutofocus]="editorVisible">
<option hidden disabled selected value></option>
<option value="=">{{ 'affinityRuleEditor.strict' | translate }}</option>
<option value="=~">{{ 'affinityRuleEditor.optional' | translate }}</option>
</select>
</div>
<div class="col-sm-2">
<select class="form-select" name="operator" formControlName="operator" [appAutofocus]="editorForm.get('strict').value">
<option hidden disabled selected value></option>
<option value="=">{{ 'affinityRuleEditor.closeTo' | translate }}</option>
<option value="!">{{ 'affinityRuleEditor.farFrom' | translate }}</option>
</select>
</div>
<div class="col-sm-3">
<select class="form-select" name="target" formControlName="target" [appAutofocus]="editorForm.get('operator').value">
<option hidden disabled selected value></option>
<option value="machine">{{ 'affinityRuleEditor.namedLike' | translate }}</option>
<option value="tagName">{{ 'affinityRuleEditor.taggedWith' | translate }}</option>
</select>
</div>
<div *ngIf="editorForm.get('target').value === 'tagName'" class="col-sm">
<input type="text" class="form-control" formControlName="tagName" placeholder="Tag name" [appAutofocus]="editorForm.get('target').value" />
</div>
<div class="col-sm">
<input type="text" class="form-control" formControlName="value" placeholder="Value" [appAutofocus]="editorForm.get('target').value"
[tooltip]="(editorForm.get('target').value === 'machine' ? 'affinityRuleEditor.valueHint' : 'affinityRuleEditor.tagHint') | translate"
placement="top" container="body" [adaptivePosition]="false" />
</div>
<div class="col-sm-1 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>

View File

@ -0,0 +1,7 @@
.form-control
{
background: #0c1321;
border-color: #00e7ff;
border-radius: 3rem;
color: #ff9c07;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AffinityRuleEditorComponent } from './affinity-rule-editor.component';
describe('AffinityRuleEditorComponent', () => {
let component: AffinityRuleEditorComponent;
let fixture: ComponentFixture<AffinityRuleEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AffinityRuleEditorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AffinityRuleEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,144 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, HostListener } from '@angular/core';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-affinity-rule-editor',
templateUrl: './affinity-rule-editor.component.html',
styleUrls: ['./affinity-rule-editor.component.scss']
})
export class AffinityRuleEditorComponent implements OnInit, OnDestroy
{
@Input()
disabled: boolean;
@Output()
saved = new EventEmitter();
editorVisible: boolean;
editorForm: FormGroup;
private destroy$ = new Subject();
// --------------------------------------------------------------------------------------------------
constructor(private readonly elementRef: ElementRef,
private readonly fb: FormBuilder) { }
// ----------------------------------------------------------------------------------------------------------------
private createForm()
{
this.editorForm = this.fb.group(
{
strict: [null, Validators.required],
operator: [null, Validators.required],
target: [null, Validators.required],
tagName: [null],
value: [null, Validators.required]
});
this.editorForm.get('target').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(target =>
{
if (target === 'tagName')
this.editorForm.get('tagName').setValidators(Validators.required);
else
this.editorForm.get('tagName').clearValidators();
});
}
// --------------------------------------------------------------------------------------------------
showEditor()
{
if (this.disabled) return;
this.editorVisible = true;
addEventListener('click', this.onDocumentClick.bind(this));
}
// --------------------------------------------------------------------------------------------------
saveChanges()
{
event.preventDefault();
event.stopPropagation();
this.editorVisible = false;
this.removeEventListeners();
let rule: string;
if (this.editorForm.get('target').value === 'machine')
rule = `machine${this.editorForm.get('operator').value}${this.editorForm.get('strict').value}${this.editorForm.get('value').value}`;
else
rule = `${this.editorForm.get('tagName').value}${this.editorForm.get('operator').value}${this.editorForm.get('strict').value}${this.editorForm.get('value').value}`;
this.saved.emit({
strict: this.editorForm.get('strict').value === '=',
closeTo: this.editorForm.get('operator').value === '=',
targetMachine: this.editorForm.get('target').value === 'machine',
tagName: this.editorForm.get('tagName').value,
value: this.editorForm.get('value').value,
rule
});
this.resetForm();
}
// --------------------------------------------------------------------------------------------------
cancelChanges()
{
this.editorVisible = false;
this.removeEventListeners();
this.resetForm();
}
// --------------------------------------------------------------------------------------------------
private resetForm()
{
this.editorForm.get('strict').setValue(null);
this.editorForm.get('operator').setValue(null);
this.editorForm.get('target').setValue(null);
this.editorForm.get('tagName').setValue(null);
this.editorForm.get('value').setValue(null);
}
// --------------------------------------------------------------------------------------------------
@HostListener('document:keydown.escape', ['$event'])
escapePressed(event)
{
this.cancelChanges();
}
// --------------------------------------------------------------------------------------------------
protected onDocumentClick(event: MouseEvent)
{
if (!this.elementRef.nativeElement.contains(event.target))
this.cancelChanges();
}
// --------------------------------------------------------------------------------------------------
private removeEventListeners()
{
removeEventListener('click', this.onDocumentClick);
removeEventListener('document:keydown.escape', this.escapePressed);
}
// --------------------------------------------------------------------------------------------------
ngOnInit(): void
{
this.createForm();
}
// --------------------------------------------------------------------------------------------------
ngOnDestroy()
{
this.removeEventListeners();
this.destroy$.next();
}
}

View File

@ -102,8 +102,7 @@ export class InlineEditorComponent implements OnInit, OnDestroy
else
this.saved.emit(this.editorForm.get('key').value);
this.editorForm.get('key').setValue(null);
this.editorForm.get('value').setValue(null);
this.resetForm();
}
// --------------------------------------------------------------------------------------------------
@ -113,6 +112,12 @@ export class InlineEditorComponent implements OnInit, OnDestroy
this.removeEventListeners();
this.resetForm();
}
// --------------------------------------------------------------------------------------------------
private resetForm()
{
this.editorForm.get('key').setValue(null);
this.editorForm.get('value').setValue(null);
}

View File

@ -3,7 +3,7 @@
<li class="nav-item">
<a class="nav-link" [routerLink]="['./']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
<fa-icon [fixedWidth]="true" icon="home"></fa-icon>
{{ 'navbar.menu.instances' | translate }}
{{ 'navbar.menu.machines' | translate }}
</a>
</li>
<li class="nav-item">

View File

@ -1,7 +1,7 @@
import { AlphaOnlyDirective } from './alpha-only.directive';
describe('AlphaOnlyDirective', () => {
it('should create an instance', () => {
it('should create an machine', () => {
const directive = new AlphaOnlyDirective();
expect(directive).toBeTruthy();
});

View File

@ -1,7 +1,7 @@
import { AutofocusDirective } from './autofocus.directive';
describe('AutofocusDirective', () => {
it('should create an instance', () => {
it('should create an machine', () => {
const directive = new AutofocusDirective();
expect(directive).toBeTruthy();
});

View File

@ -1,7 +1,7 @@
import { LazyLoadDirective } from './lazy-load.directive';
describe('LazyLoadDirective', () => {
it('should create an instance', () => {
it('should create an machine', () => {
const directive = new LazyLoadDirective();
expect(directive).toBeTruthy();
});

View File

@ -15,11 +15,11 @@ export class HelpComponent implements OnInit
contentUrl: './assets/help/account-info.html'
},
{
title: 'Provisioning compute instance',
title: 'Provisioning compute machine',
contentUrl: ''
},
{
title: 'Managing instances with Triton CLI',
title: 'Managing machines with Triton CLI',
contentUrl: ''
}
];

View File

@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceSnapshotsComponent } from './instance-snapshots.component';
describe('InstanceSnapshotsComponent', () => {
let component: InstanceSnapshotsComponent;
let fixture: ComponentFixture<InstanceSnapshotsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstanceSnapshotsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceSnapshotsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceTagEditorComponent } from './instance-tag-editor.component';
describe('InstanceTagEditorComponent', () => {
let component: InstanceTagEditorComponent;
let fixture: ComponentFixture<InstanceTagEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstanceTagEditorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceTagEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,13 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { InstancesService } from './instances.service';
import { MachinesService } from './machines.service';
describe('InstancesService', () => {
let service: InstancesService;
describe('MachinesService', () => {
let service: MachinesService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(InstancesService);
service = TestBed.inject(MachinesService);
});
it('should be created', () => {

View File

@ -1,45 +1,45 @@
import { Injectable } from '@angular/core';
import { forkJoin, from, Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Instance } from '../models/instance';
import { Machine } from '../models/machine';
import { concatMap, delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
import { InstanceRequest } from '../models/instance';
import { MachineRequest } from '../models/machine';
import { Cacheable } from 'ts-cacheable';
import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service';
const instancesCacheBuster$ = new Subject<void>();
const machinesCacheBuster$ = new Subject<void>();
@Injectable({
providedIn: 'root'
})
export class InstancesService
export class MachinesService
{
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly httpClient: HttpClient) { }
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: instancesCacheBuster$
cacheBusterObserver: machinesCacheBuster$
})
get(): Observable<Instance[]>
get(): Observable<Machine[]>
{
return this.httpClient.get<Instance[]>(`/api/my/machines`);
return this.httpClient.get<Machine[]>(`/api/my/machines`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: instancesCacheBuster$
cacheBusterObserver: machinesCacheBuster$
})
getById(instanceId: string): Observable<Instance>
getById(machineId: string): Observable<Machine>
{
return this.httpClient.get<Instance>(`/api/my/machines/${instanceId}`);
return this.httpClient.get<Machine>(`/api/my/machines/${machineId}`);
}
// ----------------------------------------------------------------------------------------------------------------
getInstanceUntilExpectedState(instance: Instance, expectedStates: string[], callbackFn?: InstanceCallbackFunction, maxRetries = 30): Observable<Instance>
getMachineUntilExpectedState(machine: Machine, expectedStates: string[], callbackFn?: MachineCallbackFunction, maxRetries = 30): Observable<Machine>
{
// Keep polling the instance until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
// Keep polling the machine until it reaches the expected state
return this.httpClient.get<Machine>(`/api/my/machines/${machine.id}`)
.pipe(
tap(x => callbackFn && callbackFn(x)),
repeatWhen(x =>
@ -51,7 +51,7 @@ export class InstancesService
map(y =>
{
if (retries++ === maxRetries)
throw { error: `Failed to retrieve the current status for machine "${instance.name}"` };
throw { error: `Failed to retrieve the current status for machine "${machine.name}"` };
return y;
}));
@ -62,14 +62,14 @@ export class InstancesService
}
// ----------------------------------------------------------------------------------------------------------------
getInstanceUntilNicRemoved(instance: any, networkName: string, callbackFn?: InstanceCallbackFunction, maxRetries = 30): Observable<Instance>
getMachineUntilNicRemoved(machine: any, networkName: string, callbackFn?: MachineCallbackFunction, maxRetries = 30): Observable<Machine>
{
networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
// Keep polling the machine until it reaches the expected state
return this.httpClient.get<Machine>(`/api/my/machines/${machine.id}`)
.pipe(
tap(instance => callbackFn && callbackFn(instance)),
tap(machine => callbackFn && callbackFn(machine)),
repeatWhen(x =>
{
let retries = 0;
@ -79,7 +79,7 @@ export class InstancesService
map(() =>
{
if (retries++ === maxRetries)
throw { error: `Failed to retrieve the current status for machine "${instance.name}"` };
throw { error: `Failed to retrieve the current status for machine "${machine.name}"` };
})
);
}),
@ -90,144 +90,144 @@ export class InstancesService
if (callbackFn)
callbackFn(x);
return instance;
return machine;
})
);
}
// ----------------------------------------------------------------------------------------------------------------
add(instance: InstanceRequest): Observable<Instance>
add(machine: MachineRequest): Observable<Machine>
{
return this.httpClient.post<Instance>(`/api/my/machines`, instance)
return this.httpClient.post<Machine>(`/api/my/machines`, machine)
.pipe(tap(() =>
{
instancesCacheBuster$.next();
machinesCacheBuster$.next();
if (instance.volumes?.length)
if (machine.volumes?.length)
volumesCacheBuster$.next();
}));
}
// ----------------------------------------------------------------------------------------------------------------
delete(instanceId: string): Observable<any>
delete(machineId: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}`)
.pipe(tap(() => instancesCacheBuster$.next()));
return this.httpClient.delete(`/api/my/machines/${machineId}`)
.pipe(tap(() => machinesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
start(instanceId: string): Observable<Instance>
start(machineId: string): Observable<Machine>
{
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=start`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=start`, {})
.pipe(tap(() => machinesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
stop(instanceId: string): Observable<Instance>
stop(machineId: string): Observable<Machine>
{
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=stop`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=stop`, {})
.pipe(tap(() => machinesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
reboot(instanceId: string): Observable<Instance>
reboot(machineId: string): Observable<Machine>
{
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=reboot`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=reboot`, {})
.pipe(tap(() => machinesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
resize(instanceId: string, packageId: string): Observable<any>
resize(machineId: string, packageId: string): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}?action=resize&package=${packageId}`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
return this.httpClient.post(`/api/my/machines/${machineId}?action=resize&package=${packageId}`, {})
.pipe(tap(() => machinesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
rename(instanceId: string, name: string): Observable<Instance>
rename(machineId: string, name: string): Observable<Machine>
{
if (!name)
throw 'Name cannot be empty';
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=rename&name=${name}`, {})
.pipe(tap(() => instancesCacheBuster$.next()));
return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=rename&name=${name}`, {})
.pipe(tap(() => machinesCacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
toggleFirewall(instanceId: string, enable: boolean): Observable<any>
toggleFirewall(machineId: string, enable: boolean): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}?action=${enable ? 'enable' : 'disable'}_firewall`, {});
return this.httpClient.post(`/api/my/machines/${machineId}?action=${enable ? 'enable' : 'disable'}_firewall`, {});
}
// ----------------------------------------------------------------------------------------------------------------
toggleDeletionProtection(instanceId: string, enable: boolean): Observable<any>
toggleDeletionProtection(machineId: string, enable: boolean): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}?action=${enable ? 'enable' : 'disable'}_deletion_protection`, {});
return this.httpClient.post(`/api/my/machines/${machineId}?action=${enable ? 'enable' : 'disable'}_deletion_protection`, {});
}
// ----------------------------------------------------------------------------------------------------------------
getTags(instanceId: string): Observable<any>
getTags(machineId: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/tags`);
return this.httpClient.get(`/api/my/machines/${machineId}/tags`);
}
// ----------------------------------------------------------------------------------------------------------------
getTag(instanceId: string, key: string): Observable<any>
getTag(machineId: string, key: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/tags/${key}`);
return this.httpClient.get(`/api/my/machines/${machineId}/tags/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
addTags(instanceId: string, tags: any): Observable<any>
addTags(machineId: string, tags: any): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}/tags`, tags);
return this.httpClient.post(`/api/my/machines/${machineId}/tags`, tags);
}
// ----------------------------------------------------------------------------------------------------------------
replaceTags(instanceId: string, tags: any): Observable<any>
replaceTags(machineId: string, tags: any): Observable<any>
{
return this.httpClient.put(`/api/my/machines/${instanceId}/tags`, tags);
return this.httpClient.put(`/api/my/machines/${machineId}/tags`, tags);
}
// ----------------------------------------------------------------------------------------------------------------
deleteAllTags(instanceId: string): Observable<any>
deleteAllTags(machineId: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/tags`);
return this.httpClient.delete(`/api/my/machines/${machineId}/tags`);
}
// ----------------------------------------------------------------------------------------------------------------
deleteTag(instanceId: string, key: string): Observable<any>
deleteTag(machineId: string, key: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/tags/${key}`);
return this.httpClient.delete(`/api/my/machines/${machineId}/tags/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
getMetadata(instanceId: string): Observable<any>
getMetadata(machineId: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata`);
return this.httpClient.get(`/api/my/machines/${machineId}/metadata`);
}
// ----------------------------------------------------------------------------------------------------------------
getMetadataValue(instanceId: string, key: string): Observable<any>
getMetadataValue(machineId: string, key: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata/${key}`);
return this.httpClient.get(`/api/my/machines/${machineId}/metadata/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
replaceMetadata(instanceId: string, metadata: any): Observable<any>
replaceMetadata(machineId: string, metadata: any): Observable<any>
{
// First retrieve current metadata
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata`)
return this.httpClient.get(`/api/my/machines/${machineId}/metadata`)
.pipe(concatMap(existingMetadata =>
{
// Compute which metadata the user chose to remove
const obsoleteMetadata: Observable<any>[] = [];
for (const key of Object.keys(existingMetadata))
if (!metadata.hasOwnProperty(key) && key !== 'root_authorized_keys') // root_authorized_keys is readonly
obsoleteMetadata.push(this.httpClient.delete(`/api/my/machines/${instanceId}/metadata/${key}`));
obsoleteMetadata.push(this.httpClient.delete(`/api/my/machines/${machineId}/metadata/${key}`));
// Any metadata keys passed in here are created if they do not exist, and overwritten if they do.
const metadataToUpsert = this.httpClient.post(`/api/my/machines/${instanceId}/metadata`, metadata);
const metadataToUpsert = this.httpClient.post(`/api/my/machines/${machineId}/metadata`, metadata);
if (obsoleteMetadata.length)
{
@ -240,15 +240,15 @@ export class InstancesService
}
// ----------------------------------------------------------------------------------------------------------------
deleteAllMetadata(instanceId: string): Observable<any>
deleteAllMetadata(machineId: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/metadata`);
return this.httpClient.delete(`/api/my/machines/${machineId}/metadata`);
}
// ----------------------------------------------------------------------------------------------------------------
getAudit(instanceId: string): Observable<any>
getAudit(machineId: string): Observable<any>
{
return this.httpClient.get(`/api/my/machines/${instanceId}/audit`);
return this.httpClient.get(`/api/my/machines/${machineId}/audit`);
}
// ----------------------------------------------------------------------------------------------------------------
@ -268,8 +268,8 @@ export class InstancesService
// ----------------------------------------------------------------------------------------------------------------
clearCache()
{
instancesCacheBuster$.next();
machinesCacheBuster$.next();
}
}
export type InstanceCallbackFunction = ((instance: Instance) => void);
export type MachineCallbackFunction = ((machine: Machine) => void);

View File

@ -27,30 +27,30 @@ export class MigrationsService
@Cacheable({
cacheBusterObserver: cacheBuster$
})
getMigration(instanceId: string, migrationId: string): Observable<any>
getMigration(machineId: string, migrationId: string): Observable<any>
{
return this.httpClient.get(`/api/my/migrations/${migrationId}`);
}
// ----------------------------------------------------------------------------------------------------------------
migrate(instanceId: string): Observable<any>
migrate(machineId: string): Observable<any>
{
// https://apidocs.Spearhead.com/cloudapi/#Migrate
return this.httpClient.post(`/api/my/machines/${instanceId}/migrate`, { action: 'begin | sync | switch | automatic | pause | abort | watch', affinity: [] })
return this.httpClient.post(`/api/my/machines/${machineId}/migrate`, { action: 'begin | sync | switch | automatic | pause | abort | watch', affinity: [] })
.pipe(tap(() => cacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
getMigrationProgress(instanceId: string): Observable<any>
getMigrationProgress(machineId: string): Observable<any>
{
// https://apidocs.Spearhead.com/cloudapi/#Migrate
return this.httpClient.get(`/api/my/machines/${instanceId}/migrate?action=watch`);
return this.httpClient.get(`/api/my/machines/${machineId}/migrate?action=watch`);
}
// ----------------------------------------------------------------------------------------------------------------
finalizeMigration(instanceId: string): Observable<any>
finalizeMigration(machineId: string): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}/migrate?action=finalize`, {})
return this.httpClient.post(`/api/my/machines/${machineId}/migrate?action=finalize`, {})
.pipe(tap(() => cacheBuster$.next()));
}
}

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Snapshot } from '../models/snapshot';
import { Instance } from '../models/instance';
import { Machine } from '../models/machine';
import { delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
import { Cacheable } from 'ts-cacheable';
@ -20,25 +20,25 @@ export class SnapshotsService
@Cacheable({
cacheBusterObserver: cacheBuster$
})
getSnapshots(instanceId: string): Observable<Snapshot[]>
getSnapshots(machineId: string): Observable<Snapshot[]>
{
return this.httpClient.get<Snapshot[]>(`/api/my/machines/${instanceId}/snapshots`);
return this.httpClient.get<Snapshot[]>(`/api/my/machines/${machineId}/snapshots`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: cacheBuster$
})
getSnapshot(instanceId: string, snapshotName: string): Observable<Snapshot>
getSnapshot(machineId: string, snapshotName: string): Observable<Snapshot>
{
return this.httpClient.get<Snapshot>(`/api/my/machines/${instanceId}/snapshots/${encodeURIComponent(snapshotName)}`);
return this.httpClient.get<Snapshot>(`/api/my/machines/${machineId}/snapshots/${encodeURIComponent(snapshotName)}`);
}
// ----------------------------------------------------------------------------------------------------------------
getSnapshotUntilExpectedState(instance: Instance, snapshot: Snapshot, expectedStates: string[], maxRetries = 10): Observable<Snapshot>
getSnapshotUntilExpectedState(machine: Machine, snapshot: Snapshot, expectedStates: string[], maxRetries = 10): Observable<Snapshot>
{
// Keep polling the snapshot until it reaches the expected state
return this.httpClient.get<Snapshot>(`/api/my/machines/${instance.id}/snapshots/${encodeURIComponent(snapshot.name)}`)
return this.httpClient.get<Snapshot>(`/api/my/machines/${machine.id}/snapshots/${encodeURIComponent(snapshot.name)}`)
.pipe(
tap(x =>
{
@ -65,23 +65,23 @@ export class SnapshotsService
}
// ----------------------------------------------------------------------------------------------------------------
createSnapshot(instanceId: string, snapshotName: string): Observable<Snapshot>
createSnapshot(machineId: string, snapshotName: string): Observable<Snapshot>
{
return this.httpClient.post<Snapshot>(`/api/my/machines/${instanceId}/snapshots?name=${encodeURIComponent(snapshotName)}`, {})
return this.httpClient.post<Snapshot>(`/api/my/machines/${machineId}/snapshots?name=${encodeURIComponent(snapshotName)}`, {})
.pipe(tap(() => cacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
deleteSnapshot(instanceId: string, snapshotName: string): Observable<any>
deleteSnapshot(machineId: string, snapshotName: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/snapshots/${encodeURIComponent(snapshotName)}`)
return this.httpClient.delete(`/api/my/machines/${machineId}/snapshots/${encodeURIComponent(snapshotName)}`)
.pipe(tap(() => cacheBuster$.next()));
}
// ----------------------------------------------------------------------------------------------------------------
startFromSnapshot(instanceId: string, snapshotName: string): Observable<any>
startFromSnapshot(machineId: string, snapshotName: string): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}/snapshots/${encodeURIComponent(snapshotName)}`, {})
return this.httpClient.post(`/api/my/machines/${machineId}/snapshots/${encodeURIComponent(snapshotName)}`, {})
.pipe(tap(() => cacheBuster$.next()));
}
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MachineHistoryComponent } from './machine-history.component';
describe('MachineHistoryComponent', () => {
let component: MachineHistoryComponent;
let fixture: ComponentFixture<MachineHistoryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MachineHistoryComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MachineHistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -3,19 +3,19 @@ import { BsModalRef } from 'ngx-bootstrap/modal';
import { NavigationStart, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { Instance } from '../models/instance';
import { InstancesService } from '../helpers/instances.service';
import { Machine } from '../models/machine';
import { MachinesService } from '../helpers/machines.service';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-instance-history',
templateUrl: './instance-history.component.html',
styleUrls: ['./instance-history.component.scss']
selector: 'app-machine-history',
templateUrl: './machine-history.component.html',
styleUrls: ['./machine-history.component.scss']
})
export class InstanceHistoryComponent implements OnInit, OnDestroy
export class MachineHistoryComponent implements OnInit, OnDestroy
{
@Input()
instance: Instance;
machine: Machine;
loading: boolean;
history: any[];
@ -25,7 +25,7 @@ export class InstanceHistoryComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly modalRef: BsModalRef,
private readonly router: Router,
private readonly instancesService: InstancesService,
private readonly machinesService: MachinesService,
private readonly toastr: ToastrService)
{
// When the user navigates away from this route, hide the modal
@ -48,7 +48,7 @@ export class InstanceHistoryComponent implements OnInit, OnDestroy
{
this.loading = true;
this.instancesService.getAudit(this.instance.id)
this.machinesService.getAudit(this.machine.id)
.subscribe(x =>
{
this.history = x;

View File

@ -1,19 +1,19 @@
<ul class="list-group list-group-flush list-info">
<li class="dropdown-header">Machine identifier</li>
<li class="list-group-item text-uppercase ps-0">
<b>{{ instance.id }}</b>
<b>{{ machine.id }}</b>
</li>
<ng-container *ngIf="dnsCount">
<li class="dropdown-header">DNS list</li>
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
*ngFor="let keyValue of instance.dnsList | keyvalue; let index = index">
*ngFor="let keyValue of machine.dnsList | keyvalue; let index = index">
<div class="text-truncate text-info text-faded" [tooltip]="keyValue.key" container="body" placement="top" [adaptivePosition]="false">
<!--<span class="ms-1" [ngClass]="keyValue.value[0] === instance.id || keyValue.value[0] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
<!--<span class="ms-1" [ngClass]="keyValue.value[0] === machine.id || keyValue.value[0] === machine.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
{{ keyValue.value[0] }}
</span>
<span *ngIf="keyValue.value[1]" [ngClass]="keyValue.value[1] === instance.id || keyValue.value[1] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
<span *ngIf="keyValue.value[1]" [ngClass]="keyValue.value[1] === machine.id || keyValue.value[1] === machine.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
{{ keyValue.value[1] }}
</span>
@ -33,9 +33,9 @@
<li class="dropdown-header">Deletion protection</li>
<li class="list-group-item ps-0 pb-0 mt-2 ms-2">
<div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="dp{{ instance.id }}" [(ngModel)]="instance.deletion_protection"
(change)="toggleDeletionProtection($event, instance)">
<label class="form-check-label" for="dp{{ instance.id }}">Prevent this machine from being deleted</label>
<input class="form-check-input mt-0" type="checkbox" id="dp{{ machine.id }}" [(ngModel)]="machine.deletion_protection"
(change)="toggleDeletionProtection($event, machine)">
<label class="form-check-label" for="dp{{ machine.id }}">Prevent this machine from being deleted</label>
</div>
</li>
</ul>

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceInfoComponent } from './instance-info.component';
import { MachineInfoComponent } from './machine-info.component';
describe('InstanceInfoComponent', () => {
let component: InstanceInfoComponent;
let fixture: ComponentFixture<InstanceInfoComponent>;
describe('MachineInfoComponent', () => {
let component: MachineInfoComponent;
let fixture: ComponentFixture<MachineInfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstanceInfoComponent ]
declarations: [ MachineInfoComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceInfoComponent);
fixture = TestBed.createComponent(MachineInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -3,19 +3,19 @@ import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service';
import { empty, forkJoin, Observable, of, Subject, ReplaySubject } from 'rxjs';
import { delay, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstancesService } from '../helpers/instances.service';
import { MachinesService } from '../helpers/machines.service';
import { BsModalService } from 'ngx-bootstrap/modal';
import { Instance } from '../models/instance';
import { Machine } from '../models/machine';
@Component({
selector: 'app-instance-info',
templateUrl: './instance-info.component.html',
styleUrls: ['./instance-info.component.scss']
selector: 'app-machine-info',
templateUrl: './machine-info.component.html',
styleUrls: ['./machine-info.component.scss']
})
export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
export class MachineInfoComponent implements OnInit, OnDestroy, OnChanges
{
@Input()
instance: Instance;
machine: Machine;
@Input()
loadInfo: boolean;
@ -40,7 +40,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
private onChanges$ = new ReplaySubject();
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService,
constructor(private readonly machinesService: MachinesService,
private readonly catalogService: CatalogService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService)
@ -48,19 +48,19 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
}
// ----------------------------------------------------------------------------------------------------------------
toggleDeletionProtection(event, instance: Instance)
toggleDeletionProtection(event, machine: Machine)
{
this.processing.emit();
this.instancesService.toggleDeletionProtection(instance.id, event.target.checked)
this.machinesService.toggleDeletionProtection(machine.id, event.target.checked)
.subscribe(() =>
{
this.toastr.info(`The deletion protection for machine "${instance.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
this.toastr.info(`The deletion protection for machine "${machine.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
this.finishedProcessing.emit();
},
err =>
{
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
this.finishedProcessing.emit();
});
}
@ -74,14 +74,14 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
// ----------------------------------------------------------------------------------------------------------------
private getInfo()
{
if (this.finishedLoading || this.instance.state === 'provisioning') return;
if (this.finishedLoading || this.machine.state === 'provisioning') return;
this.loading = true;
if (this.refresh)
this.instancesService.clearCache();
this.machinesService.clearCache();
this.instancesService.getById(this.instance.id)
this.machinesService.getById(this.machine.id)
.subscribe(x =>
{
const dnsList = {};
@ -90,7 +90,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
this.dnsCount = Object.keys(dnsList).length;
this.instance.dnsList = dnsList;
this.machine.dnsList = dnsList;
this.loading = false;
this.finishedLoading = true;
@ -99,7 +99,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Couldn't load details for machine "${this.instance.name}" ${errorDetails}`);
this.toastr.error(`Couldn't load details for machine "${this.machine.name}" ${errorDetails}`);
this.loading = false;
});
}
@ -127,7 +127,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
this.loadInfo = true;
}
if (!this.finishedLoading && this.loadInfo && !this.instance?.infoLoaded || this.refresh)
if (!this.finishedLoading && this.loadInfo && !this.machine?.infoLoaded || this.refresh)
this.getInfo();
});
}

View File

@ -22,9 +22,9 @@
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input mt-0" type="checkbox" id="fw{{ instance.id }}" [(ngModel)]="instance.firewall_enabled"
(change)="toggleCloudFirewall($event, instance)">
<label class="form-check-label" for="fw{{ instance.id }}">Toggle cloud firewall</label>
<input class="form-check-input mt-0" type="checkbox" id="fw{{ machine.id }}" [(ngModel)]="machine.firewall_enabled"
(change)="toggleCloudFirewall($event, machine)">
<label class="form-check-label" for="fw{{ machine.id }}">Toggle cloud firewall</label>
</div>
</li>
@ -59,7 +59,7 @@
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body"
*ngIf="!nic.state || nic.state === 'running' || nic.state === 'stopped'">
<button class="btn btn-link text-info" dropdownToggle [isDisabled]="instance.working"
<button class="btn btn-link text-info" dropdownToggle [isDisabled]="machine.working"
tooltip="More options" container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
</button>

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceHistoryComponent } from './instance-history.component';
import { MachineNetworksComponent } from './machine-networks.component';
describe('InstanceHistoryComponent', () => {
let component: InstanceHistoryComponent;
let fixture: ComponentFixture<InstanceHistoryComponent>;
describe('MachineNetworksComponent', () => {
let component: MachineNetworksComponent;
let fixture: ComponentFixture<MachineNetworksComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstanceHistoryComponent ]
declarations: [ MachineNetworksComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceHistoryComponent);
fixture = TestBed.createComponent(MachineNetworksComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -3,22 +3,22 @@ import { ToastrService } from 'ngx-toastr';
import { empty, forkJoin, Observable, of, Subject, ReplaySubject } from 'rxjs';
import { delay, filter, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { NetworkingService } from '../../networking/helpers/networking.service';
import { InstancesService } from '../helpers/instances.service';
import { MachinesService } from '../helpers/machines.service';
import { ConfirmationDialogComponent } from '../../components/confirmation-dialog/confirmation-dialog.component';
import { BsModalService } from 'ngx-bootstrap/modal';
import { Nic } from '../models/nic';
import { Network } from '../../networking/models/network';
import { Instance } from '../models/instance';
import { Machine } from '../models/machine';
@Component({
selector: 'app-instance-networks',
templateUrl: './instance-networks.component.html',
styleUrls: ['./instance-networks.component.scss']
selector: 'app-machine-networks',
templateUrl: './machine-networks.component.html',
styleUrls: ['./machine-networks.component.scss']
})
export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
export class MachineNetworksComponent implements OnInit, OnDestroy, OnChanges
{
@Input()
instance: Instance;
machine: Machine;
@Input()
loadNetworks: boolean;
@ -33,10 +33,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
load = new EventEmitter();
@Output()
instanceReboot = new EventEmitter();
machineReboot = new EventEmitter();
@Output()
instanceStateUpdate = new EventEmitter();
machineStateUpdate = new EventEmitter();
loading: boolean;
nics: Nic[] = [];
@ -50,7 +50,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly networkingService: NetworkingService,
private readonly instancesService: InstancesService,
private readonly machinesService: MachinesService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService)
{
@ -67,14 +67,14 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to load the list of available networks for machine "${this.instance.name}" ${errorDetails}`);
this.toastr.error(`Failed to load the list of available networks for machine "${this.machine.name}" ${errorDetails}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
private getNetworks(force = false)
{
if ((this.finishedLoading || this.instance.state === 'provisioning') && !force) return;
if ((this.finishedLoading || this.machine.state === 'provisioning') && !force) return;
const observables = this.nics.map(x => this.networkingService.getNetwork(x.network));
@ -124,19 +124,19 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
{
this.processing.emit();
this.toastr.info(`Connecting machine "${this.instance.name}" to the "${network.name}" network...`);
this.toastr.info(`Connecting machine "${this.machine.name}" to the "${network.name}" network...`);
}),
switchMap(() =>
{
return this.networkingService.addNic(this.instance.id, network.id)
return this.networkingService.addNic(this.machine.id, network.id)
.pipe(
tap(x =>
{
// Add the newly created NIC to the list, in its "provisioning" state
this.nics.unshift(x);
if (this.instance.state === 'running')
this.instanceReboot.emit();
if (this.machine.state === 'running')
this.machineReboot.emit();
}),
switchMap(x =>
{
@ -150,7 +150,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
{
// Keep polling the newly created NIC until it reaches its "running"/"stopped" state
return this.networkingService
.getNicUntilAvailable(this.instance, response.nic, network.name, n => this.nics[0].state = n.state)
.getNicUntilAvailable(this.machine, response.nic, network.name, n => this.nics[0].state = n.state)
.pipe(
takeUntil(this.destroy$),
map(y => ({ network: response.network, nic: y }))
@ -171,7 +171,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
this.load.emit(this.nics);
this.toastr.info(`The machine "${this.instance.name}" has been connected to the "${network.name}" network`);
this.toastr.info(`The machine "${this.machine.name}" has been connected to the "${network.name}" network`);
this.finishedProcessing.emit();
},
err =>
@ -181,7 +181,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
this.nics.shift();
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to connect machine "${this.instance.name}" to the "${network.name}" network ${errorDetails}`);
this.toastr.error(`Failed to connect machine "${this.machine.name}" to the "${network.name}" network ${errorDetails}`);
this.finishedProcessing.emit();
});
}
@ -209,31 +209,31 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
{
this.processing.emit();
this.toastr.info(`Removing network interface "${nic.mac.toUpperCase()}" from machine "${this.instance.name}"...`);
this.toastr.info(`Removing network interface "${nic.mac.toUpperCase()}" from machine "${this.machine.name}"...`);
}),
//filter(() => this.instance.state === 'running' || this.instance.state === 'stopped'),
//filter(() => this.machine.state === 'running' || this.machine.state === 'stopped'),
switchMap(() =>
{
return this.networkingService.deleteNic(this.instance.id, nic.mac)
return this.networkingService.deleteNic(this.machine.id, nic.mac)
.pipe(
takeUntil(this.destroy$),
tap(() =>
{
if (this.instance.state === 'running')
this.instanceReboot.emit();
if (this.machine.state === 'running')
this.machineReboot.emit();
}),
switchMap(() =>
{
// If the machine is currently running, keep polling until it finishes restarting
return this.instance.state === 'running'
? this.instancesService
.getInstanceUntilNicRemoved(this.instance, nic.networkName, x => this.instanceStateUpdate.emit(x))
return this.machine.state === 'running'
? this.machinesService
.getMachineUntilNicRemoved(this.machine, nic.networkName, x => this.machineStateUpdate.emit(x))
.pipe(delay(1000), takeUntil(this.destroy$))
: of(nic);
})
);
}),
switchMap(() => this.networkingService.getNics(this.instance.id))
switchMap(() => this.networkingService.getNics(this.machine.id))
).subscribe(nics =>
{
const index = this.nics.findIndex(x => x.network === nic.network);
@ -253,10 +253,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
this.load.emit(this.nics);
this.toastr.info(`The network interface has been removed from machine "${this.instance.name}"`);
this.toastr.info(`The network interface has been removed from machine "${this.machine.name}"`);
}, err =>
{
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
this.toastr.error(`Machine "${this.machine.name}" error: ${err.error.message}`);
this.finishedProcessing.emit();
});
}
@ -268,34 +268,34 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
}
// ----------------------------------------------------------------------------------------------------------------
toggleCloudFirewall(event, instance: Instance)
toggleCloudFirewall(event, machine: Machine)
{
instance.working = true;
machine.working = true;
this.instancesService.toggleFirewall(instance.id, event.target.checked)
this.machinesService.toggleFirewall(machine.id, event.target.checked)
.subscribe(() =>
{
this.toastr.info(`The cloud firewall for machine "${instance.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
instance.working = false;
this.toastr.info(`The cloud firewall for machine "${machine.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
machine.working = false;
},
err =>
{
instance.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
machine.working = false;
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
if (!this.instance.nics?.length || this.instance.networksLoaded)
if (!this.machine.nics?.length || this.machine.networksLoaded)
this.finishedLoading = true;
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{
if (!this.finishedLoading && this.loadNetworks && !this.instance?.networksLoaded)
if (!this.finishedLoading && this.loadNetworks && !this.machine?.networksLoaded)
{
this.nics = this.instance?.nics || [];
this.nics = this.machine?.nics || [];
this.getNetworks();
}

View File

@ -7,9 +7,9 @@
<li class="list-group-item ps-0 pe-0" *ngFor="let role of filteredRoles">
<div class="form-check form-switch align-items-center d-flex">
<input class="form-check-input pe-4" type="checkbox" id="{{ instance.id }}-role-{{ role.id }}" [(ngModel)]="role.selected"
(change)="setInstanceRole($event, role)">
<label class="form-check-label ms-2 text-truncate" for="{{ instance.id }}-role-{{ role.id }}">
<input class="form-check-input pe-4" type="checkbox" id="{{ machine.id }}-role-{{ role.id }}" [(ngModel)]="role.selected"
(change)="setMachineRole($event, role)">
<label class="form-check-label ms-2 text-truncate" for="{{ machine.id }}-role-{{ role.id }}">
<b>{{ role.name }}</b>
<span class="small ps-1" *ngFor="let policy of role.policies">{{ policy.name }}</span>
</label>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MachineSecurityComponent } from './machine-security.component';
describe('MachineSecurityComponent', () => {
let component: MachineSecurityComponent;
let fixture: ComponentFixture<MachineSecurityComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MachineSecurityComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MachineSecurityComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,26 +1,26 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service';
import { InstancesService } from '../helpers/instances.service';
import { MachinesService } from '../helpers/machines.service';
import { Subject } from 'rxjs';
import { delay, switchMap, takeUntil, tap } from 'rxjs/operators';
import Fuse from 'fuse.js';
import { SecurityService } from '../../security/helpers/security.service';
@Component({
selector: 'app-instance-security',
templateUrl: './instance-security.component.html',
styleUrls: ['./instance-security.component.scss']
selector: 'app-machine-security',
templateUrl: './machine-security.component.html',
styleUrls: ['./machine-security.component.scss']
})
export class InstanceSecurityComponent implements OnInit, OnDestroy
export class MachineSecurityComponent implements OnInit, OnDestroy
{
@Input()
instance: any;
machine: any;
@Input()
set loadRoles(value: boolean)
{
if (value && this.instance && !this.roles)
if (value && this.machine && !this.roles)
this.getRoles();
}
@ -35,7 +35,7 @@ export class InstanceSecurityComponent implements OnInit, OnDestroy
private readonly fuseJsOptions: {};
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService,
constructor(private readonly machinesService: MachinesService,
private readonly securityService: SecurityService,
private readonly toastr: ToastrService)
{
@ -83,7 +83,7 @@ export class InstanceSecurityComponent implements OnInit, OnDestroy
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceRole(event, role)
setMachineRole(event, role)
{
}
@ -113,7 +113,7 @@ export class InstanceSecurityComponent implements OnInit, OnDestroy
ngOnInit(): void
{
// TODO: Find a way to retrieve the list of RoleTags
//this.instancesService.getRoleTags(this.instance.id)
//this.machinesService.getRoleTags(this.machine.id)
// .subscribe();
}

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceSecurityComponent } from './instance-security.component';
import { MachineSnapshotsComponent } from './machine-snapshots.component';
describe('InstanceSecurityComponent', () => {
let component: InstanceSecurityComponent;
let fixture: ComponentFixture<InstanceSecurityComponent>;
describe('MachineSnapshotsComponent', () => {
let component: MachineSnapshotsComponent;
let fixture: ComponentFixture<MachineSnapshotsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstanceSecurityComponent ]
declarations: [ MachineSnapshotsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceSecurityComponent);
fixture = TestBed.createComponent(MachineSnapshotsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,6 +1,6 @@
import { Component, OnInit, OnDestroy, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { InstancesService } from '../helpers/instances.service';
import { MachinesService } from '../helpers/machines.service';
import { ReplaySubject, Subject } from 'rxjs';
import { delay, first, switchMap, takeUntil, tap } from 'rxjs/operators';
import Fuse from 'fuse.js';
@ -10,14 +10,14 @@ import { Snapshot } from '../models/snapshot';
import { SnapshotsService } from '../helpers/snapshots.service';
@Component({
selector: 'app-instance-snapshots',
templateUrl: './instance-snapshots.component.html',
styleUrls: ['./instance-snapshots.component.scss']
selector: 'app-machine-snapshots',
templateUrl: './machine-snapshots.component.html',
styleUrls: ['./machine-snapshots.component.scss']
})
export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
export class MachineSnapshotsComponent implements OnInit, OnDestroy, OnChanges
{
@Input()
instance: any;
machine: any;
@Input()
loadSnapshots: boolean;
@ -32,7 +32,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
load = new EventEmitter();
@Output()
instanceStateUpdate = new EventEmitter();
machineStateUpdate = new EventEmitter();
loadingSnapshots: boolean;
snapshotsLoaded: boolean;
@ -48,7 +48,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
private readonly fuseJsOptions: {};
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService,
constructor(private readonly machinesService: MachinesService,
private readonly snapshotsService: SnapshotsService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService)
@ -79,12 +79,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
// Clear this field
this.snapshotName = null;
this.snapshotsService.createSnapshot(this.instance.id, snapshotName)
this.snapshotsService.createSnapshot(this.machine.id, snapshotName)
.pipe(
takeUntil(this.destroy$),
delay(1000),
tap(x => this.snapshots.unshift(x)),
switchMap((x: Snapshot) => this.snapshotsService.getSnapshotUntilExpectedState(this.instance, x, ['created'])
switchMap((x: Snapshot) => this.snapshotsService.getSnapshotUntilExpectedState(this.machine, x, ['created'])
.pipe(takeUntil(this.destroy$))
)
)
@ -96,7 +96,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
this.snapshots[index] = x;
this.finishedProcessing.emit();
this.toastr.info(`A new snapshot "${snapshotName}" has been created for machine "${this.instance.name}"`);
this.toastr.info(`A new snapshot "${snapshotName}" has been created for machine "${this.machine.name}"`);
},
err =>
{
@ -107,7 +107,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
this.snapshots.splice(index, 1);
this.finishedProcessing.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
this.toastr.error(`Machine "${this.machine.name}" error: ${err.error.message}`);
});
}
@ -121,14 +121,14 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
snapshot.working = true;
// First we need to make sure the instance is stopped
if (this.instance.state !== 'stopped')
this.instancesService.stop(this.instance.id)
// First we need to make sure the machine is stopped
if (this.machine.state !== 'stopped')
this.machinesService.stop(this.machine.id)
.pipe(
takeUntil(this.destroy$),
tap(() => this.toastr.info(`Restarting machine "${this.instance.name}"`)),
tap(() => this.toastr.info(`Restarting machine "${this.machine.name}"`)),
delay(1000),
switchMap(() => this.instancesService.getInstanceUntilExpectedState(this.instance, ['stopped'], x => this.instanceStateUpdate.emit(x))
switchMap(() => this.machinesService.getMachineUntilExpectedState(this.machine, ['stopped'], x => this.machineStateUpdate.emit(x))
.pipe(takeUntil(this.destroy$))
)
).subscribe(() =>
@ -141,7 +141,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
snapshot.working = false;
this.finishedProcessing.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
this.toastr.error(`Machine "${this.machine.name}" error: ${err.error.message}`);
});
else
this.startMachineFromSnapshot(snapshot);
@ -172,13 +172,13 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
{
this.processing.emit();
this.toastr.info(`Restoring machine "${this.instance.name}" from "${snapshot.name}" snapshot`);
this.toastr.info(`Restoring machine "${this.machine.name}" from "${snapshot.name}" snapshot`);
this.snapshotsService.startFromSnapshot(this.instance.id, snapshot.name)
this.snapshotsService.startFromSnapshot(this.machine.id, snapshot.name)
.pipe(
takeUntil(this.destroy$),
delay(1000),
switchMap(() => this.instancesService.getInstanceUntilExpectedState(this.instance, ['running'], x => this.instanceStateUpdate.emit(x), 20)
switchMap(() => this.machinesService.getMachineUntilExpectedState(this.machine, ['running'], x => this.machineStateUpdate.emit(x), 20)
.pipe(takeUntil(this.destroy$))
)
)
@ -188,14 +188,14 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
this.finishedProcessing.emit();
this.toastr.info(`The machine "${this.instance.name}" has been started from the "${snapshot.name}" snapshot`);
this.toastr.info(`The machine "${this.machine.name}" has been started from the "${snapshot.name}" snapshot`);
}, err =>
{
snapshot.working = false;
this.finishedProcessing.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
this.toastr.error(`Machine "${this.machine.name}" error: ${err.error.message}`);
});
}
@ -220,7 +220,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
{
this.processing.emit();
this.snapshotsService.deleteSnapshot(this.instance.id, snapshot.name)
this.snapshotsService.deleteSnapshot(this.machine.id, snapshot.name)
.subscribe(() =>
{
const index = this.snapshots.findIndex(s => s.name === snapshot.name);
@ -242,12 +242,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
// ----------------------------------------------------------------------------------------------------------------
private getSnapshots()
{
if (this.snapshotsLoaded || this.instance.state === 'provisioning') return
if (this.snapshotsLoaded || this.machine.state === 'provisioning') return
this.loadingSnapshots = true;
// Get the list of snapshots
this.snapshotsService.getSnapshots(this.instance.id)
this.snapshotsService.getSnapshots(this.machine.id)
.subscribe(x =>
{
this.snapshots = x;
@ -297,12 +297,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
this.snapshots = this.instance?.snapshots;
this.snapshots = this.machine?.snapshots;
this.filteredSnapshots = this.snapshots;
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{
if (!this.finishedLoading && this.loadSnapshots && !this.instance?.snapshotsLoaded)
if (!this.finishedLoading && this.loadSnapshots && !this.machine?.snapshotsLoaded)
this.getSnapshots();
});
}

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceNetworksComponent } from './instance-networks.component';
import { MachineTagEditorComponent } from './machine-tag-editor.component';
describe('InstanceNetworksComponent', () => {
let component: InstanceNetworksComponent;
let fixture: ComponentFixture<InstanceNetworksComponent>;
describe('MachineTagEditorComponent', () => {
let component: MachineTagEditorComponent;
let fixture: ComponentFixture<MachineTagEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstanceNetworksComponent ]
declarations: [ MachineTagEditorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceNetworksComponent);
fixture = TestBed.createComponent(MachineTagEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -4,19 +4,19 @@ import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '
import { NavigationStart, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { InstancesService } from '../helpers/instances.service';
import { MachinesService } from '../helpers/machines.service';
import { ToastrService } from 'ngx-toastr';
import { Instance } from '../models/instance';
import { Machine } from '../models/machine';
@Component({
selector: 'app-instance-tag-editor',
templateUrl: './instance-tag-editor.component.html',
styleUrls: ['./instance-tag-editor.component.scss']
selector: 'app-machine-tag-editor',
templateUrl: './machine-tag-editor.component.html',
styleUrls: ['./machine-tag-editor.component.scss']
})
export class InstanceTagEditorComponent implements OnInit
export class MachineTagEditorComponent implements OnInit
{
@Input()
instance: Instance;
machine: Machine;
@Input()
showMetadata: boolean;
@ -30,7 +30,7 @@ export class InstanceTagEditorComponent implements OnInit
private destroy$ = new Subject();
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService,
constructor(private readonly machinesService: MachinesService,
private readonly modalRef: BsModalRef,
private readonly router: Router,
private readonly fb: FormBuilder,
@ -49,13 +49,13 @@ export class InstanceTagEditorComponent implements OnInit
private createForm()
{
const items = this.fb.array(this.showMetadata
? Object.keys(this.instance.metadata).map(key => this.fb.group({
? Object.keys(this.machine.metadata).map(key => this.fb.group({
key: [key, Validators.required],
value: [this.instance.metadata[key], Validators.required]
value: [this.machine.metadata[key], Validators.required]
}))
: Object.keys(this.instance.tags).map(key => this.fb.group({
: Object.keys(this.machine.tags).map(key => this.fb.group({
key: [key, Validators.required],
value: [this.instance.tags[key], Validators.required]
value: [this.machine.tags[key], Validators.required]
}))
);
@ -113,8 +113,8 @@ export class InstanceTagEditorComponent implements OnInit
}, {});
const observable = this.showMetadata
? this.instancesService.replaceMetadata(this.instance.id, items)
: this.instancesService.replaceTags(this.instance.id, items);
? this.machinesService.replaceMetadata(this.machine.id, items)
: this.machinesService.replaceTags(this.machine.id, items);
observable.subscribe(response =>
{
@ -131,7 +131,7 @@ export class InstanceTagEditorComponent implements OnInit
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
//this.instancesService.getTags(this.instance.id).subscribe();
//this.machinesService.getTags(this.machine.id).subscribe();
this.createForm();
}

View File

@ -192,7 +192,7 @@
</div>
<!-- Affinity settings -->
<div class="mt-3 d-flex flex-column" *ngIf="instances && instances.length">
<div class="mt-3 d-flex flex-column" *ngIf="machines && machines.length">
<button class="btn text-start w-100 mb-2" [class.btn-outline-info]="!showAffinity" [class.btn-info]="showAffinity"
(click)="showAffinity = !showAffinity">
Affinity rules
@ -200,33 +200,36 @@
</button>
<div [collapse]="!showAffinity">
<div class="row">
<div class="col-sm-4">
<select class="form-select" name="operator">
<option></option>
<option value="==">Must be close to instances</option>
<option value="==~">Should be close to instances</option>
<option value="!=">Must be far from instances</option>
<option value="!=~">Should be far from instances</option>
</select>
</div>
<div class="col-sm-3">
<select class="form-select" name="target">
<option></option>
<option value="instance">Named like</option>
<option value="tagName">Tagged with</option>
</select>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" placeholder="Value">
</div>
<div class="col-sm-1">
<button class="btn btn-outline-info">
<fa-icon icon="plus"></fa-icon>
<div class="select-list list-group select-list p-0 mb-2 py-2" tabindex="0">
<div class="list-group-item list-group-item-action" *ngFor="let affinityRule of editorForm.get('affinityRules')['controls']; let index = index">
<div class="d-flex">
<span class="flex-grow-1 text-truncate">
<span class="me-1">
{{ (affinityRule.value.description.strict ? 'affinityRuleEditor.strict' : 'affinityRuleEditor.optional') | translate }}
</span>
<span class="me-1 text-lowercase" [ngClass]="affinityRule.value.description.closeTo ? 'text-success' : 'text-danger'">
{{ (affinityRule.value.description.closeTo ? 'affinityRuleEditor.closeTo' : 'affinityRuleEditor.farFrom') | translate }}
</span>
<span class="me-1 text-lowercase">
{{ (affinityRule.value.description.targetMachine ? 'affinityRuleEditor.namedLike' : 'affinityRuleEditor.taggedWith') | translate }}
</span>
<span class="me-1 text-warning" *ngIf="affinityRule.value.description.tagName">
{{ affinityRule.value.description.tagName }}={{ affinityRule.value.description.value }}
</span>
<span class="me-1 text-warning" *ngIf="!affinityRule.value.description.tagName">
{{ affinityRule.value.description.value }}
</span>
</span>
<button class="btn btn-sm text-danger p-0" (click)="removeAffinityRule(index)"
tooltip="Remove this affinity rule" container="body" placement="top" [adaptivePosition]="false">
<fa-icon [fixedWidth]="true" icon="times"></fa-icon>
</button>
</div>
</div>
</div>
<app-affinity-rule-editor (saved)="addAffinityRule($event)"></app-affinity-rule-editor>
</div>
</div>
</div>

View File

@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceWizardComponent } from './instance-wizard.component';
import { MachineWizardComponent } from './machine-wizard.component';
describe('InstanceWizardComponent', () => {
let component: InstanceWizardComponent;
let fixture: ComponentFixture<InstanceWizardComponent>;
describe('MachineWizardComponent', () => {
let component: MachineWizardComponent;
let fixture: ComponentFixture<MachineWizardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ InstanceWizardComponent ]
declarations: [ MachineWizardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(InstanceWizardComponent);
fixture = TestBed.createComponent(MachineWizardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -2,11 +2,11 @@ import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angu
import { BsModalRef } from 'ngx-bootstrap/modal';
import { combineLatest, forkJoin, Subject } from 'rxjs';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray, ValidatorFn, ValidationErrors } from '@angular/forms';
import { Instance } from '../models/instance';
import { Machine } from '../models/machine';
import { filter, takeUntil, startWith, distinctUntilChanged } from 'rxjs/operators';
import { NavigationStart, Router } from '@angular/router';
import { FileSizePipe } from '../../pipes/file-size.pipe';
import { InstancesService } from '../helpers/instances.service';
import { MachinesService } from '../helpers/machines.service';
import { CatalogService } from '../../catalog/helpers/catalog.service';
import { NetworkingService } from '../../networking/helpers/networking.service';
import { ToastrService } from 'ngx-toastr';
@ -16,11 +16,11 @@ import { TranslateService } from '@ngx-translate/core';
import { CatalogImageType } from '../../catalog/models/image';
@Component({
selector: 'app-instance-wizard',
templateUrl: './instance-wizard.component.html',
styleUrls: ['./instance-wizard.component.scss']
selector: 'app-machine-wizard',
templateUrl: './machine-wizard.component.html',
styleUrls: ['./machine-wizard.component.scss']
})
export class InstanceWizardComponent implements OnInit, OnDestroy
export class MachineWizardComponent implements OnInit, OnDestroy
{
@Input()
add: boolean;
@ -29,7 +29,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
imageType = 1;
@Input()
instance: Instance;
machine: Machine;
private images: any[];
imageList: any[];
@ -39,12 +39,12 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
packageList: any[];
packageGroups: any[];
instances: Instance[];
machines: Machine[];
dataCenters: any[];
loadingIndicator: boolean;
loadingPackages: boolean;
save = new Subject<Instance>();
save = new Subject<Machine>();
working: boolean;
editorForm: FormGroup;
currentStep = 1;
@ -63,7 +63,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
private readonly fb: FormBuilder,
private readonly fileSizePipe: FileSizePipe,
private readonly authService: AuthService,
private readonly instancesService: InstancesService,
private readonly machinesService: MachinesService,
private readonly catalogService: CatalogService,
private readonly networkingService: NetworkingService,
private readonly volumesService: VolumesService,
@ -109,15 +109,15 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
private createForm()
{
const tags = this.fb.array(this.instance
? Object.keys(this.instance.tags)
.map(key => this.fb.group({ key, value: this.instance.tags[key] }))
const tags = this.fb.array(this.machine
? Object.keys(this.machine.tags)
.map(key => this.fb.group({ key, value: this.machine.tags[key] }))
: []);
const metadata = this.fb.array(this.instance
? Object.keys(this.instance.metadata)
const metadata = this.fb.array(this.machine
? Object.keys(this.machine.metadata)
.filter(key => key !== 'root_authorized_keys') // This shouldn't be cloned
.map(key => this.fb.group({ key, value: this.instance.metadata[key] }))
.map(key => this.fb.group({ key, value: this.machine.metadata[key] }))
: []);
this.editorForm = this.fb.group(
@ -129,14 +129,9 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
name: [null, Validators.required],
networks: this.fb.array([], { validators: this.atLeastOneSelectionValidator.bind(this) }),
firewallRules: this.fb.array([]),
cloudFirewall: [this.instance?.firewall_enabled],
cloudFirewall: [this.machine?.firewall_enabled],
volumes: this.fb.array([]),
affinity: this.fb.group(
{
strict: [{ value: false, disabled: true }],
closeTo: [],
farFrom: []
}),
affinityRules: this.fb.array([]),
dataCenter: [],
tags,
metadata,
@ -281,14 +276,6 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
this.computeEstimatedCost();
});
this.editorForm.get(['affinity', 'farFrom']).valueChanges.pipe(startWith(null))
.pipe(takeUntil(this.destroy$))
.subscribe(this.setAffinity.bind(this));
this.editorForm.get(['affinity', 'closeTo']).valueChanges.pipe(startWith(null))
.pipe(takeUntil(this.destroy$))
.subscribe(this.setAffinity.bind(this));
this.editorForm.get('estimatedMinutesRan').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(this.computeEstimatedCost.bind(this));
@ -325,15 +312,6 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
return null;
}
// ----------------------------------------------------------------------------------------------------------------
private setAffinity(affinity)
{
if (affinity)
this.editorForm.get(['affinity', 'strict']).enable();
else
this.editorForm.get(['affinity', 'strict']).disable();
}
// ----------------------------------------------------------------------------------------------------------------
close()
{
@ -347,22 +325,22 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
const changes = this.editorForm.getRawValue();
const instance: any = {};
instance.name = changes.name;
instance.image = changes.image.id;
instance.package = changes.package.id;
//instance.brand = changes.package.brand;
instance.networks = changes.networks.filter(x => x.selected).map(x => x.id);
instance.firewall_enabled = !!changes.cloudFirewall;
const machine: any = {};
machine.name = changes.name;
machine.image = changes.image.id;
machine.package = changes.package.id;
//machine.brand = changes.package.brand;
machine.networks = changes.networks.filter(x => x.selected).map(x => x.id);
machine.firewall_enabled = !!changes.cloudFirewall;
for (const tag of changes.tags)
instance[`tag.${tag.key}`] = tag.value;
machine[`tag.${tag.key}`] = tag.value;
for (const metadata of changes.metadata)
instance[`metadata.${metadata.key}`] = metadata.value;
machine[`metadata.${metadata.key}`] = metadata.value;
if (!this.kvmRequired)
instance.volumes = changes.volumes
machine.volumes = changes.volumes
.filter(x => x.mount)
.map(volume =>
({
@ -372,9 +350,9 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
mountpoint: volume.mountpoint
}));
console.log(this.editorForm.get('affinity'));
machine.affinity = changes.affinityRules.map(x => x.rule);
this.instancesService.add(instance)
this.machinesService.add(machine)
.subscribe(x =>
{
this.working = false;
@ -406,10 +384,10 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
if (this.currentStep < this.steps.length) return;
this.readyText = this.translateService.instant('dashboard.wizard.ready', {
this.readyText = this.translateService.instant('machines.wizard.ready', {
imageType: this.editorForm.get('imageType').value == 1
? this.translateService.instant('dashboard.wizard.readyImageTypeContainer')
: this.translateService.instant('dashboard.wizard.readyImageTypeVm'),
? this.translateService.instant('machines.wizard.readyImageTypeContainer')
: this.translateService.instant('machines.wizard.readyImageTypeVm'),
packageDescription: this.editorForm.get('package').value.description ||
`<b>${this.editorForm.get('package').value.vcpus || 1}</b> vCPUs, ` +
`<b>${this.fileSizePipe.transform(this.editorForm.get('package').value.memory * 1024 * 1024)}</b> RAM, ` +
@ -427,7 +405,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
this.editorForm.get('package').setValue(selection);
if (this.instance)
if (this.machine)
this.nextStep();
}
@ -469,6 +447,25 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
array.removeAt(index);
}
// ----------------------------------------------------------------------------------------------------------------
addAffinityRule(affinityRule)
{
const array = this.editorForm.get('affinityRules') as FormArray;
array.push(this.fb.group({
description: [affinityRule],
rule: [affinityRule.rule]
}));
}
// ----------------------------------------------------------------------------------------------------------------
removeAffinityRule(index)
{
const array = this.editorForm.get('affinityRules') as FormArray;
array.removeAt(index);
}
// ----------------------------------------------------------------------------------------------------------------
private getImages()
{
@ -482,9 +479,9 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
// Set the default image type (this will trigger a series of events)
this.editorForm.get('imageType').setValue(this.imageType);
if (this.instance)
if (this.machine)
{
const image = this.images.find(x => x.id === this.instance.image);
const image = this.images.find(x => x.id === this.machine.image);
this.editorForm.get('image').setValue(image);
}
@ -528,18 +525,18 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
}
// ----------------------------------------------------------------------------------------------------------------
private getInstancesAndDataCenters()
private getMachinesAndDataCenters()
{
if (this.instances || this.dataCenters)
if (this.machines || this.dataCenters)
return;
forkJoin(
this.instancesService.get(),
forkJoin([
this.machinesService.get(),
this.catalogService.getDataCenters()
)
])
.subscribe(response =>
{
this.instances = response[0];
this.machines = response[0];
this.dataCenters = Object.keys(response[1]);
@ -596,18 +593,18 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
this.getNetworksAndFirewallRules();
this.getInstancesAndDataCenters();
this.getMachinesAndDataCenters();
this.getVolumes();
if (this.instance)
if (this.machine)
{
if (this.instance.type === 'smartmachine')
if (this.machine.type === 'smartmachine')
this.imageType = 1;
else if (this.instance.type === 'virtualmachine')
else if (this.machine.type === 'virtualmachine')
this.imageType = 2;
this.preselectedPackage = this.instance.package;
this.preselectedPackage = this.machine.package;
this.nextStep();
}

View File

@ -9,7 +9,7 @@
<span class="d-none d-lg-block flex-grow-1"></span>
<ng-container *ngIf="instances && instances.length">
<ng-container *ngIf="machines && machines.length">
<div class="input-group input-group-pill me-lg-3 mb-3 mb-lg-0 w-lg-auto w-100">
<input type="text" class="form-control" placeholder="Search..." formControlName="searchTerm"
appAlphaOnly="^[A-Za-z0-9_-]+$" tooltip="Search by name, tag, metadata, operating system or brand"
@ -24,16 +24,16 @@
<div class="btn-group me-lg-3 mb-3 mb-lg-0 w-lg-auto w-100">
<button class="btn btn-outline-info dropdown-toggle" [disabled]="loadingIndicator" [popover]="filtersTemplate"
[outsideClick]="true" container="body" placement="bottom right" containerClass="menu-popover">
Showing {{ listItems.length }} / {{ instances.length }}
<ng-container *ngIf="runningInstanceCount && stoppedInstanceCount">
<span class="badge rounded-pill bg-success text-dark">{{ runningInstanceCount }} running</span>
<span class="badge rounded-pill bg-danger text-dark ms-1">{{ stoppedInstanceCount }} stopped</span>
Showing {{ listItems.length }} / {{ machines.length }}
<ng-container *ngIf="runningMachineCount && stoppedMachineCount">
<span class="badge rounded-pill bg-success text-dark">{{ runningMachineCount }} running</span>
<span class="badge rounded-pill bg-danger text-dark ms-1">{{ stoppedMachineCount }} stopped</span>
</ng-container>
<ng-container *ngIf="runningInstanceCount && !stoppedInstanceCount">
<span class="badge rounded-pill bg-success text-dark">{{ runningInstanceCount }} running</span>
<ng-container *ngIf="runningMachineCount && !stoppedMachineCount">
<span class="badge rounded-pill bg-success text-dark">{{ runningMachineCount }} running</span>
</ng-container>
<ng-container *ngIf="!runningInstanceCount && stoppedInstanceCount">
<span class="badge rounded-pill bg-danger text-dark">{{ stoppedInstanceCount }} stopped</span>
<ng-container *ngIf="!runningMachineCount && stoppedMachineCount">
<span class="badge rounded-pill bg-danger text-dark">{{ stoppedMachineCount }} stopped</span>
</ng-container>
</button>
</div>
@ -85,61 +85,61 @@
<div class="overflow-auto flex-grow-1 mt-3 d-flex flex-column" id="scrollingBlock">
<div class="container flex-grow-1 py-2">
<h2 *ngIf="listItems && listItems.length === 0 && instances && instances.length > 0" class="text-uppercase">
{{ 'dashboard.list.noResults' | translate }}
<h2 *ngIf="listItems && listItems.length === 0 && machines && machines.length > 0" class="text-uppercase">
{{ 'machines.list.noResults' | translate }}
</h2>
<virtual-scroller #scroller [items]="listItems" bufferAmount="2" class="instances"
<virtual-scroller #scroller [items]="listItems" bufferAmount="2" class="machines"
[parentScroll]="scroller.window.document.getElementById('scrollingBlock')" [scrollThrottlingTime]="250">
<div *ngFor="let instance of scroller.viewPortItems; trackBy: trackByFunction; let index = index"
<div *ngFor="let machine of scroller.viewPortItems; trackBy: trackByFunction; let index = index"
[ngClass]="showMachineDetails ? 'col-12 full-details' : 'col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12'"
[class.col-lg-6]="showMachineDetails && editorForm.get('fullDetailsTwoColumns').value" lazyLoad [lazyLoadDelay]="lazyLoadDelay"
[container]="scroller.element.nativeElement.getElementsByClassName('scrollable-content')[0]"
(canLoad)="instance.loading = false" (unload)="instance.loading = true"
(load)="loadInstanceDetails(instance)">
<fieldset class="card" [disabled]="instance.working">
(canLoad)="machine.loading = false" (unload)="machine.loading = true"
(load)="loadMachineDetails(machine)">
<fieldset class="card" [disabled]="machine.working">
<div class="row g-0">
<div class="card-info" [ngClass]="showMachineDetails ? 'col-lg-4' : 'col'">
<div>
<div class="d-flex justify-content-between">
<h5 class="card-title text-truncate" [tooltip]="instance.name" container="body" placement="top left"
<h5 class="card-title text-truncate" [tooltip]="machine.name" container="body" placement="top left"
[adaptivePosition]="false">
{{ instance.name }}
{{ machine.name }}
</h5>
<div class="btn-group btn-group-sm">
<button class="btn btn-link text-info" tooltip="Toggle instances details" container="body"
<button class="btn btn-link text-info" tooltip="Toggle machines details" container="body"
placement="top" [adaptivePosition]="false" (click)="toggleMachineDetails()">
<fa-icon icon="expand-alt" [fixedWidth]="true" size="sm"></fa-icon>
</button>
</div>
</div>
<div *ngIf="!instance.loading && instance.imageDetails"
class="text-truncate small text-info text-faded mb-1" [tooltip]="instance.imageDetails.description"
<div *ngIf="!machine.loading && machine.imageDetails"
class="text-truncate small text-info text-faded mb-1" [tooltip]="machine.imageDetails.description"
container="body" placement="top left" [adaptivePosition]="false">
{{ instance.imageDetails.name }}, v{{ instance.imageDetails.version }}
{{ machine.imageDetails.name }}, v{{ machine.imageDetails.version }}
</div>
<button *ngIf="!instance.loading"
<button *ngIf="!machine.loading"
class="btn btn-outline-info w-100 d-flex justify-content-around align-items-center text-truncate"
tooltip="Change specifications" container="body" placement="top" [adaptivePosition]="false"
(click)="resizeMachine(instance)" [disabled]="instance.brand === 'kvm'">
(click)="resizeMachine(machine)" [disabled]="machine.brand === 'kvm'">
<!--<span class="text-uppercase text-truncate">{{ instance.packageDetails.name }}</span>-->
<!--<span class="text-uppercase text-truncate">{{ machine.packageDetails.name }}</span>-->
<span class="px-1">
<fa-icon icon="microchip"></fa-icon>
{{ instance.memory * 1024 * 1024 | fileSize }}
{{ machine.memory * 1024 * 1024 | fileSize }}
</span>
<span>
<fa-icon icon="server"></fa-icon>
{{ instance.disk * 1024 * 1024 | fileSize }}
{{ machine.disk * 1024 * 1024 | fileSize }}
</span>
</button>
</div>
<div class="text-center" *ngIf="instance.working">
<div class="text-center" *ngIf="machine.working">
<div class="spinner-border spinner-border-sm text-info text-faded" role="status">
<span class="visually-hidden">Working...</span>
</div>
@ -147,35 +147,35 @@
<div>
<div class="small text-truncate my-2">
<ng-container *ngIf="instance.type === 'smartmachine'">
<ng-container *ngIf="machine.type === 'smartmachine'">
<fa-icon icon="server" size="sm" class="me-1"></fa-icon>
<span class="machine-brand" innerHtml="{{ 'dashboard.listItem.infrastructureContainer' | translate: { brand: instance.brand } }}"></span>
<span class="machine-brand" innerHtml="{{ 'machines.listItem.infrastructureContainer' | translate: { brand: machine.brand } }}"></span>
</ng-container>
<ng-container *ngIf="instance.type === 'virtualmachine'">
<ng-container *ngIf="machine.type === 'virtualmachine'">
<fa-icon icon="desktop" size="sm" class="me-1"></fa-icon>
<span class="machine-brand" innerHtml="{{ 'dashboard.listItem.virtualMachine' | translate: { brand: instance.brand } }}"></span>
<span class="machine-brand" innerHtml="{{ 'machines.listItem.virtualMachine' | translate: { brand: machine.brand } }}"></span>
</ng-container>
</div>
<div class="d-flex flex-nowrap justify-content-between align-items-center">
<button class="badge text-uppercase" [disabled]="instance.state !== 'running' && instance.state !== 'stopped'"
[class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'"
[class.bg-danger]="instance.state === 'stopped'" [class.bg-success]="instance.state === 'running'"
(click)="showMachineHistory(instance)" tooltip="{{ 'dashboard.listItem.history' | translate }}"
<button class="badge text-uppercase" [disabled]="machine.state !== 'running' && machine.state !== 'stopped'"
[class.bg-light]="machine.state !== 'running' && machine.state !== 'stopped'"
[class.bg-danger]="machine.state === 'stopped'" [class.bg-success]="machine.state === 'running'"
(click)="showMachineHistory(machine)" tooltip="{{ 'machines.listItem.history' | translate }}"
container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="history" [fixedWidth]="true"></fa-icon>
{{ instance.state }}
{{ machine.state }}
</button>
<div class="btn-group btn-group-sm" dropdown placement="bottom right" *ngIf="!instance.loading">
<button class="btn btn-link text-success" (click)="startMachine(instance)"
*ngIf="instance.state === 'stopped'">
<div class="btn-group btn-group-sm" dropdown placement="bottom right" *ngIf="!machine.loading">
<button class="btn btn-link text-success" (click)="startMachine(machine)"
*ngIf="machine.state === 'stopped'">
<fa-icon icon="power-off" [fixedWidth]="true" size="sm" tooltip="Start this machine" container="body"
placement="top" [adaptivePosition]="false"></fa-icon>
</button>
<button class="btn btn-link text-info" [popover]="instanceContextMenu" container="body"
[popoverContext]="{ instance: instance }" placement="bottom left" containerClass="menu-dropdown"
<button class="btn btn-link text-info" [popover]="machineContextMenu" container="body"
[popoverContext]="{ machine: machine }" placement="bottom left" containerClass="menu-dropdown"
[outsideClick]="true">
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
</button>
@ -186,51 +186,51 @@
<div class="col mt-sm-0 mt-3 no-overflow-sm" *ngIf="showMachineDetails">
<div class="card-header p-0 h-100">
<tabset class="dashboard-tabs" *ngIf="!instance.loading">
<tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-info">
<tabset class="dashboard-tabs" *ngIf="!machine.loading">
<tab customClass="dashboard-tab" [disabled]="machine.working" (selectTab)="tabChanged($event, machine)"
id="{{ machine.id }}-info">
<ng-template tabHeading>
<fa-icon icon="info-circle" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Info</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-info [instance]="instance" [loadInfo]="instance.shouldLoadInfo" [refresh]="instance.refreshInfo"
(load)="setInstanceInfo(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false">
</app-instance-info>
<app-machine-info [machine]="machine" [loadInfo]="machine.shouldLoadInfo" [refresh]="machine.refreshInfo"
(load)="setMachineInfo(machine, $event)" (processing)="machine.working = true"
(finishedProcessing)="machine.working = false">
</app-machine-info>
</div>
</tab>
<tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-networks">
<tab customClass="dashboard-tab" [disabled]="machine.working" (selectTab)="tabChanged($event, machine)"
id="{{ machine.id }}-networks">
<ng-template tabHeading>
<fa-icon icon="network-wired" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Network</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-networks [instance]="instance" [loadNetworks]="instance.shouldLoadNetworks"
(load)="setInstanceNetworks(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="refreshInstanceDnsList(instance)"
(instanceReboot)="watchInstanceState(instance)"
(instanceStateUpdate)="updateInstance(instance, $event)">
</app-instance-networks>
<app-machine-networks [machine]="machine" [loadNetworks]="machine.shouldLoadNetworks"
(load)="setMachineNetworks(machine, $event)" (processing)="machine.working = true"
(finishedProcessing)="refreshMachineDnsList(machine)"
(machineReboot)="watchMachineState(machine)"
(machineStateUpdate)="updateMachine(machine, $event)">
</app-machine-networks>
</div>
</tab>
<tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-snapshots" *ngIf="instance.brand !== 'kvm'">
<tab customClass="dashboard-tab" [disabled]="machine.working" (selectTab)="tabChanged($event, machine)"
id="{{ machine.id }}-snapshots" *ngIf="machine.brand !== 'kvm'">
<ng-template tabHeading>
<fa-icon icon="history" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Snapshots</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-snapshots [instance]="instance" [loadSnapshots]="instance.shouldLoadSnapshots"
(load)="setInstanceSnapshots(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false"
(instanceStateUpdate)="updateInstance(instance, $event)">
</app-instance-snapshots>
<app-machine-snapshots [machine]="machine" [loadSnapshots]="machine.shouldLoadSnapshots"
(load)="setMachineSnapshots(machine, $event)" (processing)="machine.working = true"
(finishedProcessing)="machine.working = false"
(machineStateUpdate)="updateMachine(machine, $event)">
</app-machine-snapshots>
</div>
</tab>
<tab *ngIf="false" customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-migrations">
<tab *ngIf="false" customClass="dashboard-tab" [disabled]="machine.working" (selectTab)="tabChanged($event, machine)"
id="{{ machine.id }}-migrations">
<ng-template tabHeading>
<fa-icon icon="coins" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Migrations</span>
@ -239,8 +239,8 @@
<button class="btn btn-outline-info w-100">Move to another node</button>
</div>
</tab>
<tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-volumes" *ngIf="instance.volumes && instance.volumes.length">
<tab customClass="dashboard-tab" [disabled]="machine.working" (selectTab)="tabChanged($event, machine)"
id="{{ machine.id }}-volumes" *ngIf="machine.volumes && machine.volumes.length">
<ng-template tabHeading>
<fa-icon icon="database" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Volumes</span>
@ -248,7 +248,7 @@
<div class="card-body p-2 h-100">
<ul class="list-group list-group-flush list-info">
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
*ngFor="let volume of instance.volumes">
*ngFor="let volume of machine.volumes">
<div class="text-truncate">
<fa-icon icon="database" [fixedWidth]="true" size="sm"></fa-icon>
<span class="ms-1">
@ -275,57 +275,57 @@
<ng-template #filtersTemplate [formGroup]="editorForm">
<fieldset class="filters">
<ng-container formGroupName="filters">
<div class="dropdown-header">{{ 'dashboard.list.filterByState' | translate }}</div>
<div class="dropdown-header">{{ 'machines.list.filterByState' | translate }}</div>
<div class="btn-group w-100" dropdown>
<button class="btn btn-state-filter dropdown-toggle d-flex justify-content-between align-items-center"
dropdownToggle>
<span *ngIf="!editorForm.get(['filters', 'stateFilter']).value">
{{ 'dashboard.list.anyState' | translate }}
{{ 'machines.list.anyState' | translate }}
</span>
<span *ngIf="editorForm.get(['filters', 'stateFilter']).value === 'running'">
{{ 'dashboard.listItem.stateRunning' | translate }}
{{ 'machines.listItem.stateRunning' | translate }}
</span>
<span *ngIf="editorForm.get(['filters', 'stateFilter']).value === 'stopped'">
{{ 'dashboard.listItem.stateStopped' | translate }}
{{ 'machines.listItem.stateStopped' | translate }}
</span>
</button>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-state-filter" role="menu">
<li role="menuitem">
<button class="dropdown-item" [class.active]="!editorForm.get(['filters', 'stateFilter']).value"
(click)="setStateFilter()">
{{ 'dashboard.list.anyState' | translate }}
{{ 'machines.list.anyState' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item"
[class.active]="editorForm.get(['filters', 'stateFilter']).value === 'running'"
(click)="setStateFilter('running')">
{{ 'dashboard.listItem.stateRunning' | translate }}
{{ 'machines.listItem.stateRunning' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item"
[class.active]="editorForm.get(['filters', 'stateFilter']).value === 'stopped'"
(click)="setStateFilter('stopped')">
{{ 'dashboard.listItem.stateStopped' | translate }}
{{ 'machines.listItem.stateStopped' | translate }}
</button>
</li>
</ul>
</div>
<ng-container *ngIf="memoryFilterOptions.stepsArray.length > 1">
<div class="dropdown-header">{{ 'dashboard.list.filterByMemory' | translate }}</div>
<div class="dropdown-header">{{ 'machines.list.filterByMemory' | translate }}</div>
<ngx-slider class="mb-4" formControlName="memoryFilter" [options]="memoryFilterOptions"></ngx-slider>
</ng-container>
<ng-container *ngIf="diskFilterOptions.stepsArray.length > 1">
<div class="dropdown-header">{{ 'dashboard.list.filterByDisk' | translate }}</div>
<div class="dropdown-header">{{ 'machines.list.filterByDisk' | translate }}</div>
<ngx-slider class="mb-3" formControlName="diskFilter" [options]="diskFilterOptions"></ngx-slider>
</ng-container>
<button *ngIf="memoryFilterOptions.stepsArray.length > 1 && diskFilterOptions.stepsArray.length > 1"
class="btn btn-outline-dark w-100 mt-3" (click)="clearFilters()">
{{ 'dashboard.list.resetFilters' | translate }}
{{ 'machines.list.resetFilters' | translate }}
</button>
</ng-container>
@ -333,7 +333,7 @@
<div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="showMachineDetails" formControlName="showMachineDetails">
<label class="form-check-label" for="showMachineDetails">
{{ 'dashboard.list.showDetails' | translate }}
{{ 'machines.list.showDetails' | translate }}
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1"
*ngIf="editorForm.get('showMachineDetails').disabled"></fa-icon>
</label>
@ -344,66 +344,66 @@
<input class="form-check-input mt-0" type="checkbox" id="fullDetailsTwoColumns"
formControlName="fullDetailsTwoColumns">
<label class="form-check-label" for="fullDetailsTwoColumns">
{{ 'dashboard.list.dualColumns' | translate }}
{{ 'machines.list.dualColumns' | translate }}
</label>
</div>
</div>
</fieldset>
</ng-template>
<ng-template #instanceContextMenu let-instance="instance">
<ng-template #machineContextMenu let-machine="machine">
<ul class="list-group list-group-flush" role="menu">
<li role="menuitem">
<button class="dropdown-item" (click)="renameMachine(instance)">
<button class="dropdown-item" (click)="renameMachine(machine)">
<fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.rename' | translate }}
{{ 'machines.listItem.rename' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="showTagEditor(instance)">
<button class="dropdown-item" (click)="showTagEditor(machine)">
<fa-icon icon="tags" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.editTags' | translate }}
{{ 'machines.listItem.editTags' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="showTagEditor(instance, true)">
<button class="dropdown-item" (click)="showTagEditor(machine, true)">
<fa-icon icon="tags" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.editMetadata' | translate }}
{{ 'machines.listItem.editMetadata' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
<li role="menuitem">
<button class="dropdown-item" (click)="createMachine(instance)">
<button class="dropdown-item" (click)="createMachine(machine)">
<fa-icon icon="clone" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.clone' | translate }}
{{ 'machines.listItem.clone' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="createImageFromMachine(instance)">
<button class="dropdown-item" (click)="createImageFromMachine(machine)">
<fa-icon icon="layer-group" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.createImage' | translate }}
{{ 'machines.listItem.createImage' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
<ng-container *ngIf="instance.state === 'running'">
<ng-container *ngIf="machine.state === 'running'">
<li role="menuitem">
<button class="dropdown-item" (click)="restartMachine(instance)">
<button class="dropdown-item" (click)="restartMachine(machine)">
<fa-icon icon="undo" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.restart' | translate }}
{{ 'machines.listItem.restart' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="stopMachine(instance)">
<button class="dropdown-item" (click)="stopMachine(machine)">
<fa-icon icon="stop" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.stop' | translate }}
{{ 'machines.listItem.stop' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
</ng-container>
<li role="menuitem">
<button class="dropdown-item" (click)="deleteMachine(instance)">
<button class="dropdown-item" (click)="deleteMachine(machine)">
<fa-icon icon="trash" [fixedWidth]="true" size="sm"></fa-icon>
{{ 'dashboard.listItem.delete' | translate }}
{{ 'machines.listItem.delete' | translate }}
</button>
</li>
</ul>

View File

@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { InstancesComponent } from './instances.component';
import { MachinesComponent } from './machines.component';
describe('InstancesComponent', () => {
let component: InstancesComponent;
let fixture: ComponentFixture<InstancesComponent>;
describe('MachinesComponent', () => {
let component: MachinesComponent;
let fixture: ComponentFixture<MachinesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ InstancesComponent ]
declarations: [ MachinesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(InstancesComponent);
fixture = TestBed.createComponent(MachinesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { InstancesService } from './helpers/instances.service';
import { MachinesService } from './helpers/machines.service';
import { BsModalService } from 'ngx-bootstrap/modal';
import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component';
import { Instance } from './models/instance';
import { MachineWizardComponent } from './machine-wizard/machine-wizard.component';
import { Machine } from './models/machine';
import { forkJoin, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../catalog/helpers/catalog.service';
import { PackageSelectorComponent } from './package-selector/package-selector.component';
import { PromptDialogComponent } from '../components/prompt-dialog/prompt-dialog.component';
import { InstanceTagEditorComponent } from './instance-tag-editor/instance-tag-editor.component';
import { MachineTagEditorComponent } from './machine-tag-editor/machine-tag-editor.component';
import { ConfirmationDialogComponent } from '../components/confirmation-dialog/confirmation-dialog.component';
import { InstanceHistoryComponent } from './instance-history/instance-history.component';
import { MachineHistoryComponent } from './machine-history/machine-history.component';
import { CustomImageEditorComponent } from '../catalog/custom-image-editor/custom-image-editor.component';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
import { FormGroup, FormBuilder } from '@angular/forms';
@ -24,18 +24,18 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-instances',
templateUrl: './instances.component.html',
styleUrls: ['./instances.component.scss']
selector: 'app-machines',
templateUrl: './machines.component.html',
styleUrls: ['./machines.component.scss']
})
export class InstancesComponent implements OnInit, OnDestroy
export class MachinesComponent implements OnInit, OnDestroy
{
@ViewChild(VirtualScrollerComponent)
private virtualScroller: VirtualScrollerComponent;
loadingIndicator = true;
instances: Instance[] = [];
listItems: Instance[];
machines: Machine[] = [];
listItems: Machine[];
images = [];
packages = [];
volumes = [];
@ -44,9 +44,9 @@ export class InstancesComponent implements OnInit, OnDestroy
editorForm: FormGroup;
showMachineDetails: boolean;
fullDetailsTwoColumns: boolean;
runningInstanceCount = 0;
stoppedInstanceCount = 0;
instanceStateArray: string[] = [];
runningMachineCount = 0;
stoppedMachineCount = 0;
machineStateArray: string[] = [];
memoryFilterOptions: Options = {
animate: false,
stepsArray: [],
@ -68,7 +68,7 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly fuseJsOptions: {};
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService,
constructor(private readonly machinesService: MachinesService,
private readonly catalogService: CatalogService,
private readonly volumesService: VolumesService,
private readonly modalService: BsModalService,
@ -78,7 +78,7 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('dashboard.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
translationService.get('machines.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
this.lazyLoadDelay = this.minimumLazyLoadDelay;
@ -109,7 +109,7 @@ export class InstancesComponent implements OnInit, OnDestroy
{
const formattedValue = this.fileSizePipe.transform(value * 1024 * 1024);
if (this.instances.length === 1)
if (this.machines.length === 1)
return formattedValue;
switch (label)
@ -124,28 +124,28 @@ export class InstancesComponent implements OnInit, OnDestroy
}
// ----------------------------------------------------------------------------------------------------------------
private getInstances()
private getMachines()
{
this.instancesService.get()
.subscribe(instances =>
this.machinesService.get()
.subscribe(machines =>
{
//// DEMO ONLY !!!!!
//const arr = new Array(200);
//for (let j = 0; j < 200; j++)
//{
// const el = { ...instances[0] };
// const el = { ...machines[0] };
// el.name = this.dummyNames[j];
// arr[j] = el;
//}/**/
//// DEMO ONLY !!!!!
this.instances = instances.map(instance =>
this.machines = machines.map(machine =>
{
instance.metadataKeys = Object.keys(instance.metadata);
instance.tagKeys = Object.keys(instance.tags);
machine.metadataKeys = Object.keys(machine.metadata);
machine.tagKeys = Object.keys(machine.tags);
instance.loading = true; // Required for improved scrolling experience
return instance;
machine.loading = true; // Required for improved scrolling experience
return machine;
});
this.getImagesPackagesAndVolumes();
@ -172,8 +172,8 @@ export class InstancesComponent implements OnInit, OnDestroy
this.packages = response.packages;
this.volumes = response.volumes;
for (const instance of this.instances)
this.fillInInstanceDetails(instance);
for (const machine of this.machines)
this.fillInMachineDetails(machine);
});
}
@ -191,7 +191,7 @@ export class InstancesComponent implements OnInit, OnDestroy
typeFilter: [],
memoryFilter: [[0, 0]],
diskFilter: [[0, 0]],
imageFilter: [], // instances provisioned with a certain image
imageFilter: [], // machines provisioned with a certain image
}),
filtersActive: [false],
showMachineDetails: [this.showMachineDetails],
@ -255,18 +255,18 @@ export class InstancesComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
private applyFiltersAndSort()
{
let listItems: Instance[] = null;
let listItems: Machine[] = null;
const searchTerm = this.editorForm.get('searchTerm').value;
if (searchTerm.length >= 2)
{
const fuse = new Fuse(this.instances, this.fuseJsOptions);
const fuse = new Fuse(this.machines, this.fuseJsOptions);
const fuseResults = fuse.search(searchTerm);
listItems = fuseResults.map(x => x.item);
}
if (!listItems)
listItems = [...this.instances];
listItems = [...this.machines];
const stateFilter = this.editorForm.get(['filters', 'stateFilter']).value;
if (stateFilter)
@ -326,43 +326,43 @@ export class InstancesComponent implements OnInit, OnDestroy
}
// ----------------------------------------------------------------------------------------------------------------
prepareForLoading(instances: Instance[])
prepareForLoading(machines: Machine[])
{
for (const instance of instances)
instance.loading = true;
for (const machine of machines)
machine.loading = true;
return instances;
return machines;
}
// ----------------------------------------------------------------------------------------------------------------
trackByFunction = (index: number, instance: Instance) => instance.name;
trackByFunction = (index: number, machine: Machine) => machine.name;
// ----------------------------------------------------------------------------------------------------------------
private computeFiltersOptions(computeOnlyState = false)
{
this.runningInstanceCount = 0;
this.stoppedInstanceCount = 0;
this.instanceStateArray = [];
this.runningMachineCount = 0;
this.stoppedMachineCount = 0;
this.machineStateArray = [];
const memoryValues = {};
const diskValues = {};
for (const instance of this.instances)
for (const machine of this.machines)
{
if (instance.state === 'running')
this.runningInstanceCount++;
if (machine.state === 'running')
this.runningMachineCount++;
if (instance.state === 'stopped')
this.stoppedInstanceCount++;
if (machine.state === 'stopped')
this.stoppedMachineCount++;
if (!~this.instanceStateArray.indexOf(instance.state))
this.instanceStateArray.push(instance.state);
if (!~this.machineStateArray.indexOf(machine.state))
this.machineStateArray.push(machine.state);
if (!computeOnlyState && !memoryValues[instance.memory])
memoryValues[instance.memory] = true;
if (!computeOnlyState && !memoryValues[machine.memory])
memoryValues[machine.memory] = true;
if (!computeOnlyState && !diskValues[instance.disk])
diskValues[instance.disk] = true;
if (!computeOnlyState && !diskValues[machine.disk])
diskValues[machine.disk] = true;
}
if (computeOnlyState)
@ -386,21 +386,21 @@ export class InstancesComponent implements OnInit, OnDestroy
}
// ----------------------------------------------------------------------------------------------------------------
startMachine(instance: Instance)
startMachine(machine: Machine)
{
if (instance.state !== 'stopped')
if (machine.state !== 'stopped')
return;
instance.working = true;
this.toastr.info(`Starting machine "${instance.name}"...`);
machine.working = true;
this.toastr.info(`Starting machine "${machine.name}"...`);
this.instancesService.start(instance.id)
this.machinesService.start(machine.id)
.pipe(
delay(1000),
switchMap(() =>
this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x =>
this.machinesService.getMachineUntilExpectedState(machine, ['running'], x =>
{
instance.state = x.state;
machine.state = x.state;
this.computeFiltersOptions();
})
@ -411,32 +411,32 @@ export class InstancesComponent implements OnInit, OnDestroy
{
this.computeFiltersOptions();
instance.working = false;
this.toastr.info(`The machine "${instance.name}" has been started`);
machine.working = false;
this.toastr.info(`The machine "${machine.name}" has been started`);
}, err =>
{
this.computeFiltersOptions();
instance.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
machine.working = false;
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
restartMachine(instance: Instance)
restartMachine(machine: Machine)
{
if (instance.state !== 'running')
if (machine.state !== 'running')
return;
instance.working = true;
this.toastr.info(`Restarting machine "${instance.name}"...`);
machine.working = true;
this.toastr.info(`Restarting machine "${machine.name}"...`);
this.instancesService.reboot(instance.id)
this.machinesService.reboot(machine.id)
.pipe(
delay(1000),
switchMap(() => this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x =>
switchMap(() => this.machinesService.getMachineUntilExpectedState(machine, ['running'], x =>
{
instance.state = x.state;
machine.state = x.state;
this.computeFiltersOptions();
})
@ -447,33 +447,33 @@ export class InstancesComponent implements OnInit, OnDestroy
{
this.computeFiltersOptions();
instance.working = false;
machine.working = false;
this.toastr.info(`The machine "${instance.name}" has been restarted`);
this.toastr.info(`The machine "${machine.name}" has been restarted`);
}, err =>
{
this.computeFiltersOptions();
instance.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
machine.working = false;
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
stopMachine(instance: Instance)
stopMachine(machine: Machine)
{
if (instance.state !== 'running')
if (machine.state !== 'running')
return;
instance.working = true;
this.toastr.info(`Stopping machine "${instance.name}"`);
machine.working = true;
this.toastr.info(`Stopping machine "${machine.name}"`);
this.instancesService.stop(instance.id)
this.machinesService.stop(machine.id)
.pipe(
delay(1000),
switchMap(() => this.instancesService.getInstanceUntilExpectedState(instance, ['stopped'], x =>
switchMap(() => this.machinesService.getMachineUntilExpectedState(machine, ['stopped'], x =>
{
instance.state = x.state;
machine.state = x.state;
this.computeFiltersOptions();
})
@ -484,25 +484,25 @@ export class InstancesComponent implements OnInit, OnDestroy
{
this.computeFiltersOptions();
instance.working = false;
this.toastr.info(`The machine "${instance.name}" has been stopped`);
machine.working = false;
this.toastr.info(`The machine "${machine.name}" has been stopped`);
}, err =>
{
this.computeFiltersOptions();
instance.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
machine.working = false;
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
resizeMachine(instance: Instance)
resizeMachine(machine: Machine)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { instance }
initialState: { machine }
};
const modalRef = this.modalService.show(PackageSelectorComponent, modalConfig);
@ -512,43 +512,43 @@ export class InstancesComponent implements OnInit, OnDestroy
.pipe(
tap(() =>
{
this.toastr.info(`Changing specifications for machine "${instance.name}"...`);
instance.working = true;
this.toastr.info(`Changing specifications for machine "${machine.name}"...`);
machine.working = true;
}),
first(),
switchMap(pkg => this.instancesService.resize(instance.id, pkg.id).pipe(map(() => pkg)))
switchMap(pkg => this.machinesService.resize(machine.id, pkg.id).pipe(map(() => pkg)))
)
.subscribe(pkg =>
{
instance.package = pkg.name;
instance.memory = pkg.memory;
instance.disk = pkg.disk;
machine.package = pkg.name;
machine.memory = pkg.memory;
machine.disk = pkg.disk;
this.fillInInstanceDetails(instance);
this.fillInMachineDetails(machine);
this.computeFiltersOptions();
instance.working = false;
this.toastr.info(`The specifications for machine "${instance.name}" have been changed`);
machine.working = false;
this.toastr.info(`The specifications for machine "${machine.name}" have been changed`);
}, err =>
{
instance.working = false;
machine.working = false;
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Couldn't change the specifications for machine "${instance.name}" ${errorDetails}`);
this.toastr.error(`Couldn't change the specifications for machine "${machine.name}" ${errorDetails}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
renameMachine(instance: Instance)
renameMachine(machine: Machine)
{
const instanceName = instance.name;
const machineName = machine.name;
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
value: instanceName,
value: machineName,
required: true,
title: 'Rename machine',
prompt: 'Type in the new name for your machine',
@ -561,68 +561,68 @@ export class InstancesComponent implements OnInit, OnDestroy
modalRef.content.save.pipe(first()).subscribe(name =>
{
if (name === instanceName)
if (name === machineName)
{
this.toastr.warning(`You provided the same name for machine "${instanceName}"`);
this.toastr.warning(`You provided the same name for machine "${machineName}"`);
return;
}
instance.working = true;
machine.working = true;
this.instancesService.rename(instance.id, name)
this.machinesService.rename(machine.id, name)
.subscribe(() =>
{
instance.name = name;
machine.name = name;
this.applyFiltersAndSort();
this.toastr.info(`The "${instanceName}" machine has been renamed to "${instance.name}"`);
instance.working = false;
this.toastr.info(`The "${machineName}" machine has been renamed to "${machine.name}"`);
machine.working = false;
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Couldn't rename the "${instanceName}" machine ${errorDetails}`);
instance.working = false;
this.toastr.error(`Couldn't rename the "${machineName}" machine ${errorDetails}`);
machine.working = false;
});
});
}
// ----------------------------------------------------------------------------------------------------------------
showTagEditor(instance: Instance, showMetadata = false)
showTagEditor(machine: Machine, showMetadata = false)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { instance, showMetadata }
initialState: { machine, showMetadata }
};
const modalRef = this.modalService.show(InstanceTagEditorComponent, modalConfig);
const modalRef = this.modalService.show(MachineTagEditorComponent, modalConfig);
modalRef.setClass('modal-lg');
modalRef.content.save.pipe(first()).subscribe(x =>
{
instance[showMetadata ? 'metadata' : 'tags'] = x;
machine[showMetadata ? 'metadata' : 'tags'] = x;
});
}
// ----------------------------------------------------------------------------------------------------------------
createImageFromMachine(instance: Instance)
createImageFromMachine(machine: Machine)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { instance }
initialState: { machine }
};
const modalRef = this.modalService.show(CustomImageEditorComponent, modalConfig);
modalRef.content.save.pipe(first()).subscribe(x =>
{
this.toastr.info(`Creating a new image based on the "${instance.name}" machine...`);
this.toastr.info(`Creating a new image based on the "${machine.name}" machine...`);
this.catalogService.createImage(instance.id, x.name, x.version, x.description)
this.catalogService.createImage(machine.id, x.name, x.version, x.description)
.pipe(
delay(1000),
switchMap(image => this.catalogService.getImageUntilExpectedState(image, ['active', 'failed'])
@ -632,28 +632,28 @@ export class InstancesComponent implements OnInit, OnDestroy
.subscribe(image =>
{
if (image.state === 'active')
this.toastr.info(`A new image "${x.name}" based on the "${instance.name}" machine has been created`);
this.toastr.info(`A new image "${x.name}" based on the "${machine.name}" machine has been created`);
else
this.toastr.error(`Failed to create an image based on the "${instance.name}" machine`);
this.toastr.error(`Failed to create an image based on the "${machine.name}" machine`);
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to create an image based on the "${instance.name}" machine ${errorDetails}`);
this.toastr.error(`Failed to create an image based on the "${machine.name}" machine ${errorDetails}`);
});
});
}
// ----------------------------------------------------------------------------------------------------------------
createMachine(instance?: Instance)
createMachine(machine?: Machine)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { instance }
initialState: { machine }
};
const modalRef = this.modalService.show(InstanceWizardComponent, modalConfig);
const modalRef = this.modalService.show(MachineWizardComponent, modalConfig);
modalRef.setClass('modal-xl');
modalRef.content.save.pipe(first()).subscribe(x =>
@ -662,23 +662,23 @@ export class InstancesComponent implements OnInit, OnDestroy
x.working = true;
this.fillInInstanceDetails(x);
this.fillInMachineDetails(x);
this.instances.push(x);
this.machines.push(x);
this.computeFiltersOptions();
});
}
// ----------------------------------------------------------------------------------------------------------------
deleteMachine(instance: Instance)
deleteMachine(machine: Machine)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
prompt: `Are you sure you wish to permanently delete the "${instance.name}" machine?`,
prompt: `Are you sure you wish to permanently delete the "${machine.name}" machine?`,
confirmButtonText: 'Yes, delete this machine',
declineButtonText: 'No, keep it',
confirmByDefault: false
@ -689,123 +689,123 @@ export class InstancesComponent implements OnInit, OnDestroy
modalRef.content.confirm.pipe(first()).subscribe(() =>
{
instance.working = true;
machine.working = true;
this.toastr.info(`Removing machine "${instance.name}"...`);
this.toastr.info(`Removing machine "${machine.name}"...`);
this.instancesService.delete(instance.id)
this.machinesService.delete(machine.id)
.subscribe(() =>
{
const index = this.instances.findIndex(i => i.id === instance.id);
const index = this.machines.findIndex(i => i.id === machine.id);
if (index < 0) return;
this.instances.splice(index, 1);
this.machines.splice(index, 1);
this.computeFiltersOptions();
this.toastr.info(`The machine "${instance.name}" has been removed`);
this.toastr.info(`The machine "${machine.name}" has been removed`);
},
err =>
{
instance.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
machine.working = false;
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
});
});
}
// ----------------------------------------------------------------------------------------------------------------
showMachineHistory(instance: Instance)
showMachineHistory(machine: Machine)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { instance }
initialState: { machine }
};
const modalRef = this.modalService.show(InstanceHistoryComponent, modalConfig);
const modalRef = this.modalService.show(MachineHistoryComponent, modalConfig);
modalRef.setClass('modal-lg');
}
// ----------------------------------------------------------------------------------------------------------------
tabChanged(event, instance: Instance)
tabChanged(event, machine: Machine)
{
if (event.id.endsWith('info'))
instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
machine.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('snapshots'))
instance.shouldLoadSnapshots = this.editorForm.get('showMachineDetails').value;
machine.shouldLoadSnapshots = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('networks'))
instance.shouldLoadNetworks = this.editorForm.get('showMachineDetails').value;
machine.shouldLoadNetworks = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('volumes'))
{
//instance.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value;
//machine.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value;
}
else if (event.id.endsWith('migrations'))
{
//instance.shouldLoadMigrations = this.editorForm.get('showMachineDetails').value;
//machine.shouldLoadMigrations = this.editorForm.get('showMachineDetails').value;
}
}
// ----------------------------------------------------------------------------------------------------------------
loadInstanceDetails(instance: Instance): any
loadMachineDetails(machine: Machine): any
{
instance.loading = false;
machine.loading = false;
instance.working = !this.stableStates.includes(instance.state);
machine.working = !this.stableStates.includes(machine.state);
// Keep polling the instances that are not in a "stable" state
if (instance.working)
this.instancesService.getInstanceUntilExpectedState(instance, this.stableStates, x =>
// Keep polling the machines that are not in a "stable" state
if (machine.working)
this.machinesService.getMachineUntilExpectedState(machine, this.stableStates, x =>
{
instance.state = x.state;
machine.state = x.state;
// This allows us to trigger later on when the state changes to a something stable
instance.shouldLoadInfo = false;
machine.shouldLoadInfo = false;
this.computeFiltersOptions(true);
})
.pipe(takeUntil(this.destroy$))
.subscribe(x =>
{
instance.working = false;
machine.working = false;
// Update the instance with what we got from the server
Object.assign(instance, x);
// Update the machine with what we got from the server
Object.assign(machine, x);
instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
machine.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
this.computeFiltersOptions();
}, err =>
{
if (err.status === 410)
{
const index = this.instances.findIndex(i => i.id === instance.id);
const index = this.machines.findIndex(i => i.id === machine.id);
if (index >= 0)
{
this.instances.splice(index, 1);
this.machines.splice(index, 1);
this.computeFiltersOptions();
this.toastr.error(`The machine "${instance.name}" has been removed`);
this.toastr.error(`The machine "${machine.name}" has been removed`);
}
}
else
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
instance.working = false;
machine.working = false;
});
instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
machine.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
}
// ----------------------------------------------------------------------------------------------------------------
watchInstanceState(instance: Instance)
watchMachineState(machine: Machine)
{
instance.working = true;
machine.working = true;
this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x =>
this.machinesService.getMachineUntilExpectedState(machine, ['running'], x =>
{
instance.state = x.state;
machine.state = x.state;
this.computeFiltersOptions(true);
})
@ -814,49 +814,49 @@ export class InstancesComponent implements OnInit, OnDestroy
takeUntil(this.destroy$)
).subscribe(() => { }, err =>
{
instance.working = false;
machine.working = false;
});
}
// ----------------------------------------------------------------------------------------------------------------
updateInstance(instance: Instance, updates: Instance)
updateMachine(machine: Machine, updates: Machine)
{
instance.refreshInfo = instance.state !== updates.state;
instance.state = updates.state;
machine.refreshInfo = machine.state !== updates.state;
machine.state = updates.state;
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceInfo(instance: Instance, dnsList)
setMachineInfo(machine: Machine, dnsList)
{
// Update the instance as a result of the info panel's "load" event. We do this because the intances are (un)loaded
// Update the machine as a result of the info panel's "load" event. We do this because the intances are (un)loaded
// from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.dnsList = dnsList;
instance.infoLoaded = true;
machine.dnsList = dnsList;
machine.infoLoaded = true;
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceNetworks(instance: Instance, nics)
setMachineNetworks(machine: Machine, nics)
{
// Update the instance as a result of the networks panel's "load" event. We do this because the intances are (un)loaded
// Update the machine as a result of the networks panel's "load" event. We do this because the intances are (un)loaded
// from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.nics = nics;
instance.networksLoaded = true;
machine.nics = nics;
machine.networksLoaded = true;
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceSnapshots(instance: Instance, snapshots)
setMachineSnapshots(machine: Machine, snapshots)
{
// Update the instance as a result of the snapshots panel's "load" event. We do this because the intances are (un)loaded
// Update the machine as a result of the snapshots panel's "load" event. We do this because the intances are (un)loaded
// from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.snapshots = snapshots;
instance.snapshotsLoaded = true;
machine.snapshots = snapshots;
machine.snapshotsLoaded = true;
}
// ----------------------------------------------------------------------------------------------------------------
refreshInstanceDnsList(instance: Instance)
refreshMachineDnsList(machine: Machine)
{
instance.working = false;
instance.refreshInfo = true;
machine.working = false;
machine.refreshInfo = true;
}
// ----------------------------------------------------------------------------------------------------------------
@ -867,17 +867,17 @@ export class InstancesComponent implements OnInit, OnDestroy
}
// ----------------------------------------------------------------------------------------------------------------
private fillInInstanceDetails(instance: Instance)
private fillInMachineDetails(machine: Machine)
{
const imageDetails = this.images.find(i => i.id === instance.image);
const imageDetails = this.images.find(i => i.id === machine.image);
if (imageDetails)
instance.imageDetails = imageDetails;
machine.imageDetails = imageDetails;
const packageDetails = this.packages.find(p => p.name === instance.package);
const packageDetails = this.packages.find(p => p.name === machine.package);
if (packageDetails)
instance.packageDetails = packageDetails;
machine.packageDetails = packageDetails;
instance.volumes = this.volumes.filter(i => i.refs && i.refs.includes(instance.id));
machine.volumes = this.volumes.filter(i => i.refs && i.refs.includes(machine.id));
}
// ----------------------------------------------------------------------------------------------------------------
@ -890,7 +890,7 @@ export class InstancesComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
this.getInstances();
this.getMachines();
}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -9,49 +9,49 @@ import { WebpackTranslateLoader } from '../helpers/webpack-translate-loader.serv
import { TranslateCompiler } from '@ngx-translate/core';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { InstancesComponent } from './instances.component';
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component';
import { MachinesComponent } from './machines.component';
import { MachineWizardComponent } from './machine-wizard/machine-wizard.component';
import { PackageSelectorComponent } from './package-selector/package-selector.component';
import { InstanceSnapshotsComponent } from './instance-snapshots/instance-snapshots.component';
import { InstanceNetworksComponent } from './instance-networks/instance-networks.component';
import { InstanceSecurityComponent } from './instance-security/instance-security.component';
import { InstanceTagEditorComponent } from './instance-tag-editor/instance-tag-editor.component';
import { InstanceHistoryComponent } from './instance-history/instance-history.component';
import { MachineSnapshotsComponent } from './machine-snapshots/machine-snapshots.component';
import { MachineNetworksComponent } from './machine-networks/machine-networks.component';
import { MachineSecurityComponent } from './machine-security/machine-security.component';
import { MachineTagEditorComponent } from './machine-tag-editor/machine-tag-editor.component';
import { MachineHistoryComponent } from './machine-history/machine-history.component';
import { CustomImageEditorComponent } from '../catalog/custom-image-editor/custom-image-editor.component';
import { InstanceInfoComponent } from './instance-info/instance-info.component';
import { MachineInfoComponent } from './machine-info/machine-info.component';
@NgModule({
declarations: [
InstancesComponent,
InstanceWizardComponent,
MachinesComponent,
MachineWizardComponent,
PackageSelectorComponent,
InstanceSnapshotsComponent,
InstanceNetworksComponent,
InstanceSecurityComponent,
InstanceTagEditorComponent,
InstanceHistoryComponent,
InstanceInfoComponent,
MachineSnapshotsComponent,
MachineNetworksComponent,
MachineSecurityComponent,
MachineTagEditorComponent,
MachineHistoryComponent,
MachineInfoComponent,
],
imports: [
SharedModule,
RouterModule.forChild([
{
path: '',
component: InstancesComponent,
component: MachinesComponent,
data:
{
title: 'instances.title',
subTitle: 'instances.subTitle',
title: 'machines.title',
subTitle: 'machines.subTitle',
icon: 'server'
},
children: [
{
path: 'wizard',
component: InstanceWizardComponent,
component: MachineWizardComponent,
data:
{
title: 'instances.wizard.title',
subTitle: 'instances.wizard.subTitle',
title: 'machines.wizard.title',
subTitle: 'machines.wizard.subTitle',
icon: 'hat-wizard'
}
}
@ -62,7 +62,7 @@ import { InstanceInfoComponent } from './instance-info/instance-info.component';
loader: {
provide: TranslateLoader,
//useClass: WebpackTranslateLoader
useFactory: () => new WebpackTranslateLoader('dashboard')
useFactory: () => new WebpackTranslateLoader('machines')
},
compiler: {
provide: TranslateCompiler,
@ -72,15 +72,15 @@ import { InstanceInfoComponent } from './instance-info/instance-info.component';
})
],
entryComponents: [
InstanceWizardComponent,
MachineWizardComponent,
PackageSelectorComponent,
InstanceTagEditorComponent,
InstanceHistoryComponent,
MachineTagEditorComponent,
MachineHistoryComponent,
CustomImageEditorComponent
]
})
export class InstancesModule
export class MachinesModule
{
constructor(private readonly translate: TranslateService)
{

View File

@ -1,4 +1,4 @@
export class InstanceDisk
export class MachineDisk
{
id: string;
boot: boolean;

View File

@ -1,4 +1,4 @@
export class InstanceVolume
export class MachineVolume
{
name: string;
type: string; // "tritonnfs"

View File

@ -1,11 +1,11 @@
import { Network } from '../../networking/models/network';
import { InstanceDisk } from './instance-disk';
import { MachineDisk } from './machine-disk';
import { Nic } from './nic';
import { InstanceVolume } from './instance-volume';
import { MachineVolume } from './machine-volume';
import { CatalogImage } from '../../catalog/models/image';
import { CatalogPackage } from '../../catalog/models/package';
export class InstanceRequest
export class MachineRequest
{
id: string;
name: string;
@ -19,14 +19,14 @@ export class InstanceRequest
firewall_enabled: boolean;
deletion_protection: boolean;
allow_shared_images: boolean; // Whether to allow provisioning from a shared image.
volumes: InstanceVolume[]; // list of objects representing volumes to mount when the newly created machine boots
disks: InstanceDisk[]; // An array of disk objects to be created (bhyve)
volumes: MachineVolume[]; // list of objects representing volumes to mount when the newly created machine boots
disks: MachineDisk[]; // An array of disk objects to be created (bhyve)
disk: number;
encrypted: boolean; // Place this instance into an encrypted server. Optional.
encrypted: boolean; // Place this machine into an encrypted server. Optional.
visible: boolean;
}
export class Instance extends InstanceRequest
export class Machine extends MachineRequest
{
nics: Nic[];
imageDetails: CatalogImage;

View File

@ -9,10 +9,10 @@
<p class="my-2">Pick the package that best suits your requirements</p>
<app-packages [image]="instance.imageDetails" [imageType]="imageType" [package]="instance.package" (select)="packageSelected($event)"></app-packages>
<app-packages [image]="machine.imageDetails" [imageType]="imageType" [package]="machine.package" (select)="packageSelected($event)"></app-packages>
<div class="current-package">
This machine's current package is: <b class="text-uppercase">{{ instance.package }}</b>
This machine's current package is: <b class="text-uppercase">{{ machine.package }}</b>
</div>
<p class="selected-package" *ngIf="editorForm.get('package').value">

View File

@ -14,7 +14,7 @@ import { CatalogPackage } from '../../catalog/models/package';
export class PackageSelectorComponent implements OnInit
{
@Input()
instance: any;
machine: any;
save = new Subject<CatalogPackage>();
imageType: number;
@ -67,7 +67,7 @@ export class PackageSelectorComponent implements OnInit
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
switch (this.instance.type)
switch (this.machine.type)
{
case 'virtualmachine':
this.imageType = 1;

View File

@ -64,7 +64,7 @@
<div class="d-flex justify-content-between align-items-center">
<div class="rule">
{{ control.value.type }}
<b *ngIf="control.value.config">{{ instances[control.value.config] || control.value.config }}</b>
<b *ngIf="control.value.config">{{ machines[control.value.config] || control.value.config }}</b>
</div>
<button class="btn btn-sm text-danger p-0" (click)="removeFromRule(index)"
@ -89,7 +89,7 @@
<div class="d-flex justify-content-between align-items-center">
<div class="rule">
{{ control.value.type }}
<b *ngIf="control.value.config">{{ instances[control.value.config] || control.value.config }}</b>
<b *ngIf="control.value.config">{{ machines[control.value.config] || control.value.config }}</b>
</div>
<button class="btn btn-sm text-danger p-0" (click)="removeToRule(index)"

View File

@ -8,7 +8,7 @@ import { FirewallRule } from '../models/firewall-rule';
import { FirewallRuleRequest } from '../models/firewall-rule';
import { FirewallService } from '../helpers/firewall.service';
import { ToastrService } from 'ngx-toastr';
import { InstancesService } from '../../instances/helpers/instances.service';
import { MachinesService } from '../../machines/helpers/machines.service';
@Component({
selector: 'app-firewall-editor',
@ -28,7 +28,7 @@ export class FirewallEditorComponent implements OnInit, OnDestroy
canAddFromRule: boolean;
canAddToRule: boolean;
protocolConfigRegex: string;
instances = {};
machines = {};
private destroy$ = new Subject();
@ -37,7 +37,7 @@ export class FirewallEditorComponent implements OnInit, OnDestroy
private readonly router: Router,
private readonly fb: FormBuilder,
private readonly toastr: ToastrService,
private readonly instancesService: InstancesService,
private readonly machinesService: MachinesService,
private readonly firewallService: FirewallService)
{ // When the user navigates away from this route, hide the modal
router.events
@ -47,10 +47,10 @@ export class FirewallEditorComponent implements OnInit, OnDestroy
)
.subscribe(() => this.modalRef.hide());
this.instancesService.get()
this.machinesService.get()
.subscribe(x =>
{
this.instances = x.reduce((a, b) =>
this.machines = x.reduce((a, b) =>
{
a[b.id] = b.name;
return a;

View File

@ -27,7 +27,7 @@
<div class="col-sm" *ngIf="editorForm.get('type').value === 'vm'">
<select class="form-select" formControlName="key" [appAutofocus]="editorForm.get('type').value" [appAutofocusDelay]="250">
<option *ngFor="let instance of instances" [value]="instance.id">{{ instance.name }}</option>
<option *ngFor="let machine of machines" [value]="machine.id">{{ machine.name }}</option>
</select>
</div>
</ng-container>

View File

@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, HostListener } from '@angular/core';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
import { Instance } from '../../instances/models/instance';
import { InstancesService } from '../../instances/helpers/instances.service';
import { Machine } from '../../machines/models/machine';
import { MachinesService } from '../../machines/helpers/machines.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -18,7 +18,7 @@ export class FirewallRuleEditorComponent implements OnInit, OnDestroy
@Output()
saved = new EventEmitter();
instances: Instance[];
machines: Machine[];
editorVisible: boolean;
editorForm: FormGroup;
keyRegex = '^[A-Za-z0-9-_]+$';
@ -29,9 +29,9 @@ export class FirewallRuleEditorComponent implements OnInit, OnDestroy
// --------------------------------------------------------------------------------------------------
constructor(private readonly elementRef: ElementRef,
private readonly fb: FormBuilder,
private readonly instancesService: InstancesService)
private readonly machinesService: MachinesService)
{
this.instancesService.get().subscribe(x => this.instances = x);
this.machinesService.get().subscribe(x => this.machines = x);
}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -82,15 +82,15 @@
From
<span *ngFor="let from of fw.fromArray" class="inline-list-item highlight" text="OR">
{{ from.type }}
<b *ngIf="from.config">{{ instances[from.config] || from.config }}</b>
<b *ngIf="from.config">{{ machines[from.config] || from.config }}</b>
</span>
<span>
To
<span *ngFor="let to of fw.toArray" class="inline-list-item highlight" text="OR">
{{ to.type }}
<span *ngIf="to.type === 'tag'" class="badge badge-discreet">{{ instances[to.config] || to.config }}</span>
<b *ngIf="to.type !== 'tag'">{{ instances[to.config] || to.config }}</b>
<span *ngIf="to.type === 'tag'" class="badge badge-discreet">{{ machines[to.config] || to.config }}</span>
<b *ngIf="to.type !== 'tag'">{{ machines[to.config] || to.config }}</b>
</span>
</span>
</td>

View File

@ -8,7 +8,7 @@ import { Subject, ReplaySubject } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { FirewallRule } from '../models/firewall-rule';
import { ConfirmationDialogComponent } from '../../components/confirmation-dialog/confirmation-dialog.component';
import { InstancesService } from '../../instances/helpers/instances.service';
import { MachinesService } from '../../machines/helpers/machines.service';
import { FirewallService } from '../helpers/firewall.service';
import { sortArray } from '../../helpers/utils.service';
import { Title } from "@angular/platform-browser";
@ -25,14 +25,14 @@ export class FirewallRulesComponent implements OnInit, OnDestroy
listItems: FirewallRule[];
loadingIndicator = true;
editorForm: FormGroup;
instances = {};
machines = {};
private readonly fuseJsOptions: {};
private destroy$ = new Subject();
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly firewallService: FirewallService,
private readonly instancesService: InstancesService,
private readonly machinesService: MachinesService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder,
@ -53,10 +53,10 @@ export class FirewallRulesComponent implements OnInit, OnDestroy
]
};
this.instancesService.get()
this.machinesService.get()
.subscribe(x =>
{
this.instances = x.reduce((a, b) =>
this.machines = x.reduce((a, b) =>
{
a[b.id] = b.name;
return a;

View File

@ -31,9 +31,9 @@ export class FirewallService
@Cacheable({
cacheBusterObserver: cacheBuster$
})
getInstanceFirewallRules(instanceId: string): Observable<FirewallRuleResponse[]>
getMachineFirewallRules(machineId: string): Observable<FirewallRuleResponse[]>
{
return this.httpClient.get<FirewallRuleResponse[]>(`/api/my/machines/${instanceId}/fwrules`);
return this.httpClient.get<FirewallRuleResponse[]>(`/api/my/machines/${machineId}/fwrules`);
}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -5,13 +5,13 @@ import { concatMap, delay, filter, first, flatMap, map, mergeMapTo, repeatWhen,
import { concat, empty, of, range, throwError, zip } from 'rxjs';
import { Cacheable } from 'ts-cacheable';
import { Network } from '../models/network';
import { Nic } from '../../instances/models/nic';
import { Nic } from '../../machines/models/nic';
import { VirtualAreaNetwork } from '../models/vlan';
import { VirtualAreaNetworkRequest } from '../models/vlan';
import { EditNetworkRequest } from '../models/network';
import { AddNetworkRequest } from '../models/network';
import { Instance } from 'src/app/instances/models/instance';
import { InstanceCallbackFunction } from 'src/app/instances/helpers/instances.service';
import { Machine } from 'src/app/machines/models/machine';
import { MachineCallbackFunction } from 'src/app/machines/helpers/machines.service';
const networksCacheBuster$ = new Subject<void>();
@ -135,28 +135,28 @@ export class NetworkingService
}
// ----------------------------------------------------------------------------------------------------------------
getNics(instanceId: string): Observable<Nic[]>
getNics(machineId: string): Observable<Nic[]>
{
return this.httpClient.get<Nic[]>(`/api/my/machines/${instanceId}/nics`);
return this.httpClient.get<Nic[]>(`/api/my/machines/${machineId}/nics`);
}
// ----------------------------------------------------------------------------------------------------------------
getNic(instanceId: string, macAddress: string): Observable<Nic>
getNic(machineId: string, macAddress: string): Observable<Nic>
{
return this.httpClient.get<Nic>(`/api/my/machines/${instanceId}/nics/${macAddress.replace(/:/g, '')}`);
return this.httpClient.get<Nic>(`/api/my/machines/${machineId}/nics/${macAddress.replace(/:/g, '')}`);
}
// ----------------------------------------------------------------------------------------------------------------
getNicUntilAvailable(instance: any, nic: Nic, networkName: string, callbackFn?: NicCallbackFunction, maxRetries = 30): Observable<Nic>
getNicUntilAvailable(machine: any, nic: Nic, networkName: string, callbackFn?: NicCallbackFunction, maxRetries = 30): Observable<Nic>
{
networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state
return this.getNic(instance.id, nic.mac)
// Keep polling the machine until it reaches the expected state
return this.getNic(machine.id, nic.mac)
.pipe(
tap(x =>
{
// We create our own state while the instance reboots
// We create our own state while the machine reboots
if (x.state === 'running')
x.state = 'starting';
@ -179,11 +179,11 @@ export class NetworkingService
filter(x => x.state === 'running' || x.state === 'starting'),
take(1), // needed to stop the repeatWhen loop
concatMap(nic =>
this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
this.httpClient.get<Machine>(`/api/my/machines/${machine.id}`)
.pipe(
tap(() =>
{
// We create our own state while the instance reboots
// We create our own state while the machine reboots
nic.state = 'starting';
if (callbackFn)
@ -206,7 +206,7 @@ export class NetworkingService
take(1), // needed to stop the repeatWhen loop
map(() =>
{
// We manually set the state as "running" now that the instance has rebooted
// We manually set the state as "running" now that the machine has rebooted
nic.state = 'running';
if (callbackFn)
@ -220,15 +220,15 @@ export class NetworkingService
}
// ----------------------------------------------------------------------------------------------------------------
addNic(instanceId: string, networkId: string): Observable<Nic>
addNic(machineId: string, networkId: string): Observable<Nic>
{
return this.httpClient.post<Nic>(`/api/my/machines/${instanceId}/nics`, { network: networkId });
return this.httpClient.post<Nic>(`/api/my/machines/${machineId}/nics`, { network: networkId });
}
// ----------------------------------------------------------------------------------------------------------------
deleteNic(instanceId: string, macAddress: string): Observable<any>
deleteNic(machineId: string, macAddress: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/nics/${macAddress.replace(/:/g, '')}`);
return this.httpClient.delete(`/api/my/machines/${machineId}/nics/${macAddress.replace(/:/g, '')}`);
}
}

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { DashboardComponent } from './machines.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;

View File

@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
templateUrl: './machines.component.html',
styleUrls: ['./machines.component.scss']
})
export class DashboardComponent implements OnInit {

View File

@ -1,7 +1,7 @@
import { FileSizePipe } from './file-size.pipe';
describe('FileSizePipe', () => {
it('create an instance', () => {
it('create an machine', () => {
const pipe = new FileSizePipe();
expect(pipe).toBeTruthy();
});

View File

@ -33,6 +33,7 @@ import { ConfirmationDialogComponent } from './components/confirmation-dialog/co
import { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.component';
import { CustomImageEditorComponent } from './catalog/custom-image-editor/custom-image-editor.component';
import { LazyLoadDirective } from './directives/lazy-load.directive';
import { AffinityRuleEditorComponent } from './components/affinity-rule-editor/affinity-rule-editor.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
@ -59,6 +60,7 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
PromptDialogComponent,
CustomImageEditorComponent,
LazyLoadDirective,
AffinityRuleEditorComponent,
],
imports: [
CommonModule,
@ -127,6 +129,7 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
VirtualScrollerModule,
NgxSliderModule,
ClipboardModule,
AffinityRuleEditorComponent
//HasPermissionDirective
],
providers: [

View File

@ -39,9 +39,9 @@ export class VolumesService
//@Cacheable({
// cacheBusterObserver: volumesCacheBuster$
//})
//getInstanceVolumes(instanceId: string): Observable<VolumeResponse[]>
//getMachineVolumes(machineId: string): Observable<VolumeResponse[]>
//{
// return this.httpClient.get<VolumeResponse[]>(`/api/my/machines/${instanceId}/volumes`);
// return this.httpClient.get<VolumeResponse[]>(`/api/my/machines/${machineId}/volumes`);
//}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Volume } from './models/volume';
import { VolumesService } from './helpers/volumes.service';
import { InstancesService } from '../instances/helpers/instances.service';
import { MachinesService } from '../machines/helpers/machines.service';
import { ConfirmationDialogComponent } from '../components/confirmation-dialog/confirmation-dialog.component';
import { BsModalService } from 'ngx-bootstrap/modal';
import { ToastrService } from 'ngx-toastr';
@ -28,7 +28,7 @@ export class VolumesComponent implements OnInit, OnDestroy
networks = {};
loadingIndicator = true;
editorForm: FormGroup;
instances = {};
machines = {};
private destroy$ = new Subject();
private readonly fuseJsOptions: {};
@ -36,7 +36,7 @@ export class VolumesComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly volumesService: VolumesService,
private readonly networkingService: NetworkingService,
private readonly instancesService: InstancesService,
private readonly machinesService: MachinesService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder,
@ -60,10 +60,10 @@ export class VolumesComponent implements OnInit, OnDestroy
this.createForm();
this.instancesService.get()
this.machinesService.get()
.subscribe(x =>
{
this.instances = x.reduce((a, b) =>
this.machines = x.reduce((a, b) =>
{
a[b.id] = b.name;
return a;

View File

@ -4,7 +4,7 @@
"menu":
{
"dashboard": "Dashboard",
"instances": "Machines",
"machines": "Machines",
"volumes": "Volumes",
"images": "Images",
"networks": "Networks",
@ -33,7 +33,7 @@
"title": "Dashboard",
"subTitle": ""
},
"instances":
"machines":
{
"title": "Machines",
"subTitle": ""

View File

@ -1,5 +1,5 @@
{
"dashboard":
"machines":
{
"title": "Dashboard",
"general":
@ -216,8 +216,6 @@
"addMetadata": "Add metadata",
"removeMetadata": "Remove this metadata",
"affinityHint": "Affinity",
"affinityTip": "Affinity (aka 'locality') controls whether the machine should be placed close to or away from other machines.",
"affinityRequired": "Provision only when the affinity criteria are met",
"previousStep": "Previous step",
"createButtonText": "Create machine",
"ready": "You're about to create {imageType} having {packageDescription}, named <b>{machineName}</b>, based on the <b>{imageDescription}</b>",
@ -249,5 +247,16 @@
"loadFailed": "Failed to retrieve the audit log for \"{machineName}\" machine"
}
}
},
"affinityRuleEditor":
{
"strict": "Place strictly",
"optional": "Place optionally",
"closeTo": "Close to",
"farFrom": "Far from",
"namedLike": "Machines named like",
"taggedWith": "Machines tagged with",
"valueHint": "Can be an exact string, simple *-glob, or regular expression to match against machine names or IDs",
"tagHint": "The exact tag's value"
}
}

View File

@ -2,7 +2,7 @@ virtual-scroller
{
flex-grow: 1;
&.instances .scrollable-content
&.machines .scrollable-content
{
max-width: 100%;
width: auto;
@ -29,7 +29,7 @@ virtual-scroller
@media (min-width: 576px)
{
virtual-scroller.instances .scrollable-content
virtual-scroller.machines .scrollable-content
{
--bs-gutter-x: 1.5rem;
}
@ -37,7 +37,7 @@ virtual-scroller
@media (max-width: 576px)
{
virtual-scroller.instances .scrollable-content
virtual-scroller.machines .scrollable-content
{
--bs-gutter-y: 1.5rem;
}