display packages based on the "group"

This commit is contained in:
Dragos 2021-06-01 14:23:30 +03:00
parent 87da8fc49d
commit 4a100cf9d4
10 changed files with 108 additions and 149 deletions

View File

@ -5,6 +5,8 @@ import { Cacheable } from 'ts-cacheable';
import { delay, filter, map, mergeMap, repeatWhen, take, tap } from 'rxjs/operators'; import { delay, filter, map, mergeMap, repeatWhen, take, tap } from 'rxjs/operators';
import { CatalogPackage } from '../models/package'; import { CatalogPackage } from '../models/package';
import { CatalogImage } from '../models/image'; 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 cacheBuster$ = new Subject<void>();
const imagesCacheBuster$ = new Subject<void>(); const imagesCacheBuster$ = new Subject<void>();
@ -15,7 +17,8 @@ const imagesCacheBuster$ = new Subject<void>();
export class CatalogService export class CatalogService
{ {
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly httpClient: HttpClient) { } constructor(private readonly httpClient: HttpClient,
private readonly fileSizePipe: FileSizePipe) { }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
@Cacheable({ @Cacheable({
@ -43,9 +46,23 @@ export class CatalogService
{ {
return this.httpClient.get(`./assets/data/packages.json`).pipe(map(prices => 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;
})) }))
})); }));
} }

View File

@ -0,0 +1,5 @@
export enum PackageGroupsEnum
{
Vm = 'Virtual machine',
Infra = 'Infrastructure container'
}

View File

@ -5,16 +5,9 @@
</div> </div>
<ng-container *ngIf="!loadingIndicator"> <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"> <div class="list-group list-group-flush flex-grow-1" *ngIf="packages">
<ng-container *ngFor="let pkg of packages[selectedPackageGroup]"> <ng-container *ngFor="let pkg of packages">
<a *ngIf="pkg.visible" class="list-group-item list-group-item-action d-flex align-items-center justify-content-between"> <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"> <div class="form-check">
<input class="form-check-input" type="radio" id="pkg-{{ pkg.id }}" name="pkg" [value]="pkg" [(ngModel)]="selectedPackage"> <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 }}"> <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"> <span class="h3 text-uppercase">
{{ pkg.name }} {{ pkg.name }}
<span class="price" *ngIf="pkg.price">{{ pkg.price | currency: 'USD': 'symbol': '1.0-2' }}/h</span> <span class="price" *ngIf="pkg.price">{{ pkg.price | currency: 'USD': 'symbol': '1.0-2' }}/h</span>
<!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>-->
</span> </span>
<small class="text-faded pb-1 d-block"> <small class="text-faded pb-1 d-block">
v<b>{{ pkg.version }}</b> v<b>{{ pkg.version }}</b>

View File

@ -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 { OnDestroy } from '@angular/core/core';
import { ReplaySubject, Subject } from 'rxjs'; import { ReplaySubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { FileSizePipe } from '../../pipes/file-size.pipe';
import { CatalogService } from '../helpers/catalog.service'; import { CatalogService } from '../helpers/catalog.service';
import { CatalogImage } from '../../catalog/models/image'; import { CatalogImage } from '../../catalog/models/image';
import { CatalogImageType } from '../models/image'; import { CatalogImageType } from '../models/image';
import { CatalogPackage } from '../models/package';
import { PackageGroupsEnum } from '../models/package-groups';
@Component({ @Component({
selector: 'app-packages', selector: 'app-packages',
@ -15,7 +16,7 @@ import { CatalogImageType } from '../models/image';
export class PackagesComponent implements OnInit, OnDestroy, OnChanges export class PackagesComponent implements OnInit, OnDestroy, OnChanges
{ {
@Input() @Input()
imageType: number; imageType: CatalogImageType;
@Input() @Input()
image: CatalogImage; image: CatalogImage;
@ -26,57 +27,29 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
@Output() @Output()
select = new EventEmitter(); select = new EventEmitter();
packageGroups: any[];
loadingIndicator: boolean; loadingIndicator: boolean;
selectedPackageGroup: string; packages: CatalogPackage[];
private packages: {}; private _packages: CatalogPackage[];
private _selectedPackage: {}; private _selectedPackage: CatalogPackage;
private destroy$ = new Subject(); private destroy$ = new Subject();
private onChanges$ = new ReplaySubject(); private onChanges$ = new ReplaySubject();
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly catalogService: CatalogService, constructor(private readonly catalogService: CatalogService,
private readonly fileSizePipe: FileSizePipe) private readonly elementRef: ElementRef)
{ {
this.getPackages(); this.getPackages();
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
setPackageGroup(event, packageGroup: string) set selectedPackage(value: CatalogPackage)
{
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)
{ {
this._selectedPackage = value; this._selectedPackage = value;
this.select.next(value); this.select.next(value);
} }
get selectedPackage() get selectedPackage(): CatalogPackage
{ {
return this._selectedPackage; return this._selectedPackage;
} }
@ -89,100 +62,48 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
this.catalogService.getPackages() this.catalogService.getPackages()
.subscribe(response => .subscribe(response =>
{ {
if (this.packages) this._packages = response;
return;
this.packages = response.reduce((groups, pkg) => this.setPackagesByImageType();
{
let size = this.fileSizePipe.transform(pkg.memory * 1024 * 1024);
[pkg.memorySize, pkg.memorySizeLabel] = size.split(' ');
size = this.fileSizePipe.transform(pkg.disk * 1024 * 1024); this.loadingIndicator = false;
[pkg.diskSize, pkg.diskSizeLabel] = size.split(' ');
const groupName = pkg.group.toLowerCase() || 'standard';
const group = (groups[groupName] || []);
group.push(pkg);
groups[groupName] = group;
return groups;
}, {});
this.setPackageGroups();
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private setPackageGroups() private setPackagesByImageType()
{ {
if (!this.packages || !this.image || !this.imageType) this._selectedPackage = null;
return;
// Setup the operating systems array-like object, sorted alphabetically this.packages = this._packages.filter(x =>
this.packageGroups = Object.keys(this.packages)
.filter(packageGroup =>
{ {
this.packages[packageGroup].forEach(p => if (this.imageType === CatalogImageType.InfrastructureContainer && x.group === PackageGroupsEnum.Infra ||
this.imageType === CatalogImageType.VirtualMachine && x.group === PackageGroupsEnum.Vm)
{ {
if (p.name === this.package) if (x.name === this.package)
this._selectedPackage = p; this._selectedPackage = x;
if (!p.brand || !this.image) return true;
{
p.visible = true;
return;
}
else
{
p.visible = true;
} }
if (this.image.requirements.brand) return false;
p.visible = p.visible && this.image.requirements.brand === p.brand; }).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') if (a.memory === b.memory)
p.visible = p.visible && ['Spearhead', 'Spearhead-minimal'].includes(p.brand); return a.disk > b.disk ? a.disk : b.disk;
if (this.image.type === 'lx-dataset') return a.vcpus > b.vcpus ? a.vcpus : b.vcpus;
p.visible = p.visible && p.brand === 'lx';
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) if (this._selectedPackage)
setTimeout(() =>
{ {
case CatalogImageType.InfrastructureContainer: this.elementRef.nativeElement.querySelector(`#package-${this._selectedPackage.id}`)
return this.packages[packageGroup].filter(x => x.visible).length && .scrollIntoView({behavior:'auto', block: 'center'});
(!packageGroup || ['cpu', 'disk', 'memory optimized', 'standard', 'triton'].includes(packageGroup)); }, 0);
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;
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
@ -192,7 +113,7 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
.subscribe((changes: SimpleChanges) => .subscribe((changes: SimpleChanges) =>
{ {
if (changes.image?.currentValue && changes.imageType?.currentValue) if (changes.image?.currentValue && changes.imageType?.currentValue)
this.setPackageGroups(); this.setPackagesByImageType();
}); });
} }

View File

@ -49,11 +49,11 @@
</label> </label>
</div> </div>
<div class="list-group list-group-flush flex-grow-1" *ngIf="images"> <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"> <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"> <div class="form-check">
<input class="form-check-input" type="radio" id="image-{{ image.id }}" [value]="image" formControlName="image"> <input class="form-check-input" type="radio" id="img-{{ image.id }}" [value]="image" formControlName="image">
<label class="form-check-label" for="image-{{ image.id }}"> <label class="form-check-label" for="img-{{ image.id }}">
<small class="float-end d-flex align-items-center"> <small class="float-end d-flex align-items-center">
<span class="text-faded me-1"> <span class="text-faded me-1">
<span class="d-block text-end">{{ image.published_at | timeago }}</span> <span class="d-block text-end">{{ image.published_at | timeago }}</span>

View File

@ -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 { BsModalRef } from 'ngx-bootstrap/modal';
import { combineLatest, forkJoin, Subject } from 'rxjs'; import { combineLatest, forkJoin, Subject } from 'rxjs';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray, ValidatorFn, ValidationErrors } from '@angular/forms'; import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray, ValidatorFn, ValidationErrors } from '@angular/forms';
@ -43,7 +43,6 @@ export class MachineWizardComponent implements OnInit, OnDestroy
dataCenters: any[]; dataCenters: any[];
loadingIndicator: boolean; loadingIndicator: boolean;
loadingPackages: boolean;
save = new Subject<Machine>(); save = new Subject<Machine>();
working: boolean; working: boolean;
editorForm: FormGroup; editorForm: FormGroup;
@ -68,7 +67,8 @@ export class MachineWizardComponent implements OnInit, OnDestroy
private readonly networkingService: NetworkingService, private readonly networkingService: NetworkingService,
private readonly volumesService: VolumesService, private readonly volumesService: VolumesService,
private readonly toastr: ToastrService, 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 // When the user navigates away from this route, hide the modal
router.events router.events
@ -257,8 +257,6 @@ export class MachineWizardComponent implements OnInit, OnDestroy
this.kvmRequired = x?.requirements['brand'] === 'kvm' || x?.type === 'zvol' || false; this.kvmRequired = x?.requirements['brand'] === 'kvm' || x?.type === 'zvol' || false;
this.loadingPackages = true;
this.computeEstimatedCost(); this.computeEstimatedCost();
}); });
@ -375,6 +373,13 @@ export class MachineWizardComponent implements OnInit, OnDestroy
previousStep() previousStep()
{ {
this.currentStep = this.currentStep > 1 ? this.currentStep - 1 : 1; 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) setPackage(selection: any)
{ {
this.preselectedPackage = selection.name;
this.steps[1].selection = selection; this.steps[1].selection = selection;
this.steps[1].complete = true; this.steps[1].complete = true;

View File

@ -174,9 +174,9 @@
placement="top" [adaptivePosition]="false"></fa-icon> placement="top" [adaptivePosition]="false"></fa-icon>
</button> </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" [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> <fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
</button> </button>
</div> </div>

View File

@ -388,6 +388,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
startMachine(machine: Machine) startMachine(machine: Machine)
{ {
machine.contextMenu = false;
if (machine.state !== 'stopped') if (machine.state !== 'stopped')
return; return;
@ -425,6 +427,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
restartMachine(machine: Machine) restartMachine(machine: Machine)
{ {
machine.contextMenu = false;
if (machine.state !== 'running') if (machine.state !== 'running')
return; return;
@ -462,6 +466,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
stopMachine(machine: Machine) stopMachine(machine: Machine)
{ {
machine.contextMenu = false;
if (machine.state !== 'running') if (machine.state !== 'running')
return; return;
@ -498,6 +504,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
resizeMachine(machine: Machine) resizeMachine(machine: Machine)
{ {
machine.contextMenu = false;
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
@ -541,6 +549,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
renameMachine(machine: Machine) renameMachine(machine: Machine)
{ {
machine.contextMenu = false;
const machineName = machine.name; const machineName = machine.name;
const modalConfig = { const modalConfig = {
@ -590,6 +600,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
showTagEditor(machine: Machine, showMetadata = false) showTagEditor(machine: Machine, showMetadata = false)
{ {
machine.contextMenu = false;
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
@ -609,6 +621,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
createImageFromMachine(machine: Machine) createImageFromMachine(machine: Machine)
{ {
machine.contextMenu = false;
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
@ -673,6 +687,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
deleteMachine(machine: Machine) deleteMachine(machine: Machine)
{ {
machine.contextMenu = false;
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,
@ -716,6 +732,8 @@ export class MachinesComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
showMachineHistory(machine: Machine) showMachineHistory(machine: Machine)
{ {
machine.contextMenu = false;
const modalConfig = { const modalConfig = {
ignoreBackdropClick: true, ignoreBackdropClick: true,
keyboard: false, keyboard: false,

View File

@ -51,4 +51,5 @@ export class Machine extends MachineRequest
volumesEnabled: boolean; volumesEnabled: boolean;
metadataKeys: string[]; metadataKeys: string[];
tagKeys: string[]; tagKeys: string[];
contextMenu: boolean;
} }

View File

@ -547,11 +547,10 @@ accordion
.price .price
{ {
color: #cd5c5c; color: #cd5c5c;
vertical-align: middle;
padding: 0 .5rem;
margin-bottom: .25rem;
display: inline-block; display: inline-block;
text-transform: none; text-transform: none;
font-size: 1rem;
vertical-align: baseline;
} }
.badge-discreet .badge-discreet