display packages based on the "group"
This commit is contained in:
parent
87da8fc49d
commit
4a100cf9d4
@ -5,6 +5,8 @@ import { Cacheable } from 'ts-cacheable';
|
||||
import { delay, filter, map, mergeMap, repeatWhen, take, tap } from 'rxjs/operators';
|
||||
import { CatalogPackage } from '../models/package';
|
||||
import { CatalogImage } from '../models/image';
|
||||
import { PackageGroupsEnum } from '../models/package-groups';
|
||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe';
|
||||
|
||||
const cacheBuster$ = new Subject<void>();
|
||||
const imagesCacheBuster$ = new Subject<void>();
|
||||
@ -15,7 +17,8 @@ const imagesCacheBuster$ = new Subject<void>();
|
||||
export class CatalogService
|
||||
{
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
constructor(private readonly httpClient: HttpClient) { }
|
||||
constructor(private readonly httpClient: HttpClient,
|
||||
private readonly fileSizePipe: FileSizePipe) { }
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@Cacheable({
|
||||
@ -43,9 +46,23 @@ export class CatalogService
|
||||
{
|
||||
return this.httpClient.get(`./assets/data/packages.json`).pipe(map(prices =>
|
||||
{
|
||||
packages.forEach(x => x.price = prices[x.id])
|
||||
let filteredPackages: CatalogPackage[] = [];
|
||||
|
||||
return packages;
|
||||
for (let pkg of packages)
|
||||
if (pkg.group === PackageGroupsEnum.Vm || pkg.group === PackageGroupsEnum.Infra)
|
||||
{
|
||||
pkg.price = prices[pkg.id];
|
||||
|
||||
let size = this.fileSizePipe.transform(pkg.memory * 1024 * 1024);
|
||||
[pkg.memorySize, pkg.memorySizeLabel] = size.split(' ');
|
||||
|
||||
size = this.fileSizePipe.transform(pkg.disk * 1024 * 1024);
|
||||
[pkg.diskSize, pkg.diskSizeLabel] = size.split(' ');
|
||||
|
||||
filteredPackages.push(pkg);
|
||||
}
|
||||
|
||||
return filteredPackages;
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
5
app/src/app/catalog/models/package-groups.ts
Normal file
5
app/src/app/catalog/models/package-groups.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum PackageGroupsEnum
|
||||
{
|
||||
Vm = 'Virtual machine',
|
||||
Infra = 'Infrastructure container'
|
||||
}
|
@ -5,16 +5,9 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!loadingIndicator">
|
||||
<div class="btn-group w-100" btnRadioGroup>
|
||||
<label [btnRadio]="group" class="btn" [class.active]="group === selectedPackageGroup" *ngFor="let group of packageGroups"
|
||||
(click)="setPackageGroup($event, group)">
|
||||
{{ group }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-flush flex-grow-1" *ngIf="packages">
|
||||
<ng-container *ngFor="let pkg of packages[selectedPackageGroup]">
|
||||
<a *ngIf="pkg.visible" class="list-group-item list-group-item-action d-flex align-items-center justify-content-between">
|
||||
<ng-container *ngFor="let pkg of packages">
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center justify-content-between" id="package-{{ pkg.id }}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="pkg-{{ pkg.id }}" name="pkg" [value]="pkg" [(ngModel)]="selectedPackage">
|
||||
<label class="form-check-label d-flex justify-content-between align-items-center pb-2" for="pkg-{{ pkg.id }}">
|
||||
@ -23,7 +16,6 @@
|
||||
<span class="h3 text-uppercase">
|
||||
{{ pkg.name }}
|
||||
<span class="price" *ngIf="pkg.price">{{ pkg.price | currency: 'USD': 'symbol': '1.0-2' }}/h</span>
|
||||
<!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>-->
|
||||
</span>
|
||||
<small class="text-faded pb-1 d-block">
|
||||
v<b>{{ pkg.version }}</b>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Component, OnInit, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, OnChanges, Input, Output, EventEmitter, SimpleChanges, ElementRef } from '@angular/core';
|
||||
import { OnDestroy } from '@angular/core/core';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { FileSizePipe } from '../../pipes/file-size.pipe';
|
||||
import { CatalogService } from '../helpers/catalog.service';
|
||||
import { CatalogImage } from '../../catalog/models/image';
|
||||
import { CatalogImageType } from '../models/image';
|
||||
import { CatalogPackage } from '../models/package';
|
||||
import { PackageGroupsEnum } from '../models/package-groups';
|
||||
|
||||
@Component({
|
||||
selector: 'app-packages',
|
||||
@ -15,7 +16,7 @@ import { CatalogImageType } from '../models/image';
|
||||
export class PackagesComponent implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
@Input()
|
||||
imageType: number;
|
||||
imageType: CatalogImageType;
|
||||
|
||||
@Input()
|
||||
image: CatalogImage;
|
||||
@ -26,57 +27,29 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
|
||||
@Output()
|
||||
select = new EventEmitter();
|
||||
|
||||
packageGroups: any[];
|
||||
loadingIndicator: boolean;
|
||||
selectedPackageGroup: string;
|
||||
|
||||
private packages: {};
|
||||
private _selectedPackage: {};
|
||||
packages: CatalogPackage[];
|
||||
|
||||
private _packages: CatalogPackage[];
|
||||
private _selectedPackage: CatalogPackage;
|
||||
private destroy$ = new Subject();
|
||||
private onChanges$ = new ReplaySubject();
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
constructor(private readonly catalogService: CatalogService,
|
||||
private readonly fileSizePipe: FileSizePipe)
|
||||
private readonly elementRef: ElementRef)
|
||||
{
|
||||
this.getPackages();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
setPackageGroup(event, packageGroup: string)
|
||||
{
|
||||
this.selectedPackageGroup = packageGroup;
|
||||
|
||||
if (!packageGroup) return;
|
||||
|
||||
switch (packageGroup)
|
||||
{
|
||||
case 'cpu':
|
||||
this.packages[packageGroup].sort((a, b) => (a.vcpus || 1) - (b.vcpus || 1));
|
||||
break;
|
||||
|
||||
case 'disk':
|
||||
this.packages[packageGroup].sort((a, b) => a.disk - b.disk);
|
||||
break;
|
||||
|
||||
case 'memory optimized':
|
||||
this.packages[packageGroup].sort((a, b) => a.memory - b.memory);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.packages[packageGroup].sort((a, b) => ((a.vcpus || 1) - (b.vcpus || 1)) || (a.memory - b.memory) || (a.disk - b.disk));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
set selectedPackage(value)
|
||||
set selectedPackage(value: CatalogPackage)
|
||||
{
|
||||
this._selectedPackage = value;
|
||||
|
||||
this.select.next(value);
|
||||
}
|
||||
get selectedPackage()
|
||||
get selectedPackage(): CatalogPackage
|
||||
{
|
||||
return this._selectedPackage;
|
||||
}
|
||||
@ -87,102 +60,50 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.loadingIndicator = true;
|
||||
|
||||
this.catalogService.getPackages()
|
||||
.subscribe(response =>
|
||||
{
|
||||
if (this.packages)
|
||||
return;
|
||||
|
||||
this.packages = response.reduce((groups, pkg) =>
|
||||
.subscribe(response =>
|
||||
{
|
||||
let size = this.fileSizePipe.transform(pkg.memory * 1024 * 1024);
|
||||
[pkg.memorySize, pkg.memorySizeLabel] = size.split(' ');
|
||||
this._packages = response;
|
||||
|
||||
size = this.fileSizePipe.transform(pkg.disk * 1024 * 1024);
|
||||
[pkg.diskSize, pkg.diskSizeLabel] = size.split(' ');
|
||||
this.setPackagesByImageType();
|
||||
|
||||
const groupName = pkg.group.toLowerCase() || 'standard';
|
||||
|
||||
const group = (groups[groupName] || []);
|
||||
group.push(pkg);
|
||||
groups[groupName] = group;
|
||||
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
this.setPackageGroups();
|
||||
});
|
||||
this.loadingIndicator = false;
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
private setPackageGroups()
|
||||
private setPackagesByImageType()
|
||||
{
|
||||
if (!this.packages || !this.image || !this.imageType)
|
||||
return;
|
||||
|
||||
// Setup the operating systems array-like object, sorted alphabetically
|
||||
this.packageGroups = Object.keys(this.packages)
|
||||
.filter(packageGroup =>
|
||||
this._selectedPackage = null;
|
||||
|
||||
this.packages = this._packages.filter(x =>
|
||||
{
|
||||
if (this.imageType === CatalogImageType.InfrastructureContainer && x.group === PackageGroupsEnum.Infra ||
|
||||
this.imageType === CatalogImageType.VirtualMachine && x.group === PackageGroupsEnum.Vm)
|
||||
{
|
||||
this.packages[packageGroup].forEach(p =>
|
||||
{
|
||||
if (p.name === this.package)
|
||||
this._selectedPackage = p;
|
||||
if (x.name === this.package)
|
||||
this._selectedPackage = x;
|
||||
|
||||
if (!p.brand || !this.image)
|
||||
{
|
||||
p.visible = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
p.visible = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.image.requirements.brand)
|
||||
p.visible = p.visible && this.image.requirements.brand === p.brand;
|
||||
return false;
|
||||
}).sort((a, b) =>
|
||||
{
|
||||
if (a.vcpus === b.vcpus && a.memory === b.memory)
|
||||
return a.memory > b.memory ? a.memory : b.memory;
|
||||
|
||||
if (this.image.type === 'zone-dataset')
|
||||
p.visible = p.visible && ['Spearhead', 'Spearhead-minimal'].includes(p.brand);
|
||||
if (a.memory === b.memory)
|
||||
return a.disk > b.disk ? a.disk : b.disk;
|
||||
|
||||
if (this.image.type === 'lx-dataset')
|
||||
p.visible = p.visible && p.brand === 'lx';
|
||||
return a.vcpus > b.vcpus ? a.vcpus : b.vcpus;
|
||||
});
|
||||
|
||||
if (this.image.type === 'zvol')
|
||||
p.visible = p.visible && ['bhyve', 'kvm'].includes(p.brand);
|
||||
|
||||
if (this.imageType === CatalogImageType.InfrastructureContainer)
|
||||
p.visible = p.visible && packageGroup === 'infrastructure container';
|
||||
else if (this.imageType === CatalogImageType.VirtualMachine)
|
||||
p.visible = p.visible && packageGroup === 'virtual machine';
|
||||
});
|
||||
|
||||
switch (this.imageType | 0)
|
||||
{
|
||||
case CatalogImageType.InfrastructureContainer:
|
||||
return this.packages[packageGroup].filter(x => x.visible).length &&
|
||||
(!packageGroup || ['cpu', 'disk', 'memory optimized', 'standard', 'triton'].includes(packageGroup));
|
||||
|
||||
case CatalogImageType.VirtualMachine:
|
||||
return this.packages[packageGroup].filter(x => x.visible).length &&
|
||||
(!packageGroup || ['standard', 'triton', 'bhyve'].includes(packageGroup));
|
||||
|
||||
case CatalogImageType.Custom:
|
||||
return this.packages[packageGroup].filter(x => x.visible).length &&
|
||||
packageGroup !== 'infrastructure container' && packageGroup !== 'virtual machine';
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
// Set the pre-selected package group
|
||||
this.selectedPackageGroup = this.packageGroups[0];
|
||||
|
||||
if (this.selectedPackage)
|
||||
this.select.emit(this.selectedPackage);
|
||||
|
||||
this.loadingIndicator = false;
|
||||
if (this._selectedPackage)
|
||||
setTimeout(() =>
|
||||
{
|
||||
this.elementRef.nativeElement.querySelector(`#package-${this._selectedPackage.id}`)
|
||||
.scrollIntoView({behavior:'auto', block: 'center'});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@ -192,7 +113,7 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
|
||||
.subscribe((changes: SimpleChanges) =>
|
||||
{
|
||||
if (changes.image?.currentValue && changes.imageType?.currentValue)
|
||||
this.setPackageGroups();
|
||||
this.setPackagesByImageType();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,11 +49,11 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-flush flex-grow-1" *ngIf="images">
|
||||
<div class="list-group-item list-group-item-action p-0 pe-2" *ngFor="let image of imageList">
|
||||
<div class="list-group list-group-flush flex-grow-1">
|
||||
<div class="list-group-item list-group-item-action p-0 pe-2" *ngFor="let image of imageList" id="image-{{ image.id }}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="image-{{ image.id }}" [value]="image" formControlName="image">
|
||||
<label class="form-check-label" for="image-{{ image.id }}">
|
||||
<input class="form-check-input" type="radio" id="img-{{ image.id }}" [value]="image" formControlName="image">
|
||||
<label class="form-check-label" for="img-{{ image.id }}">
|
||||
<small class="float-end d-flex align-items-center">
|
||||
<span class="text-faded me-1">
|
||||
<span class="d-block text-end">{{ image.published_at | timeago }}</span>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { BsModalRef } from 'ngx-bootstrap/modal';
|
||||
import { combineLatest, forkJoin, Subject } from 'rxjs';
|
||||
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray, ValidatorFn, ValidationErrors } from '@angular/forms';
|
||||
@ -43,7 +43,6 @@ export class MachineWizardComponent implements OnInit, OnDestroy
|
||||
dataCenters: any[];
|
||||
|
||||
loadingIndicator: boolean;
|
||||
loadingPackages: boolean;
|
||||
save = new Subject<Machine>();
|
||||
working: boolean;
|
||||
editorForm: FormGroup;
|
||||
@ -68,7 +67,8 @@ export class MachineWizardComponent implements OnInit, OnDestroy
|
||||
private readonly networkingService: NetworkingService,
|
||||
private readonly volumesService: VolumesService,
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly translateService: TranslateService)
|
||||
private readonly translateService: TranslateService,
|
||||
private readonly elementRef: ElementRef)
|
||||
{
|
||||
// When the user navigates away from this route, hide the modal
|
||||
router.events
|
||||
@ -257,8 +257,6 @@ export class MachineWizardComponent implements OnInit, OnDestroy
|
||||
|
||||
this.kvmRequired = x?.requirements['brand'] === 'kvm' || x?.type === 'zvol' || false;
|
||||
|
||||
this.loadingPackages = true;
|
||||
|
||||
this.computeEstimatedCost();
|
||||
});
|
||||
|
||||
@ -375,6 +373,13 @@ export class MachineWizardComponent implements OnInit, OnDestroy
|
||||
previousStep()
|
||||
{
|
||||
this.currentStep = this.currentStep > 1 ? this.currentStep - 1 : 1;
|
||||
|
||||
if (this.currentStep === 1)
|
||||
setTimeout(() =>
|
||||
{
|
||||
this.elementRef.nativeElement.querySelector(`#image-${this.editorForm.get('image').value.id}`)
|
||||
.scrollIntoView({behavior:'auto', block: 'center'});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@ -400,6 +405,7 @@ export class MachineWizardComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
setPackage(selection: any)
|
||||
{
|
||||
this.preselectedPackage = selection.name;
|
||||
this.steps[1].selection = selection;
|
||||
this.steps[1].complete = true;
|
||||
|
||||
|
@ -174,9 +174,9 @@
|
||||
placement="top" [adaptivePosition]="false"></fa-icon>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link text-info" [popover]="machineContextMenu" container="body"
|
||||
<button class="btn btn-link text-info" [popover]="machineContextMenu" container="body" (click)="machine.contextMenu = true"
|
||||
[popoverContext]="{ machine: machine }" placement="bottom left" containerClass="menu-dropdown"
|
||||
[outsideClick]="true">
|
||||
[outsideClick]="true" triggers="" [isOpen]="machine.contextMenu" (onHidden)="machine.contextMenu = false">
|
||||
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -388,6 +388,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
startMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
if (machine.state !== 'stopped')
|
||||
return;
|
||||
|
||||
@ -425,6 +427,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
restartMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
if (machine.state !== 'running')
|
||||
return;
|
||||
|
||||
@ -462,6 +466,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
stopMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
if (machine.state !== 'running')
|
||||
return;
|
||||
|
||||
@ -498,6 +504,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
resizeMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
const modalConfig = {
|
||||
ignoreBackdropClick: true,
|
||||
keyboard: false,
|
||||
@ -541,6 +549,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
renameMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
const machineName = machine.name;
|
||||
|
||||
const modalConfig = {
|
||||
@ -590,6 +600,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
showTagEditor(machine: Machine, showMetadata = false)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
const modalConfig = {
|
||||
ignoreBackdropClick: true,
|
||||
keyboard: false,
|
||||
@ -609,6 +621,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
createImageFromMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
const modalConfig = {
|
||||
ignoreBackdropClick: true,
|
||||
keyboard: false,
|
||||
@ -673,6 +687,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
deleteMachine(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
const modalConfig = {
|
||||
ignoreBackdropClick: true,
|
||||
keyboard: false,
|
||||
@ -716,6 +732,8 @@ export class MachinesComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
showMachineHistory(machine: Machine)
|
||||
{
|
||||
machine.contextMenu = false;
|
||||
|
||||
const modalConfig = {
|
||||
ignoreBackdropClick: true,
|
||||
keyboard: false,
|
||||
|
@ -51,4 +51,5 @@ export class Machine extends MachineRequest
|
||||
volumesEnabled: boolean;
|
||||
metadataKeys: string[];
|
||||
tagKeys: string[];
|
||||
contextMenu: boolean;
|
||||
}
|
||||
|
@ -547,11 +547,10 @@ accordion
|
||||
.price
|
||||
{
|
||||
color: #cd5c5c;
|
||||
vertical-align: middle;
|
||||
padding: 0 .5rem;
|
||||
margin-bottom: .25rem;
|
||||
display: inline-block;
|
||||
text-transform: none;
|
||||
font-size: 1rem;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.badge-discreet
|
||||
|
Reference in New Issue
Block a user