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

View File

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

View File

@ -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>

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

View File

@ -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>

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 { 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;

View File

@ -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>

View File

@ -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,

View File

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

View File

@ -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