fixed issue found during tests

This commit is contained in:
Dragos 2021-04-26 15:34:17 +03:00
parent 20ee57102e
commit 066ec2b96f
49 changed files with 1384 additions and 723 deletions

View File

@ -27,7 +27,7 @@
</div>
</div>
<div class="col-sm-10">
<!--<div class="col-sm-10">
<div class="form-floating">
<input type="text" class="form-control" id="address" formControlName="address" placeholder="Address">
<label for="address">Address</label>
@ -42,8 +42,8 @@
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control" id="city" formControlName="city" placeholder="City">
<label for="city">City</label>
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
<label for="country">Country</label>
</div>
</div>
<div class="col-sm-4">
@ -54,10 +54,10 @@
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
<label for="country">Country</label>
</div>
<input type="text" class="form-control" id="city" formControlName="city" placeholder="City">
<label for="city">City</label>
</div>
</div>-->
<div class="col-sm-6">
<div class="form-floating">

View File

@ -1,14 +1,18 @@
<div *ngIf="userInfo" class="pt-3">
<div class="row">
<div class="d-flex flex-column h-100">
<div class="overflow-auto flex-grow-1">
<div class="container mb-3">
<div *ngIf="userInfo" class="row pt-2">
<div class="col-sm-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4>My profile</h4>
<fieldset>
<legend>
{{ 'account.myProfile' | translate }}
<button class="btn btn-sm btn-outline-info" (click)="showEditor()">Update profile</button>
</div>
<button class="btn btn-outline-info" container="body" (click)="showEditor()">
{{ 'account.updateProfile' | translate }}
</button>
</legend>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item">
Name: <b>{{ userInfo.firstName }} {{ userInfo.lastName }}</b>
@ -30,24 +34,25 @@
</span>
</li>
</ul>
</div>
</div>
</fieldset>
</div>
<div class="col-sm-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4>My SSH keys</h4>
<fieldset>
<legend>
{{ 'account.myKeys' | translate }}
<button class="btn btn-sm btn-outline-info" (click)="addSshKey()">Add SSH key</button>
</div>
<button class="btn btn-outline-info" container="body" (click)="addSshKey()">
{{ 'account.addKey' | translate }}
</button>
</legend>
<div class="card-body">
<ol class="list-group list-group-flush">
<li class="list-group-item" *ngFor="let userKey of userKeys">
{{ userKey.name }}: <b class="text-uppercase">{{ userKey.fingerprint }}</b>
</li>
</ol>
</fieldset>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
ul, ol
:host
{
background-color: rgba(16, 21, 39, .75);
height: 100%;
overflow: hidden;
flex-grow: 1;
}
h4
@ -11,10 +11,10 @@ h4
.list-group-item
{
background: none;
padding: 1rem;
border-color: rgb(61, 94, 142, .25);
background-color: transparent;
border-color: #354164;
color: #5a8cd8;
padding: .5rem 1rem;
b
{
@ -22,21 +22,32 @@ h4
}
}
.card
fieldset
{
border: 1px solid rgba(0, 0, 0, 0.5);
background-color: rgba(16, 21, 39, 0.5);
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
transition: box-shadow 0.15s ease-out;
background-color: rgba(16,21,39, .5);
border-radius: .3rem;
height: 100%;
transition: all 0.3s cubic-bezier(0.46, 0.03, 0.52, 0.96);
height: 100%;
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
&:hover
{
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgba(18, 203, 240, .4), 0 0 10px 3px #0e162a;
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgb(18 203 240 / 40%), 0 0 10px 3px #0e162a;
}
.card-body
legend
{
padding: 0;
font-family: 'Bebas Neue', sans-serif;
line-height: 1.2;
color: #3d5e8e;
padding: .75rem .5rem .75rem 1rem;
position: relative;
background-color: rgba(16,21,39, .5);
border-radius: .3rem .3rem 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
}

View File

@ -9,6 +9,8 @@ import { BsModalService } from 'ngx-bootstrap/modal';
import { AccountEditorComponent } from './account-editor/account-editor.component';
import { ToastrService } from 'ngx-toastr';
import { SshKeyEditorComponent } from './ssh-key-editor/ssh-key-editor.component';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-account',
@ -26,8 +28,12 @@ export class AccountComponent implements OnInit, OnDestroy
constructor(private readonly accountService: AccountService,
private readonly authService: AuthService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService)
private readonly toastr: ToastrService,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('account.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
//accountService.getUsers().subscribe(x => console.log(x));
accountService.getUserLimits().subscribe(x => console.log(x));
@ -64,17 +70,6 @@ export class AccountComponent implements OnInit, OnDestroy
};
const modalRef = this.modalService.show(SshKeyEditorComponent, modalConfig);
modalRef.content.save.pipe(first()).subscribe(x => this.userKeys = [...this.userKeys, x]);
// this.accountService.addKey('test',
// 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAzf7Cbu8tPvxgwG3MhXK959F7TtsSCQQXb3jSPAJtQT+CltA+OYLod/ojclfQfnutIHUpqq6PsCD/nhxiF2JYkKWve7olJV6akvXQOGNLqRdXTcEouUhevLAQV3sB+YNvjr5FRpspNK8prAn7UU4vyZhCKBT8VAgwkio3u8eR/26XDNow1C9NXC6P+2BYWjjKbJCI41XpLFIzsmHBw+XZox+IbVg8mcVsWfdhEHRDyxM1HgvOKU9vkCwigmww9nsIatSQuM0jCtohQRkddc2DlfKieBmpeC/VqNoWE77iei/nVOcgIaLjwwevdCGHhwtSBmkE+W14JCwFbzl0yThL2w== rsa-key-20210314',
// 'ba:04:55:94:64:24:75:a4:b2:60:e5:bf:77:19:df:34')
// .subscribe(response => this.userKeys = [...this.userKeys, response],
// err =>
// {
// this.toastr.error(err.error.message)
// });
}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -46,12 +46,10 @@
</nav>
</header>
<div class="no-overflow flex-grow-1">
<div class="h-100">
<div class="no-overflow flex-grow-1 d-flex flex-column">
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>
</div>
</main>

View File

@ -0,0 +1,5 @@
:host
{
flex-grow: 1;
overflow: hidden;
}

View File

@ -10,21 +10,13 @@ import { TranslateCompiler } from '@ngx-translate/core';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { CatalogComponent } from './catalog.component';
import { CustomImagesComponent } from './custom-images/custom-images.component';
import { DockerImagesComponent } from './docker-images/docker-images.component';
import { DockerRegistryComponent } from './docker-registry/docker-registry.component';
import { DockerImageEditorComponent } from './docker-image-editor/docker-image-editor.component';
import { DockerRegistryEditorComponent } from './docker-registry-editor/docker-registry-editor.component';
import { ImagesComponent } from './images/images.component';
import { CustomImageEditorComponent } from './custom-image-editor/custom-image-editor.component';
@NgModule({
declarations: [
CatalogComponent,
CustomImagesComponent,
DockerImagesComponent,
DockerRegistryComponent,
DockerImageEditorComponent,
DockerRegistryEditorComponent
ImagesComponent
],
imports: [
SharedModule,
@ -35,38 +27,19 @@ import { CustomImageEditorComponent } from './custom-image-editor/custom-image-e
children: [
{
path: '',
redirectTo: 'custom-images'
redirectTo: 'images'
},
{
path: 'custom-images',
component: CustomImagesComponent,
path: 'images',
component: ImagesComponent,
data:
{
title: 'catalog.customImages.title',
subTitle: 'catalog.customImages.subTitle',
title: 'catalog.images.title',
subTitle: 'catalog.images.subTitle',
icon: 'layer-group'
}
},
{
path: 'docker-images',
component: DockerImagesComponent,
data:
{
title: 'catalog.dockerImages.title',
subTitle: 'catalog.dockerImages.subTitle',
icon: ['fab', 'docker']
}
},
{
path: 'docker-registry',
component: DockerRegistryComponent,
data:
{
title: 'catalog.dockerRegistry.title',
subTitle: 'catalog.dockerRegistry.subTitle',
icon: ['fab', 'docker']
}
}]
]
}
]),
TranslateModule.forChild({
@ -83,8 +56,6 @@ import { CustomImageEditorComponent } from './custom-image-editor/custom-image-e
})
],
entryComponents: [
DockerImageEditorComponent,
DockerRegistryEditorComponent,
CustomImageEditorComponent
]
})

View File

@ -59,15 +59,6 @@ export class CatalogService
return this.httpClient.get<CatalogImage[]>(`/api/my/images?${allStates ? 'state=all' : ''}`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: imagesCacheBuster$
})
getCustomImages(ownerId: string): Observable<CatalogImage[]>
{
return this.httpClient.get<CatalogImage[]>(`/api/my/images?$state=all&owner=${ownerId}`);
}
// ----------------------------------------------------------------------------------------------------------------
@Cacheable({
cacheBusterObserver: imagesCacheBuster$

View File

@ -1,20 +1,200 @@
<table class="table">
<div class="d-flex flex-column h-100">
<div class="container text-center mt-1" [formGroup]="editorForm">
<div class="btn-toolbar pt-2">
<span class="d-none d-sm-block flex-grow-1"></span>
<ng-container *ngIf="images && images.length">
<div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
<input type="text" class="form-control" placeholder="Search by name..." formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$">
<button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
</button>
</div>
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom left">
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
Sort by
<b *ngIf="editorForm.get('sortProperty').value === 'name'">name</b>
<b *ngIf="editorForm.get('sortProperty').value === 'description'">description</b>
<b *ngIf="editorForm.get('sortProperty').value === 'os'">operating system</b>
<b *ngIf="editorForm.get('sortProperty').value === 'type'">type</b>
<b *ngIf="editorForm.get('sortProperty').value === 'state'">status</b>
</button>
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'" (click)="setSortProperty('name')">
Name
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'description'" (click)="setSortProperty('description')">
Description
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'" (click)="setSortProperty('os')">
Operating system
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'type'" (click)="setSortProperty('type')">
Type
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'status'" (click)="setSortProperty('status')">
Status
</button>
</li>
</ul>
</div>
</ng-container>
</div>
<div class="spinner-border text-center text-info text-faded" role="status" *ngIf="loadingIndicator">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="overflow-auto flex-grow-1 my-3">
<div class="container my-2">
<accordion [isAnimated]="false" [closeOthers]="false">
<accordion-group [isOpen]="myImagesExpanded">
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
tooltip="Show or hide my images" placement="top" container="body">
<h4 class="text-info text-uppercase mb-0">My images</h4>
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="myImagesExpanded ? 90 : 0" class="text-info"></fa-icon>
</div>
<div class="table-responsive" *ngIf="!loadingIndicator">
<p *ngIf="!images.length" class="text-center text-info text-faded p-3 mb-0">
You don't have any custom images yet
</p>
<table class="table table-hover" *ngIf="myImages.length">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th class="w-max-200">Description</th>
<th>OS</th>
<th>Type</th>
<th>Brand</th>
<th>Publish date</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let image of images">
<td>{{ image.name }}</td>
<td>{{ image.description }}</td>
<td>{{ image.os }}</td>
<tr *ngFor="let image of myListItems">
<td>
<button class="btn btn-sm btn-secondary" (click)="select.emit(image)">select</button>
<b>{{ image.name }}</b>
</td>
<td class="w-max-200">
<div class="text-truncate" [tooltip]="image.description" placement="top" [adaptivePosition]="false">{{ image.description }}</div>
</td>
<td class="text-uppercase">
{{ image.os }}
</td>
<td class="text-uppercase">
{{ image.type }}
</td>
<td class="text-uppercase">
<span class="badge text-warning border border-warning" *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
tooltip="More options" container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
</button>
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="button-split">
<li role="menuitem">
<button class="dropdown-item" (click)="deleteCustomImage(image)">
<fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
Delete this image
</button>
</li>
</ul>
</div>
</td>
</tr>
</tbody>
<tfoot *ngIf="!listItems.length">
<tr>
<td colspan="7" class="text-uppercase">
No images match your search criteria
</td>
</tr>
</tfoot>
</table>
</div>
</accordion-group>
<!--<accordion-group>
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
tooltip="Show or hide other images" placement="top" container="body">
<h4 class="text-info text-uppercase mb-0">Other images</h4>
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="otherImagesExpanded ? 90 : 0" class="text-info"></fa-icon>
</div>
<table class="table table-hover" *ngIf="images.length">
<thead>
<tr>
<th>Name</th>
<th class="w-max-200">Description</th>
<th>OS</th>
<th>Type</th>
<th>Brand</th>
<th>Publish date</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let image of listItems">
<td>
<b>{{ image.name }}</b>
</td>
<td class="w-max-200">
<div class="text-truncate" [tooltip]="image.description" placement="top" [adaptivePosition]="false">{{ image.description }}</div>
</td>
<td class="text-uppercase">
{{ image.os }}
</td>
<td class="text-uppercase">
{{ image.type }}
</td>
<td class="text-uppercase">
<span class="badge text-warning border border-warning" *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"></td>
</tr>
</tbody>
<tfoot *ngIf="!listItems.length">
<tr>
<td colspan="7" class="text-uppercase">
No images match your search criteria
</td>
</tr>
</tfoot>
</table>
</accordion-group>-->
</accordion>
</div>
</div>
</div>

View File

@ -0,0 +1,32 @@
:host
{
flex-grow: 1;
overflow: hidden;
}
.table-responsive
{
.highlight
{
color: #8881ff;
}
.text-truncate
{
max-width: 350px;
}
}
th, td
{
&.w-max-200
{
max-width: 200px;
}
b, .strong
{
color: #ff9c07;
font-weight: normal;
}
}

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ImagesComponent } from './images.component';
describe('ImagesComponent', () => {
describe('CustomImagesComponent', () => {
let component: ImagesComponent;
let fixture: ComponentFixture<ImagesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ImagesComponent ]
})
.compileComponents();
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ImagesComponent);

View File

@ -1,37 +1,222 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ColumnMode, SelectionType } from '@swimlane/ngx-datatable';
import { CatalogService } from '../helpers/catalog.service';
import { AuthService } from '../../helpers/auth.service';
import { debounceTime, distinctUntilChanged, filter, first, map, switchMap, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { CatalogImage } from '../models/image';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
import Fuse from 'fuse.js';
import { sortArray } from '../../helpers/utils.service';
import { BsModalService } from 'ngx-bootstrap/modal';
import { ConfirmationDialogComponent } from '../../components/confirmation-dialog/confirmation-dialog.component';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-images',
templateUrl: './images.component.html',
styleUrls: ['./images.component.scss']
})
export class ImagesComponent implements OnInit
export class ImagesComponent implements OnInit, OnDestroy
{
@Output()
select = new EventEmitter();
images: any[];
myImages: CatalogImage[] = [];
myListItems: CatalogImage[] = [];
images: CatalogImage[] = [];
listItems: CatalogImage[] = [];
editorForm: FormGroup;
loadingIndicator = true;
selectionType = SelectionType;
columnMode = ColumnMode;
myImagesExpanded = true;
otherImagesExpanded = true;
private destroy$ = new Subject();
private readonly fuseJsOptions: {};
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly catalogService: CatalogService)
constructor(private readonly catalogService: CatalogService,
private readonly modalService: BsModalService,
private readonly authService: AuthService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
catalogService.getImages().subscribe(x =>
translationService.get('catalog.images.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
// Configure FuseJs
this.fuseJsOptions = {
includeScore: false,
minMatchCharLength: 2,
includeMatches: true,
shouldSort: false,
threshold: .3, // Lower value means a more exact search
keys: [
{ name: 'name', weight: .9 },
{ name: 'description', weight: .8 },
{ name: 'os', weight: .7 },
{ name: 'type', weight: .7 }
]
};
this.createForm();
}
// ----------------------------------------------------------------------------------------------------------------
private createForm()
{
this.images = x;
this.editorForm = this.fb.group(
{
searchTerm: [''],
sortProperty: ['name']
});
this.editorForm.get('searchTerm').valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntil(this.destroy$)
)
.subscribe(() => this.applyFiltersAndSort());
this.editorForm.get('sortProperty').valueChanges
.pipe(
distinctUntilChanged(),
takeUntil(this.destroy$)
)
.subscribe(() => this.applyFiltersAndSort());
}
// ----------------------------------------------------------------------------------------------------------------
private applyFiltersAndSort()
{
let myListItems: CatalogImage[] = null;
let listItems: CatalogImage[] = null;
const searchTerm = this.editorForm.get('searchTerm').value;
if (searchTerm.length >= 2)
{
let fuse = new Fuse(this.myImages, this.fuseJsOptions);
let fuseResults = fuse.search(searchTerm);
myListItems = fuseResults.map(x => x.item as CatalogImage);
fuse = new Fuse(this.images, this.fuseJsOptions);
fuseResults = fuse.search(searchTerm);
listItems = fuseResults.map(x => x.item as CatalogImage);
}
if (!myListItems)
myListItems = [...this.myImages];
this.myListItems = sortArray(myListItems, this.editorForm.get('sortProperty').value);
if (!listItems)
listItems = [...this.images];
this.listItems = sortArray(listItems, this.editorForm.get('sortProperty').value);
}
// ----------------------------------------------------------------------------------------------------------------
setSortProperty(propertyName: string)
{
this.editorForm.get('sortProperty').setValue(propertyName);
}
// ----------------------------------------------------------------------------------------------------------------
clearSearch()
{
this.editorForm.get('searchTerm').setValue('');
}
// ----------------------------------------------------------------------------------------------------------------
private getCustomImages()
{
this.loadingIndicator = true;
this.authService.userInfoUpdated$
.pipe(
takeUntil(this.destroy$),
filter(userInfo => userInfo != null),
switchMap(userInfo => this.catalogService.getImages()
.pipe(map(images => ({ userId: userInfo.id, images })))
)
)
.subscribe(response =>
{
this.myImages = [];
this.images = [];
for (const image of response.images)
{
if (image.owner === response.userId)
this.myImages.push(image);
else
this.images.push(image);
}
this.applyFiltersAndSort();
this.loadingIndicator = false;
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to retrieve the list of custom images ${errorDetails}`);
this.loadingIndicator = false;
});
}
// ----------------------------------------------------------------------------------------------------------------
deleteCustomImage(image: CatalogImage)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
prompt: `Are you sure you wish to permanently delete the "${image.name}" image?`,
confirmButtonText: 'Yes, delete this image',
declineButtonText: 'No, keep it',
confirmByDefault: false
}
};
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
modalRef.content.confirm.pipe(first()).subscribe(() =>
{
this.toastr.info(`Removing machine "${image.name}"...`);
this.catalogService.deleteImage(image.id)
.subscribe(() =>
{
const index = this.images.findIndex(i => i.id === image.id);
if (index >= 0)
this.images.splice(index, 1);
this.applyFiltersAndSort();
this.toastr.info(`The image "${image.name}" has been removed`);
},
err =>
{
this.toastr.error(`Failed to delete the "${image.name}" image ${err.error.message}`);
});
});
catalogService.getDataCenters().subscribe(console.log);
}
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
this.getCustomImages();
}
// ----------------------------------------------------------------------------------------------------------------
ngOnDestroy()
{
this.destroy$.next();
}
}

View File

@ -40,9 +40,9 @@
</li>-->
<li class="dropdown-divider"></li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['./catalog/custom-images']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
<a class="nav-link" [routerLink]="['./catalog/images']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
<fa-icon [fixedWidth]="true" icon="layer-group"></fa-icon>
{{ 'navbar.menu.customImages' | translate }}
{{ 'navbar.menu.images' | translate }}
</a>
</li>
<!--<li class="nav-item">

View File

@ -36,7 +36,7 @@ export class AuthGuardService implements CanActivate, CanLoad
if (this.tokenService.accessTokenUpdated$.getValue())
return true;
this.router.navigate(['/unauthorized'], { state: { data: route.data } });
//this.router.navigate(['/unauthorized'], { state: { data: route.data } });
return false;
}
}

View File

@ -20,7 +20,7 @@
<th>Action</th>
<th>Time</th>
<th>Finished</th>
<th></th>
<th>User</th>
</tr>
</thead>
<tbody>
@ -35,9 +35,7 @@
</span>
</td>
<td>
<fa-icon icon="user" [fixedWidth]="true" size="sm"
[tooltip]="info.caller.keyId" placement="top" container="body" [adaptivePosition]="false">
</fa-icon>
<div class="text-truncate">{{ info.caller.keyId }}</div>
</td>
</tr>
</tbody>

View File

@ -1,14 +1,15 @@
<ul class="list-group list-group-flush list-info">
<li class="list-group-item text-uppercase ps-0" tooltip="Machine ID" container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="fingerprint" [fixedWidth]="true" size="sm"></fa-icon>
<b class="ms-1">{{ instance.id }}</b>
<li class="dropdown-header">Machine identifier</li>
<li class="list-group-item text-uppercase ps-0">
<b>{{ instance.id }}</b>
</li>
<ng-container *ngIf="instance.dnsList">
<li class="dropdown-header">DNS list</li>
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
*ngFor="let keyValue of instance.dnsList | keyvalue; let index = index">
<div class="text-truncate">
<fa-icon icon="link" [fixedWidth]="true" size="sm"></fa-icon>
<span class="ms-1" [ngClass]="keyValue.value[0] === instance.id || keyValue.value[0] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
<div class="text-truncate text-info text-faded" [tooltip]="keyValue.key" container="body" placement="top" [adaptivePosition]="false">
<!--<span class="ms-1" [ngClass]="keyValue.value[0] === instance.id || keyValue.value[0] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
{{ keyValue.value[0] }}
</span>
@ -16,7 +17,8 @@
{{ keyValue.value[1] }}
</span>
{{ keyValue.value[2] }}
{{ keyValue.value[2] }}-->
{{ keyValue.key }}
</div>
<div class="btn-group btn-group-sm" *ngxClipboardIfSupported
@ -26,7 +28,9 @@
</button>
</div>
</li>
</ng-container>
<li class="dropdown-header">Deletion protection</li>
<li class="list-group-item ps-0 pb-0">
<div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="dp{{ instance.id }}" [(ngModel)]="instance.deletion_protection"

View File

@ -24,4 +24,16 @@
{
color: rgba(#ff9c07, .75);
}
+ .dropdown-header
{
margin-top: .5rem;
}
}
.dropdown-header
{
opacity: 1;
color: #3d5e8e;
padding: .5rem 0 0;
}

View File

@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, OnDestroy, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service';
import { empty, forkJoin, Observable, of, Subject } from 'rxjs';
import { empty, forkJoin, Observable, of, Subject, ReplaySubject } from 'rxjs';
import { delay, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstancesService } from '../helpers/instances.service';
import { BsModalService } from 'ngx-bootstrap/modal';
@ -12,20 +12,19 @@ import { Instance } from '../models/instance';
templateUrl: './instance-info.component.html',
styleUrls: ['./instance-info.component.scss']
})
export class InstanceInfoComponent implements OnInit, OnDestroy
export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
{
@Input()
instance: Instance;
@Input()
set loadInfo(value: boolean)
{
if (!this.finishedLoading && value && this.instance)
this.getInfo();
}
loadInfo: boolean;
@Output()
beforeLoad = new EventEmitter();
processing = new EventEmitter();
@Output()
finishedProcessing = new EventEmitter();
@Output()
load = new EventEmitter();
@ -34,6 +33,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
private finishedLoading: boolean;
private destroy$ = new Subject();
private onChanges$ = new ReplaySubject();
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService,
@ -46,18 +46,18 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
toggleDeletionProtection(event, instance: Instance)
{
this.beforeLoad.emit();
this.processing.emit();
this.instancesService.toggleDeletionProtection(instance.id, event.target.checked)
.subscribe(() =>
{
this.toastr.info(`The deletion protection for machine "${instance.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
this.load.emit();
this.finishedProcessing.emit();
},
err =>
{
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
this.load.emit();
this.finishedProcessing.emit();
});
}
@ -85,6 +85,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
this.loading = false;
this.finishedLoading = true;
this.load.emit(dnsList);
},
err =>
{
@ -109,8 +110,21 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{
if (!this.finishedLoading && this.loadInfo && !this.instance?.infoLoaded)
this.getInfo();
});
}
// ----------------------------------------------------------------------------------------------------------------
ngOnChanges(changes: SimpleChanges): void
{
// Since we can't control if ngOnChanges is executed before ngOnInit, we need this trick
this.onChanges$.next(changes);
}
// ----------------------------------------------------------------------------------------------------------------
ngOnDestroy()
{

View File

@ -24,7 +24,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
loadNetworks: boolean;
@Output()
beforeLoad = new EventEmitter();
processing = new EventEmitter();
@Output()
finishedProcessing = new EventEmitter();
@Output()
load = new EventEmitter();
@ -70,6 +73,8 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
// ----------------------------------------------------------------------------------------------------------------
private getNetworks()
{
if (this.finishedLoading) return;
this.loading = true;
const observables = this.nics.map(x => this.networkingService.getNetwork(x.network));
@ -83,8 +88,9 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
nic.networkName = nic.networkDetails ? nic.networkDetails.name : '';
}
this.finishedLoading = true;
this.loading = false;
this.finishedLoading = true;
this.load.emit(this.nics);
},
err =>
{
@ -113,7 +119,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
first(),
tap(() =>
{
this.beforeLoad.emit();
this.processing.emit();
this.toastr.info(`Connecting machine "${this.instance.name}" to the "${network.name}" network...`);
}),
@ -160,8 +166,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
nic.networkName = nic.networkDetails?.name || '';
}
this.load.emit(this.nics);
this.toastr.info(`The machine "${this.instance.name}" has been connected to the "${network.name}" network`);
this.load.emit();
this.finishedProcessing.emit();
},
err =>
{
@ -171,7 +179,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to connect machine "${this.instance.name}" to the "${network.name}" network ${errorDetails}`);
this.load.emit();
this.finishedProcessing.emit();
});
}
@ -196,7 +204,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
first(),
tap(() =>
{
this.beforeLoad.emit();
this.processing.emit();
this.toastr.info(`Removing network interface "${nic.mac.toUpperCase()}" from machine "${this.instance.name}"...`);
}),
@ -238,13 +246,15 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
found.primary = networkInterface.primary;
}
this.load.emit();
this.finishedProcessing.emit();
this.load.emit(this.nics);
this.toastr.info(`The network interface has been removed from machine "${this.instance.name}"`);
}, err =>
{
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
this.load.emit();
this.finishedProcessing.emit();
});
}
@ -277,9 +287,12 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
{
this.nics = this.instance?.nics || [];
if (this.instance.networksLoaded)
this.finishedLoading = true;
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{
if (!this.finishedLoading && this.loadNetworks && this.instance)
if (!this.finishedLoading && this.loadNetworks && !this.instance?.networksLoaded)
this.getNetworks();
});
}

View File

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, OnDestroy, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service';
import { InstancesService } from '../helpers/instances.service';
import { Subject } from 'rxjs';
import { ReplaySubject, Subject } from 'rxjs';
import { delay, first, switchMap, takeUntil, tap } from 'rxjs/operators';
import Fuse from 'fuse.js';
import { BsModalService } from 'ngx-bootstrap/modal';
@ -15,20 +15,19 @@ import { SnapshotsService } from '../helpers/snapshots.service';
templateUrl: './instance-snapshots.component.html',
styleUrls: ['./instance-snapshots.component.scss']
})
export class InstanceSnapshotsComponent implements OnInit, OnDestroy
export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
{
@Input()
instance: any;
@Input()
set loadSnapshots(value: boolean)
{
if (value && this.instance && !this.snapshots)
this.getSnapshots();
}
loadSnapshots: boolean;
@Output()
beforeLoad = new EventEmitter();
processing = new EventEmitter();
@Output()
processingFinished = new EventEmitter();
@Output()
load = new EventEmitter();
@ -37,13 +36,16 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
instanceStateUpdate = new EventEmitter();
loadingSnapshots: boolean;
snapshotsLoaded: boolean;
filteredSnapshots: any[];
snapshotName: string;
_searchTerm: string;
shouldSearch: boolean;
private destroy$ = new Subject();
private onChanges$ = new ReplaySubject();
private snapshots: any[];
private finishedLoading: boolean
private readonly fuseJsOptions: {};
// ----------------------------------------------------------------------------------------------------------------
@ -69,7 +71,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
createSnapshot()
{
this.beforeLoad.emit();
this.processing.emit();
this.snapshots = this.snapshots || [];
@ -95,7 +97,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
if (index >= 0)
this.snapshots[index] = x;
this.load.emit();
this.processingFinished.emit();
this.toastr.info(`A new snapshot "${snapshotName}" has been created for machine "${this.instance.name}"`);
},
err =>
@ -106,7 +108,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
if (index >= 0)
this.snapshots.splice(index, 1);
this.load.emit();
this.processingFinished.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
});
}
@ -117,7 +119,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
this.confirmRestore(snapshot)
.subscribe(() =>
{
this.beforeLoad.emit();
this.processing.emit();
snapshot.working = true;
@ -135,7 +137,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
err =>
{
snapshot.working = false;
this.load.emit();
this.processingFinished.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
});
@ -166,7 +168,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
private startMachineFromSnapshot(snapshot: Snapshot)
{
this.beforeLoad.emit();
this.processing.emit();
this.toastr.info(`Restoring machine "${this.instance.name}" from "${snapshot.name}" snapshot`);
@ -182,14 +184,14 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
{
snapshot.working = false;
this.load.emit();
this.processingFinished.emit();
this.toastr.info(`The machine "${this.instance.name}" has been started from the "${snapshot.name}" snapshot`);
}, err =>
{
snapshot.working = false;
this.load.emit();
this.processingFinished.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
});
@ -214,7 +216,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
modalRef.content.confirm.pipe(first()).subscribe(() =>
{
this.beforeLoad.emit();
this.processing.emit();
this.snapshotsService.deleteSnapshot(this.instance.id, snapshot.name)
.subscribe(() =>
@ -223,12 +225,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
if (index >= 0)
this.snapshots.splice(index, 1);
this.load.emit();
this.processingFinished.emit();
this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`);
}, err =>
{
this.load.emit();
this.processingFinished.emit();
this.toastr.error(`The "${snapshot.name}" snapshot couldn't be deleted: ${err.error.message}`);
});
@ -238,6 +240,8 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
private getSnapshots()
{
if (this.snapshotsLoaded) return
this.loadingSnapshots = true;
// Get the list of snapshots
@ -246,7 +250,10 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
{
this.snapshots = x;
this.filteredSnapshots = x;
this.loadingSnapshots = false;
this.snapshotsLoaded = true;
this.load.emit(x);
},
err =>
{
@ -288,6 +295,21 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
this.snapshots = this.instance?.snapshots;
this.filteredSnapshots = this.snapshots;
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{
if (!this.finishedLoading && this.loadSnapshots && !this.instance?.snapshotsLoaded)
this.getSnapshots();
});
}
// ----------------------------------------------------------------------------------------------------------------
ngOnChanges(changes: SimpleChanges): void
{
// Since we can't control if ngOnChanges is executed before ngOnInit, we need this trick
this.onChanges$.next(changes);
}
// ----------------------------------------------------------------------------------------------------------------

View File

@ -11,7 +11,7 @@ import { CatalogService } from '../../catalog/helpers/catalog.service';
import { NetworkingService } from '../../networking/helpers/networking.service';
import { ToastrService } from 'ngx-toastr';
import { VolumesService } from '../../volumes/helpers/volumes.service';
import { VolumeResponse } from '../../volumes/models/volume';
import { AuthService } from '../../helpers/auth.service';
@Component({
selector: 'app-instance-wizard',
@ -51,12 +51,14 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
kvmRequired: boolean;
private destroy$ = new Subject();
private userId: string;
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly modalRef: BsModalRef,
private readonly router: Router,
private readonly fb: FormBuilder,
private readonly fileSizePipe: FileSizePipe,
private readonly authService: AuthService,
private readonly instancesService: InstancesService,
private readonly catalogService: CatalogService,
private readonly networkingService: NetworkingService,
@ -93,6 +95,10 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
description: 'Tag and create your machine'
}
];
authService.userInfoUpdated$
.pipe(takeUntil(this.destroy$))
.subscribe(x => this.userId = x.id);
}
// ----------------------------------------------------------------------------------------------------------------
@ -163,7 +169,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
if (imageType === 1)
{
for (const image of this.images)
if (['zvol'].includes(image.type))
if (['lx-dataset', 'zone-dataset'].includes(image.type) && image.owner !== this.userId)
{
operatingSystems[image.os] = true;
imageList.push(image);
@ -172,7 +178,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 2)
{
for (const image of this.images)
if (['lx-dataset', 'zone-dataset'].includes(image.type))
if (['zvol'].includes(image.type) && image.owner !== this.userId)
{
operatingSystems[image.os] = true;
imageList.push(image);
@ -181,7 +187,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 3)
{
for (const image of this.images)
if (!['zvol', 'lx-dataset', 'zone-dataset'].includes(image.type))
if (image.owner === this.userId)
{
operatingSystems[image.os] = true;
imageList.push(image);
@ -213,7 +219,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
if (imageType === 1)
{
for (const image of this.images)
if (['zvol'].includes(image.type) && (!imageOs || imageOs === image.os))
if (['lx-dataset', 'zone-dataset'].includes(image.type) && (!imageOs || imageOs === image.os) && image.owner !== this.userId)
{
operatingSystems[image.os] = true;
imageList.push(image);
@ -222,7 +228,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 2)
{
for (const image of this.images)
if (['lx-dataset', 'zone-dataset'].includes(image.type) && (!imageOs || imageOs === image.os))
if (['zvol'].includes(image.type) && (!imageOs || imageOs === image.os) && image.owner !== this.userId)
{
operatingSystems[image.os] = true;
imageList.push(image);
@ -231,7 +237,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 3)
{
for (const image of this.images)
if (!['zvol', 'lx-dataset', 'zone-dataset'].includes(image.type) && (!imageOs || imageOs === image.os))
if (image.owner === this.userId)
{
operatingSystems[image.os] = true;
imageList.push(image);
@ -552,9 +558,9 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
if (this.instance)
{
if (this.instance.type === 'virtualmachine')
if (this.instance.type === 'smartmachine')
this.imageType = 1;
else if (this.instance.type === 'smartmachine')
else if (this.instance.type === 'virtualmachine')
this.imageType = 2;
this.preselectedPackage = this.instance.package;

View File

@ -1,28 +1,29 @@
<div class="d-flex flex-column h-100" [formGroup]="editorForm">
<div class="container text-center mt-1">
<div class="btn-toolbar pt-2">
<div class="btn-group flex-grow-1 flex-grow-sm-0">
<div class="btn-group mb-3 mb-lg-0 w-lg-auto w-100">
<button class="btn btn-lg btn-info" (click)="createMachine()" [disabled]="loadingIndicator">
Create a new machine
</button>
</div>
<span class="d-none d-sm-block flex-grow-1"></span>
<span class="d-none d-lg-block flex-grow-1"></span>
<ng-container *ngIf="instances && instances.length">
<div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
<input type="text" class="form-control" placeholder="Search..."
formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$"
tooltip="Search by name, tag, metadata, operating system or brand" placement="top" container="body" [adaptivePosition]="false">
<button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
<div class="input-group input-group-pill me-lg-3 mb-3 mb-lg-0 w-lg-auto w-100">
<input type="text" class="form-control" placeholder="Search..." formControlName="searchTerm"
appAlphaOnly="^[A-Za-z0-9_-]+$" tooltip="Search by name, tag, metadata, operating system or brand"
placement="top" container="body" [adaptivePosition]="false">
<button class="btn btn-outline-info" type="button" (click)="clearSearch()"
[disabled]="!editorForm.get('searchTerm').value" tooltip="Clear search" container="body" placement="top"
[adaptivePosition]="false">
<fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
</button>
</div>
<div class="btn-group flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
<button class="btn btn-outline-info dropdown-toggle" [disabled]="loadingIndicator"
[popover]="filtersTemplate" [outsideClick]="true" container="body" placement="bottom right" containerClass="menu-popover">
<div class="btn-group me-lg-3 mb-3 mb-lg-0 w-lg-auto w-100">
<button class="btn btn-outline-info dropdown-toggle" [disabled]="loadingIndicator" [popover]="filtersTemplate"
[outsideClick]="true" container="body" placement="bottom right" containerClass="menu-popover">
Showing {{ listItems.length }} / {{ instances.length }}
<ng-container *ngIf="runningInstanceCount && stoppedInstanceCount">
<span class="badge rounded-pill bg-success text-dark">{{ runningInstanceCount }} running</span>
@ -37,33 +38,38 @@
</button>
</div>
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom right">
<div class="btn-group w-lg-auto w-100" dropdown placement="bottom right">
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
Sort by <b>{{ editorForm.get('sortProperty').value }}</b>
</button>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'" (click)="setSortProperty('name')">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'"
(click)="setSortProperty('name')">
Name
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'" (click)="setSortProperty('os')">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'"
(click)="setSortProperty('os')">
Operating system
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'brand'" (click)="setSortProperty('brand')">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'brand'"
(click)="setSortProperty('brand')">
Brand
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'image'" (click)="setSortProperty('image')">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'image'"
(click)="setSortProperty('image')">
Image
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'state'" (click)="setSortProperty('state')">
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'state'"
(click)="setSortProperty('state')">
State
</button>
</li>
@ -77,32 +83,37 @@
</div>
</div>
<div class="overflow-auto my-2" id="scrollingBlock">
<div class="container">
<div class="overflow-auto flex-grow-1 mt-3" id="scrollingBlock">
<div class="container py-2">
<h2 *ngIf="listItems && listItems.length === 0 && instances && instances.length > 0" class="text-uppercase">
No machine matches your filters
{{ 'dashboard.list.noResults' | translate }}
</h2>
<virtual-scroller #scroller [items]="listItems" bufferAmount="2" class="instances"
[parentScroll]="scroller.window.document.getElementById('scrollingBlock')" [scrollThrottlingTime]="250">
<div *ngFor="let instance of scroller.viewPortItems; trackBy: trackByFunction; let index = index"
[ngClass]="showMachineDetails ? 'col-lg-6 col-12 full-details' : 'col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12'"
lazyLoad [lazyLoadDelay]="lazyLoadDelay" [container]="scroller.element.nativeElement.getElementsByClassName('scrollable-content')[0]"
(canLoad)="instance.loading = false" (unload)="instance.loading = true" (load)="loadInstanceDetails(instance)">
[ngClass]="showMachineDetails ? 'col-12 full-details' : 'col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12'"
[class.col-lg-6]="editorForm.get('fullDetailsTwoColumns').value" lazyLoad [lazyLoadDelay]="lazyLoadDelay"
[container]="scroller.element.nativeElement.getElementsByClassName('scrollable-content')[0]"
(canLoad)="instance.loading = false" (unload)="instance.loading = true"
(load)="loadInstanceDetails(instance)">
<fieldset class="card" [disabled]="instance.working">
<div class="row g-0">
<div class="card-info" [ngClass]="showMachineDetails ? 'col-lg-4' : 'col'">
<div>
<h5 class="card-title text-truncate" [tooltip]="instance.name" container="body" placement="top" [adaptivePosition]="false">
<h5 class="card-title text-truncate" [tooltip]="instance.name" container="body" placement="top"
[adaptivePosition]="false">
{{ instance.name }}
</h5>
<div *ngIf="!instance.loading && instance.imageDetails" class="text-truncate small text-info text-faded mb-1"
[tooltip]="instance.imageDetails.description" container="body" placement="top" [adaptivePosition]="false">
<div *ngIf="!instance.loading && instance.imageDetails"
class="text-truncate small text-info text-faded mb-1" [tooltip]="instance.imageDetails.description"
container="body" placement="top" [adaptivePosition]="false">
{{ instance.imageDetails.name }}, v{{ instance.imageDetails.version }}
</div>
<button *ngIf="!instance.loading" class="btn btn-outline-info w-100 d-flex justify-content-around align-items-center text-truncate"
<button *ngIf="!instance.loading"
class="btn btn-outline-info w-100 d-flex justify-content-around align-items-center text-truncate"
tooltip="Change specifications" container="body" placement="top" [adaptivePosition]="false"
(click)="resizeMachine(instance)" [disabled]="instance.brand === 'kvm'">
@ -127,37 +138,41 @@
<div>
<div class="small text-truncate my-2">
<ng-container *ngIf="instance.type === 'virtualmachine'">
<fa-icon icon="server" size="sm"></fa-icon>
<b class="text-uppercase ms-1">{{ instance.brand }}</b> - infrastructure container
</ng-container>
<ng-container *ngIf="instance.type === 'smartmachine'">
<fa-icon icon="desktop" size="sm"></fa-icon>
<b class="text-uppercase ms-1">{{ instance.brand }}</b> - virtual machine
<fa-icon icon="server" size="sm" class="me-1"></fa-icon>
<span class="machine-brand" innerHtml="{{ 'dashboard.listItem.infrastructureContainer' | translate: { brand: instance.brand } }}"></span>
</ng-container>
<ng-container *ngIf="instance.type === 'virtualmachine'">
<fa-icon icon="desktop" size="sm" class="me-1"></fa-icon>
<span class="machine-brand" innerHtml="{{ 'dashboard.listItem.virtualMachine' | translate: { brand: instance.brand } }}"></span>
</ng-container>
</div>
<div class="d-flex flex-nowrap justify-content-between align-items-center">
<a href="javascript:void(0)" class="badge text-uppercase"
[class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'"
[class.bg-danger]="instance.state === 'stopped'"
[class.bg-success]="instance.state === 'running'"
(click)="showMachineHistory(instance)" tooltip="Show machine history" container="body" placement="top" [adaptivePosition]="false">
[class.bg-danger]="instance.state === 'stopped'" [class.bg-success]="instance.state === 'running'"
(click)="showMachineHistory(instance)" tooltip="{{ 'dashboard.listItem.history' | translate }}"
container="body" placement="top" [adaptivePosition]="false">
{{ instance.state }}
</a>
<div class="btn-group btn-group-sm" dropdown placement="bottom right" *ngIf="!instance.loading">
<button class="btn btn-link text-warning" (click)="restartMachine(instance)" *ngIf="instance.state === 'running'">
<fa-icon icon="power-off" [fixedWidth]="true" size="sm" tooltip="Restart this machine" container="body" placement="top" [adaptivePosition]="false"></fa-icon>
<button class="btn btn-link text-warning" (click)="restartMachine(instance)"
*ngIf="instance.state === 'running'">
<fa-icon icon="power-off" [fixedWidth]="true" size="sm" tooltip="Restart this machine"
container="body" placement="top" [adaptivePosition]="false"></fa-icon>
</button>
<button class="btn btn-link text-success" (click)="startMachine(instance)" *ngIf="instance.state === 'stopped'">
<fa-icon icon="play" [fixedWidth]="true" size="sm" tooltip="Start this machine" container="body" placement="top" [adaptivePosition]="false"></fa-icon>
<button class="btn btn-link text-success" (click)="startMachine(instance)"
*ngIf="instance.state === 'stopped'">
<fa-icon icon="play" [fixedWidth]="true" size="sm" tooltip="Start this machine" container="body"
placement="top" [adaptivePosition]="false"></fa-icon>
</button>
<button class="btn btn-link text-info"
[popover]="instanceContextMenu" [popoverContext]="{ instance: instance }"
placement="bottom left" containerClass="menu-dropdown" [outsideClick]="true">
<button class="btn btn-link text-info" [popover]="instanceContextMenu"
[popoverContext]="{ instance: instance }" placement="bottom left" containerClass="menu-dropdown"
[outsideClick]="true">
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
</button>
</div>
@ -168,49 +183,68 @@
<div class="col mt-sm-0 mt-3 no-overflow-sm" *ngIf="showMachineDetails">
<div class="card-header p-0 h-100">
<tabset class="dashboard-tabs" *ngIf="!instance.loading">
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-info">
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-info">
<ng-template tabHeading>
<fa-icon icon="info-circle" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Info</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-info [instance]="instance" [loadInfo]="true"
(beforeLoad)="instance.working = true" (load)="instance.working = false">
<app-instance-info [instance]="instance" [loadInfo]="instance.shouldLoadInfo"
(load)="setInstanceInfo(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false">
</app-instance-info>
</div>
</tab>
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-networks">
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-networks">
<ng-template tabHeading>
<fa-icon icon="network-wired" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Network</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-networks [instance]="instance" [loadNetworks]="instance.shouldLoadNetworks"
(beforeLoad)="instance.working = true" (load)="instance.working = false"
(instanceReboot)="watchInstanceState(instance)" (instanceStateUpdate)="updateInstance(instance, $event)">
(load)="setInstanceNetworks(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false"
(instanceReboot)="watchInstanceState(instance)"
(instanceStateUpdate)="updateInstance(instance, $event)">
</app-instance-networks>
</div>
</tab>
<!--<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-firewall">
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-snapshots" *ngIf="instance.brand !== 'kvm'">
<ng-template tabHeading>
<fa-icon icon="fire-alt" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Firewall</span>
<fa-icon icon="history" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Snapshots</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-firewall-rules [instance]="instance" [loadFirewallRules]="instance.shouldLoadFirewallRules"
(cloudFirewallChange)="instance.firewall_enabled = $event">
</app-instance-firewall-rules>
<app-instance-snapshots [instance]="instance" [loadSnapshots]="instance.shouldLoadSnapshots"
(load)="setInstanceSnapshots(instance, $event)" (processing)="instance.working = true"
(finishedProcessing)="instance.working = false"
(instanceStateUpdate)="updateInstance(instance, $event)">
</app-instance-snapshots>
</div>
</tab>-->
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-volumes"
*ngIf="instance.volumes && instance.volumes.length">
</tab>
<tab *ngIf="false" customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-migrations">
<ng-template tabHeading>
<fa-icon icon="coins" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Migrations</span>
</ng-template>
<div class="card-body p-2 h-100">
<button class="btn btn-outline-info w-100">Move to another node</button>
</div>
</tab>
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
id="{{ instance.id }}-volumes" *ngIf="instance.volumes && instance.volumes.length">
<ng-template tabHeading>
<fa-icon icon="database" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Volumes</span>
</ng-template>
<div class="card-body p-2 h-100">
<ul class="list-group list-group-flush list-info">
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
<li
class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
*ngFor="let volume of instance.volumes">
<div class="text-truncate">
<fa-icon icon="database" [fixedWidth]="true" size="sm"></fa-icon>
@ -224,28 +258,6 @@
</ul>
</div>
</tab>
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-snapshots"
*ngIf="instance.brand !== 'kvm'">
<ng-template tabHeading>
<fa-icon icon="history" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Snapshots</span>
</ng-template>
<div class="card-body p-2 h-100">
<app-instance-snapshots [instance]="instance" [loadSnapshots]="instance.shouldLoadSnapshots"
(beforeLoad)="instance.working = true" (load)="instance.working = false"
(instanceStateUpdate)="updateInstance(instance, $event)">
</app-instance-snapshots>
</div>
</tab>
<tab *ngIf="false" customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-migrations">
<ng-template tabHeading>
<fa-icon icon="coins" class="d-sm-none"></fa-icon>
<span class="d-none d-sm-inline-block ms-1">Migrations</span>
</ng-template>
<div class="card-body p-2 h-100">
<button class="btn btn-outline-info w-100">Move to another node</button>
</div>
</tab>
</tabset>
</div>
</div>
@ -260,29 +272,70 @@
<ng-template #filtersTemplate [formGroup]="editorForm">
<fieldset class="filters">
<ng-container formGroupName="filters">
<div class="dropdown-header">Filter by state</div>
<select class="form-control mb-3" formControlName="stateFilter">
<option [ngValue]="null">Any state</option>
<option *ngFor="let state of instanceStateArray" [value]="state">{{ state }}</option>
</select>
<div class="dropdown-header">{{ 'dashboard.list.filterByState' | translate }}</div>
<div class="btn-group w-100 mb-3" dropdown>
<button class="btn btn-state-filter dropdown-toggle d-flex justify-content-between align-items-center"
dropdownToggle>
<span *ngIf="!editorForm.get(['filters', 'stateFilter']).value">{{ 'dashboard.list.anyState' | translate
}}</span>
<span *ngIf="editorForm.get(['filters', 'stateFilter']).value === 'running'">{{
'dashboard.listItem.stateRunning' | translate }}</span>
<span *ngIf="editorForm.get(['filters', 'stateFilter']).value === 'stopped'">{{
'dashboard.listItem.stateStopped' | translate }}</span>
</button>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-state-filter" role="menu">
<li role="menuitem">
<button class="dropdown-item" [class.active]="!editorForm.get(['filters', 'stateFilter']).value"
(click)="setStateFilter()">
{{ 'dashboard.list.anyState' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item"
[class.active]="editorForm.get(['filters', 'stateFilter']).value === 'running'"
(click)="setStateFilter('running')">
{{ 'dashboard.listItem.stateRunning' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item"
[class.active]="editorForm.get(['filters', 'stateFilter']).value === 'stopped'"
(click)="setStateFilter('stopped')">
{{ 'dashboard.listItem.stateStopped' | translate }}
</button>
</li>
</ul>
</div>
<div class="dropdown-header">Filter by memory</div>
<div class="dropdown-header">{{ 'dashboard.list.filterByMemory' | translate }}</div>
<ngx-slider class="mb-4" formControlName="memoryFilter" [options]="memoryFilterOptions"></ngx-slider>
<div class="dropdown-header">Filter by disk size</div>
<div class="dropdown-header">{{ 'dashboard.list.filterByDisk' | translate }}</div>
<ngx-slider class="mb-3" formControlName="diskFilter" [options]="diskFilterOptions"></ngx-slider>
<button class="btn btn-outline-dark w-100 mt-3" (click)="clearFilters()">Reset filters</button>
<button class="btn btn-outline-dark w-100 mt-3" (click)="clearFilters()">{{ 'dashboard.list.resetFilters' |
translate }}</button>
</ng-container>
<div class="dropdown-divider"></div>
<div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="showMachineDetails" formControlName="showMachineDetails">
<label class="form-check-label" for="showMachineDetails">
Show machine details
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1" *ngIf="editorForm.get('showMachineDetails').disabled"></fa-icon>
{{ 'dashboard.list.showDetails' | translate }}
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1"
*ngIf="editorForm.get('showMachineDetails').disabled"></fa-icon>
</label>
</div>
<div [collapse]="!editorForm.get('showMachineDetails').value" [isAnimated]="true">
<div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="fullDetailsTwoColumns"
formControlName="fullDetailsTwoColumns">
<label class="form-check-label" for="fullDetailsTwoColumns">
{{ 'dashboard.list.dualColumns' | translate }}
</label>
</div>
</div>
</fieldset>
</ng-template>
@ -291,32 +344,32 @@
<li role="menuitem">
<button class="dropdown-item" (click)="renameMachine(instance)">
<fa-icon icon="pen" [fixedWidth]="true"></fa-icon>
Rename this machine
{{ 'dashboard.listItem.rename' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="showTagEditor(instance)">
<fa-icon icon="tags" [fixedWidth]="true"></fa-icon>
Edit machine tags
{{ 'dashboard.listItem.editTags' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="showTagEditor(instance, true)">
<fa-icon icon="tags" [fixedWidth]="true"></fa-icon>
Edit machine metadata
{{ 'dashboard.listItem.editMetadata' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
<li role="menuitem">
<button class="dropdown-item" (click)="createMachine(instance)">
<fa-icon icon="clone" [fixedWidth]="true"></fa-icon>
Clone this machine
{{ 'dashboard.listItem.clone' | translate }}
</button>
</li>
<li role="menuitem">
<button class="dropdown-item" (click)="createImageFromMachine(instance)">
<fa-icon icon="layer-group" [fixedWidth]="true"></fa-icon>
Create an image from this machine
{{ 'dashboard.listItem.createImage' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
@ -324,7 +377,7 @@
<li role="menuitem">
<button class="dropdown-item" (click)="stopMachine(instance)">
<fa-icon icon="stop" [fixedWidth]="true"></fa-icon>
Stop this machine
{{ 'dashboard.listItem.stop' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
@ -332,7 +385,7 @@
<li role="menuitem">
<button class="dropdown-item" (click)="deleteMachine(instance)">
<fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
Delete this machine
{{ 'dashboard.listItem.delete' | translate }}
</button>
</li>
</ul>

View File

@ -1,65 +1,61 @@
.ips
{
+ .ips
{
:host {
flex-direction: column;
overflow : hidden;
}
.ips {
+.ips {
margin-left : .5rem;
padding-left: .5rem;
border-left : 2px solid #2b3540;
}
}
.card
{
.card {
border : 1px solid rgba(0, 0, 0, .5);
background-color: rgba(16, 21, 39, .5);
box-shadow : 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
transition : box-shadow .15s ease-out;
&:hover
{
&:hover {
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgba(18, 203, 240, .4), 0 0 10px 3px #0e162a;
}
.card-title
{
.card-title {
color : #ff9c07;
margin-bottom: 0;
font-weight : bold;
}
.card-body
{
.card-body {
background-color: rgba(16, 21, 39, .5);
max-height : 205px;
overflow : auto;
padding-top : .7rem !important;
}
.list-group-item
{
.list-group-item {
background: none;
span
{
span {
color: #8881ff;
}
b, .strong
{
b,
.strong {
color : #ff9c07;
font-weight: bold;
}
}
.btn-info, .btn-outline-info
{
.btn-info,
.btn-outline-info {
font-size : 1rem;
line-height: 1;
}
}
.card-info
{
.card-info {
background-color: rgba(16, 21, 39, .75);
display : flex;
flex-direction : column;
@ -68,85 +64,106 @@
height : 170px;
}
.full-details .card-info
{
.full-details .card-info {
height: 240px;
}
@media (max-width: 576px)
{
.card-info
{
@media (max-width: 576px) {
.card-info {
height: auto;
}
}
.no-overflow-sm
{
.no-overflow-sm {
overflow: hidden;
}
@media (max-width: 992px)
{
.no-overflow-sm
{
@media (max-width: 992px) {
.no-overflow-sm {
overflow: visible;
}
}
.open .dropdown-toggle
{
}
.open .dropdown-toggle {}
.filters
{
.filters {
width: 240px;
}
.form-check-label
{
.form-check-label {
color: inherit;
}
.btn-toolbar .btn
{
.badge
{
.btn-toolbar .btn {
.badge {
padding : 0.1rem 0.35rem 0;
border : 1px solid transparent;
text-shadow: 0 0 3px rgb(255 255 255 / 25%);
&:first-letter
{
&:first-letter {
font-size: 1.2rem;
}
}
&:hover
{
.badge
{
&:hover {
.badge {
border-color: #000;
}
}
}
@keyframes flash
{
from, 50%, to
{
@keyframes flash {
from,
50%,
to {
opacity: 1;
}
25%, 75%
{
25%,
75% {
opacity: 0;
}
}
.flash
{
.flash {
animation-name : flash;
animation-duration : calc(.9s * 1.3);
animation-timing-function: ease-in-out;
}
.btn-state-filter:not(.btn-lg):not(.btn-sm) {
background-color: #11182b;
color : #ff9c07;
padding : .5rem 1rem;
}
.open.show .btn-state-filter {
border-radius: 1.25rem 1.25rem 0 0;
}
.dropdown-menu.dropdown-menu-state-filter {
background : #11182b;
color : #ff9c07;
width : 100%;
padding : 0;
border-radius: 0 0 .25rem .25rem;
.dropdown-item {
color: inherit;
&.active {
color: #0dcaf0;
}
&:hover {
background-color: rgba(11, 172, 204, .2);
}
}
}
.form-switch,
.form-switch input,
.form-switch label {
cursor: pointer;
}

View File

@ -3,7 +3,6 @@ import { InstancesService } from './helpers/instances.service';
import { BsModalService } from 'ngx-bootstrap/modal';
import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component';
import { SelectionType, ColumnMode } from '@swimlane/ngx-datatable';
import { Instance } from './models/instance';
import { forkJoin, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
@ -21,6 +20,8 @@ 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 { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-instances',
@ -42,6 +43,7 @@ export class InstancesComponent implements OnInit, OnDestroy
canPrepareForLoading: boolean;
editorForm: FormGroup;
showMachineDetails: boolean;
fullDetailsTwoColumns: boolean;
runningInstanceCount = 0;
stoppedInstanceCount = 0;
instanceStateArray: string[] = [];
@ -72,8 +74,12 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder,
private readonly fileSizePipe: FileSizePipe)
private readonly fileSizePipe: FileSizePipe,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('dashboard.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
this.lazyLoadDelay = this.minimumLazyLoadDelay;
// Configure FuseJs
@ -93,6 +99,7 @@ export class InstancesComponent implements OnInit, OnDestroy
};
this.showMachineDetails = !!JSON.parse(localStorage.getItem('showMachineDetails') || '0');
this.fullDetailsTwoColumns = !!JSON.parse(localStorage.getItem('fullDetailsTwoColumns') || '1');
this.createForm();
}
@ -108,9 +115,9 @@ export class InstancesComponent implements OnInit, OnDestroy
switch (label)
{
case LabelType.Low:
return `<b>Between</b> ${formattedValue}`;
return `Between ${formattedValue}`;
case LabelType.High:
return `<b>and</b> ${formattedValue}`;
return `and ${formattedValue}`;
default:
return formattedValue;
}
@ -187,7 +194,8 @@ export class InstancesComponent implements OnInit, OnDestroy
imageFilter: [], // instances provisioned with a certain image
}),
filtersActive: [false],
showMachineDetails: [this.showMachineDetails]
showMachineDetails: [this.showMachineDetails],
fullDetailsTwoColumns: [{ value: this.fullDetailsTwoColumns, disabled: !this.showMachineDetails }]
});
this.editorForm.get('searchTerm').valueChanges
@ -232,7 +240,15 @@ export class InstancesComponent implements OnInit, OnDestroy
// Store this setting in the local storage
localStorage.setItem('showMachineDetails', JSON.stringify(showMachineDetails));
setTimeout(() => this.editorForm.get('showMachineDetails').enable(), 300);
setTimeout(() =>
{
this.editorForm.get('showMachineDetails').enable();
if (showMachineDetails)
this.editorForm.get('fullDetailsTwoColumns').enable();
else
this.editorForm.get('fullDetailsTwoColumns').disable();
}, 300);
});
}
@ -291,6 +307,12 @@ export class InstancesComponent implements OnInit, OnDestroy
this.editorForm.get('sortProperty').setValue(propertyName);
}
// ----------------------------------------------------------------------------------------------------------------
setStateFilter(state?: string)
{
this.editorForm.get(['filters', 'stateFilter']).setValue(state);
}
// ----------------------------------------------------------------------------------------------------------------
clearSearch()
{
@ -700,6 +722,7 @@ export class InstancesComponent implements OnInit, OnDestroy
};
const modalRef = this.modalService.show(InstanceHistoryComponent, modalConfig);
modalRef.setClass('modal-lg');
}
// ----------------------------------------------------------------------------------------------------------------
@ -711,8 +734,6 @@ export class InstancesComponent implements OnInit, OnDestroy
instance.shouldLoadSnapshots = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('networks'))
instance.shouldLoadNetworks = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('firewall'))
instance.shouldLoadFirewallRules = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('volumes'))
{
//instance.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value;
@ -782,6 +803,33 @@ export class InstancesComponent implements OnInit, OnDestroy
instance.state = updates.state;
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceInfo(instance: Instance, dnsList)
{
// Update the instance as a result of the info panel's "load" event. We do this because the intances are (un)loaded
// from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.dnsList = dnsList;
instance.infoLoaded = true;
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceNetworks(instance: Instance, nics)
{
// Update the instance as a result of the networks panel's "load" event. We do this because the intances are (un)loaded
// from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.nics = nics;
instance.networksLoaded = true;
}
// ----------------------------------------------------------------------------------------------------------------
setInstanceSnapshot(instance: Instance, snapshots)
{
// Update the instance as a result of the snapshots panel's "load" event. We do this because the intances are (un)loaded
// from the viewport as the user scrolls through the page, to optimize memory consumption.
instance.snapshots = snapshots;
instance.snapshotsLoaded = true;
}
// ----------------------------------------------------------------------------------------------------------------
private fillInInstanceDetails(instance: Instance)
{

View File

@ -32,18 +32,20 @@ export class Instance extends InstanceRequest
imageDetails: CatalogImage;
packageDetails: CatalogPackage;
dns_names: string[];
dnsList: {};
dnsList: any;
memory: number;
type: string;
state: string;
snapshots: any[];
loading: boolean;
working: boolean;
shouldLoadInfo: boolean;
infoLoaded: boolean;
shouldLoadNetworks: boolean;
shouldLoadSecurity: boolean;
networksLoaded: boolean;
shouldLoadSnapshots: boolean;
shouldLoadFirewallRules: boolean;
snapshotsLoaded: boolean;
volumesEnabled: boolean;
metadataKeys: string[];
tagKeys: string[];

View File

@ -51,8 +51,8 @@
</div>
</div>
<div class="overflow-auto">
<div class="container my-4">
<div class="overflow-auto flex-grow-1 my-3">
<div class="container my-2">
<div class="table-responsive" *ngIf="!loadingIndicator">
<p *ngIf="!firewallRules.length" class="text-center text-info text-faded p-3 mb-0">
There are no firewall rules yet.

View File

@ -1,3 +1,9 @@
:host
{
overflow: hidden;
flex-grow: 1;
}
.table-responsive
{
background-color: rgba(16, 21, 39, 0.75);
@ -32,6 +38,11 @@
max-width: 350px;
}
.text-transform-none
{
text-transform: none;
}
.inline-list-item + .inline-list-item
{
padding-left: .25rem;

View File

@ -11,6 +11,8 @@ import { ConfirmationDialogComponent } from '../../components/confirmation-dialo
import { InstancesService } from '../../instances/helpers/instances.service';
import { FirewallService } from '../helpers/firewall.service';
import { sortArray } from '../../helpers/utils.service';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-firewall-rules',
@ -33,8 +35,12 @@ export class FirewallRulesComponent implements OnInit, OnDestroy
private readonly instancesService: InstancesService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder)
private readonly fb: FormBuilder,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('networking.firewall.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
// Configure FuseJs
this.fuseJsOptions = {
includeScore: false,

View File

@ -43,12 +43,12 @@
</div>
</div>
<div class="overflow-auto">
<div class="container my-4">
<div class="overflow-auto flex-grow-1 mt-1">
<div class="container my-2">
<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="Click to expand this virtual network and show all its networks" placement="top" container="body">
tooltip="Show or hide this VLAN's networks" placement="top" container="body">
<h4 class="mb-0">
<span class="text-info me-2">{{ vlan.name }}</span>
<span class="vlan-id">
@ -133,17 +133,29 @@
</div>
<div class="card-footer px-2 d-flex justify-content-between align-items-center">
<button class="btn btn-sm btn-link text-info px-1" tooltip="Rename this virtual network" container="body" placement="top" [adaptivePosition]="false"
(click)="showVlanEditor(vlan)">
<fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon>
</button>
<button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Configure a new network</button>
<button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Add a network</button>
<button class="btn btn-sm btn-link text-danger px-1" tooltip="Delete this virtual network" container="body" placement="top" [adaptivePosition]="false"
(click)="deleteVlan(vlan)">
<fa-icon icon="trash" [fixedWidth]="true" size="sm"></fa-icon>
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body">
<button class="btn btn-link text-info" dropdownToggle
tooltip="Virtual network options" container="body" placement="top" [adaptivePosition]="false">
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
</button>
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="button-split">
<li role="menuitem">
<button class="dropdown-item" (click)="showVlanEditor(vlan)">
<fa-icon icon="pen" [fixedWidth]="true"></fa-icon>
Rename this virtual network
</button>
</li>
<li class="dropdown-divider"></li>
<li role="menuitem">
<button class="dropdown-item" (click)="deleteVlan(vlan)">
<fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
Delete this virtual network
</button>
</li>
</ul>
</div>
</div>
</accordion-group>
</accordion>

View File

@ -1,14 +1,14 @@
:host
{
flex-grow: 1;
overflow : hidden;
}
.vlan-id
{
color: #3d5e8e;
}
h4, .network-name
{
text-transform: uppercase;
color: #8881ff;
}
.network-name
{
color: #ff9c07;

View File

@ -12,6 +12,8 @@ import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '
import Fuse from 'fuse.js';
import { Subject } from 'rxjs';
import { sortArray } from '../../helpers/utils.service';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-networks',
@ -32,8 +34,12 @@ export class NetworksComponent implements OnInit, OnDestroy
constructor(private readonly networkingService: NetworkingService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder)
private readonly fb: FormBuilder,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('networking.networks.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
this.getVlans();
// Configure FuseJs

View File

@ -1,6 +1,7 @@
<div class="overflow-auto h-100">
<div class="container h-100 py-3">
<div class="row h-100" id="securitySettings">
<div class="d-flex flex-column h-100">
<div class="overflow-auto flex-grow-1 d-flex">
<div class="container flex-grow-1 d-flex mb-3">
<div class="row flex-grow-1">
<div class="col-sm py-2">
<fieldset>
<legend>
@ -10,17 +11,20 @@
</span>
<button class="btn btn-outline-info" container="body" (click)="showPolicyEditor()">
{{ 'security.addPolicy' | translate }}
<span class="d-none d-lg-inline-block">{{ 'security.addPolicy' | translate }}</span>
<fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addPolicy' | translate }}"
container="body" placement="top" [adaptivePosition]="false">
</fa-icon>
</button>
</legend>
<p class="px-3">Policies are just access rules grouped together</p>
<ul class="list-group list-group-flush p-0" id="policies"
cdkDropList [cdkDropListData]="policies" [cdkDropListConnectedTo]="roleDropLists"
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="noReturnPredicate">
<li class="list-group-item pr-1" *ngFor="let policy of policies"
cdkDrag [cdkDragData]="policy" cdkDragBoundary="#securitySettings">
<ul class="list-group list-group-flush p-0" id="policies" cdkDropList [cdkDropListData]="policies"
[cdkDropListConnectedTo]="roleDropLists" cdkDropListSortingDisabled sortingDisabled
[cdkDropListEnterPredicate]="noReturnPredicate">
<li class="list-group-item pr-1" *ngFor="let policy of policies" cdkDrag [cdkDragData]="policy"
cdkDragBoundary="#securitySettings">
<div class="d-flex justify-content-between align-items-center">
<span class="grip" cdkDragHandle>
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
@ -71,26 +75,28 @@
</span>
<button class="btn btn-outline-info" container="body" (click)="showRoleEditor()">
{{ 'security.addRole' | translate }}
<span class="d-none d-lg-inline-block">{{ 'security.addRole' | translate }}</span>
<fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addRole' | translate }}"
container="body" placement="top" [adaptivePosition]="false">
</fa-icon>
</button>
</legend>
<p class="px-3">To assign policies drag them over a role</p>
<ul class="list-group list-group-flush p-0" id="roles"
cdkDropList [cdkDropListData]="roles" [cdkDropListConnectedTo]="userDropLists"
cdkDropListSortingDisabled sortingDisabled>
<ul class="list-group list-group-flush p-0" id="roles" cdkDropList [cdkDropListData]="roles"
[cdkDropListConnectedTo]="userDropLists" cdkDropListSortingDisabled sortingDisabled>
<li class="list-group-item px-1" *ngFor="let role of roles; let i = index" id="{{ 'role' + i }}"
cdkDropList [cdkDropListData]="role"
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="rolesEnterPredicate"
(cdkDropListDropped)="drop($event)"
cdkDrag [cdkDragData]="role" cdkDragBoundary="#securitySettings">
cdkDropList [cdkDropListData]="role" cdkDropListSortingDisabled sortingDisabled
[cdkDropListEnterPredicate]="rolesEnterPredicate" (cdkDropListDropped)="drop($event)" cdkDrag
[cdkDragData]="role" cdkDragBoundary="#securitySettings">
<div class="d-flex justify-content-between align-items-center">
<div>
<fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="role.collapsed ? 0 : 90"
class="px-2 py-1 float-start" [ngClass]="role.policies.length ? 'text-secondary' : 'text-muted'"
(click)="role.policies.length && role.collapsed = !role.collapsed"
tooltip="{{ 'security.togglePolicies' | translate }}" container="body"></fa-icon>
tooltip="{{ 'security.togglePolicies' | translate }}" container="body" placement="top"
[adaptivePosition]="false"></fa-icon>
<div class="grip" cdkDragHandle>
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
@ -136,7 +142,8 @@
<fa-icon [fixedWidth]="true" icon="key" size="sm"></fa-icon>
{{ policy.name }}
<div class="btn-group btn-group-sm float-end">
<button class="btn text-danger" tooltip="{{ 'security.removeRolePolicy' | translate }}" container="body"
<button class="btn text-danger" tooltip="{{ 'security.removeRolePolicy' | translate }}"
container="body" placement="top" [adaptivePosition]="false"
(click)="removePolicyFromRole(policy, role)">
<fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon>
</button>
@ -157,7 +164,10 @@
</span>
<button class="btn btn-outline-info" container="body" (click)="showUserEditor()">
{{ 'security.addUser' | translate }}
<span class="d-none d-lg-inline-block">{{ 'security.addUser' | translate }}</span>
<fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addUser' | translate }}"
container="body" placement="top" [adaptivePosition]="false">
</fa-icon>
</button>
</legend>
@ -165,15 +175,15 @@
<ul class="list-group list-group-flush p-0">
<li class="list-group-item px-1" *ngFor="let user of users; let i = index" id="{{ 'user' + i }}"
cdkDropList [cdkDropListData]="user"
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="usersEnterPredicate"
(cdkDropListDropped)="drop($event)">
cdkDropList [cdkDropListData]="user" cdkDropListSortingDisabled sortingDisabled
[cdkDropListEnterPredicate]="usersEnterPredicate" (cdkDropListDropped)="drop($event)">
<div class="d-flex justify-content-between align-items-center">
<div>
<fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="user.collapsed ? 0 : 90"
class="px-2 py-1" [ngClass]="user.roles.length ? 'text-secondary' : 'text-muted'"
<fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="user.collapsed ? 0 : 90" class="px-2 py-1"
[ngClass]="user.roles.length ? 'text-secondary' : 'text-muted'"
(click)="user.roles.length && user.collapsed = !user.collapsed"
tooltip="{{ 'security.toggleRoles' | translate }}" container="body"></fa-icon>
tooltip="{{ 'security.toggleRoles' | translate }}" container="body" placement="top"
[adaptivePosition]="false"></fa-icon>
{{ user.login }} <sup *ngIf="user.roles?.length">({{ user.roles.length }})</sup>
</div>
@ -219,8 +229,8 @@
<fa-icon [fixedWidth]="true" icon="tag" size="sm"></fa-icon>
{{ role.name }}
<div class="btn-group btn-group-sm float-end">
<button class="btn text-danger" tooltip="{{ 'security.removeUserRole' | translate }}" container="body"
(click)="removeRoleFromUser(role, user)">
<button class="btn text-danger" tooltip="{{ 'security.removeUserRole' | translate }}"
container="body" (click)="removeRoleFromUser(role, user)">
<fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon>
</button>
</div>
@ -235,3 +245,4 @@
</div>
</div>
</div>
</div>

View File

@ -1,3 +1,9 @@
:host
{
overflow: hidden;
flex-grow: 1;
}
fieldset
{
background-color: rgba(16,21,39, .5);

View File

@ -16,6 +16,8 @@ import { ConfirmationDialogComponent } from '../components/confirmation-dialog/c
import { PromptDialogComponent } from '../components/prompt-dialog/prompt-dialog.component';
import { RolePolicy } from './models/role-policy';
import { RoleUser } from './models/role-user';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-security',
@ -33,8 +35,12 @@ export class SecurityComponent implements OnInit, OnDestroy
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly securityService: SecurityService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService)
private readonly toastr: ToastrService,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('security.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
forkJoin({
users: securityService.getUsers(),
roles: securityService.getRoles(),

View File

@ -65,7 +65,7 @@
</div>
</div>
<div class="col-sm-9">
<!-- <div class="col-sm-9">
<div class="form-floating">
<input type="text" class="form-control" id="address" formControlName="address" placeholder="Address">
<label for="address">Address</label>
@ -95,7 +95,7 @@
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
<label for="country">Country</label>
</div>
</div>
</div> -->
</div>
<div class="d-flex justify-content-end align-items-center mt-5">

View File

@ -39,7 +39,6 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
import { AutofocusDirective } from './directives/autofocus.directive';
import { InlineEditorComponent } from './components/inline-editor/inline-editor.component';
import { ImagesComponent } from './catalog/images/images.component';
import { PackagesComponent } from './catalog/packages/packages.component';
import { FileSizePipe } from './pipes/file-size.pipe';
import { AutosizeModule } from 'ngx-autosize';
@ -54,7 +53,6 @@ import { LazyLoadDirective } from './directives/lazy-load.directive';
AutofocusDirective,
//HasPermissionDirective,
InlineEditorComponent,
ImagesComponent,
PackagesComponent,
FileSizePipe,
AlphaOnlyDirective,
@ -118,7 +116,6 @@ import { LazyLoadDirective } from './directives/lazy-load.directive';
AutofocusDirective,
TimeagoModule,
NgxDatatableModule,
ImagesComponent,
PackagesComponent,
FileSizePipe,
InlineEditorComponent,

View File

@ -43,8 +43,8 @@
</div>
</div>
<div class="overflow-auto">
<div class="container my-4">
<div class="overflow-auto flex-grow-1 my-3">
<div class="container my-2">
<div class="table-responsive" *ngIf="!loadingIndicator">
<p *ngIf="!volumes.length" class="text-center text-info text-faded p-3 mb-0">
There are no volumes yet.
@ -62,7 +62,7 @@
<tbody>
<tr *ngFor="let volume of listItems">
<td>
<b class="text-uppercase">{{ volume.name }}</b>
<b>{{ volume.name }}</b>
</td>
<td>
{{ volume.size * 1024 * 1024 | fileSize}}
@ -93,6 +93,9 @@
</li>
</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>
</td>
</tr>
</tbody>

View File

@ -1,3 +1,9 @@
:host
{
flex-grow: 1;
overflow : hidden;
}
.table-responsive
{
background-color: rgba(16, 21, 39, 0.75);
@ -36,4 +42,9 @@
color: #3d5e8e;
}
}
.in-use
{
padding: 0 .75rem;
}
}

View File

@ -13,6 +13,8 @@ import { sortArray } from '../helpers/utils.service';
import Fuse from 'fuse.js';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
import { distinctUntilChanged, first, takeUntil, debounceTime, filter, switchMap } from 'rxjs/operators';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-volumes',
@ -37,8 +39,11 @@ export class VolumesComponent implements OnInit, OnDestroy
private readonly instancesService: InstancesService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly fb: FormBuilder)
private readonly fb: FormBuilder,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('volumes.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
// Configure FuseJs
this.fuseJsOptions = {

View File

@ -34,7 +34,7 @@ import { VolumeEditorComponent } from './volume-editor/volume-editor.component';
loader: {
provide: TranslateLoader,
//useClass: WebpackTranslateLoader
useFactory: () => new WebpackTranslateLoader('networking')
useFactory: () => new WebpackTranslateLoader('volumes')
},
compiler: {
provide: TranslateCompiler,

View File

@ -1,5 +1,10 @@
{
"account":
{
"title": "Account",
"myProfile": "My profile",
"updateProfile": "Update profile",
"myKeys": "My keys",
"addKey": "Add key"
}
}

View File

@ -7,9 +7,9 @@
"memory": "Memory",
"disk": "Storage"
},
"custom":
"images":
{
"title": "Images"
}
}
}

View File

@ -1,6 +1,7 @@
{
"dashboard":
{
"title": "Dashboard",
"general":
{
"save": "Save changes",
@ -15,23 +16,25 @@
"noResults": "No machine matches your filters",
"filters": "Showing {filteredCount} (out of {totalCount}: {totalRunning} running, {totalStopped} stopped)",
"filterByState": "Filter by state",
"anyState": "Any state",
"filterByMemory": "Filter by memory",
"filterByDisk": "Filter by disk size",
"between": "Between",
"and": "and",
"resetFilters": "Reset filters",
"showDetails": "Show machine details",
"sortBy": "Sort by {}",
"dualColumns": "2 columns on large displays",
"sortBy": "Sort by {sortProperty}",
"sortByName": "Name",
"sortByOs": "Operating system",
"sortByBrand": "Brand",
"sortByImage": "Image",
"sortByState": "State",
"between": "Between",
"and": "and"
"sortByState": "State"
},
"listItem":
{
"infrastructureContainer": "{type} - insfrastructure container",
"virtualMachine": "{type} - virtual machine",
"infrastructureContainer": "<b>{brand}</b> - Infrastructure container",
"virtualMachine": "<b>{brand}</b> - Virtual machine",
"stateRunning": "Running",
"stateStopping": "Stopping",
"stateProvisioning": "Provisioning",

View File

@ -6,9 +6,7 @@
"dashboard": "Dashboard",
"fileManager": "File Manager",
"volumes": "Volumes",
"customImages": "Custom Images",
"dockerImages": "Docker Images",
"dockerRegistry": "Docker Registry",
"images": "Images",
"networks": "Networks",
"virtualNetworks": "Virtual Networks",
"firewallRules": "Firewall rules",
@ -23,19 +21,9 @@
},
"catalog":
{
"customImages":
"images":
{
"title": "Custom Images",
"subTitle": ""
},
"dockerImages":
{
"title": "Docker Images",
"subTitle": ""
},
"dockerRegistry":
{
"title": "Docker Registry",
"title": "Images",
"subTitle": ""
}
},

View File

@ -1,6 +1,13 @@
{
"networking":
{
"networks":
{
"title": "Networks"
},
"firewall":
{
"title": "Firewall rules"
}
}
}

View File

@ -1,6 +1,7 @@
{
"security":
{
"title": "Security",
"users": "Users",
"roles": "Roles",
"policies": "Policies",

View File

@ -1,5 +1,6 @@
{
"volumes":
{
"title": "Volumes"
}
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Manta</title>
<title>Joyent</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -11,7 +11,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<style type="text/css">.spinner{font-size:10px;text-indent:-9999em;width:10em;height:10em;border-radius:50%;background:#0dcaf0;background:linear-gradient(to right,#0dcaf0 10%,rgba(17,124,238,0) 42%);position:relative;animation:load3 1.4s infinite linear;transform:translateZ(0) translate(-50%,-50%);transform-origin:0 0;position:absolute;left:50%;top:50%}.spinner:before{width:50%;height:50%;background:#0dcaf0;border-radius:100% 0 0 0;position:absolute;top:0;left:0;content:''}.spinner:after{background:#090b17;width:75%;height:75%;border-radius:50%;content:'';margin:auto;position:absolute;top:0;left:0;bottom:0;right:0}@keyframes load3{0%{transform:rotate(0) translateZ(0) translate(-50%,-50%)}100%{transform:rotate(360deg) translateZ(0) translate(-50%,-50%)}}</style>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Mukta&display=swap" rel="stylesheet">
</head>
<body>

View File

@ -13,6 +13,7 @@ body
color: #3d5e8e;
font-family: 'Mukta', sans-serif;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
body, div, virtual-scroller
@ -450,7 +451,7 @@ body, div, virtual-scroller
{
+ .panel
{
margin-top: 2rem;
margin-top: 1.5rem;
}
.card
@ -609,9 +610,6 @@ virtual-scroller
flex-wrap: wrap;
justify-content: start;
margin-top: -1rem;
/* margin-top: calc(var(--bs-gutter-y) * -1);
margin-right: calc(var(--bs-gutter-x)/ -2);
margin-left: calc(var(--bs-gutter-x)/ -2); */
--bs-gutter-y: 2rem;
position: relative;
@ -621,7 +619,8 @@ virtual-scroller
max-width: 100%;
padding-right: calc(var(--bs-gutter-x)/ 2);
padding-left: calc(var(--bs-gutter-x)/ 2);
margin-top: var(--bs-gutter-y);
padding-bottom: calc(var(--bs-gutter-y) / 2);
margin-top: calc(var(--bs-gutter-y) / 2);
}
}
}
@ -652,6 +651,14 @@ virtual-scroller
}
}
@media (min-width: 992px)
{
.w-lg-auto
{
width: auto !important;
}
}
accordion
{
.panel-heading
@ -673,7 +680,7 @@ accordion
font-family: "Bebas Neue", sans-serif;
font-size: 1.2rem;
z-index: 1020;
max-height: 50vh;
max-height: 60vh;
overflow: auto;
}
@ -720,14 +727,17 @@ accordion
}
}
.form-control
.form-control, .form-select
{
border-color: #11182b;
background: #11182b;
box-shadow: 0 0 0 1px rgb(0 231 255 / 75%) inset;
background-color: #11182b;
padding: .5rem .75rem .375rem;
}
.form-select
{
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ff9c07' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
}
.form-switch
{
display: inline-flex;
@ -928,3 +938,14 @@ accordion
{
overflow: hidden;
}
.list-group-item
{
border: none;
padding: .25rem 1rem;
}
.machine-brand b
{
text-transform: uppercase;
}