fixed some issues when adding/deleting instance networks

This commit is contained in:
Dragos 2021-05-31 11:12:37 +03:00
parent 861ada1aa7
commit 96c76c845b
12 changed files with 201 additions and 74 deletions

View File

@ -61,6 +61,40 @@ export class InstancesService
); );
} }
// ----------------------------------------------------------------------------------------------------------------
getInstanceUntilNicRemoved(instance: any, networkName: string, callbackFn?: InstanceCallbackFunction, maxRetries = 30): Observable<Instance>
{
networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state
return this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
.pipe(
tap(instance => callbackFn && callbackFn(instance)),
repeatWhen(x =>
{
let retries = 0;
return x.pipe(
delay(3000),
map(() =>
{
if (retries++ === maxRetries)
throw { error: `Failed to retrieve the current status for machine "${instance.name}"` };
})
);
}),
filter(x => x.state === 'running' && !x.dns_names.some(d => d.toLocaleLowerCase().indexOf(networkName) >= 0)),
take(1), // needed to stop the repeatWhen loop
map(x =>
{
if (callbackFn)
callbackFn(x);
return instance;
})
);
}
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
add(instance: InstanceRequest): Observable<Instance> add(instance: InstanceRequest): Observable<Instance>
{ {
@ -230,6 +264,12 @@ export class InstancesService
{ {
return this.httpClient.get(`./assets/data/packages.json`); return this.httpClient.get(`./assets/data/packages.json`);
} }
// ----------------------------------------------------------------------------------------------------------------
clearCache()
{
instancesCacheBuster$.next();
}
} }
export type InstanceCallbackFunction = ((instance: Instance) => void); export type InstanceCallbackFunction = ((instance: Instance) => void);

View File

@ -4,7 +4,7 @@
<b>{{ instance.id }}</b> <b>{{ instance.id }}</b>
</li> </li>
<ng-container *ngIf="instance.dnsList"> <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 instance.dnsList | keyvalue; let index = index">

View File

@ -20,6 +20,9 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
@Input() @Input()
loadInfo: boolean; loadInfo: boolean;
@Input()
refresh: boolean;
@Output() @Output()
processing = new EventEmitter(); processing = new EventEmitter();
@ -30,6 +33,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
load = new EventEmitter(); load = new EventEmitter();
loading: boolean; loading: boolean;
dnsCount: number;
private finishedLoading: boolean; private finishedLoading: boolean;
private destroy$ = new Subject(); private destroy$ = new Subject();
@ -70,10 +74,13 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private getInfo() private getInfo()
{ {
if (this.finishedLoading) return; if (this.finishedLoading || this.instance.state === 'provisioning') return;
this.loading = true; this.loading = true;
if (this.refresh)
this.instancesService.clearCache();
this.instancesService.getById(this.instance.id) this.instancesService.getById(this.instance.id)
.subscribe(x => .subscribe(x =>
{ {
@ -81,6 +88,8 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
for (const dns of x.dns_names.sort((a, b) => b.localeCompare(a))) for (const dns of x.dns_names.sort((a, b) => b.localeCompare(a)))
dnsList[dns] = this.getParsedDns(dns); dnsList[dns] = this.getParsedDns(dns);
this.dnsCount = Object.keys(dnsList).length;
this.instance.dnsList = dnsList; this.instance.dnsList = dnsList;
this.loading = false; this.loading = false;
@ -112,7 +121,13 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
{ {
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() => this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{ {
if (!this.finishedLoading && this.loadInfo && !this.instance?.infoLoaded) if (this.refresh)
{
this.finishedLoading = false;
this.loadInfo = true;
}
if (!this.finishedLoading && this.loadInfo && !this.instance?.infoLoaded || this.refresh)
this.getInfo(); this.getInfo();
}); });
} }
@ -124,7 +139,6 @@ export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
this.onChanges$.next(changes); this.onChanges$.next(changes);
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
ngOnDestroy() ngOnDestroy()
{ {

View File

@ -39,13 +39,14 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
instanceStateUpdate = new EventEmitter(); instanceStateUpdate = new EventEmitter();
loading: boolean; loading: boolean;
nics: Nic[]; nics: Nic[] = [];
publicNetworks: Network[] = []; publicNetworks: Network[] = [];
fabricNetworks: Network[] = []; fabricNetworks: Network[] = [];
finishedLoading: boolean; finishedLoading: boolean;
private destroy$ = new Subject(); private destroy$ = new Subject();
private onChanges$ = new ReplaySubject(); private onChanges$ = new ReplaySubject();
private networks: Network[];
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly networkingService: NetworkingService, constructor(private readonly networkingService: NetworkingService,
@ -71,17 +72,19 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private getNetworks() private getNetworks(force = false)
{ {
if (this.finishedLoading) return; if ((this.finishedLoading || this.instance.state === 'provisioning') && !force) return;
this.loading = true;
const observables = this.nics.map(x => this.networkingService.getNetwork(x.network)); const observables = this.nics.map(x => this.networkingService.getNetwork(x.network));
this.loading = observables.length > 0;
forkJoin(observables) forkJoin(observables)
.subscribe(networks => .subscribe(networks =>
{ {
this.networks = networks;
for (const nic of this.nics) for (const nic of this.nics)
{ {
nic.networkDetails = networks.find(x => x.id === nic.network); nic.networkDetails = networks.find(x => x.id === nic.network);
@ -147,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
.getNicUntilExpectedState(this.instance, response.nic, ['running', 'stopped'], n => this.nics[0].state = n.state) .getNicUntilAvailable(this.instance, 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 }))
@ -224,8 +227,8 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
// 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.instance.state === 'running'
? this.instancesService ? this.instancesService
.getInstanceUntilExpectedState(this.instance, ['running'], x => this.instanceStateUpdate.emit(x)) .getInstanceUntilNicRemoved(this.instance, nic.networkName, x => this.instanceStateUpdate.emit(x))
.pipe(takeUntil(this.destroy$)) .pipe(delay(1000), takeUntil(this.destroy$))
: of(nic); : of(nic);
}) })
); );
@ -285,15 +288,17 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void ngOnInit(): void
{ {
this.nics = this.instance?.nics || []; if (!this.instance.nics?.length || this.instance.networksLoaded)
if (this.instance.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.instance?.networksLoaded)
{
this.nics = this.instance?.nics || [];
this.getNetworks(); this.getNetworks();
}
}); });
} }

View File

@ -1,6 +1,5 @@
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 { CatalogService } from '../../catalog/helpers/catalog.service';
import { InstancesService } from '../helpers/instances.service'; import { InstancesService } from '../helpers/instances.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';
@ -27,7 +26,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
processing = new EventEmitter(); processing = new EventEmitter();
@Output() @Output()
processingFinished = new EventEmitter(); finishedProcessing = new EventEmitter();
@Output() @Output()
load = new EventEmitter(); load = new EventEmitter();
@ -51,7 +50,6 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService, constructor(private readonly instancesService: InstancesService,
private readonly snapshotsService: SnapshotsService, private readonly snapshotsService: SnapshotsService,
private readonly catalogService: CatalogService,
private readonly modalService: BsModalService, private readonly modalService: BsModalService,
private readonly toastr: ToastrService) private readonly toastr: ToastrService)
{ {
@ -97,7 +95,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
if (index >= 0) if (index >= 0)
this.snapshots[index] = x; this.snapshots[index] = x;
this.processingFinished.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.instance.name}"`);
}, },
err => err =>
@ -108,7 +106,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
if (index >= 0) if (index >= 0)
this.snapshots.splice(index, 1); this.snapshots.splice(index, 1);
this.processingFinished.emit(); this.finishedProcessing.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
}); });
} }
@ -133,11 +131,15 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
switchMap(() => this.instancesService.getInstanceUntilExpectedState(this.instance, ['stopped'], x => this.instanceStateUpdate.emit(x)) switchMap(() => this.instancesService.getInstanceUntilExpectedState(this.instance, ['stopped'], x => this.instanceStateUpdate.emit(x))
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
) )
).subscribe(() => this.startMachineFromSnapshot(snapshot), ).subscribe(() =>
{
snapshot.working = false;
this.startMachineFromSnapshot(snapshot);
},
err => err =>
{ {
snapshot.working = false; snapshot.working = false;
this.processingFinished.emit(); this.finishedProcessing.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
}); });
@ -184,14 +186,14 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
{ {
snapshot.working = false; snapshot.working = false;
this.processingFinished.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.instance.name}" has been started from the "${snapshot.name}" snapshot`);
}, err => }, err =>
{ {
snapshot.working = false; snapshot.working = false;
this.processingFinished.emit(); this.finishedProcessing.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
}); });
@ -225,12 +227,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
if (index >= 0) if (index >= 0)
this.snapshots.splice(index, 1); this.snapshots.splice(index, 1);
this.processingFinished.emit(); this.finishedProcessing.emit();
this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`); this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`);
}, err => }, err =>
{ {
this.processingFinished.emit(); this.finishedProcessing.emit();
this.toastr.error(`The "${snapshot.name}" snapshot couldn't be deleted: ${err.error.message}`); this.toastr.error(`The "${snapshot.name}" snapshot couldn't be deleted: ${err.error.message}`);
}); });
@ -240,7 +242,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private getSnapshots() private getSnapshots()
{ {
if (this.snapshotsLoaded) return if (this.snapshotsLoaded || this.instance.state === 'provisioning') return
this.loadingSnapshots = true; this.loadingSnapshots = true;

View File

@ -108,7 +108,7 @@
<div *ngIf="!instance.loading && instance.imageDetails" <div *ngIf="!instance.loading && instance.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]="instance.imageDetails.description"
container="body" placement="top" [adaptivePosition]="false"> container="body" placement="top left" [adaptivePosition]="false">
{{ instance.imageDetails.name }}, v{{ instance.imageDetails.version }} {{ instance.imageDetails.name }}, v{{ instance.imageDetails.version }}
</div> </div>
@ -149,14 +149,14 @@
</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">
<a href="javascript:void(0)" class="badge text-uppercase" <button class="badge text-uppercase" [disabled]="instance.state !== 'running' && instance.state !== 'stopped'"
[class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'" [class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'"
[class.bg-danger]="instance.state === 'stopped'" [class.bg-success]="instance.state === 'running'" [class.bg-danger]="instance.state === 'stopped'" [class.bg-success]="instance.state === 'running'"
(click)="showMachineHistory(instance)" tooltip="{{ 'dashboard.listItem.history' | translate }}" (click)="showMachineHistory(instance)" tooltip="{{ 'dashboard.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 }} {{ instance.state }}
</a> </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="!instance.loading">
<button class="btn btn-link text-success" (click)="startMachine(instance)" <button class="btn btn-link text-success" (click)="startMachine(instance)"
@ -178,20 +178,20 @@
<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="!instance.loading">
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" <tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-info"> id="{{ instance.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" <app-instance-info [instance]="instance" [loadInfo]="instance.shouldLoadInfo" [refresh]="instance.refreshInfo"
(load)="setInstanceInfo(instance, $event)" (processing)="instance.working = true" (load)="setInstanceInfo(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false"> (finishedProcessing)="instance.working = false">
</app-instance-info> </app-instance-info>
</div> </div>
</tab> </tab>
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" <tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-networks"> id="{{ instance.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>
@ -200,13 +200,13 @@
<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-instance-networks [instance]="instance" [loadNetworks]="instance.shouldLoadNetworks"
(load)="setInstanceNetworks(instance, $event)" (processing)="instance.working = true" (load)="setInstanceNetworks(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false" (finishedProcessing)="refreshInstanceDnsList(instance)"
(instanceReboot)="watchInstanceState(instance)" (instanceReboot)="watchInstanceState(instance)"
(instanceStateUpdate)="updateInstance(instance, $event)"> (instanceStateUpdate)="updateInstance(instance, $event)">
</app-instance-networks> </app-instance-networks>
</div> </div>
</tab> </tab>
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" <tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-snapshots" *ngIf="instance.brand !== 'kvm'"> id="{{ instance.id }}-snapshots" *ngIf="instance.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>
@ -220,7 +220,7 @@
</app-instance-snapshots> </app-instance-snapshots>
</div> </div>
</tab> </tab>
<tab *ngIf="false" customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" <tab *ngIf="false" customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-migrations"> id="{{ instance.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>
@ -230,7 +230,7 @@
<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" (selectTab)="tabChanged($event, instance)" <tab customClass="dashboard-tab" [disabled]="instance.working" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-volumes" *ngIf="instance.volumes && instance.volumes.length"> id="{{ instance.id }}-volumes" *ngIf="instance.volumes && instance.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>

View File

@ -209,7 +209,7 @@
font-weight: 400; font-weight: 400;
} }
a.badge button.badge
{ {
text-decoration: none; border: none;
} }

View File

@ -14,7 +14,7 @@ import { ConfirmationDialogComponent } from '../components/confirmation-dialog/c
import { InstanceHistoryComponent } from './instance-history/instance-history.component'; import { InstanceHistoryComponent } from './instance-history/instance-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, Validators, AbstractControl, FormArray } from '@angular/forms'; import { FormGroup, FormBuilder } from '@angular/forms';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { LabelType, Options } from '@angular-slider/ngx-slider'; import { LabelType, Options } from '@angular-slider/ngx-slider';
import { FileSizePipe } from '../pipes/file-size.pipe'; import { FileSizePipe } from '../pipes/file-size.pipe';
@ -660,6 +660,8 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
if (!x) return; if (!x) return;
x.working = true;
this.fillInInstanceDetails(x); this.fillInInstanceDetails(x);
this.instances.push(x); this.instances.push(x);
@ -757,6 +759,9 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
instance.state = x.state; instance.state = x.state;
// This allows us to trigger later on when the state changes to a something stable
instance.shouldLoadInfo = false;
this.computeFiltersOptions(true); this.computeFiltersOptions(true);
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
@ -765,13 +770,11 @@ export class InstancesComponent implements OnInit, OnDestroy
instance.working = false; instance.working = false;
// Update the instance with what we got from the server // Update the instance with what we got from the server
const index = this.instances.findIndex(i => i.id === instance.id); Object.assign(instance, x);
if (index >= 0)
{ instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
this.instances.splice(index, 1, x);
this.computeFiltersOptions(); this.computeFiltersOptions();
}
}, err => }, err =>
{ {
if (err.status === 410) if (err.status === 410)
@ -792,7 +795,7 @@ export class InstancesComponent implements OnInit, OnDestroy
instance.working = false; instance.working = false;
}); });
instance.shouldLoadInfo = true; instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
@ -818,6 +821,7 @@ export class InstancesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
updateInstance(instance: Instance, updates: Instance) updateInstance(instance: Instance, updates: Instance)
{ {
instance.refreshInfo = instance.state !== updates.state;
instance.state = updates.state; instance.state = updates.state;
} }
@ -840,7 +844,7 @@ export class InstancesComponent implements OnInit, OnDestroy
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
setInstanceSnapshot(instance: Instance, snapshots) setInstanceSnapshots(instance: Instance, 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 instance 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.
@ -848,6 +852,13 @@ export class InstancesComponent implements OnInit, OnDestroy
instance.snapshotsLoaded = true; instance.snapshotsLoaded = true;
} }
// ----------------------------------------------------------------------------------------------------------------
refreshInstanceDnsList(instance: Instance)
{
instance.working = false;
instance.refreshInfo = true;
}
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private fillInInstanceDetails(instance: Instance) private fillInInstanceDetails(instance: Instance)
{ {

View File

@ -42,8 +42,10 @@ export class Instance extends InstanceRequest
working: boolean; working: boolean;
shouldLoadInfo: boolean; shouldLoadInfo: boolean;
infoLoaded: boolean; infoLoaded: boolean;
refreshInfo: boolean;
shouldLoadNetworks: boolean; shouldLoadNetworks: boolean;
networksLoaded: boolean; networksLoaded: boolean;
refreshNetworks: boolean;
shouldLoadSnapshots: boolean; shouldLoadSnapshots: boolean;
snapshotsLoaded: boolean; snapshotsLoaded: boolean;
volumesEnabled: boolean; volumesEnabled: boolean;

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; 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 { delay, filter, first, flatMap, map, mergeMapTo, repeatWhen, switchMap, switchMapTo, take, takeUntil, tap } from 'rxjs/operators'; import { concatMap, delay, filter, first, flatMap, map, mergeMapTo, repeatWhen, switchMap, switchMapTo, take, takeUntil, tap } from 'rxjs/operators';
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';
@ -10,6 +10,8 @@ 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 { InstanceCallbackFunction } from 'src/app/instances/helpers/instances.service';
const networksCacheBuster$ = new Subject<void>(); const networksCacheBuster$ = new Subject<void>();
@ -145,12 +147,22 @@ export class NetworkingService
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
getNicUntilExpectedState(instance: any, nic: Nic, expectedStates: string[], callbackFn?: NicCallbackFunction, maxRetries = 30): Observable<Nic> getNicUntilAvailable(instance: any, nic: Nic, networkName: string, callbackFn?: NicCallbackFunction, maxRetries = 30): Observable<Nic>
{ {
// Keep polling the snapshot until it reaches the expected state networkName = networkName.toLocaleLowerCase();
// Keep polling the instance until it reaches the expected state
return this.getNic(instance.id, nic.mac) return this.getNic(instance.id, nic.mac)
.pipe( .pipe(
tap(x => callbackFn && callbackFn(x)), tap(x =>
{
// We create our own state while the instance reboots
if (x.state === 'running')
x.state = 'starting';
if (callbackFn)
callbackFn(x);
}),
repeatWhen(x => repeatWhen(x =>
{ {
let retries = 0; let retries = 0;
@ -164,8 +176,46 @@ export class NetworkingService
}) })
); );
}), }),
filter(x => expectedStates.includes(x.state)), 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 =>
this.httpClient.get<Instance>(`/api/my/machines/${instance.id}`)
.pipe(
tap(() =>
{
// We create our own state while the instance reboots
nic.state = 'starting';
if (callbackFn)
callbackFn(nic);
}),
repeatWhen(x =>
{
let retries = 0;
return x.pipe(
delay(3000),
map(() =>
{
if (retries++ === maxRetries)
throw { error: { message: `Failed to retrieve the current status for network interface "${nic.mac}"` } };
})
);
}),
filter(x => x.state === 'running' && x.dns_names.some(d => d.toLocaleLowerCase().indexOf(networkName) >= 0)),
take(1), // needed to stop the repeatWhen loop
map(() =>
{
// We manually set the state as "running" now that the instance has rebooted
nic.state = 'running';
if (callbackFn)
callbackFn(nic);
return nic;
})
)
)
); );
} }

View File

@ -41,7 +41,7 @@
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu"> <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="showPolicyEditor(policy)"> <button class="dropdown-item" (click)="showPolicyEditor(policy)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <fa-icon [fixedWidth]="true" icon="pen"></fa-icon>
{{ 'security.editPolicy' | translate }} {{ 'security.editPolicy' | translate }}
</button> </button>
</li> </li>
@ -114,7 +114,7 @@
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu"> <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="showRoleEditor(role)"> <button class="dropdown-item" (click)="showRoleEditor(role)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <fa-icon [fixedWidth]="true" icon="pen"></fa-icon>
{{ 'security.editRole' | translate }} {{ 'security.editRole' | translate }}
</button> </button>
</li> </li>
@ -195,13 +195,13 @@
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu"> <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="showUserEditor(user)"> <button class="dropdown-item" (click)="showUserEditor(user)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <fa-icon [fixedWidth]="true" icon="pen"></fa-icon>
{{ 'security.editUser' | translate }} {{ 'security.editUser' | translate }}
</button> </button>
</li> </li>
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="showUserEditor(user, true)"> <button class="dropdown-item" (click)="showUserEditor(user, true)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <fa-icon [fixedWidth]="true" icon="pen"></fa-icon>
{{ 'security.changePassword' | translate }} {{ 'security.changePassword' | translate }}
</button> </button>
</li> </li>

View File

@ -172,7 +172,11 @@ body, div, virtual-scroller
.dropdown-item .dropdown-item
{ {
padding: .5rem 1rem; padding: .5rem 1rem;
}
}
.dropdown-item
{
&:focus, &:hover &:focus, &:hover
{ {
color: #1e2125; color: #1e2125;
@ -190,7 +194,6 @@ body, div, virtual-scroller
font-size: 1rem; font-size: 1rem;
vertical-align: middle; vertical-align: middle;
} }
}
} }
.panel .panel