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",
"\\src\\assets\\i18n\\networking" "\\src\\assets\\i18n\\networking"
], ],
"SelectedNode": "\\src\\app\\instances\\instances.component.html", "SelectedNode": "\\src\\app\\instances\\machines.component.html",
"PreviewInSolutionExplorer": false "PreviewInSolutionExplorer": false
} }

View File

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

View File

@ -14,7 +14,7 @@ import { SharedModule } from './shared.module';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; 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 { UnauthorizedComponent } from './pages/unauthorized/unauthorized.component';
import { NotFoundComponent } from './pages/not-found/not-found.component'; import { NotFoundComponent } from './pages/not-found/not-found.component';
import { NavMenuComponent } from './components/nav-menu/nav-menu.component'; import { NavMenuComponent } from './components/nav-menu/nav-menu.component';

View File

@ -7,7 +7,7 @@
<div class="content"> <div class="content">
<h4 class="mb-3">Create image from machine</h4> <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"> <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 export class CustomImageEditorComponent implements OnInit
{ {
@Input() @Input()
instance: any; machine: any;
save = new Subject<any>(); save = new Subject<any>();
editorForm: FormGroup; 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> description?: string, homepage?: string, eula?: string, acl?: string, tags?: string): Observable<CatalogImage>
{ {
return this.httpClient.post<any>(`/api/my/images`, return this.httpClient.post<any>(`/api/my/images`,
{ {
machine: instanceId, machine: machineId,
name, name,
version, version,
description, 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 else
this.saved.emit(this.editorForm.get('key').value); this.saved.emit(this.editorForm.get('key').value);
this.editorForm.get('key').setValue(null); this.resetForm();
this.editorForm.get('value').setValue(null);
} }
// -------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------
@ -113,6 +112,12 @@ export class InlineEditorComponent implements OnInit, OnDestroy
this.removeEventListeners(); this.removeEventListeners();
this.resetForm();
}
// --------------------------------------------------------------------------------------------------
private resetForm()
{
this.editorForm.get('key').setValue(null); this.editorForm.get('key').setValue(null);
this.editorForm.get('value').setValue(null); this.editorForm.get('value').setValue(null);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -15,11 +15,11 @@ export class HelpComponent implements OnInit
contentUrl: './assets/help/account-info.html' contentUrl: './assets/help/account-info.html'
}, },
{ {
title: 'Provisioning compute instance', title: 'Provisioning compute machine',
contentUrl: '' contentUrl: ''
}, },
{ {
title: 'Managing instances with Triton CLI', title: 'Managing machines with Triton CLI',
contentUrl: '' 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 { TestBed } from '@angular/core/testing';
import { InstancesService } from './instances.service'; import { MachinesService } from './machines.service';
describe('InstancesService', () => { describe('MachinesService', () => {
let service: InstancesService; let service: MachinesService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); TestBed.configureTestingModule({});
service = TestBed.inject(InstancesService); service = TestBed.inject(MachinesService);
}); });
it('should be created', () => { it('should be created', () => {

View File

@ -1,45 +1,45 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { forkJoin, from, Observable, Subject } from 'rxjs'; import { forkJoin, from, Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http'; 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 { 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 { Cacheable } from 'ts-cacheable';
import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service'; import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service';
const instancesCacheBuster$ = new Subject<void>(); const machinesCacheBuster$ = new Subject<void>();
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class InstancesService export class MachinesService
{ {
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly httpClient: HttpClient) { } constructor(private readonly httpClient: HttpClient) { }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
@Cacheable({ @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({ @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 // Keep polling the machine until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`) return this.httpClient.get<Machine>(`/api/my/machines/${machine.id}`)
.pipe( .pipe(
tap(x => callbackFn && callbackFn(x)), tap(x => callbackFn && callbackFn(x)),
repeatWhen(x => repeatWhen(x =>
@ -51,7 +51,7 @@ export class InstancesService
map(y => map(y =>
{ {
if (retries++ === maxRetries) 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; 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(); networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state // Keep polling the machine until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`) return this.httpClient.get<Machine>(`/api/my/machines/${machine.id}`)
.pipe( .pipe(
tap(instance => callbackFn && callbackFn(instance)), tap(machine => callbackFn && callbackFn(machine)),
repeatWhen(x => repeatWhen(x =>
{ {
let retries = 0; let retries = 0;
@ -79,7 +79,7 @@ export class InstancesService
map(() => map(() =>
{ {
if (retries++ === maxRetries) 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) if (callbackFn)
callbackFn(x); 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(() => .pipe(tap(() =>
{ {
instancesCacheBuster$.next(); machinesCacheBuster$.next();
if (instance.volumes?.length) if (machine.volumes?.length)
volumesCacheBuster$.next(); volumesCacheBuster$.next();
})); }));
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
delete(instanceId: string): Observable<any> delete(machineId: string): Observable<any>
{ {
return this.httpClient.delete(`/api/my/machines/${instanceId}`) return this.httpClient.delete(`/api/my/machines/${machineId}`)
.pipe(tap(() => instancesCacheBuster$.next())); .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`, {}) return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=start`, {})
.pipe(tap(() => instancesCacheBuster$.next())); .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`, {}) return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=stop`, {})
.pipe(tap(() => instancesCacheBuster$.next())); .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`, {}) return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=reboot`, {})
.pipe(tap(() => instancesCacheBuster$.next())); .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}`, {}) return this.httpClient.post(`/api/my/machines/${machineId}?action=resize&package=${packageId}`, {})
.pipe(tap(() => instancesCacheBuster$.next())); .pipe(tap(() => machinesCacheBuster$.next()));
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
rename(instanceId: string, name: string): Observable<Instance> rename(machineId: string, name: string): Observable<Machine>
{ {
if (!name) if (!name)
throw 'Name cannot be empty'; throw 'Name cannot be empty';
return this.httpClient.post<Instance>(`/api/my/machines/${instanceId}?action=rename&name=${name}`, {}) return this.httpClient.post<Machine>(`/api/my/machines/${machineId}?action=rename&name=${name}`, {})
.pipe(tap(() => instancesCacheBuster$.next())); .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 // 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 => .pipe(concatMap(existingMetadata =>
{ {
// Compute which metadata the user chose to remove // Compute which metadata the user chose to remove
const obsoleteMetadata: Observable<any>[] = []; const obsoleteMetadata: Observable<any>[] = [];
for (const key of Object.keys(existingMetadata)) for (const key of Object.keys(existingMetadata))
if (!metadata.hasOwnProperty(key) && key !== 'root_authorized_keys') // root_authorized_keys is readonly 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. // 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) 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() 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({ @Cacheable({
cacheBusterObserver: cacheBuster$ cacheBusterObserver: cacheBuster$
}) })
getMigration(instanceId: string, migrationId: string): Observable<any> getMigration(machineId: string, migrationId: string): Observable<any>
{ {
return this.httpClient.get(`/api/my/migrations/${migrationId}`); return this.httpClient.get(`/api/my/migrations/${migrationId}`);
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
migrate(instanceId: string): Observable<any> migrate(machineId: string): Observable<any>
{ {
// https://apidocs.Spearhead.com/cloudapi/#Migrate // 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())); .pipe(tap(() => cacheBuster$.next()));
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
getMigrationProgress(instanceId: string): Observable<any> getMigrationProgress(machineId: string): Observable<any>
{ {
// https://apidocs.Spearhead.com/cloudapi/#Migrate // 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())); .pipe(tap(() => cacheBuster$.next()));
} }
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Snapshot } from '../models/snapshot'; 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 { delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
import { Cacheable } from 'ts-cacheable'; import { Cacheable } from 'ts-cacheable';
@ -20,25 +20,25 @@ export class SnapshotsService
@Cacheable({ @Cacheable({
cacheBusterObserver: cacheBuster$ 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({ @Cacheable({
cacheBusterObserver: cacheBuster$ 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 // 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( .pipe(
tap(x => 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())); .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())); .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())); .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 { NavigationStart, Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators'; import { filter, takeUntil } from 'rxjs/operators';
import { Instance } from '../models/instance'; import { Machine } from '../models/machine';
import { InstancesService } from '../helpers/instances.service'; import { MachinesService } from '../helpers/machines.service';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@Component({ @Component({
selector: 'app-instance-history', selector: 'app-machine-history',
templateUrl: './instance-history.component.html', templateUrl: './machine-history.component.html',
styleUrls: ['./instance-history.component.scss'] styleUrls: ['./machine-history.component.scss']
}) })
export class InstanceHistoryComponent implements OnInit, OnDestroy export class MachineHistoryComponent implements OnInit, OnDestroy
{ {
@Input() @Input()
instance: Instance; machine: Machine;
loading: boolean; loading: boolean;
history: any[]; history: any[];
@ -25,7 +25,7 @@ export class InstanceHistoryComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly modalRef: BsModalRef, constructor(private readonly modalRef: BsModalRef,
private readonly router: Router, private readonly router: Router,
private readonly instancesService: InstancesService, private readonly machinesService: MachinesService,
private readonly toastr: ToastrService) private readonly toastr: ToastrService)
{ {
// When the user navigates away from this route, hide the modal // 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.loading = true;
this.instancesService.getAudit(this.instance.id) this.machinesService.getAudit(this.machine.id)
.subscribe(x => .subscribe(x =>
{ {
this.history = x; this.history = x;

View File

@ -1,19 +1,19 @@
<ul class="list-group list-group-flush list-info"> <ul class="list-group list-group-flush list-info">
<li class="dropdown-header">Machine identifier</li> <li class="dropdown-header">Machine identifier</li>
<li class="list-group-item text-uppercase ps-0"> <li class="list-group-item text-uppercase ps-0">
<b>{{ instance.id }}</b> <b>{{ machine.id }}</b>
</li> </li>
<ng-container *ngIf="dnsCount"> <ng-container *ngIf="dnsCount">
<li class="dropdown-header">DNS list</li> <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" <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"> <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] }} {{ keyValue.value[0] }}
</span> </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] }} {{ keyValue.value[1] }}
</span> </span>
@ -33,9 +33,9 @@
<li class="dropdown-header">Deletion protection</li> <li class="dropdown-header">Deletion protection</li>
<li class="list-group-item ps-0 pb-0 mt-2 ms-2"> <li class="list-group-item ps-0 pb-0 mt-2 ms-2">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="dp{{ instance.id }}" [(ngModel)]="instance.deletion_protection" <input class="form-check-input mt-0" type="checkbox" id="dp{{ machine.id }}" [(ngModel)]="machine.deletion_protection"
(change)="toggleDeletionProtection($event, instance)"> (change)="toggleDeletionProtection($event, machine)">
<label class="form-check-label" for="dp{{ instance.id }}">Prevent this machine from being deleted</label> <label class="form-check-label" for="dp{{ machine.id }}">Prevent this machine from being deleted</label>
</div> </div>
</li> </li>
</ul> </ul>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,9 +7,9 @@
<li class="list-group-item ps-0 pe-0" *ngFor="let role of filteredRoles"> <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"> <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" <input class="form-check-input pe-4" type="checkbox" id="{{ machine.id }}-role-{{ role.id }}" [(ngModel)]="role.selected"
(change)="setInstanceRole($event, role)"> (change)="setMachineRole($event, role)">
<label class="form-check-label ms-2 text-truncate" for="{{ instance.id }}-role-{{ role.id }}"> <label class="form-check-label ms-2 text-truncate" for="{{ machine.id }}-role-{{ role.id }}">
<b>{{ role.name }}</b> <b>{{ role.name }}</b>
<span class="small ps-1" *ngFor="let policy of role.policies">{{ policy.name }}</span> <span class="small ps-1" *ngFor="let policy of role.policies">{{ policy.name }}</span>
</label> </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 { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service'; import { CatalogService } from '../../catalog/helpers/catalog.service';
import { InstancesService } from '../helpers/instances.service'; import { MachinesService } from '../helpers/machines.service';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { delay, switchMap, takeUntil, tap } from 'rxjs/operators'; import { delay, switchMap, takeUntil, tap } from 'rxjs/operators';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { SecurityService } from '../../security/helpers/security.service'; import { SecurityService } from '../../security/helpers/security.service';
@Component({ @Component({
selector: 'app-instance-security', selector: 'app-machine-security',
templateUrl: './instance-security.component.html', templateUrl: './machine-security.component.html',
styleUrls: ['./instance-security.component.scss'] styleUrls: ['./machine-security.component.scss']
}) })
export class InstanceSecurityComponent implements OnInit, OnDestroy export class MachineSecurityComponent implements OnInit, OnDestroy
{ {
@Input() @Input()
instance: any; machine: any;
@Input() @Input()
set loadRoles(value: boolean) set loadRoles(value: boolean)
{ {
if (value && this.instance && !this.roles) if (value && this.machine && !this.roles)
this.getRoles(); this.getRoles();
} }
@ -35,7 +35,7 @@ export class InstanceSecurityComponent implements OnInit, OnDestroy
private readonly fuseJsOptions: {}; private readonly fuseJsOptions: {};
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService, constructor(private readonly machinesService: MachinesService,
private readonly securityService: SecurityService, private readonly securityService: SecurityService,
private readonly toastr: ToastrService) 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 ngOnInit(): void
{ {
// TODO: Find a way to retrieve the list of RoleTags // TODO: Find a way to retrieve the list of RoleTags
//this.instancesService.getRoleTags(this.instance.id) //this.machinesService.getRoleTags(this.machine.id)
// .subscribe(); // .subscribe();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; 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 { BsModalService } from 'ngx-bootstrap/modal';
import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component'; import { MachineWizardComponent } from './machine-wizard/machine-wizard.component';
import { Instance } from './models/instance'; import { Machine } from './models/machine';
import { forkJoin, Subject } from 'rxjs'; import { forkJoin, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../catalog/helpers/catalog.service'; import { CatalogService } from '../catalog/helpers/catalog.service';
import { PackageSelectorComponent } from './package-selector/package-selector.component'; import { PackageSelectorComponent } from './package-selector/package-selector.component';
import { PromptDialogComponent } from '../components/prompt-dialog/prompt-dialog.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 { 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 { CustomImageEditorComponent } from '../catalog/custom-image-editor/custom-image-editor.component';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller'; import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
import { FormGroup, FormBuilder } from '@angular/forms'; import { FormGroup, FormBuilder } from '@angular/forms';
@ -24,18 +24,18 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-instances', selector: 'app-machines',
templateUrl: './instances.component.html', templateUrl: './machines.component.html',
styleUrls: ['./instances.component.scss'] styleUrls: ['./machines.component.scss']
}) })
export class InstancesComponent implements OnInit, OnDestroy export class MachinesComponent implements OnInit, OnDestroy
{ {
@ViewChild(VirtualScrollerComponent) @ViewChild(VirtualScrollerComponent)
private virtualScroller: VirtualScrollerComponent; private virtualScroller: VirtualScrollerComponent;
loadingIndicator = true; loadingIndicator = true;
instances: Instance[] = []; machines: Machine[] = [];
listItems: Instance[]; listItems: Machine[];
images = []; images = [];
packages = []; packages = [];
volumes = []; volumes = [];
@ -44,9 +44,9 @@ export class InstancesComponent implements OnInit, OnDestroy
editorForm: FormGroup; editorForm: FormGroup;
showMachineDetails: boolean; showMachineDetails: boolean;
fullDetailsTwoColumns: boolean; fullDetailsTwoColumns: boolean;
runningInstanceCount = 0; runningMachineCount = 0;
stoppedInstanceCount = 0; stoppedMachineCount = 0;
instanceStateArray: string[] = []; machineStateArray: string[] = [];
memoryFilterOptions: Options = { memoryFilterOptions: Options = {
animate: false, animate: false,
stepsArray: [], stepsArray: [],
@ -68,7 +68,7 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly fuseJsOptions: {}; private readonly fuseJsOptions: {};
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService, constructor(private readonly machinesService: MachinesService,
private readonly catalogService: CatalogService, private readonly catalogService: CatalogService,
private readonly volumesService: VolumesService, private readonly volumesService: VolumesService,
private readonly modalService: BsModalService, private readonly modalService: BsModalService,
@ -78,7 +78,7 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly titleService: Title, private readonly titleService: Title,
private readonly translationService: TranslateService) 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; this.lazyLoadDelay = this.minimumLazyLoadDelay;
@ -109,7 +109,7 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
const formattedValue = this.fileSizePipe.transform(value * 1024 * 1024); const formattedValue = this.fileSizePipe.transform(value * 1024 * 1024);
if (this.instances.length === 1) if (this.machines.length === 1)
return formattedValue; return formattedValue;
switch (label) switch (label)
@ -124,28 +124,28 @@ export class InstancesComponent implements OnInit, OnDestroy
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private getInstances() private getMachines()
{ {
this.instancesService.get() this.machinesService.get()
.subscribe(instances => .subscribe(machines =>
{ {
//// DEMO ONLY !!!!! //// DEMO ONLY !!!!!
//const arr = new Array(200); //const arr = new Array(200);
//for (let j = 0; j < 200; j++) //for (let j = 0; j < 200; j++)
//{ //{
// const el = { ...instances[0] }; // const el = { ...machines[0] };
// el.name = this.dummyNames[j]; // el.name = this.dummyNames[j];
// arr[j] = el; // arr[j] = el;
//}/**/ //}/**/
//// DEMO ONLY !!!!! //// DEMO ONLY !!!!!
this.instances = instances.map(instance => this.machines = machines.map(machine =>
{ {
instance.metadataKeys = Object.keys(instance.metadata); machine.metadataKeys = Object.keys(machine.metadata);
instance.tagKeys = Object.keys(instance.tags); machine.tagKeys = Object.keys(machine.tags);
instance.loading = true; // Required for improved scrolling experience machine.loading = true; // Required for improved scrolling experience
return instance; return machine;
}); });
this.getImagesPackagesAndVolumes(); this.getImagesPackagesAndVolumes();
@ -172,8 +172,8 @@ export class InstancesComponent implements OnInit, OnDestroy
this.packages = response.packages; this.packages = response.packages;
this.volumes = response.volumes; this.volumes = response.volumes;
for (const instance of this.instances) for (const machine of this.machines)
this.fillInInstanceDetails(instance); this.fillInMachineDetails(machine);
}); });
} }
@ -191,7 +191,7 @@ export class InstancesComponent implements OnInit, OnDestroy
typeFilter: [], typeFilter: [],
memoryFilter: [[0, 0]], memoryFilter: [[0, 0]],
diskFilter: [[0, 0]], diskFilter: [[0, 0]],
imageFilter: [], // instances provisioned with a certain image imageFilter: [], // machines provisioned with a certain image
}), }),
filtersActive: [false], filtersActive: [false],
showMachineDetails: [this.showMachineDetails], showMachineDetails: [this.showMachineDetails],
@ -255,18 +255,18 @@ export class InstancesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private applyFiltersAndSort() private applyFiltersAndSort()
{ {
let listItems: Instance[] = null; let listItems: Machine[] = null;
const searchTerm = this.editorForm.get('searchTerm').value; const searchTerm = this.editorForm.get('searchTerm').value;
if (searchTerm.length >= 2) 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); const fuseResults = fuse.search(searchTerm);
listItems = fuseResults.map(x => x.item); listItems = fuseResults.map(x => x.item);
} }
if (!listItems) if (!listItems)
listItems = [...this.instances]; listItems = [...this.machines];
const stateFilter = this.editorForm.get(['filters', 'stateFilter']).value; const stateFilter = this.editorForm.get(['filters', 'stateFilter']).value;
if (stateFilter) if (stateFilter)
@ -326,43 +326,43 @@ export class InstancesComponent implements OnInit, OnDestroy
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
prepareForLoading(instances: Instance[]) prepareForLoading(machines: Machine[])
{ {
for (const instance of instances) for (const machine of machines)
instance.loading = true; 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) private computeFiltersOptions(computeOnlyState = false)
{ {
this.runningInstanceCount = 0; this.runningMachineCount = 0;
this.stoppedInstanceCount = 0; this.stoppedMachineCount = 0;
this.instanceStateArray = []; this.machineStateArray = [];
const memoryValues = {}; const memoryValues = {};
const diskValues = {}; const diskValues = {};
for (const instance of this.instances) for (const machine of this.machines)
{ {
if (instance.state === 'running') if (machine.state === 'running')
this.runningInstanceCount++; this.runningMachineCount++;
if (instance.state === 'stopped') if (machine.state === 'stopped')
this.stoppedInstanceCount++; this.stoppedMachineCount++;
if (!~this.instanceStateArray.indexOf(instance.state)) if (!~this.machineStateArray.indexOf(machine.state))
this.instanceStateArray.push(instance.state); this.machineStateArray.push(machine.state);
if (!computeOnlyState && !memoryValues[instance.memory]) if (!computeOnlyState && !memoryValues[machine.memory])
memoryValues[instance.memory] = true; memoryValues[machine.memory] = true;
if (!computeOnlyState && !diskValues[instance.disk]) if (!computeOnlyState && !diskValues[machine.disk])
diskValues[instance.disk] = true; diskValues[machine.disk] = true;
} }
if (computeOnlyState) 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; return;
instance.working = true; machine.working = true;
this.toastr.info(`Starting machine "${instance.name}"...`); this.toastr.info(`Starting machine "${machine.name}"...`);
this.instancesService.start(instance.id) this.machinesService.start(machine.id)
.pipe( .pipe(
delay(1000), delay(1000),
switchMap(() => switchMap(() =>
this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x => this.machinesService.getMachineUntilExpectedState(machine, ['running'], x =>
{ {
instance.state = x.state; machine.state = x.state;
this.computeFiltersOptions(); this.computeFiltersOptions();
}) })
@ -411,32 +411,32 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
this.computeFiltersOptions(); this.computeFiltersOptions();
instance.working = false; machine.working = false;
this.toastr.info(`The machine "${instance.name}" has been started`); this.toastr.info(`The machine "${machine.name}" has been started`);
}, err => }, err =>
{ {
this.computeFiltersOptions(); this.computeFiltersOptions();
instance.working = false; machine.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`); 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; return;
instance.working = true; machine.working = true;
this.toastr.info(`Restarting machine "${instance.name}"...`); this.toastr.info(`Restarting machine "${machine.name}"...`);
this.instancesService.reboot(instance.id) this.machinesService.reboot(machine.id)
.pipe( .pipe(
delay(1000), 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(); this.computeFiltersOptions();
}) })
@ -447,33 +447,33 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
this.computeFiltersOptions(); 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 => }, err =>
{ {
this.computeFiltersOptions(); this.computeFiltersOptions();
instance.working = false; machine.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`); 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; return;
instance.working = true; machine.working = true;
this.toastr.info(`Stopping machine "${instance.name}"`); this.toastr.info(`Stopping machine "${machine.name}"`);
this.instancesService.stop(instance.id) this.machinesService.stop(machine.id)
.pipe( .pipe(
delay(1000), 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(); this.computeFiltersOptions();
}) })
@ -484,25 +484,25 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
this.computeFiltersOptions(); this.computeFiltersOptions();
instance.working = false; machine.working = false;
this.toastr.info(`The machine "${instance.name}" has been stopped`); this.toastr.info(`The machine "${machine.name}" has been stopped`);
}, err => }, err =>
{ {
this.computeFiltersOptions(); this.computeFiltersOptions();
instance.working = false; machine.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`); this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
resizeMachine(instance: Instance) resizeMachine(machine: Machine)
{ {
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, animated: true,
initialState: { instance } initialState: { machine }
}; };
const modalRef = this.modalService.show(PackageSelectorComponent, modalConfig); const modalRef = this.modalService.show(PackageSelectorComponent, modalConfig);
@ -512,43 +512,43 @@ export class InstancesComponent implements OnInit, OnDestroy
.pipe( .pipe(
tap(() => tap(() =>
{ {
this.toastr.info(`Changing specifications for machine "${instance.name}"...`); this.toastr.info(`Changing specifications for machine "${machine.name}"...`);
instance.working = true; machine.working = true;
}), }),
first(), 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 => .subscribe(pkg =>
{ {
instance.package = pkg.name; machine.package = pkg.name;
instance.memory = pkg.memory; machine.memory = pkg.memory;
instance.disk = pkg.disk; machine.disk = pkg.disk;
this.fillInInstanceDetails(instance); this.fillInMachineDetails(machine);
this.computeFiltersOptions(); this.computeFiltersOptions();
instance.working = false; machine.working = false;
this.toastr.info(`The specifications for machine "${instance.name}" have been changed`); this.toastr.info(`The specifications for machine "${machine.name}" have been changed`);
}, err => }, err =>
{ {
instance.working = false; machine.working = false;
const errorDetails = err.error?.message ? `(${err.error.message})` : ''; 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 = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, animated: true,
initialState: { initialState: {
value: instanceName, value: machineName,
required: true, required: true,
title: 'Rename machine', title: 'Rename machine',
prompt: 'Type in the new name for your 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 => 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; return;
} }
instance.working = true; machine.working = true;
this.instancesService.rename(instance.id, name) this.machinesService.rename(machine.id, name)
.subscribe(() => .subscribe(() =>
{ {
instance.name = name; machine.name = name;
this.applyFiltersAndSort(); this.applyFiltersAndSort();
this.toastr.info(`The "${instanceName}" machine has been renamed to "${instance.name}"`); this.toastr.info(`The "${machineName}" machine has been renamed to "${machine.name}"`);
instance.working = false; machine.working = false;
}, err => }, err =>
{ {
const errorDetails = err.error?.message ? `(${err.error.message})` : ''; const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Couldn't rename the "${instanceName}" machine ${errorDetails}`); this.toastr.error(`Couldn't rename the "${machineName}" machine ${errorDetails}`);
instance.working = false; machine.working = false;
}); });
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
showTagEditor(instance: Instance, showMetadata = false) showTagEditor(machine: Machine, showMetadata = false)
{ {
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, 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.setClass('modal-lg');
modalRef.content.save.pipe(first()).subscribe(x => 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 = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, animated: true,
initialState: { instance } initialState: { machine }
}; };
const modalRef = this.modalService.show(CustomImageEditorComponent, modalConfig); const modalRef = this.modalService.show(CustomImageEditorComponent, modalConfig);
modalRef.content.save.pipe(first()).subscribe(x => 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( .pipe(
delay(1000), delay(1000),
switchMap(image => this.catalogService.getImageUntilExpectedState(image, ['active', 'failed']) switchMap(image => this.catalogService.getImageUntilExpectedState(image, ['active', 'failed'])
@ -632,28 +632,28 @@ export class InstancesComponent implements OnInit, OnDestroy
.subscribe(image => .subscribe(image =>
{ {
if (image.state === 'active') 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 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 => }, err =>
{ {
const errorDetails = err.error?.message ? `(${err.error.message})` : ''; 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 = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, 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.setClass('modal-xl');
modalRef.content.save.pipe(first()).subscribe(x => modalRef.content.save.pipe(first()).subscribe(x =>
@ -662,23 +662,23 @@ export class InstancesComponent implements OnInit, OnDestroy
x.working = true; x.working = true;
this.fillInInstanceDetails(x); this.fillInMachineDetails(x);
this.instances.push(x); this.machines.push(x);
this.computeFiltersOptions(); this.computeFiltersOptions();
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
deleteMachine(instance: Instance) deleteMachine(machine: Machine)
{ {
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, animated: true,
initialState: { 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', confirmButtonText: 'Yes, delete this machine',
declineButtonText: 'No, keep it', declineButtonText: 'No, keep it',
confirmByDefault: false confirmByDefault: false
@ -689,123 +689,123 @@ export class InstancesComponent implements OnInit, OnDestroy
modalRef.content.confirm.pipe(first()).subscribe(() => 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(() => .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; if (index < 0) return;
this.instances.splice(index, 1); this.machines.splice(index, 1);
this.computeFiltersOptions(); this.computeFiltersOptions();
this.toastr.info(`The machine "${instance.name}" has been removed`); this.toastr.info(`The machine "${machine.name}" has been removed`);
}, },
err => err =>
{ {
instance.working = false; machine.working = false;
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`); this.toastr.error(`Machine "${machine.name}" error: ${err.error.message}`);
}); });
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
showMachineHistory(instance: Instance) showMachineHistory(machine: Machine)
{ {
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
animated: true, animated: true,
initialState: { instance } initialState: { machine }
}; };
const modalRef = this.modalService.show(InstanceHistoryComponent, modalConfig); const modalRef = this.modalService.show(MachineHistoryComponent, modalConfig);
modalRef.setClass('modal-lg'); modalRef.setClass('modal-lg');
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
tabChanged(event, instance: Instance) tabChanged(event, machine: Machine)
{ {
if (event.id.endsWith('info')) 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')) 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')) 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')) 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')) 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 // Keep polling the machines that are not in a "stable" state
if (instance.working) if (machine.working)
this.instancesService.getInstanceUntilExpectedState(instance, this.stableStates, x => 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 // This allows us to trigger later on when the state changes to a something stable
instance.shouldLoadInfo = false; machine.shouldLoadInfo = false;
this.computeFiltersOptions(true); this.computeFiltersOptions(true);
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(x => .subscribe(x =>
{ {
instance.working = false; machine.working = false;
// Update the instance with what we got from the server // Update the machine with what we got from the server
Object.assign(instance, x); Object.assign(machine, x);
instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value; machine.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
this.computeFiltersOptions(); this.computeFiltersOptions();
}, err => }, err =>
{ {
if (err.status === 410) 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) if (index >= 0)
{ {
this.instances.splice(index, 1); this.machines.splice(index, 1);
this.computeFiltersOptions(); this.computeFiltersOptions();
this.toastr.error(`The machine "${instance.name}" has been removed`); this.toastr.error(`The machine "${machine.name}" has been removed`);
} }
} }
else 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); this.computeFiltersOptions(true);
}) })
@ -814,49 +814,49 @@ export class InstancesComponent implements OnInit, OnDestroy
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(() => { }, err => ).subscribe(() => { }, err =>
{ {
instance.working = false; machine.working = false;
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
updateInstance(instance: Instance, updates: Instance) updateMachine(machine: Machine, updates: Machine)
{ {
instance.refreshInfo = instance.state !== updates.state; machine.refreshInfo = machine.state !== updates.state;
instance.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. // from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.dnsList = dnsList; machine.dnsList = dnsList;
instance.infoLoaded = true; 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. // from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.nics = nics; machine.nics = nics;
instance.networksLoaded = true; 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. // from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.snapshots = snapshots; machine.snapshots = snapshots;
instance.snapshotsLoaded = true; machine.snapshotsLoaded = true;
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
refreshInstanceDnsList(instance: Instance) refreshMachineDnsList(machine: Machine)
{ {
instance.working = false; machine.working = false;
instance.refreshInfo = true; 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) 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) 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 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 { TranslateCompiler } from '@ngx-translate/core';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler'; import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { InstancesComponent } from './instances.component'; import { MachinesComponent } from './machines.component';
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component'; import { MachineWizardComponent } from './machine-wizard/machine-wizard.component';
import { PackageSelectorComponent } from './package-selector/package-selector.component'; import { PackageSelectorComponent } from './package-selector/package-selector.component';
import { InstanceSnapshotsComponent } from './instance-snapshots/instance-snapshots.component'; import { MachineSnapshotsComponent } from './machine-snapshots/machine-snapshots.component';
import { InstanceNetworksComponent } from './instance-networks/instance-networks.component'; import { MachineNetworksComponent } from './machine-networks/machine-networks.component';
import { InstanceSecurityComponent } from './instance-security/instance-security.component'; import { MachineSecurityComponent } from './machine-security/machine-security.component';
import { InstanceTagEditorComponent } from './instance-tag-editor/instance-tag-editor.component'; import { MachineTagEditorComponent } from './machine-tag-editor/machine-tag-editor.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 { 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({ @NgModule({
declarations: [ declarations: [
InstancesComponent, MachinesComponent,
InstanceWizardComponent, MachineWizardComponent,
PackageSelectorComponent, PackageSelectorComponent,
InstanceSnapshotsComponent, MachineSnapshotsComponent,
InstanceNetworksComponent, MachineNetworksComponent,
InstanceSecurityComponent, MachineSecurityComponent,
InstanceTagEditorComponent, MachineTagEditorComponent,
InstanceHistoryComponent, MachineHistoryComponent,
InstanceInfoComponent, MachineInfoComponent,
], ],
imports: [ imports: [
SharedModule, SharedModule,
RouterModule.forChild([ RouterModule.forChild([
{ {
path: '', path: '',
component: InstancesComponent, component: MachinesComponent,
data: data:
{ {
title: 'instances.title', title: 'machines.title',
subTitle: 'instances.subTitle', subTitle: 'machines.subTitle',
icon: 'server' icon: 'server'
}, },
children: [ children: [
{ {
path: 'wizard', path: 'wizard',
component: InstanceWizardComponent, component: MachineWizardComponent,
data: data:
{ {
title: 'instances.wizard.title', title: 'machines.wizard.title',
subTitle: 'instances.wizard.subTitle', subTitle: 'machines.wizard.subTitle',
icon: 'hat-wizard' icon: 'hat-wizard'
} }
} }
@ -62,7 +62,7 @@ import { InstanceInfoComponent } from './instance-info/instance-info.component';
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
//useClass: WebpackTranslateLoader //useClass: WebpackTranslateLoader
useFactory: () => new WebpackTranslateLoader('dashboard') useFactory: () => new WebpackTranslateLoader('machines')
}, },
compiler: { compiler: {
provide: TranslateCompiler, provide: TranslateCompiler,
@ -72,15 +72,15 @@ import { InstanceInfoComponent } from './instance-info/instance-info.component';
}) })
], ],
entryComponents: [ entryComponents: [
InstanceWizardComponent, MachineWizardComponent,
PackageSelectorComponent, PackageSelectorComponent,
InstanceTagEditorComponent, MachineTagEditorComponent,
InstanceHistoryComponent, MachineHistoryComponent,
CustomImageEditorComponent CustomImageEditorComponent
] ]
}) })
export class InstancesModule export class MachinesModule
{ {
constructor(private readonly translate: TranslateService) constructor(private readonly translate: TranslateService)
{ {

View File

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

View File

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

View File

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

View File

@ -9,10 +9,10 @@
<p class="my-2">Pick the package that best suits your requirements</p> <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"> <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> </div>
<p class="selected-package" *ngIf="editorForm.get('package').value"> <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 export class PackageSelectorComponent implements OnInit
{ {
@Input() @Input()
instance: any; machine: any;
save = new Subject<CatalogPackage>(); save = new Subject<CatalogPackage>();
imageType: number; imageType: number;
@ -67,7 +67,7 @@ export class PackageSelectorComponent implements OnInit
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void ngOnInit(): void
{ {
switch (this.instance.type) switch (this.machine.type)
{ {
case 'virtualmachine': case 'virtualmachine':
this.imageType = 1; this.imageType = 1;

View File

@ -64,7 +64,7 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="rule"> <div class="rule">
{{ control.value.type }} {{ 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> </div>
<button class="btn btn-sm text-danger p-0" (click)="removeFromRule(index)" <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="d-flex justify-content-between align-items-center">
<div class="rule"> <div class="rule">
{{ control.value.type }} {{ 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> </div>
<button class="btn btn-sm text-danger p-0" (click)="removeToRule(index)" <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 { FirewallRuleRequest } from '../models/firewall-rule';
import { FirewallService } from '../helpers/firewall.service'; import { FirewallService } from '../helpers/firewall.service';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { InstancesService } from '../../instances/helpers/instances.service'; import { MachinesService } from '../../machines/helpers/machines.service';
@Component({ @Component({
selector: 'app-firewall-editor', selector: 'app-firewall-editor',
@ -28,7 +28,7 @@ export class FirewallEditorComponent implements OnInit, OnDestroy
canAddFromRule: boolean; canAddFromRule: boolean;
canAddToRule: boolean; canAddToRule: boolean;
protocolConfigRegex: string; protocolConfigRegex: string;
instances = {}; machines = {};
private destroy$ = new Subject(); private destroy$ = new Subject();
@ -37,7 +37,7 @@ export class FirewallEditorComponent implements OnInit, OnDestroy
private readonly router: Router, private readonly router: Router,
private readonly fb: FormBuilder, private readonly fb: FormBuilder,
private readonly toastr: ToastrService, private readonly toastr: ToastrService,
private readonly instancesService: InstancesService, private readonly machinesService: MachinesService,
private readonly firewallService: FirewallService) private readonly firewallService: FirewallService)
{ // When the user navigates away from this route, hide the modal { // When the user navigates away from this route, hide the modal
router.events router.events
@ -47,10 +47,10 @@ export class FirewallEditorComponent implements OnInit, OnDestroy
) )
.subscribe(() => this.modalRef.hide()); .subscribe(() => this.modalRef.hide());
this.instancesService.get() this.machinesService.get()
.subscribe(x => .subscribe(x =>
{ {
this.instances = x.reduce((a, b) => this.machines = x.reduce((a, b) =>
{ {
a[b.id] = b.name; a[b.id] = b.name;
return a; return a;

View File

@ -27,7 +27,7 @@
<div class="col-sm" *ngIf="editorForm.get('type').value === 'vm'"> <div class="col-sm" *ngIf="editorForm.get('type').value === 'vm'">
<select class="form-select" formControlName="key" [appAutofocus]="editorForm.get('type').value" [appAutofocusDelay]="250"> <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> </select>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, HostListener } from '@angular/core';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms'; import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
import { Instance } from '../../instances/models/instance'; import { Machine } from '../../machines/models/machine';
import { InstancesService } from '../../instances/helpers/instances.service'; import { MachinesService } from '../../machines/helpers/machines.service';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -18,7 +18,7 @@ export class FirewallRuleEditorComponent implements OnInit, OnDestroy
@Output() @Output()
saved = new EventEmitter(); saved = new EventEmitter();
instances: Instance[]; machines: Machine[];
editorVisible: boolean; editorVisible: boolean;
editorForm: FormGroup; editorForm: FormGroup;
keyRegex = '^[A-Za-z0-9-_]+$'; keyRegex = '^[A-Za-z0-9-_]+$';
@ -29,9 +29,9 @@ export class FirewallRuleEditorComponent implements OnInit, OnDestroy
// -------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------
constructor(private readonly elementRef: ElementRef, constructor(private readonly elementRef: ElementRef,
private readonly fb: FormBuilder, 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 From
<span *ngFor="let from of fw.fromArray" class="inline-list-item highlight" text="OR"> <span *ngFor="let from of fw.fromArray" class="inline-list-item highlight" text="OR">
{{ from.type }} {{ from.type }}
<b *ngIf="from.config">{{ instances[from.config] || from.config }}</b> <b *ngIf="from.config">{{ machines[from.config] || from.config }}</b>
</span> </span>
<span> <span>
To To
<span *ngFor="let to of fw.toArray" class="inline-list-item highlight" text="OR"> <span *ngFor="let to of fw.toArray" class="inline-list-item highlight" text="OR">
{{ to.type }} {{ to.type }}
<span *ngIf="to.type === 'tag'" class="badge badge-discreet">{{ instances[to.config] || to.config }}</span> <span *ngIf="to.type === 'tag'" class="badge badge-discreet">{{ machines[to.config] || to.config }}</span>
<b *ngIf="to.type !== 'tag'">{{ instances[to.config] || to.config }}</b> <b *ngIf="to.type !== 'tag'">{{ machines[to.config] || to.config }}</b>
</span> </span>
</span> </span>
</td> </td>

View File

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

View File

@ -31,9 +31,9 @@ export class FirewallService
@Cacheable({ @Cacheable({
cacheBusterObserver: cacheBuster$ 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 { concat, empty, of, range, throwError, zip } from 'rxjs';
import { Cacheable } from 'ts-cacheable'; import { Cacheable } from 'ts-cacheable';
import { Network } from '../models/network'; import { Network } from '../models/network';
import { Nic } from '../../instances/models/nic'; import { Nic } from '../../machines/models/nic';
import { VirtualAreaNetwork } from '../models/vlan'; import { VirtualAreaNetwork } from '../models/vlan';
import { VirtualAreaNetworkRequest } from '../models/vlan'; import { VirtualAreaNetworkRequest } from '../models/vlan';
import { EditNetworkRequest } from '../models/network'; import { EditNetworkRequest } from '../models/network';
import { AddNetworkRequest } from '../models/network'; import { AddNetworkRequest } from '../models/network';
import { Instance } from 'src/app/instances/models/instance'; import { Machine } from 'src/app/machines/models/machine';
import { InstanceCallbackFunction } from 'src/app/instances/helpers/instances.service'; import { MachineCallbackFunction } from 'src/app/machines/helpers/machines.service';
const networksCacheBuster$ = new Subject<void>(); 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(); networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state // Keep polling the machine until it reaches the expected state
return this.getNic(instance.id, nic.mac) return this.getNic(machine.id, nic.mac)
.pipe( .pipe(
tap(x => tap(x =>
{ {
// We create our own state while the instance reboots // We create our own state while the machine reboots
if (x.state === 'running') if (x.state === 'running')
x.state = 'starting'; x.state = 'starting';
@ -179,11 +179,11 @@ export class NetworkingService
filter(x => x.state === 'running' || x.state === 'starting'), filter(x => x.state === 'running' || x.state === 'starting'),
take(1), // needed to stop the repeatWhen loop take(1), // needed to stop the repeatWhen loop
concatMap(nic => concatMap(nic =>
this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`) this.httpClient.get<Machine>(`/api/my/machines/${machine.id}`)
.pipe( .pipe(
tap(() => tap(() =>
{ {
// We create our own state while the instance reboots // We create our own state while the machine reboots
nic.state = 'starting'; nic.state = 'starting';
if (callbackFn) if (callbackFn)
@ -206,7 +206,7 @@ export class NetworkingService
take(1), // needed to stop the repeatWhen loop take(1), // needed to stop the repeatWhen loop
map(() => 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'; nic.state = 'running';
if (callbackFn) 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component'; import { DashboardComponent } from './machines.component';
describe('DashboardComponent', () => { describe('DashboardComponent', () => {
let component: DashboardComponent; let component: DashboardComponent;

View File

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

View File

@ -1,7 +1,7 @@
import { FileSizePipe } from './file-size.pipe'; import { FileSizePipe } from './file-size.pipe';
describe('FileSizePipe', () => { describe('FileSizePipe', () => {
it('create an instance', () => { it('create an machine', () => {
const pipe = new FileSizePipe(); const pipe = new FileSizePipe();
expect(pipe).toBeTruthy(); 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 { PromptDialogComponent } from './components/prompt-dialog/prompt-dialog.component';
import { CustomImageEditorComponent } from './catalog/custom-image-editor/custom-image-editor.component'; import { CustomImageEditorComponent } from './catalog/custom-image-editor/custom-image-editor.component';
import { LazyLoadDirective } from './directives/lazy-load.directive'; 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 { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core'; import { library } from '@fortawesome/fontawesome-svg-core';
@ -59,6 +60,7 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
PromptDialogComponent, PromptDialogComponent,
CustomImageEditorComponent, CustomImageEditorComponent,
LazyLoadDirective, LazyLoadDirective,
AffinityRuleEditorComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -127,6 +129,7 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
VirtualScrollerModule, VirtualScrollerModule,
NgxSliderModule, NgxSliderModule,
ClipboardModule, ClipboardModule,
AffinityRuleEditorComponent
//HasPermissionDirective //HasPermissionDirective
], ],
providers: [ providers: [

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{ {
"dashboard": "machines":
{ {
"title": "Dashboard", "title": "Dashboard",
"general": "general":
@ -216,8 +216,6 @@
"addMetadata": "Add metadata", "addMetadata": "Add metadata",
"removeMetadata": "Remove this metadata", "removeMetadata": "Remove this metadata",
"affinityHint": "Affinity", "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", "previousStep": "Previous step",
"createButtonText": "Create machine", "createButtonText": "Create machine",
"ready": "You're about to create {imageType} having {packageDescription}, named <b>{machineName}</b>, based on the <b>{imageDescription}</b>", "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" "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; flex-grow: 1;
&.instances .scrollable-content &.machines .scrollable-content
{ {
max-width: 100%; max-width: 100%;
width: auto; width: auto;
@ -29,7 +29,7 @@ virtual-scroller
@media (min-width: 576px) @media (min-width: 576px)
{ {
virtual-scroller.instances .scrollable-content virtual-scroller.machines .scrollable-content
{ {
--bs-gutter-x: 1.5rem; --bs-gutter-x: 1.5rem;
} }
@ -37,7 +37,7 @@ virtual-scroller
@media (max-width: 576px) @media (max-width: 576px)
{ {
virtual-scroller.instances .scrollable-content virtual-scroller.machines .scrollable-content
{ {
--bs-gutter-y: 1.5rem; --bs-gutter-y: 1.5rem;
} }