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>Brand</th>
<th>Publish date</th>
<th>Status</th>
<th></th>
</tr>
</thead>
@ -101,14 +100,11 @@
{{ image.type }}
</td>
<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>
{{ image.published_at ? (image.published_at | timeago) : '' }}
</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">
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body" [isDisabled]="image.working">
<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>
<!--<small *ngIf="pkg.brand">{{ pkg.brand }}</small>-->
</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>
</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));
case 2:
return this.packages[x].length && (!x || ['standard', 'triton'].includes(x));
return this.packages[x].length && (!x || ['standard', 'triton', 'bhyve'].includes(x));
default:
return false;

View File

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

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { forkJoin, from, Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
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 { Cacheable } from 'ts-cacheable';
import { volumesCacheBuster$ } from '../../volumes/helpers/volumes.service';
@ -179,16 +179,30 @@ export class InstancesService
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>
{
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`);
}
// ----------------------------------------------------------------------------------------------------------------
deleteMetadata(instanceId: string, key: string): Observable<any>
{
return this.httpClient.delete(`/api/my/machines/${instanceId}/metadata/${key}`);
}
// ----------------------------------------------------------------------------------------------------------------
getAudit(instanceId: string): Observable<any>
{

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@
[appAutofocus]="focus" [appAutofocusDelay]="focus === 1 ? 600 : 0" />
<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>
<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 tag</span>
</button>
@ -35,7 +35,7 @@
</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">
<fa-icon icon="times" [fixedWidth]="true" size="sm"></fa-icon>
</button>
@ -47,7 +47,10 @@
<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-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>
</fieldset>

View File

@ -6,6 +6,7 @@ import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { InstancesService } from '../helpers/instances.service';
import { ToastrService } from 'ngx-toastr';
import { Instance } from '../models/instance';
@Component({
selector: 'app-instance-tag-editor',
@ -15,7 +16,7 @@ import { ToastrService } from 'ngx-toastr';
export class InstanceTagEditorComponent implements OnInit
{
@Input()
instance: any;
instance: Instance;
@Input()
showMetadata: boolean;
@ -48,14 +49,20 @@ export class InstanceTagEditorComponent implements OnInit
private createForm()
{
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.tags).map(key => this.fb.group({ key, value: this.instance.tags[key] }))
? Object.keys(this.instance.metadata).map(key => this.fb.group({
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({
items,
key: [null, Validators.required],
value: [null, Validators.required]
key: [null],
value: [null]
});
}
@ -91,13 +98,14 @@ export class InstanceTagEditorComponent implements OnInit
// ----------------------------------------------------------------------------------------------------------------
close()
{
this.save.next();
this.modalRef.hide();
}
// ----------------------------------------------------------------------------------------------------------------
saveChanges()
{
this.working = true;
const items = this.editorForm.getRawValue().items.reduce((map, item) =>
{
map[item.key] = item.value;
@ -110,17 +118,21 @@ export class InstanceTagEditorComponent implements OnInit
observable.subscribe(response =>
{
this.working = false;
this.save.next(response);
this.modalRef.hide();
}, err =>
{
this.toastr.error(err.error.message);
this.working = false;
});
}
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
//this.instancesService.getTags(this.instance.id).subscribe();
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">
<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 }}">
<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 }}
<small class="text-faded">{{ network.get('description').value }}</small>
</label>
@ -142,42 +142,43 @@
<!-- Volumes and disks -->
<div class="mt-3 d-flex flex-column">
<button class="btn btn-outline-info text-start" (click)="showVolumes = !showVolumes">
Choose the <b>volumes</b> you wish to mount
<button class="btn btn-outline-info text-start mb-2" (click)="showVolumes = !showVolumes">
Volumes
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="showVolumes ? 90 : 0" class="float-end"></fa-icon>
</button>
<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">
<table class="table mb-0">
<thead>
<tr>
<th>Volume name</th>
<th>Mount point</th>
<th class="text-end">Read only</th>
</tr>
<tr>
<th>Volume name</th>
<th>Mount point</th>
<th class="text-end">Read only</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let volume of editorForm.get('volumes')['controls']; let index = index" [formGroupName]="index">
<td>
<div class="form-check">
<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 }}">
{{ volume.get('name').value }}
</label>
</div>
</td>
<td>
<input type="text" class="form-control" formControlName="mountpoint" placeholder="/[...]" minlength="2"
[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" />
</td>
<td class="text-end ps-1">
<div class="form-check form-switch float-end">
<input class="form-check-input" type="checkbox" formControlName="ro">
</div>
</td>
</tr>
<tr *ngFor="let volume of editorForm.get('volumes')['controls']; let index = index" [formGroupName]="index">
<td>
<div class="form-check">
<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 }}">
{{ volume.get('name').value }}
</label>
</div>
</td>
<td>
<input type="text" class="form-control" formControlName="mountpoint" placeholder="/[...]" minlength="2"
[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"/>
</td>
<td class="text-end ps-1">
<div class="form-check form-switch float-end">
<input class="form-check-input" type="checkbox" formControlName="ro">
</div>
</td>
</tr>
</tbody>
</table>
</div>
@ -191,7 +192,7 @@
<!-- Affinity settings -->
<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
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="showAffinity ? 90 : 0" class="float-end"></fa-icon>
</button>
@ -239,7 +240,7 @@
</div>
<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">
<label class="form-check-label" for="strict">
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>
<div class="row">
<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="d-flex" [tooltip]="tag.value.value" container="body">
<div class="d-flex">
<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 }}
</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>
</button>
</div>
@ -288,19 +291,22 @@
<div class="col-sm">
<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="d-flex" [tooltip]="meta.value.value" container="body" containerClass="tooltip-wrap">
<div class="d-flex">
<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 }}
</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>
</button>
</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>

View File

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

View File

@ -353,6 +353,16 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
//instance.brand = changes.package.brand;
instance.networks = changes.networks.filter(x => x.selected).map(x => x.id);
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)
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 { sortArray } from '../helpers/utils.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';
@Component({
@ -574,7 +574,6 @@ export class InstancesComponent implements OnInit, OnDestroy
{
instance.name = name;
this.applyFiltersAndSort();
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 =>
{
// TODO: Refresh list
instance[showMetadata ? 'metadata' : 'tags'] = x;
});
}
@ -696,8 +695,9 @@ export class InstancesComponent implements OnInit, OnDestroy
.subscribe(() =>
{
const index = this.instances.findIndex(i => i.id === instance.id);
if (index >= 0)
this.instances.splice(index, 1);
if (index < 0) return;
this.instances.splice(index, 1);
this.computeFiltersOptions();
@ -767,10 +767,28 @@ export class InstancesComponent implements OnInit, OnDestroy
// Update the instance with what we got from the server
const index = this.instances.findIndex(i => i.id === instance.id);
if (index >= 0)
{
this.instances.splice(index, 1, x);
this.computeFiltersOptions();
}
}, err =>
{
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;
});

View File

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

View File

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

View File

@ -62,7 +62,7 @@
<thead>
<tr>
<th>Action</th>
<th>Status</th>
<th>Enabled</th>
<th>Description</th>
<th></th>
</tr>
@ -89,7 +89,8 @@
To
<span *ngFor="let to of fw.toArray" class="inline-list-item highlight" text="OR">
{{ 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>
</td>

View File

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

View File

@ -95,7 +95,7 @@ export class FirewallService
return {
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.fromValue = from.join(',');
rule.toArray = toArray;
rule.toValue = to.join(',');
rule.toValue = to.join(',').replace(/"/g, '').replace(/ = /g, ':');
rule.action = ruleAction.trim();
rule.protocol = protocolAndPortOrCode[0];
rule.protocolConfig = protocolAndPortOrCode[2] + (protocolAndPortOrCode.length > 3 ? `:${protocolAndPortOrCode[4]}` : '');

View File

@ -48,13 +48,12 @@
<accordion [isAnimated]="false" [closeOthers]="false">
<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
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">
<span class="text-info me-2">{{ vlan.name }}</span>
<span class="vlan-id">
<fa-icon icon="fingerprint" [fixedWidth]="true" size="sm"></fa-icon>
<span>{{ vlan.vlan_id }}</span>
</span>
<small class="vlan-id text-faded">
Unique ID: <b>{{ vlan.vlan_id }}</b>
</small>
</h4>
<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">
<td>
<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>
</td>
@ -102,14 +101,15 @@
{{ network.gateway }}
</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>
</div>
</td>
<td class="text-end">
<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"
(click)="showNetworkEditor(vlan, network)">
<button class="btn btn-link text-info" tooltip="Edit this network" container="body"
placement="top" [adaptivePosition]="false" (click)="showNetworkEditor(vlan, network)">
<fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon>
</button>
@ -133,7 +133,7 @@
</div>
<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">
<button class="btn btn-link text-info" dropdownToggle

View File

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

View File

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

View File

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

View File

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

View File

@ -98,6 +98,13 @@ export class VolumeEditorComponent implements OnInit
const changes = this.editorForm.getRawValue();
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.working = false;

View File

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

View File

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

View File

@ -78,7 +78,8 @@
"deleting": "Removing the \"{machineName}\" machine...",
"deleted": "The \"{machineName}\" machine has been removed",
"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":

View File

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

View File

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

View File

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