fixed bugs reported by Marsell

This commit is contained in:
Dragos 2021-05-06 19:21:09 +03:00
parent 6fe512f051
commit 621e223b57
36 changed files with 1286 additions and 144 deletions

View File

@ -0,0 +1,16 @@
{
"ExpandedNodes": [
"",
"\\src",
"\\src\\app",
"\\src\\app\\instances",
"\\src\\app\\instances\\instance-info",
"\\src\\app\\instances\\instance-networks",
"\\src\\app\\instances\\instance-security",
"\\src\\app\\networking",
"\\src\\app\\networking\\helpers",
"\\src\\app\\networking\\virtual-network-editor"
],
"SelectedNode": "\\src\\app\\networking\\networks\\networks.component.ts",
"PreviewInSolutionExplorer": false
}

File diff suppressed because it is too large Load Diff

BIN
app/.vs/app/v16/.suo Normal file

Binary file not shown.

BIN
app/.vs/slnx.sqlite Normal file

Binary file not shown.

View File

@ -82,7 +82,6 @@
<th>Type</th> <th>Type</th>
<th>Brand</th> <th>Brand</th>
<th>Publish date</th> <th>Publish date</th>
<th>Status</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -101,14 +100,11 @@
{{ image.type }} {{ image.type }}
</td> </td>
<td class="text-uppercase"> <td class="text-uppercase">
<span class="badge text-warning border border-warning" *ngIf="image.requirements">{{ image.requirements.brand }}</span> <span class="badge badge-discreet text-uppercase" *ngIf="image.requirements">{{ image.requirements.brand }}</span>
</td> </td>
<td> <td>
{{ image.published_at ? (image.published_at | timeago) : '' }} {{ image.published_at ? (image.published_at | timeago) : '' }}
</td> </td>
<td>
<span class="badge text-uppercase" [ngClass]="image.state === 'active' ? 'bg-success' : 'bg-warning text-dark'">{{ image.state }}</span>
</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body" [isDisabled]="image.working"> <div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body" [isDisabled]="image.working">
<button class="btn btn-link text-info" dropdownToggle <button class="btn btn-link text-info" dropdownToggle

View File

@ -25,7 +25,10 @@
<span class="price" *ngIf="pkg.price">{{ pkg.price | currency }}/h</span> <span class="price" *ngIf="pkg.price">{{ pkg.price | currency }}/h</span>
<!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>--> <!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>-->
</span> </span>
<small class="text-faded pb-1 d-block">v<b>{{ pkg.version }}</b></small> <small class="text-faded pb-1 d-block">
v<b>{{ pkg.version }}</b>
<small class="badge badge-discreet ms-2" *ngIf="pkg.flexible_disk">Flexible Disk</small>
</small>
<small class="mb-0 pe-3 text-faded" *ngIf="pkg.description">{{ pkg.description }}</small> <small class="mb-0 pe-3 text-faded" *ngIf="pkg.description">{{ pkg.description }}</small>
</span> </span>
</span> </span>

View File

@ -152,7 +152,7 @@ export class PackagesComponent implements OnInit, OnDestroy, OnChanges
return this.packages[x].length && (!x || ['cpu', 'disk', 'memory optimized', 'standard', 'triton'].includes(x)); return this.packages[x].length && (!x || ['cpu', 'disk', 'memory optimized', 'standard', 'triton'].includes(x));
case 2: case 2:
return this.packages[x].length && (!x || ['standard', 'triton'].includes(x)); return this.packages[x].length && (!x || ['standard', 'triton', 'bhyve'].includes(x));
default: default:
return false; return false;

View File

@ -121,7 +121,7 @@ export class InlineEditorComponent implements OnInit, OnDestroy
@HostListener('document:keydown.enter', ['$event']) @HostListener('document:keydown.enter', ['$event'])
returnPressed(event) returnPressed(event)
{ {
if (event.currentTarget === this.elementRef.nativeElement && this.singleLine) if (event.target === this.elementRef.nativeElement.querySelector('textarea') && this.singleLine)
{ {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { forkJoin, from, Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Instance } from '../models/instance'; import { Instance } from '../models/instance';
import { delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators'; import { concatMap, delay, filter, map, repeatWhen, take, tap } from 'rxjs/operators';
import { InstanceRequest } from '../models/instance'; import { InstanceRequest } from '../models/instance';
import { Cacheable } from 'ts-cacheable'; import { Cacheable } from 'ts-cacheable';
import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service'; import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service';
@ -179,16 +179,30 @@ export class InstancesService
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata/${key}`); return this.httpClient.get(`/api/my/machines/${instanceId}/metadata/${key}`);
} }
// ----------------------------------------------------------------------------------------------------------------
addMetadata(instanceId: string, metadata: any): Observable<any>
{
return this.httpClient.post(`/api/my/machines/${instanceId}/metadata`, metadata);
}
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
replaceMetadata(instanceId: string, metadata: any): Observable<any> replaceMetadata(instanceId: string, metadata: any): Observable<any>
{ {
return this.httpClient.put(`/api/my/machines/${instanceId}/metadata`, metadata); // First retrieve current metadata
return this.httpClient.get(`/api/my/machines/${instanceId}/metadata`)
.pipe(concatMap(existingMetadata =>
{
// Compute which metadata the user chose to remove
const obsoleteMetadata: Observable<any>[] = [];
for (const key of Object.keys(existingMetadata))
if (!metadata.hasOwnProperty(key) && key !== 'root_authorized_keys') // root_authorized_keys is readonly
obsoleteMetadata.push(this.httpClient.delete(`/api/my/machines/${instanceId}/metadata/${key}`));
// Any metadata keys passed in here are created if they do not exist, and overwritten if they do.
const metadataToUpsert = this.httpClient.post(`/api/my/machines/${instanceId}/metadata`, metadata);
if (obsoleteMetadata.length)
{
// In multiple concurrent requests delete the obsolete metadata, then upsert the remaining ones
return forkJoin(obsoleteMetadata).pipe(concatMap(() => metadataToUpsert.pipe(map(() => metadata))));
}
else
return metadataToUpsert;
}));
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
@ -197,12 +211,6 @@ export class InstancesService
return this.httpClient.delete(`/api/my/machines/${instanceId}/metadata`); return this.httpClient.delete(`/api/my/machines/${instanceId}/metadata`);
} }
// ----------------------------------------------------------------------------------------------------------------
deleteMetadata(instanceId: string, key: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/metadata/${key}`);
}
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
getAudit(instanceId: string): Observable<any> getAudit(instanceId: string): Observable<any>
{ {

View File

@ -1,3 +1,5 @@
@import "../../../styles/_variables.scss";
.list-group-item .list-group-item
{ {
background: none; background: none;
@ -10,7 +12,7 @@
&:after &:after
{ {
content: '.'; content: '.';
color: #3d5e8e; color: $table-header-color;
} }
} }
@ -34,6 +36,6 @@
.dropdown-header .dropdown-header
{ {
opacity: 1; opacity: 1;
color: #3d5e8e; color: $table-header-color;
padding: .5rem 0 0; padding: .5rem 0 0;
} }

View File

@ -1,7 +1,9 @@
@import "../../../styles/_variables.scss";
.list-group-item .list-group-item
{ {
background: none; background: none;
color: #3d5e8e; color: $table-header-color;
.highlight .highlight
{ {

View File

@ -1,7 +1,9 @@
@import "../../../styles/_variables.scss";
.list-group-item .list-group-item
{ {
background: none; background: none;
color: #3d5e8e; color: $table-header-color;
span span
{ {

View File

@ -1,7 +1,9 @@
@import "../../../styles/_variables.scss";
.list-group-item .list-group-item
{ {
background: none; background: none;
color: #3d5e8e; color: $table-header-color;
span span
{ {

View File

@ -19,7 +19,7 @@
[appAutofocus]="focus" [appAutofocusDelay]="focus === 1 ? 600 : 0" /> [appAutofocus]="focus" [appAutofocusDelay]="focus === 1 ? 600 : 0" />
<input *ngIf="!showMetadata" class="form-control" type="text" formControlName="value" placeholder="Value" /> <input *ngIf="!showMetadata" class="form-control" type="text" formControlName="value" placeholder="Value" />
<textarea *ngIf="showMetadata" class="form-control" rows="2" formControlName="value" placeholder="Value"></textarea> <textarea *ngIf="showMetadata" class="form-control" rows="2" formControlName="value" placeholder="Value"></textarea>
<button class="btn btn-outline-info" (click)="addTag()" [disabled]="editorForm.invalid"> <button class="btn btn-outline-info" (click)="addTag()" [disabled]="!editorForm.get('key').value || !editorForm.get('value').value">
<span *ngIf="showMetadata">Add metadata</span> <span *ngIf="showMetadata">Add metadata</span>
<span *ngIf="!showMetadata">Add tag</span> <span *ngIf="!showMetadata">Add tag</span>
</button> </button>
@ -35,7 +35,7 @@
</div> </div>
<div> <div>
<button class="btn btn-link text-danger" (click)="deleteTag(index)" <button class="btn btn-link text-danger" (click)="deleteTag(index)" *ngIf="item.get('key').value !== 'root_authorized_keys'"
[tooltip]="showMetadata ? 'Remove this metadata' : 'Remove this tag'" container="body" placement="top" [adaptivePosition]="false"> [tooltip]="showMetadata ? 'Remove this metadata' : 'Remove this tag'" container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="times" [fixedWidth]="true" size="sm"></fa-icon> <fa-icon icon="times" [fixedWidth]="true" size="sm"></fa-icon>
</button> </button>
@ -47,7 +47,10 @@
<div class="d-flex justify-content-end align-items-center mt-5"> <div class="d-flex justify-content-end align-items-center mt-5">
<button class="btn btn-link text-info me-3" (click)="close()">Close without saving</button> <button class="btn btn-link text-info me-3" (click)="close()">Close without saving</button>
<button class="btn btn-info" (click)="saveChanges()">Save changes</button> <button class="btn btn-info" (click)="saveChanges()" [disabled]="editorForm.invalid">
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1" *ngIf="working"></fa-icon>
Save changes
</button>
</div> </div>
</div> </div>
</fieldset> </fieldset>

View File

@ -6,6 +6,7 @@ import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators'; import { filter, takeUntil } from 'rxjs/operators';
import { InstancesService } from '../helpers/instances.service'; import { InstancesService } from '../helpers/instances.service';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { Instance } from '../models/instance';
@Component({ @Component({
selector: 'app-instance-tag-editor', selector: 'app-instance-tag-editor',
@ -15,7 +16,7 @@ import { ToastrService } from 'ngx-toastr';
export class InstanceTagEditorComponent implements OnInit export class InstanceTagEditorComponent implements OnInit
{ {
@Input() @Input()
instance: any; instance: Instance;
@Input() @Input()
showMetadata: boolean; showMetadata: boolean;
@ -48,14 +49,20 @@ export class InstanceTagEditorComponent implements OnInit
private createForm() private createForm()
{ {
const items = this.fb.array(this.showMetadata const items = this.fb.array(this.showMetadata
? Object.keys(this.instance.metadata).map(key => this.fb.group({ key, value: this.instance.metadata[key] })) ? Object.keys(this.instance.metadata).map(key => this.fb.group({
: Object.keys(this.instance.tags).map(key => this.fb.group({ key, value: this.instance.tags[key] })) key: [key, Validators.required],
value: [this.instance.metadata[key], Validators.required]
}))
: Object.keys(this.instance.tags).map(key => this.fb.group({
key: [key, Validators.required],
value: [this.instance.tags[key], Validators.required]
}))
); );
this.editorForm = this.fb.group({ this.editorForm = this.fb.group({
items, items,
key: [null, Validators.required], key: [null],
value: [null, Validators.required] value: [null]
}); });
} }
@ -91,13 +98,14 @@ export class InstanceTagEditorComponent implements OnInit
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
close() close()
{ {
this.save.next();
this.modalRef.hide(); this.modalRef.hide();
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
saveChanges() saveChanges()
{ {
this.working = true;
const items = this.editorForm.getRawValue().items.reduce((map, item) => const items = this.editorForm.getRawValue().items.reduce((map, item) =>
{ {
map[item.key] = item.value; map[item.key] = item.value;
@ -110,17 +118,21 @@ export class InstanceTagEditorComponent implements OnInit
observable.subscribe(response => observable.subscribe(response =>
{ {
this.working = false;
this.save.next(response); this.save.next(response);
this.modalRef.hide(); this.modalRef.hide();
}, err => }, err =>
{ {
this.toastr.error(err.error.message); this.toastr.error(err.error.message);
this.working = false;
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void ngOnInit(): void
{ {
//this.instancesService.getTags(this.instance.id).subscribe();
this.createForm(); this.createForm();
} }
} }

View File

@ -110,7 +110,7 @@
<div class="form-check" *ngFor="let network of editorForm.get('networks')['controls']; let index = index" [formGroupName]="index"> <div class="form-check" *ngFor="let network of editorForm.get('networks')['controls']; let index = index" [formGroupName]="index">
<input class="form-check-input" type="checkbox" [value]="network.get('id')" id="network-{{ network.get('id').value }}" formControlName="selected"> <input class="form-check-input" type="checkbox" [value]="network.get('id')" id="network-{{ network.get('id').value }}" formControlName="selected">
<label class="form-check-label text-truncate" for="network-{{ network.get('id').value }}"> <label class="form-check-label text-truncate" for="network-{{ network.get('id').value }}">
<small class="badge text-secondary border float-end" *ngIf="network.get('public').value">public</small> <small class="badge badge-discreet float-end" *ngIf="network.get('public').value">public</small>
{{ network.get('name').value }} {{ network.get('name').value }}
<small class="text-faded">{{ network.get('description').value }}</small> <small class="text-faded">{{ network.get('description').value }}</small>
</label> </label>
@ -142,42 +142,43 @@
<!-- Volumes and disks --> <!-- Volumes and disks -->
<div class="mt-3 d-flex flex-column"> <div class="mt-3 d-flex flex-column">
<button class="btn btn-outline-info text-start" (click)="showVolumes = !showVolumes"> <button class="btn btn-outline-info text-start mb-2" (click)="showVolumes = !showVolumes">
Choose the <b>volumes</b> you wish to mount Volumes
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="showVolumes ? 90 : 0" class="float-end"></fa-icon> <fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="showVolumes ? 90 : 0" class="float-end"></fa-icon>
</button> </button>
<div [collapse]="!showVolumes"> <div [collapse]="!showVolumes">
<h5>Choose the <b>volumes</b> you wish to mount</h5>
<div class="select-list flex-grow-1" formArrayName="volumes" tabindex="0" *ngIf="!kvmRequired"> <div class="select-list flex-grow-1" formArrayName="volumes" tabindex="0" *ngIf="!kvmRequired">
<table class="table mb-0"> <table class="table mb-0">
<thead> <thead>
<tr> <tr>
<th>Volume name</th> <th>Volume name</th>
<th>Mount point</th> <th>Mount point</th>
<th class="text-end">Read only</th> <th class="text-end">Read only</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let volume of editorForm.get('volumes')['controls']; let index = index" [formGroupName]="index"> <tr *ngFor="let volume of editorForm.get('volumes')['controls']; let index = index" [formGroupName]="index">
<td> <td>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="vol-mount-{{ volume.get('name').value }}" formControlName="mount"> <input class="form-check-input" type="checkbox" id="vol-mount-{{ volume.get('name').value }}" formControlName="mount">
<label class="form-check-label" for="vol-mount-{{ volume.get('name').value }}"> <label class="form-check-label" for="vol-mount-{{ volume.get('name').value }}">
{{ volume.get('name').value }} {{ volume.get('name').value }}
</label> </label>
</div> </div>
</td> </td>
<td> <td>
<input type="text" class="form-control" formControlName="mountpoint" placeholder="/[...]" minlength="2" <input type="text" class="form-control" formControlName="mountpoint" placeholder="/[...]" minlength="2"
[appAutofocus]="volume.get('mount').value && !volume.get('mountpoint').value" [appAutofocusDelay]="250" [appAutofocus]="volume.get('mount').value && !volume.get('mountpoint').value" [appAutofocusDelay]="250"
tooltip="Must begin with a '/' and be at least 2 characters long" container="body" pacement="top" [adaptivePosition]="false" /> tooltip="Must begin with a '/' and be at least 2 characters long" container="body" pacement="top" [adaptivePosition]="false"/>
</td> </td>
<td class="text-end ps-1"> <td class="text-end ps-1">
<div class="form-check form-switch float-end"> <div class="form-check form-switch float-end">
<input class="form-check-input" type="checkbox" formControlName="ro"> <input class="form-check-input" type="checkbox" formControlName="ro">
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -191,7 +192,7 @@
<!-- Affinity settings --> <!-- Affinity settings -->
<div class="mt-3 d-flex flex-column" *ngIf="instances && instances.length"> <div class="mt-3 d-flex flex-column" *ngIf="instances && instances.length">
<button class="btn btn-outline-info text-start w-100 mb-3" (click)="showAffinity = !showAffinity"> <button class="btn btn-outline-info text-start w-100 mb-2" (click)="showAffinity = !showAffinity">
Affinity Affinity
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="showAffinity ? 90 : 0" class="float-end"></fa-icon> <fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="showAffinity ? 90 : 0" class="float-end"></fa-icon>
</button> </button>
@ -239,7 +240,7 @@
</div> </div>
<div class="col-sm" formGroupName="affinity"> <div class="col-sm" formGroupName="affinity">
<div class="form-check ms-4"> <div class="form-check ms-0">
<input class="form-check-input" type="checkbox" id="strict" formControlName="strict"> <input class="form-check-input" type="checkbox" id="strict" formControlName="strict">
<label class="form-check-label" for="strict"> <label class="form-check-label" for="strict">
Provision only when the affinity criteria are met Provision only when the affinity criteria are met
@ -262,15 +263,17 @@
<h5 class="text-truncate"><b>Tags</b> make it easier to lookup an machine</h5> <h5 class="text-truncate"><b>Tags</b> make it easier to lookup an machine</h5>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<div class="select-list list-group select-list p-0 mb-2" tabindex="0"> <div class="select-list list-group select-list p-0 mb-2 py-2" tabindex="0">
<div class="list-group-item list-group-item-action" *ngFor="let tag of editorForm.get('tags')['controls']; let index = index"> <div class="list-group-item list-group-item-action" *ngFor="let tag of editorForm.get('tags')['controls']; let index = index">
<div class="d-flex" [tooltip]="tag.value.value" container="body"> <div class="d-flex">
<b>{{ tag.value.key }}</b>: <b>{{ tag.value.key }}</b>:
<span class="text-truncate flex-grow-1 ps-2 tag-value"> <span class="text-truncate flex-grow-1 ps-2 tag-value"
[tooltip]="tag.value.value" container="body" placement="top left" [adaptivePosition]="false">
{{ tag.value.value }} {{ tag.value.value }}
</span> </span>
<button class="btn btn-sm text-danger p-0" (click)="removeTag(index)"> <button class="btn btn-sm text-danger p-0" (click)="removeTag(index)"
tooltip="Remove this tag" container="body" placement="top left" [adaptivePosition]="false">
<fa-icon [fixedWidth]="true" icon="times"></fa-icon> <fa-icon [fixedWidth]="true" icon="times"></fa-icon>
</button> </button>
</div> </div>
@ -288,19 +291,22 @@
<div class="col-sm"> <div class="col-sm">
<div class="select-list list-group select-list p-0 mb-2" tabindex="0"> <div class="select-list list-group select-list p-0 mb-2" tabindex="0">
<div class="list-group-item list-group-item-action" *ngFor="let meta of editorForm.get('metadata')['controls']; let index = index"> <div class="list-group-item list-group-item-action" *ngFor="let meta of editorForm.get('metadata')['controls']; let index = index">
<div class="d-flex" [tooltip]="meta.value.value" container="body" containerClass="tooltip-wrap"> <div class="d-flex">
<b>{{ meta.value.key }}</b>: <b>{{ meta.value.key }}</b>:
<span class="text-truncate flex-grow-1 ps-2 tag-value"> <span class="text-truncate flex-grow-1 ps-2 tag-value"
[tooltip]="meta.value.value" container="body" containerClass="tooltip-wrap"
placement="top left" [adaptivePosition]="false">
{{ meta.value.value }} {{ meta.value.value }}
</span> </span>
<button class="btn btn-sm text-danger p-0" (click)="removeMetadata(index)"> <button class="btn btn-sm text-danger p-0" (click)="removeMetadata(index)"
tooltip="Remove this metadata" container="body" placement="top left" [adaptivePosition]="false">
<fa-icon [fixedWidth]="true" icon="times"></fa-icon> <fa-icon [fixedWidth]="true" icon="times"></fa-icon>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<app-inline-editor buttonTitle="Add metadata" (saved)="addMetadata($event)" keyPattern="A-Za-z0-9-_"></app-inline-editor> <app-inline-editor buttonTitle="Add metadata" (saved)="addMetadata($event)" keyPattern="^[A-Za-z0-9_-]+$"></app-inline-editor>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,3 +1,5 @@
@import "../../../styles/_variables.scss";
fieldset fieldset
{ {
height: 80vh; height: 80vh;
@ -15,7 +17,7 @@ h5, h6
p p
{ {
color: #3d5e8e; color: $table-header-color;
} }
.steps .steps
@ -40,7 +42,7 @@ p
background-color: transparent; background-color: transparent;
border: none; border: none;
font-size: 1.5rem; font-size: 1.5rem;
color: #3d5e8e; color: $table-header-color;
padding-right: .5rem; padding-right: .5rem;
font-family: "Bebas Neue", sans-serif; font-family: "Bebas Neue", sans-serif;
position: relative; position: relative;
@ -120,7 +122,7 @@ p
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0; padding: 0;
margin: 0 0 0 2.5rem; margin: 0 0 0 1rem;
cursor: pointer; cursor: pointer;
flex-grow: 1; flex-grow: 1;
@ -130,7 +132,7 @@ p
float: none; float: none;
width: 1.4em; width: 1.4em;
max-width: 1rem; max-width: 1rem;
margin-bottom: .75rem; margin: .75rem .5rem .75rem 0;
cursor: inherit; cursor: inherit;
background-color: #0dc3e9; background-color: #0dc3e9;
border-color: #0dc3e9; border-color: #0dc3e9;
@ -350,12 +352,17 @@ p.lead b
.input-group-text .input-group-text
{ {
background: transparent; background: transparent;
color: #3d5e8e; color: $table-header-color;
border-color: #3d5e8e; border-color: $table-header-color;
b b
{ {
color: #ff9c07; color: #ff9c07;
} }
} }
} }
.select-list
{
height: 150px;
}

View File

@ -353,6 +353,16 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
//instance.brand = changes.package.brand; //instance.brand = changes.package.brand;
instance.networks = changes.networks.filter(x => x.selected).map(x => x.id); instance.networks = changes.networks.filter(x => x.selected).map(x => x.id);
instance.firewall_enabled = !!changes.cloudFirewall; instance.firewall_enabled = !!changes.cloudFirewall;
instance.tags = changes.tags.reduce((a, b) =>
{
a[`tag.${b.key}`] = b.value;
return a;
}, {});
instance.metadata = changes.metadata.reduce((a, b) =>
{
a[`metadata.${b.key}`] = b.value;
return a;
}, {});
if (!this.kvmRequired) if (!this.kvmRequired)
instance.volumes = changes.volumes instance.volumes = changes.volumes

View File

@ -20,7 +20,7 @@ import { LabelType, Options } from '@angular-slider/ngx-slider';
import { FileSizePipe } from '../pipes/file-size.pipe'; import { FileSizePipe } from '../pipes/file-size.pipe';
import { sortArray } from '../helpers/utils.service'; import { sortArray } from '../helpers/utils.service';
import { VolumesService } from '../volumes/helpers/volumes.service'; import { VolumesService } from '../volumes/helpers/volumes.service';
import { Title } from "@angular/platform-browser"; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
@ -574,7 +574,6 @@ export class InstancesComponent implements OnInit, OnDestroy
{ {
instance.name = name; instance.name = name;
this.applyFiltersAndSort(); this.applyFiltersAndSort();
this.toastr.info(`The "${instanceName}" machine has been renamed to "${instance.name}"`); this.toastr.info(`The "${instanceName}" machine has been renamed to "${instance.name}"`);
@ -603,7 +602,7 @@ export class InstancesComponent implements OnInit, OnDestroy
modalRef.content.save.pipe(first()).subscribe(x => modalRef.content.save.pipe(first()).subscribe(x =>
{ {
// TODO: Refresh list instance[showMetadata ? 'metadata' : 'tags'] = x;
}); });
} }
@ -696,8 +695,9 @@ export class InstancesComponent implements OnInit, OnDestroy
.subscribe(() => .subscribe(() =>
{ {
const index = this.instances.findIndex(i => i.id === instance.id); const index = this.instances.findIndex(i => i.id === instance.id);
if (index >= 0) if (index < 0) return;
this.instances.splice(index, 1);
this.instances.splice(index, 1);
this.computeFiltersOptions(); this.computeFiltersOptions();
@ -767,10 +767,28 @@ export class InstancesComponent implements OnInit, OnDestroy
// Update the instance with what we got from the server // Update the instance with what we got from the server
const index = this.instances.findIndex(i => i.id === instance.id); const index = this.instances.findIndex(i => i.id === instance.id);
if (index >= 0) if (index >= 0)
{
this.instances.splice(index, 1, x); this.instances.splice(index, 1, x);
this.computeFiltersOptions();
}
}, err => }, err =>
{ {
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`); 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.working = false;
}); });

View File

@ -12,8 +12,8 @@ export class InstanceRequest
package: string; package: string;
image: string; image: string;
networks: Network[]; networks: Network[];
tags: { key: string; value: string }; tags: { key: string; value: string }[];
metadata: { key: string; value: string }; metadata: { key: string; value: string }[];
affinity: any[]; // Optional affinity: any[]; // Optional
brand: string; brand: string;
firewall_enabled: boolean; firewall_enabled: boolean;

View File

@ -1,3 +1,5 @@
@import "../../../styles/_variables.scss";
fieldset fieldset
{ {
height: 80vh; height: 80vh;
@ -22,7 +24,7 @@ p
.current-package .current-package
{ {
color: #3d5e8e; color: $table-header-color;
margin: 1rem 0 0; margin: 1rem 0 0;
} }

View File

@ -62,7 +62,7 @@
<thead> <thead>
<tr> <tr>
<th>Action</th> <th>Action</th>
<th>Status</th> <th>Enabled</th>
<th>Description</th> <th>Description</th>
<th></th> <th></th>
</tr> </tr>
@ -89,7 +89,8 @@
To To
<span *ngFor="let to of fw.toArray" class="inline-list-item highlight" text="OR"> <span *ngFor="let to of fw.toArray" class="inline-list-item highlight" text="OR">
{{ to.type }} {{ to.type }}
<b *ngIf="to.config">{{ instances[to.config] || to.config }}</b> <span *ngIf="to.type === 'tag'" class="badge badge-discreet">{{ instances[to.config] || to.config }}</span>
<b *ngIf="to.type !== 'tag'">{{ instances[to.config] || to.config }}</b>
</span> </span>
</span> </span>
</td> </td>

View File

@ -1,3 +1,5 @@
@import "../../../styles/_variables.scss";
:host :host
{ {
overflow: hidden; overflow: hidden;
@ -19,7 +21,7 @@
.rule .rule
{ {
text-transform: uppercase; text-transform: uppercase;
color: #3d5e8e; color: $table-header-color;
} }
.highlight .highlight
@ -50,7 +52,12 @@
&:before &:before
{ {
content: attr(text); content: attr(text);
color: #3d5e8e; color: $table-header-color;
} }
} }
} }
.badge-discreet
{
margin-bottom: .125rem;
}

View File

@ -95,7 +95,7 @@ export class FirewallService
return { return {
type: parts[0], type: parts[0],
config: x.substr(parts[0].length + 1) config: x.substr(parts[0].length + 1).replace(/"/g, '').replace(/ = /g, ':')
} }
}); });
@ -107,7 +107,7 @@ export class FirewallService
rule.fromArray = fromArray; rule.fromArray = fromArray;
rule.fromValue = from.join(','); rule.fromValue = from.join(',');
rule.toArray = toArray; rule.toArray = toArray;
rule.toValue = to.join(','); rule.toValue = to.join(',').replace(/"/g, '').replace(/ = /g, ':');
rule.action = ruleAction.trim(); rule.action = ruleAction.trim();
rule.protocol = protocolAndPortOrCode[0]; rule.protocol = protocolAndPortOrCode[0];
rule.protocolConfig = protocolAndPortOrCode[2] + (protocolAndPortOrCode.length > 3 ? `:${protocolAndPortOrCode[4]}` : ''); rule.protocolConfig = protocolAndPortOrCode[2] + (protocolAndPortOrCode.length > 3 ? `:${protocolAndPortOrCode[4]}` : '');

View File

@ -48,13 +48,12 @@
<accordion [isAnimated]="false" [closeOthers]="false"> <accordion [isAnimated]="false" [closeOthers]="false">
<accordion-group *ngFor="let vlan of listItems" (isOpenChange)="getNetworks($event, vlan)"> <accordion-group *ngFor="let vlan of listItems" (isOpenChange)="getNetworks($event, vlan)">
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading <div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
tooltip="Show or hide this VLAN's networks" placement="top" container="body"> tooltip="Show or hide this VLAN's networks" placement="top" [adaptivePosition]="false" container="body">
<h4 class="mb-0"> <h4 class="mb-0">
<span class="text-info me-2">{{ vlan.name }}</span> <span class="text-info me-2">{{ vlan.name }}</span>
<span class="vlan-id"> <small class="vlan-id text-faded">
<fa-icon icon="fingerprint" [fixedWidth]="true" size="sm"></fa-icon> Unique ID: <b>{{ vlan.vlan_id }}</b>
<span>{{ vlan.vlan_id }}</span> </small>
</span>
</h4> </h4>
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="vlan.expanded ? 90 : 0" class="text-info"></fa-icon> <fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="vlan.expanded ? 90 : 0" class="text-info"></fa-icon>
@ -86,7 +85,7 @@
<tr *ngFor="let network of vlan.networks"> <tr *ngFor="let network of vlan.networks">
<td> <td>
<div> <div>
<span class="badge border border-secondary text-secondary text-uppercase float-end">{{ network.fabric ? 'fabric' : 'global' }}</span> <span class="badge badge-discreet text-uppercase float-end">{{ network.fabric ? 'fabric' : 'global' }}</span>
<div class="network-name float-start">{{ network.name }}</div> <div class="network-name float-start">{{ network.name }}</div>
</div> </div>
</td> </td>
@ -102,14 +101,15 @@
{{ network.gateway }} {{ network.gateway }}
</td> </td>
<td> <td>
<div class="text-truncate resolvers" [tooltip]="network.resolvers | json" placement="top" container="body" [adaptivePosition]="false"> <div class="text-truncate resolvers" [tooltip]="network.resolvers | json" placement="top"
container="body" [adaptivePosition]="false">
<span *ngFor="let ip of network.resolvers" class="resolver">{{ ip }}</span> <span *ngFor="let ip of network.resolvers" class="resolver">{{ ip }}</span>
</div> </div>
</td> </td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body"> <div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body">
<button class="btn btn-link text-info" tooltip="Edit this network" container="body" placement="top" [adaptivePosition]="false" <button class="btn btn-link text-info" tooltip="Edit this network" container="body"
(click)="showNetworkEditor(vlan, network)"> placement="top" [adaptivePosition]="false" (click)="showNetworkEditor(vlan, network)">
<fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon> <fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon>
</button> </button>
@ -133,7 +133,7 @@
</div> </div>
<div class="card-footer px-2 d-flex justify-content-between align-items-center"> <div class="card-footer px-2 d-flex justify-content-between align-items-center">
<button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Configure a new network</button> <button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Create a new network</button>
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body"> <div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body">
<button class="btn btn-link text-info" dropdownToggle <button class="btn btn-link text-info" dropdownToggle

View File

@ -1,3 +1,5 @@
@import "../../../styles/_variables.scss";
:host :host
{ {
flex-grow: 1; flex-grow: 1;
@ -6,7 +8,7 @@
.vlan-id .vlan-id
{ {
color: #3d5e8e; color: $table-header-color;
} }
.network-name .network-name

View File

@ -194,23 +194,13 @@ export class NetworksComponent implements OnInit, OnDestroy
modalRef.content.save.pipe(first()).subscribe(x => modalRef.content.save.pipe(first()).subscribe(x =>
{ {
const observable = vlan if (vlan)
? this.networkingService.editFabricVirtualLocalAreaNetwork(x.vlan_id, x.name, x.description)
: this.networkingService.addFabricVirtualLocalAreaNetwork(x);
observable.subscribe(response =>
{ {
if (vlan) vlan.name = x.name;
{ vlan.description = x.description;
vlan.name = x.name; }
vlan.description = x.description; else
} this.vlans.push(x);
else
this.vlans.push(response);
}, err =>
{
this.toastr.error(err.error.message);
});
}); });
} }

View File

@ -66,9 +66,10 @@ export class VirtualNetworkEditorComponent implements OnInit
const changes = this.editorForm.getRawValue(); const changes = this.editorForm.getRawValue();
const vlan = new VirtualAreaNetworkRequest(); const vlan = new VirtualAreaNetworkRequest();
vlan.name = changes.name;
vlan.description = changes.description;
vlan.vlan_id = changes.id; vlan.vlan_id = changes.id;
vlan.name = changes.name;
if (changes.description)
vlan.description = changes.description;
const observable = this.vlan const observable = this.vlan
? this.networkingService.editFabricVirtualLocalAreaNetwork(this.vlan.vlan_id, vlan.name, vlan.description) ? this.networkingService.editFabricVirtualLocalAreaNetwork(this.vlan.vlan_id, vlan.name, vlan.description)

View File

@ -1,3 +1,5 @@
@import "../../styles/_variables.scss";
:host :host
{ {
overflow: hidden; overflow: hidden;
@ -51,7 +53,7 @@ legend
{ {
font-family: 'Bebas Neue', sans-serif; font-family: 'Bebas Neue', sans-serif;
line-height: 1.2; line-height: 1.2;
color: #3d5e8e; color: $table-header-color;
padding: .75rem .5rem .75rem 1rem; padding: .75rem .5rem .75rem 1rem;
position: relative; position: relative;
background-color: rgba(16,21,39, .5); background-color: rgba(16,21,39, .5);

View File

@ -98,6 +98,13 @@ export class VolumeEditorComponent implements OnInit
const changes = this.editorForm.getRawValue(); const changes = this.editorForm.getRawValue();
changes.networks = changes.networks.map(x => x.id); changes.networks = changes.networks.map(x => x.id);
// These tags can be referenced by affinity rules
changes.tags = changes.tags.reduce((a, b) =>
{
a[b.key] = b.value;
return a;
}, {});
this.volumesService.addVolume(changes).subscribe(x => this.volumesService.addVolume(changes).subscribe(x =>
{ {
this.working = false; this.working = false;

View File

@ -56,6 +56,7 @@
<th>Name</th> <th>Name</th>
<th>Size</th> <th>Size</th>
<th>Networks</th> <th>Networks</th>
<th>Tags</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -68,10 +69,15 @@
{{ volume.size * 1024 * 1024 | fileSize}} {{ volume.size * 1024 * 1024 | fileSize}}
</td> </td>
<td> <td>
<ul class="list-inline"> <ul class="list-inline mb-0">
<li class="list-inline-item" *ngFor="let network of volume.networks">{{ networks[network] || network }}</li> <li class="list-inline-item" *ngFor="let network of volume.networks">{{ networks[network] || network }}</li>
</ul> </ul>
</td> </td>
<td>
<span class="badge badge-discreet" *ngFor="let tag of volume.tags | keyvalue">
{{ tag.key }}:{{ tag.value }}
</span>
</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body" [isDisabled]="volume.working" <div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body" [isDisabled]="volume.working"
*ngIf="!volume.refs || !volume.refs.length"> *ngIf="!volume.refs || !volume.refs.length">
@ -94,8 +100,9 @@
</ul> </ul>
</div> </div>
<fa-icon icon="lock" class="in-use text-danger" *ngIf="volume.refs && volume.refs.length" <fa-icon icon="lock" class="in-use text-danger text-faded" *ngIf="volume.refs && volume.refs.length"
tooltip="In use by one or more machines" placement="top" [adaptivePosition]="false"></fa-icon> tooltip="In use by one or more machines" container="body" placement="top" [adaptivePosition]="false">
</fa-icon>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,3 +1,5 @@
@import "../../styles/_variables.scss";
:host :host
{ {
flex-grow: 1; flex-grow: 1;
@ -39,7 +41,7 @@
&:before &:before
{ {
content: attr(text); content: attr(text);
color: #3d5e8e; color: $table-header-color;
} }
} }

View File

@ -78,7 +78,8 @@
"deleting": "Removing the \"{machineName}\" machine...", "deleting": "Removing the \"{machineName}\" machine...",
"deleted": "The \"{machineName}\" machine has been removed", "deleted": "The \"{machineName}\" machine has been removed",
"deletingFailed": "Failed to delete the \"{machineName}\" machine", "deletingFailed": "Failed to delete the \"{machineName}\" machine",
"loadingFailed": "Failed to load additional details for the \"{machineName}\" machine" "loadingFailed": "Failed to load additional details for the \"{machineName}\" machine",
"gone": "The machine \"{machineName}\" has been removed"
} }
}, },
"infoTab": "infoTab":

View File

@ -1,3 +1,5 @@
@import "_variables.scss";
.modal .modal
{ {
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
@ -28,7 +30,7 @@
h4 h4
{ {
color: #3d5e8e; color: $table-header-color;
font-family: "Bebas Neue", sans-serif; font-family: "Bebas Neue", sans-serif;
} }
} }
@ -54,5 +56,5 @@
border: none; border: none;
background: none; background: none;
font-size: 2rem; font-size: 2rem;
color: #3d5e8e; color: $table-header-color;
} }

View File

@ -9,3 +9,5 @@ $success: #2AAA65;
$success-color: #0bb13b; $success-color: #0bb13b;
$danger-color: #ff384b; $danger-color: #ff384b;
$table-header-color: #3d5e8e;
$table-body-color: #7dbbf1;

View File

@ -14,7 +14,7 @@ html, body
body body
{ {
background-color: #090b17; background-color: #090b17;
color: #3d5e8e; color: $table-header-color;
font-family: 'Mukta', sans-serif; font-family: 'Mukta', sans-serif;
line-height: 1; line-height: 1;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -46,6 +46,7 @@ body, div, virtual-scroller
.tooltip-wrap .tooltip-inner .tooltip-wrap .tooltip-inner
{ {
white-space: pre-line; white-space: pre-line;
width: 400px;
} }
.navbar-nav .nav-link .navbar-nav .nav-link
@ -477,19 +478,18 @@ accordion
{ {
font-family: "Bebas Neue", sans-serif; font-family: "Bebas Neue", sans-serif;
font-size: 1.2rem; font-size: 1.2rem;
color: #A3A4B8; color: $table-header-color;
} }
tbody tbody
{ {
color: #7dbbf1; color: $table-body-color;
} }
th th
{ {
background-color: rgba(16, 21, 39, 0.75); background-color: rgba(16, 21, 39, 0.75);
padding: 1rem .5rem 1rem .75rem; padding: 1rem .5rem 1rem .75rem;
color: #3d5e8e;
border-bottom-color: transparent; border-bottom-color: transparent;
} }
@ -498,13 +498,12 @@ accordion
border-style: none; border-style: none;
vertical-align: middle; vertical-align: middle;
padding-left: .75rem; padding-left: .75rem;
color: #8881ff;
} }
&.table-hove tr:hover td &.table-hover tr:hover td
{ {
background-color: rgba(0, 0, 0, .5); background-color: rgba(0, 0, 0, .25);
color: #FFF; color: $table-body-color;
} }
} }
@ -550,4 +549,13 @@ accordion
margin-bottom: .25rem; margin-bottom: .25rem;
display: inline-block; display: inline-block;
text-transform: none; text-transform: none;
} }
.badge-discreet
{
color: lighten($table-header-color, 15);
border: 1px solid;
text-transform: none;
vertical-align: middle;
margin-left: .125rem;
}