fixed bugs reported by Marsell
This commit is contained in:
parent
6fe512f051
commit
621e223b57
16
app/.vs/VSWorkspaceState.json
Normal file
16
app/.vs/VSWorkspaceState.json
Normal 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
|
||||
}
|
1021
app/.vs/app/config/applicationhost.config
Normal file
1021
app/.vs/app/config/applicationhost.config
Normal file
File diff suppressed because it is too large
Load Diff
BIN
app/.vs/app/v16/.suo
Normal file
BIN
app/.vs/app/v16/.suo
Normal file
Binary file not shown.
BIN
app/.vs/slnx.sqlite
Normal file
BIN
app/.vs/slnx.sqlite
Normal file
Binary file not shown.
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
@import "../../../styles/_variables.scss";
|
||||
|
||||
.list-group-item
|
||||
{
|
||||
background: none;
|
||||
color: #3d5e8e;
|
||||
color: $table-header-color;
|
||||
|
||||
.highlight
|
||||
{
|
||||
|
@ -1,7 +1,9 @@
|
||||
@import "../../../styles/_variables.scss";
|
||||
|
||||
.list-group-item
|
||||
{
|
||||
background: none;
|
||||
color: #3d5e8e;
|
||||
color: $table-header-color;
|
||||
|
||||
span
|
||||
{
|
||||
|
@ -1,7 +1,9 @@
|
||||
@import "../../../styles/_variables.scss";
|
||||
|
||||
.list-group-item
|
||||
{
|
||||
background: none;
|
||||
color: #3d5e8e;
|
||||
color: $table-header-color;
|
||||
|
||||
span
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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]}` : '');
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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":
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -9,3 +9,5 @@ $success: #2AAA65;
|
||||
|
||||
$success-color: #0bb13b;
|
||||
$danger-color: #ff384b;
|
||||
$table-header-color: #3d5e8e;
|
||||
$table-body-color: #7dbbf1;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user