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>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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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]}` : '');
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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":
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user