sc-portal/app/src/app/instances/instance-snapshots/instance-snapshots.componen...

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();
}
}