884 lines
29 KiB
TypeScript
884 lines
29 KiB
TypeScript
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
|
import { InstancesService } from './helpers/instances.service';
|
|
import { BsModalService } from 'ngx-bootstrap/modal';
|
|
import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
|
|
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component';
|
|
import { Instance } from './models/instance';
|
|
import { forkJoin, Subject } from 'rxjs';
|
|
import { ToastrService } from 'ngx-toastr';
|
|
import { CatalogService } from '../catalog/helpers/catalog.service';
|
|
import { PackageSelectorComponent } from './package-selector/package-selector.component';
|
|
import { PromptDialogComponent } from '../components/prompt-dialog/prompt-dialog.component';
|
|
import { InstanceTagEditorComponent } from './instance-tag-editor/instance-tag-editor.component';
|
|
import { ConfirmationDialogComponent } from '../components/confirmation-dialog/confirmation-dialog.component';
|
|
import { InstanceHistoryComponent } from './instance-history/instance-history.component';
|
|
import { CustomImageEditorComponent } from '../catalog/custom-image-editor/custom-image-editor.component';
|
|
import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
|
|
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
|
|
import Fuse from 'fuse.js';
|
|
import { LabelType, Options } from '@angular-slider/ngx-slider';
|
|
import { FileSizePipe } from '../pipes/file-size.pipe';
|
|
import { sortArray } from '../helpers/utils.service';
|
|
import { VolumesService } from '../volumes/helpers/volumes.service';
|
|
import { Title } from '@angular/platform-browser';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
|
|
@Component({
|
|
selector: 'app-instances',
|
|
templateUrl: './instances.component.html',
|
|
styleUrls: ['./instances.component.scss']
|
|
})
|
|
export class InstancesComponent implements OnInit, OnDestroy
|
|
{
|
|
@ViewChild(VirtualScrollerComponent)
|
|
private virtualScroller: VirtualScrollerComponent;
|
|
|
|
loadingIndicator = true;
|
|
instances: Instance[] = [];
|
|
listItems: Instance[];
|
|
images = [];
|
|
packages = [];
|
|
volumes = [];
|
|
lazyLoadDelay: number;
|
|
canPrepareForLoading: boolean;
|
|
editorForm: FormGroup;
|
|
showMachineDetails: boolean;
|
|
fullDetailsTwoColumns: boolean;
|
|
runningInstanceCount = 0;
|
|
stoppedInstanceCount = 0;
|
|
instanceStateArray: string[] = [];
|
|
memoryFilterOptions: Options = {
|
|
animate: false,
|
|
stepsArray: [],
|
|
draggableRange: true,
|
|
showTicks: true,
|
|
translate: this.translateBytes.bind(this)
|
|
};
|
|
diskFilterOptions: Options = {
|
|
animate: false,
|
|
stepsArray: [],
|
|
draggableRange: true,
|
|
showTicks: true,
|
|
translate: this.translateBytes.bind(this)
|
|
};
|
|
|
|
private destroy$ = new Subject();
|
|
private stableStates = ['running', 'stopped'];
|
|
private minimumLazyLoadDelay = 1000;
|
|
private readonly fuseJsOptions: {};
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
constructor(private readonly instancesService: InstancesService,
|
|
private readonly catalogService: CatalogService,
|
|
private readonly volumesService: VolumesService,
|
|
private readonly modalService: BsModalService,
|
|
private readonly toastr: ToastrService,
|
|
private readonly fb: FormBuilder,
|
|
private readonly fileSizePipe: FileSizePipe,
|
|
private readonly titleService: Title,
|
|
private readonly translationService: TranslateService)
|
|
{
|
|
translationService.get('dashboard.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
|
|
|
|
this.lazyLoadDelay = this.minimumLazyLoadDelay;
|
|
|
|
// Configure FuseJs
|
|
this.fuseJsOptions = {
|
|
includeScore: false,
|
|
minMatchCharLength: 2,
|
|
includeMatches: true,
|
|
shouldSort: false,
|
|
threshold: .3, // Lower value means a more exact search
|
|
keys: [
|
|
{ name: 'name', weight: .9 },
|
|
{ name: 'metadataKeys', weight: .7 },
|
|
{ name: 'tagKeys', weight: .7 },
|
|
{ name: 'os', weight: .5 },
|
|
{ name: 'brand', weight: .5 }
|
|
]
|
|
};
|
|
|
|
this.showMachineDetails = !!JSON.parse(localStorage.getItem('showMachineDetails') || '0');
|
|
this.fullDetailsTwoColumns = !!JSON.parse(localStorage.getItem('fullDetailsTwoColumns') || '1');
|
|
|
|
this.createForm();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private translateBytes(value: number, label: LabelType): string
|
|
{
|
|
const formattedValue = this.fileSizePipe.transform(value * 1024 * 1024);
|
|
|
|
if (this.instances.length === 1)
|
|
return formattedValue;
|
|
|
|
switch (label)
|
|
{
|
|
case LabelType.Low:
|
|
return `Between ${formattedValue}`;
|
|
case LabelType.High:
|
|
return `and ${formattedValue}`;
|
|
default:
|
|
return formattedValue;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private getInstances()
|
|
{
|
|
this.instancesService.get()
|
|
.subscribe(instances =>
|
|
{
|
|
//// DEMO ONLY !!!!!
|
|
//const arr = new Array(200);
|
|
//for (let j = 0; j < 200; j++)
|
|
//{
|
|
// const el = { ...instances[0] };
|
|
// el.name = this.dummyNames[j];
|
|
// arr[j] = el;
|
|
//}/**/
|
|
//// DEMO ONLY !!!!!
|
|
|
|
this.instances = instances.map(instance =>
|
|
{
|
|
instance.metadataKeys = Object.keys(instance.metadata);
|
|
instance.tagKeys = Object.keys(instance.tags);
|
|
|
|
instance.loading = true; // Required for improved scrolling experience
|
|
return instance;
|
|
});
|
|
|
|
this.getImagesPackagesAndVolumes();
|
|
|
|
this.computeFiltersOptions();
|
|
|
|
this.applyFiltersAndSort();
|
|
|
|
this.loadingIndicator = false;
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private getImagesPackagesAndVolumes()
|
|
{
|
|
forkJoin({
|
|
images: this.catalogService.getImages(),
|
|
packages: this.catalogService.getPackages(),
|
|
volumes: this.volumesService.getVolumes()
|
|
})
|
|
.subscribe(response =>
|
|
{
|
|
this.images = response.images;
|
|
this.packages = response.packages;
|
|
this.volumes = response.volumes;
|
|
|
|
for (const instance of this.instances)
|
|
this.fillInInstanceDetails(instance);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private createForm()
|
|
{
|
|
this.editorForm = this.fb.group(
|
|
{
|
|
searchTerm: [''],
|
|
sortProperty: ['name'],
|
|
filters: this.fb.group(
|
|
{
|
|
stateFilter: [],
|
|
brandFilter: [],
|
|
typeFilter: [],
|
|
memoryFilter: [[0, 0]],
|
|
diskFilter: [[0, 0]],
|
|
imageFilter: [], // instances provisioned with a certain image
|
|
}),
|
|
filtersActive: [false],
|
|
showMachineDetails: [this.showMachineDetails],
|
|
fullDetailsTwoColumns: [{ value: this.fullDetailsTwoColumns, disabled: !this.showMachineDetails }]
|
|
});
|
|
|
|
this.editorForm.get('searchTerm').valueChanges
|
|
.pipe(
|
|
debounceTime(300),
|
|
distinctUntilChanged(),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe(() => this.applyFiltersAndSort());
|
|
|
|
this.editorForm.get('sortProperty').valueChanges
|
|
.pipe(
|
|
debounceTime(300),
|
|
distinctUntilChanged(),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe(() => this.applyFiltersAndSort());
|
|
|
|
this.editorForm.get('filters').valueChanges
|
|
.pipe(
|
|
debounceTime(300),
|
|
distinctUntilChanged(),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe(() => this.applyFiltersAndSort());
|
|
|
|
this.editorForm.get('showMachineDetails').valueChanges
|
|
.pipe(
|
|
debounceTime(300),
|
|
distinctUntilChanged(),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe(showMachineDetails =>
|
|
{
|
|
this.editorForm.get('showMachineDetails').disable();
|
|
|
|
// Performance hack
|
|
setTimeout(() => this.showMachineDetails = !!showMachineDetails, 0);
|
|
|
|
this.updateList();
|
|
|
|
// Store this setting in the local storage
|
|
localStorage.setItem('showMachineDetails', JSON.stringify(showMachineDetails));
|
|
|
|
setTimeout(() =>
|
|
{
|
|
this.editorForm.get('showMachineDetails').enable();
|
|
|
|
if (showMachineDetails)
|
|
this.editorForm.get('fullDetailsTwoColumns').enable();
|
|
else
|
|
this.editorForm.get('fullDetailsTwoColumns').disable();
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private applyFiltersAndSort()
|
|
{
|
|
let listItems: Instance[] = null;
|
|
|
|
const searchTerm = this.editorForm.get('searchTerm').value;
|
|
if (searchTerm.length >= 2)
|
|
{
|
|
const fuse = new Fuse(this.instances, this.fuseJsOptions);
|
|
const fuseResults = fuse.search(searchTerm);
|
|
listItems = fuseResults.map(x => x.item);
|
|
}
|
|
|
|
if (!listItems)
|
|
listItems = [...this.instances];
|
|
|
|
const stateFilter = this.editorForm.get(['filters', 'stateFilter']).value;
|
|
if (stateFilter)
|
|
{
|
|
listItems = listItems.filter(x => x.state === stateFilter);
|
|
this.editorForm.get('filtersActive').setValue(true);
|
|
}
|
|
|
|
const memoryFilter = this.editorForm.get(['filters', 'memoryFilter']).value;
|
|
if (memoryFilter.every(x => !!x))
|
|
{
|
|
listItems = listItems.filter(x => x.memory >= memoryFilter[0] && x.memory <= memoryFilter[1]);
|
|
//this.editorForm.get('filtersActive').setValue(true);
|
|
}
|
|
|
|
const diskFilter = this.editorForm.get(['filters', 'diskFilter']).value;
|
|
if (memoryFilter.every(x => !!x))
|
|
{
|
|
listItems = listItems.filter(x => x.disk >= diskFilter[0] && x.disk <= diskFilter[1]);
|
|
//this.editorForm.get('filtersActive').setValue(true);
|
|
}
|
|
|
|
this.listItems = sortArray(listItems, this.editorForm.get('sortProperty').value);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
clearFilters()
|
|
{
|
|
this.editorForm.get('filters').reset();
|
|
this.editorForm.get('filtersActive').setValue(false);
|
|
|
|
this.computeFiltersOptions();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
setSortProperty(propertyName: string)
|
|
{
|
|
this.editorForm.get('sortProperty').setValue(propertyName);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
setStateFilter(state?: string)
|
|
{
|
|
this.editorForm.get(['filters', 'stateFilter']).setValue(state);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
clearSearch()
|
|
{
|
|
this.editorForm.get('searchTerm').setValue('');
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
updateList()
|
|
{
|
|
this.virtualScroller.refresh();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
prepareForLoading(instances: Instance[])
|
|
{
|
|
for (const instance of instances)
|
|
instance.loading = true;
|
|
|
|
return instances;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
trackByFunction = (index: number, instance: Instance) => instance.name;
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private computeFiltersOptions(computeOnlyState = false)
|
|
{
|
|
this.runningInstanceCount = 0;
|
|
this.stoppedInstanceCount = 0;
|
|
this.instanceStateArray = [];
|
|
|
|
const memoryValues = {};
|
|
const diskValues = {};
|
|
|
|
for (const instance of this.instances)
|
|
{
|
|
if (instance.state === 'running')
|
|
this.runningInstanceCount++;
|
|
|
|
if (instance.state === 'stopped')
|
|
this.stoppedInstanceCount++;
|
|
|
|
if (!~this.instanceStateArray.indexOf(instance.state))
|
|
this.instanceStateArray.push(instance.state);
|
|
|
|
if (!computeOnlyState && !memoryValues[instance.memory])
|
|
memoryValues[instance.memory] = true;
|
|
|
|
if (!computeOnlyState && !diskValues[instance.disk])
|
|
diskValues[instance.disk] = true;
|
|
}
|
|
|
|
if (computeOnlyState)
|
|
return;
|
|
|
|
const memoryValuesArray = Object.keys(memoryValues);
|
|
this.memoryFilterOptions.stepsArray = memoryValuesArray.map(x => ({ legend: '', value: parseInt(x) }));
|
|
if (this.memoryFilterOptions.stepsArray.length)
|
|
this.editorForm.get(['filters', 'memoryFilter']).setValue([
|
|
this.memoryFilterOptions.stepsArray[0].value,
|
|
this.memoryFilterOptions.stepsArray[this.memoryFilterOptions.stepsArray.length - 1].value
|
|
]);
|
|
|
|
const diskValuesArray = Object.keys(diskValues);
|
|
this.diskFilterOptions.stepsArray = diskValuesArray.map(x => ({ legend: '', value: parseInt(x) }));
|
|
if (this.diskFilterOptions.stepsArray.length)
|
|
this.editorForm.get(['filters', 'diskFilter']).setValue([
|
|
this.diskFilterOptions.stepsArray[0].value,
|
|
this.diskFilterOptions.stepsArray[this.diskFilterOptions.stepsArray.length - 1].value
|
|
]);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
startMachine(instance: Instance)
|
|
{
|
|
if (instance.state !== 'stopped')
|
|
return;
|
|
|
|
instance.working = true;
|
|
this.toastr.info(`Starting machine "${instance.name}"...`);
|
|
|
|
this.instancesService.start(instance.id)
|
|
.pipe(
|
|
delay(1000),
|
|
switchMap(() =>
|
|
this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x =>
|
|
{
|
|
instance.state = x.state;
|
|
|
|
this.computeFiltersOptions();
|
|
})
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
)
|
|
.subscribe(() =>
|
|
{
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
this.toastr.info(`The machine "${instance.name}" has been started`);
|
|
}, err =>
|
|
{
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
restartMachine(instance: Instance)
|
|
{
|
|
if (instance.state !== 'running')
|
|
return;
|
|
|
|
instance.working = true;
|
|
this.toastr.info(`Restarting machine "${instance.name}"...`);
|
|
|
|
this.instancesService.reboot(instance.id)
|
|
.pipe(
|
|
delay(1000),
|
|
switchMap(() => this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x =>
|
|
{
|
|
instance.state = x.state;
|
|
|
|
this.computeFiltersOptions();
|
|
})
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
)
|
|
.subscribe(() =>
|
|
{
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
|
|
this.toastr.info(`The machine "${instance.name}" has been restarted`);
|
|
}, err =>
|
|
{
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
stopMachine(instance: Instance)
|
|
{
|
|
if (instance.state !== 'running')
|
|
return;
|
|
|
|
instance.working = true;
|
|
this.toastr.info(`Stopping machine "${instance.name}"`);
|
|
|
|
this.instancesService.stop(instance.id)
|
|
.pipe(
|
|
delay(1000),
|
|
switchMap(() => this.instancesService.getInstanceUntilExpectedState(instance, ['stopped'], x =>
|
|
{
|
|
instance.state = x.state;
|
|
|
|
this.computeFiltersOptions();
|
|
})
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
)
|
|
.subscribe(() =>
|
|
{
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
this.toastr.info(`The machine "${instance.name}" has been stopped`);
|
|
}, err =>
|
|
{
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
resizeMachine(instance: Instance)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: { instance }
|
|
};
|
|
|
|
const modalRef = this.modalService.show(PackageSelectorComponent, modalConfig);
|
|
modalRef.setClass('modal-lg');
|
|
|
|
modalRef.content.save
|
|
.pipe(
|
|
tap(() =>
|
|
{
|
|
this.toastr.info(`Changing specifications for machine "${instance.name}"...`);
|
|
instance.working = true;
|
|
}),
|
|
first(),
|
|
switchMap(pkg => this.instancesService.resize(instance.id, pkg.id).pipe(map(() => pkg)))
|
|
)
|
|
.subscribe(pkg =>
|
|
{
|
|
instance.package = pkg.name;
|
|
instance.memory = pkg.memory;
|
|
instance.disk = pkg.disk;
|
|
|
|
this.fillInInstanceDetails(instance);
|
|
|
|
this.computeFiltersOptions();
|
|
|
|
instance.working = false;
|
|
this.toastr.info(`The specifications for machine "${instance.name}" have been changed`);
|
|
}, err =>
|
|
{
|
|
instance.working = false;
|
|
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
|
|
this.toastr.error(`Couldn't change the specifications for machine "${instance.name}" ${errorDetails}`);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
renameMachine(instance: Instance)
|
|
{
|
|
const instanceName = instance.name;
|
|
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: {
|
|
value: instanceName,
|
|
required: true,
|
|
title: 'Rename machine',
|
|
prompt: 'Type in the new name for your machine',
|
|
placeholder: 'New machine name',
|
|
saveButtonText: 'Change machine name'
|
|
}
|
|
};
|
|
|
|
const modalRef = this.modalService.show(PromptDialogComponent, modalConfig);
|
|
|
|
modalRef.content.save.pipe(first()).subscribe(name =>
|
|
{
|
|
if (name === instanceName)
|
|
{
|
|
this.toastr.warning(`You provided the same name for machine "${instanceName}"`);
|
|
return;
|
|
}
|
|
|
|
instance.working = true;
|
|
|
|
this.instancesService.rename(instance.id, name)
|
|
.subscribe(() =>
|
|
{
|
|
instance.name = name;
|
|
|
|
this.applyFiltersAndSort();
|
|
|
|
this.toastr.info(`The "${instanceName}" machine has been renamed to "${instance.name}"`);
|
|
instance.working = false;
|
|
}, err =>
|
|
{
|
|
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
|
|
this.toastr.error(`Couldn't rename the "${instanceName}" machine ${errorDetails}`);
|
|
instance.working = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
showTagEditor(instance: Instance, showMetadata = false)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: { instance, showMetadata }
|
|
};
|
|
|
|
const modalRef = this.modalService.show(InstanceTagEditorComponent, modalConfig);
|
|
modalRef.setClass('modal-lg');
|
|
|
|
modalRef.content.save.pipe(first()).subscribe(x =>
|
|
{
|
|
instance[showMetadata ? 'metadata' : 'tags'] = x;
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
createImageFromMachine(instance: Instance)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: { instance }
|
|
};
|
|
|
|
const modalRef = this.modalService.show(CustomImageEditorComponent, modalConfig);
|
|
|
|
modalRef.content.save.pipe(first()).subscribe(x =>
|
|
{
|
|
this.toastr.info(`Creating a new image based on the "${instance.name}" machine...`);
|
|
|
|
this.catalogService.createImage(instance.id, x.name, x.version, x.description)
|
|
.pipe(
|
|
delay(1000),
|
|
switchMap(image => this.catalogService.getImageUntilExpectedState(image, ['active', 'failed'])
|
|
.pipe(takeUntil(this.destroy$))
|
|
)
|
|
)
|
|
.subscribe(image =>
|
|
{
|
|
if (image.state === 'active')
|
|
this.toastr.info(`A new image "${x.name}" based on the "${instance.name}" machine has been created`);
|
|
else
|
|
this.toastr.error(`Failed to create an image based on the "${instance.name}" machine`);
|
|
}, err =>
|
|
{
|
|
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
|
|
this.toastr.error(`Failed to create an image based on the "${instance.name}" machine ${errorDetails}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
createMachine(instance?: Instance)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: { instance }
|
|
};
|
|
|
|
const modalRef = this.modalService.show(InstanceWizardComponent, modalConfig);
|
|
modalRef.setClass('modal-xl');
|
|
|
|
modalRef.content.save.pipe(first()).subscribe(x =>
|
|
{
|
|
if (!x) return;
|
|
|
|
this.fillInInstanceDetails(x);
|
|
|
|
this.instances.push(x);
|
|
|
|
this.computeFiltersOptions();
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
deleteMachine(instance: Instance)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: {
|
|
prompt: `Are you sure you wish to permanently delete the "${instance.name}" machine?`,
|
|
confirmButtonText: 'Yes, delete this machine',
|
|
declineButtonText: 'No, keep it',
|
|
confirmByDefault: false
|
|
}
|
|
};
|
|
|
|
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
|
|
|
|
modalRef.content.confirm.pipe(first()).subscribe(() =>
|
|
{
|
|
instance.working = true;
|
|
|
|
this.toastr.info(`Removing machine "${instance.name}"...`);
|
|
|
|
this.instancesService.delete(instance.id)
|
|
.subscribe(() =>
|
|
{
|
|
const index = this.instances.findIndex(i => i.id === instance.id);
|
|
if (index < 0) return;
|
|
|
|
this.instances.splice(index, 1);
|
|
|
|
this.computeFiltersOptions();
|
|
|
|
this.toastr.info(`The machine "${instance.name}" has been removed`);
|
|
},
|
|
err =>
|
|
{
|
|
instance.working = false;
|
|
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
showMachineHistory(instance: Instance)
|
|
{
|
|
const modalConfig = {
|
|
ignoreBackdropClick: true,
|
|
keyboard: false,
|
|
animated: true,
|
|
initialState: { instance }
|
|
};
|
|
|
|
const modalRef = this.modalService.show(InstanceHistoryComponent, modalConfig);
|
|
modalRef.setClass('modal-lg');
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
tabChanged(event, instance: Instance)
|
|
{
|
|
if (event.id.endsWith('info'))
|
|
instance.shouldLoadInfo = this.editorForm.get('showMachineDetails').value;
|
|
else if (event.id.endsWith('snapshots'))
|
|
instance.shouldLoadSnapshots = this.editorForm.get('showMachineDetails').value;
|
|
else if (event.id.endsWith('networks'))
|
|
instance.shouldLoadNetworks = this.editorForm.get('showMachineDetails').value;
|
|
else if (event.id.endsWith('volumes'))
|
|
{
|
|
//instance.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value;
|
|
}
|
|
else if (event.id.endsWith('migrations'))
|
|
{
|
|
//instance.shouldLoadMigrations = this.editorForm.get('showMachineDetails').value;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
loadInstanceDetails(instance: Instance): any
|
|
{
|
|
instance.loading = false;
|
|
|
|
instance.working = !this.stableStates.includes(instance.state);
|
|
|
|
// Keep polling the instances that are not in a "stable" state
|
|
if (instance.working)
|
|
this.instancesService.getInstanceUntilExpectedState(instance, this.stableStates, x =>
|
|
{
|
|
instance.state = x.state;
|
|
|
|
this.computeFiltersOptions(true);
|
|
})
|
|
.pipe(takeUntil(this.destroy$))
|
|
.subscribe(x =>
|
|
{
|
|
instance.working = false;
|
|
|
|
// Update the instance with what we got from the server
|
|
const index = this.instances.findIndex(i => i.id === instance.id);
|
|
if (index >= 0)
|
|
{
|
|
this.instances.splice(index, 1, x);
|
|
|
|
this.computeFiltersOptions();
|
|
}
|
|
}, err =>
|
|
{
|
|
if (err.status === 410)
|
|
{
|
|
const index = this.instances.findIndex(i => i.id === instance.id);
|
|
if (index >= 0)
|
|
{
|
|
this.instances.splice(index, 1);
|
|
|
|
this.computeFiltersOptions();
|
|
|
|
this.toastr.error(`The machine "${instance.name}" has been removed`);
|
|
}
|
|
}
|
|
else
|
|
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
|
|
|
|
instance.working = false;
|
|
});
|
|
|
|
instance.shouldLoadInfo = true;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
watchInstanceState(instance: Instance)
|
|
{
|
|
instance.working = true;
|
|
|
|
this.instancesService.getInstanceUntilExpectedState(instance, ['running'], x =>
|
|
{
|
|
instance.state = x.state;
|
|
|
|
this.computeFiltersOptions(true);
|
|
})
|
|
.pipe(
|
|
delay(3000),
|
|
takeUntil(this.destroy$)
|
|
).subscribe(() => { }, err =>
|
|
{
|
|
instance.working = false;
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
updateInstance(instance: Instance, updates: Instance)
|
|
{
|
|
instance.state = updates.state;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
setInstanceInfo(instance: Instance, dnsList)
|
|
{
|
|
// Update the instance as a result of the info panel's "load" event. We do this because the intances are (un)loaded
|
|
// from the viewport as the user scrolls through the page, to optimize memory consumption.
|
|
instance.dnsList = dnsList;
|
|
instance.infoLoaded = true;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
setInstanceNetworks(instance: Instance, nics)
|
|
{
|
|
// Update the instance as a result of the networks panel's "load" event. We do this because the intances are (un)loaded
|
|
// from the viewport as the user scrolls through the page, to optimize memory consumption.
|
|
instance.nics = nics;
|
|
instance.networksLoaded = true;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
setInstanceSnapshot(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
|
|
// from the viewport as the user scrolls through the page, to optimize memory consumption.
|
|
instance.snapshots = snapshots;
|
|
instance.snapshotsLoaded = true;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private fillInInstanceDetails(instance: Instance)
|
|
{
|
|
const imageDetails = this.images.find(i => i.id === instance.image);
|
|
if (imageDetails)
|
|
instance.imageDetails = imageDetails;
|
|
|
|
const packageDetails = this.packages.find(p => p.name === instance.package);
|
|
if (packageDetails)
|
|
instance.packageDetails = packageDetails;
|
|
|
|
instance.volumes = this.volumes.filter(i => i.refs && i.refs.includes(instance.id));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
private randomIntFromInterval(min, max)
|
|
{
|
|
// min and max included
|
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
ngOnInit(): void
|
|
{
|
|
this.getInstances();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
ngOnDestroy()
|
|
{
|
|
this.destroy$.next();
|
|
}
|
|
}
|