323 lines
10 KiB
TypeScript
323 lines
10 KiB
TypeScript
import { Component, OnInit, OnDestroy, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
|
|
import { ToastrService } from 'ngx-toastr';
|
|
import { InstancesService } from '../helpers/instances.service';
|
|
import { ReplaySubject, Subject } from 'rxjs';
|
|
import { delay, first, switchMap, takeUntil, tap } from 'rxjs/operators';
|
|
import Fuse from 'fuse.js';
|
|
import { BsModalService } from 'ngx-bootstrap/modal';
|
|
import { ConfirmationDialogComponent } from '../../components/confirmation-dialog/confirmation-dialog.component';
|
|
import { Snapshot } from '../models/snapshot';
|
|
import { SnapshotsService } from '../helpers/snapshots.service';
|
|
|
|
@Component({
|
|
selector: 'app-instance-snapshots',
|
|
templateUrl: './instance-snapshots.component.html',
|
|
styleUrls: ['./instance-snapshots.component.scss']
|
|
})
|
|
export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
|
|
{
|
|
@Input()
|
|
instance: any;
|
|
|
|
@Input()
|
|
loadSnapshots: boolean;
|
|
|
|
@Output()
|
|
processing = new EventEmitter();
|
|
|
|
@Output()
|
|
finishedProcessing = new EventEmitter();
|
|
|
|
@Output()
|
|
load = new EventEmitter();
|
|
|
|
@Output()
|
|
instanceStateUpdate = new EventEmitter();
|
|
|
|
loadingSnapshots: boolean;
|
|
snapshotsLoaded: boolean;
|
|
filteredSnapshots: any[];
|
|
snapshotName: string;
|
|
_searchTerm: string;
|
|
shouldSearch: boolean;
|
|
|
|
private destroy$ = new Subject();
|
|
private onChanges$ = new ReplaySubject();
|
|
private snapshots: any[];
|
|
private finishedLoading: boolean
|
|
private readonly fuseJsOptions: {};
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
constructor(private readonly instancesService: InstancesService,
|
|
private readonly snapshotsService: SnapshotsService,
|
|
private readonly modalService: BsModalService,
|
|
private readonly toastr: ToastrService)
|
|
{
|
|
// Configure FuseJs
|
|
this.fuseJsOptions = {
|
|
includeScore: false,
|
|
minMatchCharLength: 2,
|
|
includeMatches: false,
|
|
shouldSort: false,
|
|
threshold: .3, // Lower value means a more exact search
|
|
keys: [
|
|
{ name: 'name', weight: .9 }
|
|
]
|
|
};
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
createSnapshot()
|
|
{
|
|
this.processing.emit();
|
|
|
|
this.snapshots = this.snapshots || [];
|
|
|
|
// Spaces are not allowed in snapshot names (not documented)!
|
|
const snapshotName = this.snapshotName;
|
|
|
|
// Clear this field
|
|
this.snapshotName = null;
|
|
|
|
this.snapshotsService.createSnapshot(this.instance.id, snapshotName)
|
|
.pipe(
|
|
takeUntil(this.destroy$),
|
|
delay(1000),
|
|
tap(x => this.snapshots.unshift(x)),
|
|
switchMap((x: Snapshot) => this.snapshotsService.getSnapshotUntilExpectedState(this.instance, x, ['created'])
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
)
|
|
.subscribe(x =>
|
|
{
|
|
const index = this.snapshots.findIndex(s => s.name === snapshotName);
|
|
|
|
if (index >= 0)
|
|
this.snapshots[index] = x;
|
|
|
|
this.finishedProcessing.emit();
|
|
this.toastr.info(`A new snapshot "${snapshotName}" has been created for machine "${this.instance.name}"`);
|
|
},
|
|
err =>
|
|
{
|
|
const index = this.snapshots.findIndex(s => s.name === snapshotName);
|
|
|
|
// Remove this snapshot from the list since it couldn't be created
|
|
if (index >= 0)
|
|
this.snapshots.splice(index, 1);
|
|
|
|
this.finishedProcessing.emit();
|
|
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
restoreSnapshot(snapshot: Snapshot)
|
|
{
|
|
this.confirmRestore(snapshot)
|
|
.subscribe(() =>
|
|
{
|
|
this.processing.emit();
|
|
|
|
snapshot.working = true;
|
|
|
|
// First we need to make sure the instance is stopped
|
|
if (this.instance.state !== 'stopped')
|
|
this.instancesService.stop(this.instance.id)
|
|
.pipe(
|
|
takeUntil(this.destroy$),
|
|
tap(() => this.toastr.info(`Restarting machine "${this.instance.name}"`)),
|
|
delay(1000),
|
|
switchMap(() => this.instancesService.getInstanceUntilExpectedState(this.instance, ['stopped'], x => this.instanceStateUpdate.emit(x))
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
).subscribe(() =>
|
|
{
|
|
snapshot.working = false;
|
|
this.startMachineFromSnapshot(snapshot);
|
|
},
|
|
err =>
|
|
{
|
|
snapshot.working = false;
|
|
this.finishedProcessing.emit();
|
|
|
|
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
|
});
|
|
else
|
|
this.startMachineFromSnapshot(snapshot);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private confirmRestore(snapshot: Snapshot)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: {
|
|
prompt: `Restoring the "${snapshot.name}" snapshot will reboot your machine. Do you wish to continue?`,
|
|
confirmButtonText: 'Yes, reboot and restore',
|
|
declineButtonText: "No, don't restore"
|
|
}
|
|
};
|
|
|
|
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
|
|
|
|
return modalRef.content.confirm.pipe(first());
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private startMachineFromSnapshot(snapshot: Snapshot)
|
|
{
|
|
this.processing.emit();
|
|
|
|
this.toastr.info(`Restoring machine "${this.instance.name}" from "${snapshot.name}" snapshot`);
|
|
|
|
this.snapshotsService.startFromSnapshot(this.instance.id, snapshot.name)
|
|
.pipe(
|
|
takeUntil(this.destroy$),
|
|
delay(1000),
|
|
switchMap(() => this.instancesService.getInstanceUntilExpectedState(this.instance, ['running'], x => this.instanceStateUpdate.emit(x), 20)
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
)
|
|
.subscribe(() =>
|
|
{
|
|
snapshot.working = false;
|
|
|
|
this.finishedProcessing.emit();
|
|
|
|
this.toastr.info(`The machine "${this.instance.name}" has been started from the "${snapshot.name}" snapshot`);
|
|
}, err =>
|
|
{
|
|
snapshot.working = false;
|
|
|
|
this.finishedProcessing.emit();
|
|
|
|
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
deleteSnapshot(snapshot: Snapshot)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: {
|
|
prompt: `Are you sure you wish to permanently delete the "${snapshot.name}" snapshot?`,
|
|
confirmButtonText: 'Yes, delete this snapshot',
|
|
declineButtonText: 'No, keep it',
|
|
confirmByDefault: false
|
|
}
|
|
};
|
|
|
|
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
|
|
|
|
modalRef.content.confirm.pipe(first()).subscribe(() =>
|
|
{
|
|
this.processing.emit();
|
|
|
|
this.snapshotsService.deleteSnapshot(this.instance.id, snapshot.name)
|
|
.subscribe(() =>
|
|
{
|
|
const index = this.snapshots.findIndex(s => s.name === snapshot.name);
|
|
if (index >= 0)
|
|
this.snapshots.splice(index, 1);
|
|
|
|
this.finishedProcessing.emit();
|
|
|
|
this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`);
|
|
}, err =>
|
|
{
|
|
this.finishedProcessing.emit();
|
|
|
|
this.toastr.error(`The "${snapshot.name}" snapshot couldn't be deleted: ${err.error.message}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private getSnapshots()
|
|
{
|
|
if (this.snapshotsLoaded || this.instance.state === 'provisioning') return
|
|
|
|
this.loadingSnapshots = true;
|
|
|
|
// Get the list of snapshots
|
|
this.snapshotsService.getSnapshots(this.instance.id)
|
|
.subscribe(x =>
|
|
{
|
|
this.snapshots = x;
|
|
this.filteredSnapshots = x;
|
|
|
|
this.loadingSnapshots = false;
|
|
this.snapshotsLoaded = true;
|
|
this.load.emit(x);
|
|
},
|
|
err =>
|
|
{
|
|
this.toastr.error(err.error.message);
|
|
this.loadingSnapshots = false;
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
searchBoxFocused(isFocused = true)
|
|
{
|
|
if (isFocused)
|
|
this.shouldSearch = true;
|
|
else
|
|
this.shouldSearch = !!this.searchTerm;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
get searchTerm()
|
|
{
|
|
return this._searchTerm;
|
|
}
|
|
set searchTerm(value: string)
|
|
{
|
|
this._searchTerm = value;
|
|
|
|
if (!value)
|
|
{
|
|
this.filteredSnapshots = this.snapshots;
|
|
}
|
|
else
|
|
{
|
|
// Use fuzzy search for lookups
|
|
const fuse = new Fuse(this.snapshots, this.fuseJsOptions);
|
|
this.filteredSnapshots = fuse.search(value).map(x => x.item);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
ngOnInit(): void
|
|
{
|
|
this.snapshots = this.instance?.snapshots;
|
|
this.filteredSnapshots = this.snapshots;
|
|
|
|
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
|
|
{
|
|
if (!this.finishedLoading && this.loadSnapshots && !this.instance?.snapshotsLoaded)
|
|
this.getSnapshots();
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
ngOnChanges(changes: SimpleChanges): void
|
|
{
|
|
// Since we can't control if ngOnChanges is executed before ngOnInit, we need this trick
|
|
this.onChanges$.next(changes);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
ngOnDestroy()
|
|
{
|
|
this.destroy$.next();
|
|
}
|
|
}
|