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

View File

@ -1,55 +1,60 @@
<div *ngIf="userInfo" class="pt-3"> <div class="d-flex flex-column h-100">
<div class="row"> <div class="overflow-auto flex-grow-1">
<div class="col-sm-6"> <div class="container mb-3">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4>My profile</h4>
<button class="btn btn-sm btn-outline-info" (click)="showEditor()">Update profile</button> <div *ngIf="userInfo" class="row pt-2">
<div class="col-sm-6">
<fieldset>
<legend>
{{ 'account.myProfile' | translate }}
<button class="btn btn-outline-info" container="body" (click)="showEditor()">
{{ 'account.updateProfile' | translate }}
</button>
</legend>
<ul class="list-group list-group-flush">
<li class="list-group-item">
Name: <b>{{ userInfo.firstName }} {{ userInfo.lastName }}</b>
</li>
<li class="list-group-item">
Username: <b>{{ userInfo.login }}</b>
</li>
<li class="list-group-item">
Email: <b>{{ userInfo.email }}</b>
</li>
<li class="list-group-item">
Phone: <b>{{ userInfo.phone }}</b>
</li>
<li class="list-group-item">
Container Name Service:
<span class="badge border ms-1 text-uppercase"
[ngClass]="userInfo.triton_cns_enabled ? 'border-success text-success' : 'danger-success text-danger'">
{{ userInfo.triton_cns_enabled ? 'enabled' : 'disabled' }}
</span>
</li>
</ul>
</fieldset>
</div> </div>
<div class="card-body"> <div class="col-sm-6">
<ul class="list-group list-group-flush"> <fieldset>
<li class="list-group-item"> <legend>
Name: <b>{{ userInfo.firstName }} {{ userInfo.lastName }}</b> {{ 'account.myKeys' | translate }}
</li>
<li class="list-group-item">
Username: <b>{{ userInfo.login }}</b>
</li>
<li class="list-group-item">
Email: <b>{{ userInfo.email }}</b>
</li>
<li class="list-group-item">
Phone: <b>{{ userInfo.phone }}</b>
</li>
<li class="list-group-item">
Container Name Service:
<span class="badge border ms-1 text-uppercase"
[ngClass]="userInfo.triton_cns_enabled ? 'border-success text-success' : 'danger-success text-danger'">
{{ userInfo.triton_cns_enabled ? 'enabled' : 'disabled' }}
</span>
</li>
</ul>
</div>
</div>
</div>
<div class="col-sm-6"> <button class="btn btn-outline-info" container="body" (click)="addSshKey()">
<div class="card"> {{ 'account.addKey' | translate }}
<div class="card-header d-flex justify-content-between align-items-center"> </button>
<h4>My SSH keys</h4> </legend>
<button class="btn btn-sm btn-outline-info" (click)="addSshKey()">Add SSH key</button> <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 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>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@ -1,7 +1,7 @@
ul, ol :host
{ {
background-color: rgba(16, 21, 39, .75); overflow: hidden;
height: 100%; flex-grow: 1;
} }
h4 h4
@ -11,10 +11,10 @@ h4
.list-group-item .list-group-item
{ {
background: none; background-color: transparent;
padding: 1rem; border-color: #354164;
border-color: rgb(61, 94, 142, .25);
color: #5a8cd8; color: #5a8cd8;
padding: .5rem 1rem;
b b
{ {
@ -22,21 +22,32 @@ h4
} }
} }
.card fieldset
{ {
border: 1px solid rgba(0, 0, 0, 0.5); background-color: rgba(16,21,39, .5);
background-color: rgba(16, 21, 39, 0.5); border-radius: .3rem;
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
transition: box-shadow 0.15s ease-out;
height: 100%; 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 &: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 { AccountEditorComponent } from './account-editor/account-editor.component';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { SshKeyEditorComponent } from './ssh-key-editor/ssh-key-editor.component'; import { SshKeyEditorComponent } from './ssh-key-editor/ssh-key-editor.component';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-account', selector: 'app-account',
@ -26,8 +28,12 @@ export class AccountComponent implements OnInit, OnDestroy
constructor(private readonly accountService: AccountService, constructor(private readonly accountService: AccountService,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly modalService: BsModalService, 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.getUsers().subscribe(x => console.log(x));
accountService.getUserLimits().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); 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,10 +46,8 @@
</nav> </nav>
</header> </header>
<div class="no-overflow flex-grow-1"> <div class="no-overflow flex-grow-1 d-flex flex-column">
<div class="h-100"> <router-outlet></router-outlet>
<router-outlet></router-outlet>
</div>
</div> </div>
</div> </div>
</div> </div>

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 { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { CatalogComponent } from './catalog.component'; import { CatalogComponent } from './catalog.component';
import { CustomImagesComponent } from './custom-images/custom-images.component'; import { ImagesComponent } from './images/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 { CustomImageEditorComponent } from './custom-image-editor/custom-image-editor.component'; import { CustomImageEditorComponent } from './custom-image-editor/custom-image-editor.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
CatalogComponent, CatalogComponent,
CustomImagesComponent, ImagesComponent
DockerImagesComponent,
DockerRegistryComponent,
DockerImageEditorComponent,
DockerRegistryEditorComponent
], ],
imports: [ imports: [
SharedModule, SharedModule,
@ -35,38 +27,19 @@ import { CustomImageEditorComponent } from './custom-image-editor/custom-image-e
children: [ children: [
{ {
path: '', path: '',
redirectTo: 'custom-images' redirectTo: 'images'
}, },
{ {
path: 'custom-images', path: 'images',
component: CustomImagesComponent, component: ImagesComponent,
data: data:
{ {
title: 'catalog.customImages.title', title: 'catalog.images.title',
subTitle: 'catalog.customImages.subTitle', subTitle: 'catalog.images.subTitle',
icon: 'layer-group' 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({ TranslateModule.forChild({
@ -83,8 +56,6 @@ import { CustomImageEditorComponent } from './custom-image-editor/custom-image-e
}) })
], ],
entryComponents: [ entryComponents: [
DockerImageEditorComponent,
DockerRegistryEditorComponent,
CustomImageEditorComponent CustomImageEditorComponent
] ]
}) })

View File

@ -59,15 +59,6 @@ export class CatalogService
return this.httpClient.get<CatalogImage[]>(`/api/my/images?${allStates ? 'state=all' : ''}`); 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({ @Cacheable({
cacheBusterObserver: imagesCacheBuster$ cacheBusterObserver: imagesCacheBuster$

View File

@ -1,20 +1,200 @@
<table class="table"> <div class="d-flex flex-column h-100">
<thead> <div class="container text-center mt-1" [formGroup]="editorForm">
<tr> <div class="btn-toolbar pt-2">
<th>Name</th> <span class="d-none d-sm-block flex-grow-1"></span>
<th>Description</th>
<th>OS</th> <ng-container *ngIf="images && images.length">
<th></th> <div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
</tr> <input type="text" class="form-control" placeholder="Search by name..." formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$">
</thead> <button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
<tbody> tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
<tr *ngFor="let image of images"> <fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
<td>{{ image.name }}</td> </button>
<td>{{ image.description }}</td> </div>
<td>{{ image.os }}</td>
<td> <div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom left">
<button class="btn btn-sm btn-secondary" (click)="select.emit(image)">select</button> <button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
</td> Sort by
</tr> <b *ngIf="editorForm.get('sortProperty').value === 'name'">name</b>
</tbody> <b *ngIf="editorForm.get('sortProperty').value === 'description'">description</b>
</table> <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 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 myListItems">
<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">
<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'; import { ImagesComponent } from './images.component';
describe('ImagesComponent', () => { describe('CustomImagesComponent', () => {
let component: ImagesComponent; let component: ImagesComponent;
let fixture: ComponentFixture<ImagesComponent>; let fixture: ComponentFixture<ImagesComponent>;
beforeEach(async () => { beforeEach(async(() => {
await TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ ImagesComponent ] declarations: [ ImagesComponent ]
}) })
.compileComponents(); .compileComponents();
}); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ImagesComponent); 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 { ColumnMode, SelectionType } from '@swimlane/ngx-datatable';
import { CatalogService } from '../helpers/catalog.service'; 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({ @Component({
selector: 'app-images', selector: 'app-images',
templateUrl: './images.component.html', templateUrl: './images.component.html',
styleUrls: ['./images.component.scss'] styleUrls: ['./images.component.scss']
}) })
export class ImagesComponent implements OnInit export class ImagesComponent implements OnInit, OnDestroy
{ {
@Output() myImages: CatalogImage[] = [];
select = new EventEmitter(); myListItems: CatalogImage[] = [];
images: CatalogImage[] = [];
images: any[]; listItems: CatalogImage[] = [];
editorForm: FormGroup;
loadingIndicator = true; loadingIndicator = true;
selectionType = SelectionType; myImagesExpanded = true;
columnMode = ColumnMode; 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}`));
{
this.images = x;
this.loadingIndicator = false; // 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.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 ngOnInit(): void
{ {
this.getCustomImages();
} }
// ----------------------------------------------------------------------------------------------------------------
ngOnDestroy()
{
this.destroy$.next();
}
} }

View File

@ -40,9 +40,9 @@
</li>--> </li>-->
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
<li class="nav-item"> <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> <fa-icon [fixedWidth]="true" icon="layer-group"></fa-icon>
{{ 'navbar.menu.customImages' | translate }} {{ 'navbar.menu.images' | translate }}
</a> </a>
</li> </li>
<!--<li class="nav-item"> <!--<li class="nav-item">

View File

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

View File

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

View File

@ -1,32 +1,36 @@
<ul class="list-group list-group-flush list-info"> <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"> <li class="dropdown-header">Machine identifier</li>
<fa-icon icon="fingerprint" [fixedWidth]="true" size="sm"></fa-icon> <li class="list-group-item text-uppercase ps-0">
<b class="ms-1">{{ instance.id }}</b> <b>{{ instance.id }}</b>
</li> </li>
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center" <ng-container *ngIf="instance.dnsList">
*ngFor="let keyValue of instance.dnsList | keyvalue; let index = index"> <li class="dropdown-header">DNS list</li>
<div class="text-truncate"> <li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
<fa-icon icon="link" [fixedWidth]="true" size="sm"></fa-icon> *ngFor="let keyValue of instance.dnsList | keyvalue; let index = index">
<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">
{{ keyValue.value[0] }} <!--<span class="ms-1" [ngClass]="keyValue.value[0] === instance.id || keyValue.value[0] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
</span> {{ keyValue.value[0] }}
</span>
<span *ngIf="keyValue.value[1]" [ngClass]="keyValue.value[1] === instance.id || keyValue.value[1] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'"> <span *ngIf="keyValue.value[1]" [ngClass]="keyValue.value[1] === instance.id || keyValue.value[1] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
{{ keyValue.value[1] }} {{ keyValue.value[1] }}
</span> </span>
{{ keyValue.value[2] }} {{ keyValue.value[2] }}-->
</div> {{ keyValue.key }}
</div>
<div class="btn-group btn-group-sm" *ngxClipboardIfSupported <div class="btn-group btn-group-sm" *ngxClipboardIfSupported
tooltip="Copy link to clipboard" container="body" placement="top" [adaptivePosition]="false"> tooltip="Copy link to clipboard" container="body" placement="top" [adaptivePosition]="false">
<button class="btn btn-link py-0 text-info" ngxClipboard [cbContent]="keyValue.key" (cbOnSuccess)="dnsCopied($event)"> <button class="btn btn-link py-0 text-info" ngxClipboard [cbContent]="keyValue.key" (cbOnSuccess)="dnsCopied($event)">
<fa-icon icon="clipboard" [fixedWidth]="true" size="sm"></fa-icon> <fa-icon icon="clipboard" [fixedWidth]="true" size="sm"></fa-icon>
</button> </button>
</div> </div>
</li> </li>
</ng-container>
<li class="dropdown-header">Deletion protection</li>
<li class="list-group-item ps-0 pb-0"> <li class="list-group-item ps-0 pb-0">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input mt-0" type="checkbox" id="dp{{ instance.id }}" [(ngModel)]="instance.deletion_protection" <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); 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 { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service'; 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 { delay, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstancesService } from '../helpers/instances.service'; import { InstancesService } from '../helpers/instances.service';
import { BsModalService } from 'ngx-bootstrap/modal'; import { BsModalService } from 'ngx-bootstrap/modal';
@ -12,20 +12,19 @@ import { Instance } from '../models/instance';
templateUrl: './instance-info.component.html', templateUrl: './instance-info.component.html',
styleUrls: ['./instance-info.component.scss'] styleUrls: ['./instance-info.component.scss']
}) })
export class InstanceInfoComponent implements OnInit, OnDestroy export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
{ {
@Input() @Input()
instance: Instance; instance: Instance;
@Input() @Input()
set loadInfo(value: boolean) loadInfo: boolean;
{
if (!this.finishedLoading && value && this.instance)
this.getInfo();
}
@Output() @Output()
beforeLoad = new EventEmitter(); processing = new EventEmitter();
@Output()
finishedProcessing = new EventEmitter();
@Output() @Output()
load = new EventEmitter(); load = new EventEmitter();
@ -34,6 +33,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
private finishedLoading: boolean; private finishedLoading: boolean;
private destroy$ = new Subject(); private destroy$ = new Subject();
private onChanges$ = new ReplaySubject();
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly instancesService: InstancesService, constructor(private readonly instancesService: InstancesService,
@ -46,18 +46,18 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
toggleDeletionProtection(event, instance: Instance) toggleDeletionProtection(event, instance: Instance)
{ {
this.beforeLoad.emit(); this.processing.emit();
this.instancesService.toggleDeletionProtection(instance.id, event.target.checked) this.instancesService.toggleDeletionProtection(instance.id, event.target.checked)
.subscribe(() => .subscribe(() =>
{ {
this.toastr.info(`The deletion protection for machine "${instance.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`); this.toastr.info(`The deletion protection for machine "${instance.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
this.load.emit(); this.finishedProcessing.emit();
}, },
err => err =>
{ {
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`); 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.loading = false;
this.finishedLoading = true; this.finishedLoading = true;
this.load.emit(dnsList);
}, },
err => err =>
{ {
@ -109,8 +110,21 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void 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() ngOnDestroy()
{ {

View File

@ -24,7 +24,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
loadNetworks: boolean; loadNetworks: boolean;
@Output() @Output()
beforeLoad = new EventEmitter(); processing = new EventEmitter();
@Output()
finishedProcessing = new EventEmitter();
@Output() @Output()
load = new EventEmitter(); load = new EventEmitter();
@ -62,14 +65,16 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
} }
}, err => }, err =>
{ {
const errorDetails = err.error?.message ? `(${err.error.message})` : ''; const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to load the list of available networks for machine "${this.instance.name}" ${errorDetails}`); this.toastr.error(`Failed to load the list of available networks for machine "${this.instance.name}" ${errorDetails}`);
}); });
} }
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
private getNetworks() private getNetworks()
{ {
if (this.finishedLoading) return;
this.loading = true; this.loading = true;
const observables = this.nics.map(x => this.networkingService.getNetwork(x.network)); 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 : ''; nic.networkName = nic.networkDetails ? nic.networkDetails.name : '';
} }
this.finishedLoading = true;
this.loading = false; this.loading = false;
this.finishedLoading = true;
this.load.emit(this.nics);
}, },
err => err =>
{ {
@ -113,7 +119,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
first(), first(),
tap(() => tap(() =>
{ {
this.beforeLoad.emit(); this.processing.emit();
this.toastr.info(`Connecting machine "${this.instance.name}" to the "${network.name}" network...`); 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 || ''; 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.toastr.info(`The machine "${this.instance.name}" has been connected to the "${network.name}" network`);
this.load.emit(); this.finishedProcessing.emit();
}, },
err => err =>
{ {
@ -171,7 +179,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
const errorDetails = err.error?.message ? `(${err.error.message})` : ''; 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.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(), first(),
tap(() => tap(() =>
{ {
this.beforeLoad.emit(); this.processing.emit();
this.toastr.info(`Removing network interface "${nic.mac.toUpperCase()}" from machine "${this.instance.name}"...`); 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; 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}"`); this.toastr.info(`The network interface has been removed from machine "${this.instance.name}"`);
}, err => }, err =>
{ {
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); 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 || []; this.nics = this.instance?.nics || [];
if (this.instance.networksLoaded)
this.finishedLoading = true;
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() => this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
{ {
if (!this.finishedLoading && this.loadNetworks && this.instance) if (!this.finishedLoading && this.loadNetworks && !this.instance?.networksLoaded)
this.getNetworks(); 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 { ToastrService } from 'ngx-toastr';
import { CatalogService } from '../../catalog/helpers/catalog.service'; import { CatalogService } from '../../catalog/helpers/catalog.service';
import { InstancesService } from '../helpers/instances.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 { delay, first, switchMap, takeUntil, tap } from 'rxjs/operators';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { BsModalService } from 'ngx-bootstrap/modal'; import { BsModalService } from 'ngx-bootstrap/modal';
@ -15,20 +15,19 @@ import { SnapshotsService } from '../helpers/snapshots.service';
templateUrl: './instance-snapshots.component.html', templateUrl: './instance-snapshots.component.html',
styleUrls: ['./instance-snapshots.component.scss'] styleUrls: ['./instance-snapshots.component.scss']
}) })
export class InstanceSnapshotsComponent implements OnInit, OnDestroy export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
{ {
@Input() @Input()
instance: any; instance: any;
@Input() @Input()
set loadSnapshots(value: boolean) loadSnapshots: boolean;
{
if (value && this.instance && !this.snapshots)
this.getSnapshots();
}
@Output() @Output()
beforeLoad = new EventEmitter(); processing = new EventEmitter();
@Output()
processingFinished = new EventEmitter();
@Output() @Output()
load = new EventEmitter(); load = new EventEmitter();
@ -37,13 +36,16 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
instanceStateUpdate = new EventEmitter(); instanceStateUpdate = new EventEmitter();
loadingSnapshots: boolean; loadingSnapshots: boolean;
snapshotsLoaded: boolean;
filteredSnapshots: any[]; filteredSnapshots: any[];
snapshotName: string; snapshotName: string;
_searchTerm: string; _searchTerm: string;
shouldSearch: boolean; shouldSearch: boolean;
private destroy$ = new Subject(); private destroy$ = new Subject();
private onChanges$ = new ReplaySubject();
private snapshots: any[]; private snapshots: any[];
private finishedLoading: boolean
private readonly fuseJsOptions: {}; private readonly fuseJsOptions: {};
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
@ -69,7 +71,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
createSnapshot() createSnapshot()
{ {
this.beforeLoad.emit(); this.processing.emit();
this.snapshots = this.snapshots || []; this.snapshots = this.snapshots || [];
@ -95,7 +97,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
if (index >= 0) if (index >= 0)
this.snapshots[index] = x; 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}"`); this.toastr.info(`A new snapshot "${snapshotName}" has been created for machine "${this.instance.name}"`);
}, },
err => err =>
@ -106,7 +108,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
if (index >= 0) if (index >= 0)
this.snapshots.splice(index, 1); this.snapshots.splice(index, 1);
this.load.emit(); this.processingFinished.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
}); });
} }
@ -117,7 +119,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
this.confirmRestore(snapshot) this.confirmRestore(snapshot)
.subscribe(() => .subscribe(() =>
{ {
this.beforeLoad.emit(); this.processing.emit();
snapshot.working = true; snapshot.working = true;
@ -135,7 +137,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
err => err =>
{ {
snapshot.working = false; snapshot.working = false;
this.load.emit(); this.processingFinished.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); 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) private startMachineFromSnapshot(snapshot: Snapshot)
{ {
this.beforeLoad.emit(); this.processing.emit();
this.toastr.info(`Restoring machine "${this.instance.name}" from "${snapshot.name}" snapshot`); 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; 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`); this.toastr.info(`The machine "${this.instance.name}" has been started from the "${snapshot.name}" snapshot`);
}, err => }, err =>
{ {
snapshot.working = false; snapshot.working = false;
this.load.emit(); this.processingFinished.emit();
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`); 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(() => modalRef.content.confirm.pipe(first()).subscribe(() =>
{ {
this.beforeLoad.emit(); this.processing.emit();
this.snapshotsService.deleteSnapshot(this.instance.id, snapshot.name) this.snapshotsService.deleteSnapshot(this.instance.id, snapshot.name)
.subscribe(() => .subscribe(() =>
@ -223,12 +225,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
if (index >= 0) if (index >= 0)
this.snapshots.splice(index, 1); this.snapshots.splice(index, 1);
this.load.emit(); this.processingFinished.emit();
this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`); this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`);
}, err => }, err =>
{ {
this.load.emit(); this.processingFinished.emit();
this.toastr.error(`The "${snapshot.name}" snapshot couldn't be deleted: ${err.error.message}`); 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() private getSnapshots()
{ {
if (this.snapshotsLoaded) return
this.loadingSnapshots = true; this.loadingSnapshots = true;
// Get the list of snapshots // Get the list of snapshots
@ -246,7 +250,10 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
{ {
this.snapshots = x; this.snapshots = x;
this.filteredSnapshots = x; this.filteredSnapshots = x;
this.loadingSnapshots = false; this.loadingSnapshots = false;
this.snapshotsLoaded = true;
this.load.emit(x);
}, },
err => err =>
{ {
@ -288,6 +295,21 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void 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 { NetworkingService } from '../../networking/helpers/networking.service';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { VolumesService } from '../../volumes/helpers/volumes.service'; import { VolumesService } from '../../volumes/helpers/volumes.service';
import { VolumeResponse } from '../../volumes/models/volume'; import { AuthService } from '../../helpers/auth.service';
@Component({ @Component({
selector: 'app-instance-wizard', selector: 'app-instance-wizard',
@ -51,12 +51,14 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
kvmRequired: boolean; kvmRequired: boolean;
private destroy$ = new Subject(); private destroy$ = new Subject();
private userId: string;
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly modalRef: BsModalRef, constructor(private readonly modalRef: BsModalRef,
private readonly router: Router, private readonly router: Router,
private readonly fb: FormBuilder, private readonly fb: FormBuilder,
private readonly fileSizePipe: FileSizePipe, private readonly fileSizePipe: FileSizePipe,
private readonly authService: AuthService,
private readonly instancesService: InstancesService, private readonly instancesService: InstancesService,
private readonly catalogService: CatalogService, private readonly catalogService: CatalogService,
private readonly networkingService: NetworkingService, private readonly networkingService: NetworkingService,
@ -93,6 +95,10 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
description: 'Tag and create your machine' 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) if (imageType === 1)
{ {
for (const image of this.images) 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; operatingSystems[image.os] = true;
imageList.push(image); imageList.push(image);
@ -172,7 +178,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 2) else if (imageType === 2)
{ {
for (const image of this.images) 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; operatingSystems[image.os] = true;
imageList.push(image); imageList.push(image);
@ -181,7 +187,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 3) else if (imageType === 3)
{ {
for (const image of this.images) for (const image of this.images)
if (!['zvol', 'lx-dataset', 'zone-dataset'].includes(image.type)) if (image.owner === this.userId)
{ {
operatingSystems[image.os] = true; operatingSystems[image.os] = true;
imageList.push(image); imageList.push(image);
@ -213,7 +219,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
if (imageType === 1) if (imageType === 1)
{ {
for (const image of this.images) 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; operatingSystems[image.os] = true;
imageList.push(image); imageList.push(image);
@ -222,7 +228,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 2) else if (imageType === 2)
{ {
for (const image of this.images) 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; operatingSystems[image.os] = true;
imageList.push(image); imageList.push(image);
@ -231,7 +237,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
else if (imageType === 3) else if (imageType === 3)
{ {
for (const image of this.images) 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; operatingSystems[image.os] = true;
imageList.push(image); imageList.push(image);
@ -552,9 +558,9 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
if (this.instance) if (this.instance)
{ {
if (this.instance.type === 'virtualmachine') if (this.instance.type === 'smartmachine')
this.imageType = 1; this.imageType = 1;
else if (this.instance.type === 'smartmachine') else if (this.instance.type === 'virtualmachine')
this.imageType = 2; this.imageType = 2;
this.preselectedPackage = this.instance.package; this.preselectedPackage = this.instance.package;

View File

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

View File

@ -1,152 +1,169 @@
.ips :host {
{ flex-direction: column;
+ .ips overflow : hidden;
{ }
margin-left: .5rem;
.ips {
+.ips {
margin-left : .5rem;
padding-left: .5rem; padding-left: .5rem;
border-left: 2px solid #2b3540; border-left : 2px solid #2b3540;
} }
} }
.card .card {
{ border : 1px solid rgba(0, 0, 0, .5);
border: 1px solid rgba(0, 0, 0, .5);
background-color: rgba(16, 21, 39, .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; box-shadow : 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
transition: box-shadow .15s ease-out; 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; 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;
color: #ff9c07;
margin-bottom: 0; margin-bottom: 0;
font-weight: bold; font-weight : bold;
} }
.card-body .card-body {
{
background-color: rgba(16, 21, 39, .5); background-color: rgba(16, 21, 39, .5);
max-height: 205px; max-height : 205px;
overflow: auto; overflow : auto;
padding-top: .7rem !important; padding-top : .7rem !important;
} }
.list-group-item .list-group-item {
{
background: none; background: none;
span span {
{
color: #8881ff; color: #8881ff;
} }
b, .strong b,
{ .strong {
color: #ff9c07; color : #ff9c07;
font-weight: bold; font-weight: bold;
} }
} }
.btn-info, .btn-outline-info .btn-info,
{ .btn-outline-info {
font-size: 1rem; font-size : 1rem;
line-height: 1; line-height: 1;
} }
} }
.card-info .card-info {
{
background-color: rgba(16, 21, 39, .75); background-color: rgba(16, 21, 39, .75);
display: flex; display : flex;
flex-direction: column; flex-direction : column;
justify-content: space-between; justify-content : space-between;
padding: .25rem .5rem .5rem; padding : .25rem .5rem .5rem;
height: 170px; height : 170px;
} }
.full-details .card-info .full-details .card-info {
{
height: 240px; height: 240px;
} }
@media (max-width: 576px) @media (max-width: 576px) {
{ .card-info {
.card-info
{
height: auto; height: auto;
} }
} }
.no-overflow-sm .no-overflow-sm {
{
overflow: hidden; overflow: hidden;
} }
@media (max-width: 992px) @media (max-width: 992px) {
{ .no-overflow-sm {
.no-overflow-sm
{
overflow: visible; overflow: visible;
} }
} }
.open .dropdown-toggle .open .dropdown-toggle {}
{
}
.filters .filters {
{
width: 240px; width: 240px;
} }
.form-check-label .form-check-label {
{
color: inherit; color: inherit;
} }
.btn-toolbar .btn .btn-toolbar .btn {
{ .badge {
.badge padding : 0.1rem 0.35rem 0;
{ border : 1px solid transparent;
padding: 0.1rem 0.35rem 0;
border: 1px solid transparent;
text-shadow: 0 0 3px rgb(255 255 255 / 25%); text-shadow: 0 0 3px rgb(255 255 255 / 25%);
&:first-letter &:first-letter {
{
font-size: 1.2rem; font-size: 1.2rem;
} }
} }
&:hover &:hover {
{ .badge {
.badge
{
border-color: #000; border-color: #000;
} }
} }
} }
@keyframes flash @keyframes flash {
{
from, 50%, to from,
{ 50%,
to {
opacity: 1; opacity: 1;
} }
25%, 75% 25%,
{ 75% {
opacity: 0; opacity: 0;
} }
} }
.flash .flash {
{ animation-name : flash;
animation-name: flash; animation-duration : calc(.9s * 1.3);
animation-duration: calc(.9s * 1.3);
animation-timing-function: ease-in-out; 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 { BsModalService } from 'ngx-bootstrap/modal';
import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component'; import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component';
import { SelectionType, ColumnMode } from '@swimlane/ngx-datatable';
import { Instance } from './models/instance'; import { Instance } from './models/instance';
import { forkJoin, Subject } from 'rxjs'; import { forkJoin, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr'; 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 { FileSizePipe } from '../pipes/file-size.pipe';
import { sortArray } from '../helpers/utils.service'; import { sortArray } from '../helpers/utils.service';
import { VolumesService } from '../volumes/helpers/volumes.service'; import { VolumesService } from '../volumes/helpers/volumes.service';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-instances', selector: 'app-instances',
@ -42,6 +43,7 @@ export class InstancesComponent implements OnInit, OnDestroy
canPrepareForLoading: boolean; canPrepareForLoading: boolean;
editorForm: FormGroup; editorForm: FormGroup;
showMachineDetails: boolean; showMachineDetails: boolean;
fullDetailsTwoColumns: boolean;
runningInstanceCount = 0; runningInstanceCount = 0;
stoppedInstanceCount = 0; stoppedInstanceCount = 0;
instanceStateArray: string[] = []; instanceStateArray: string[] = [];
@ -72,8 +74,12 @@ export class InstancesComponent implements OnInit, OnDestroy
private readonly modalService: BsModalService, private readonly modalService: BsModalService,
private readonly toastr: ToastrService, private readonly toastr: ToastrService,
private readonly fb: FormBuilder, 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; this.lazyLoadDelay = this.minimumLazyLoadDelay;
// Configure FuseJs // Configure FuseJs
@ -93,6 +99,7 @@ export class InstancesComponent implements OnInit, OnDestroy
}; };
this.showMachineDetails = !!JSON.parse(localStorage.getItem('showMachineDetails') || '0'); this.showMachineDetails = !!JSON.parse(localStorage.getItem('showMachineDetails') || '0');
this.fullDetailsTwoColumns = !!JSON.parse(localStorage.getItem('fullDetailsTwoColumns') || '1');
this.createForm(); this.createForm();
} }
@ -108,9 +115,9 @@ export class InstancesComponent implements OnInit, OnDestroy
switch (label) switch (label)
{ {
case LabelType.Low: case LabelType.Low:
return `<b>Between</b> ${formattedValue}`; return `Between ${formattedValue}`;
case LabelType.High: case LabelType.High:
return `<b>and</b> ${formattedValue}`; return `and ${formattedValue}`;
default: default:
return formattedValue; return formattedValue;
} }
@ -187,7 +194,8 @@ export class InstancesComponent implements OnInit, OnDestroy
imageFilter: [], // instances provisioned with a certain image imageFilter: [], // instances provisioned with a certain image
}), }),
filtersActive: [false], filtersActive: [false],
showMachineDetails: [this.showMachineDetails] showMachineDetails: [this.showMachineDetails],
fullDetailsTwoColumns: [{ value: this.fullDetailsTwoColumns, disabled: !this.showMachineDetails }]
}); });
this.editorForm.get('searchTerm').valueChanges this.editorForm.get('searchTerm').valueChanges
@ -232,7 +240,15 @@ export class InstancesComponent implements OnInit, OnDestroy
// Store this setting in the local storage // Store this setting in the local storage
localStorage.setItem('showMachineDetails', JSON.stringify(showMachineDetails)); 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); this.editorForm.get('sortProperty').setValue(propertyName);
} }
// ----------------------------------------------------------------------------------------------------------------
setStateFilter(state?: string)
{
this.editorForm.get(['filters', 'stateFilter']).setValue(state);
}
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
clearSearch() clearSearch()
{ {
@ -700,6 +722,7 @@ export class InstancesComponent implements OnInit, OnDestroy
}; };
const modalRef = this.modalService.show(InstanceHistoryComponent, modalConfig); 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; instance.shouldLoadSnapshots = this.editorForm.get('showMachineDetails').value;
else if (event.id.endsWith('networks')) else if (event.id.endsWith('networks'))
instance.shouldLoadNetworks = this.editorForm.get('showMachineDetails').value; 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')) else if (event.id.endsWith('volumes'))
{ {
//instance.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value; //instance.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value;
@ -782,6 +803,33 @@ export class InstancesComponent implements OnInit, OnDestroy
instance.state = updates.state; 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) private fillInInstanceDetails(instance: Instance)
{ {

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ import { ConfirmationDialogComponent } from '../../components/confirmation-dialo
import { InstancesService } from '../../instances/helpers/instances.service'; import { InstancesService } from '../../instances/helpers/instances.service';
import { FirewallService } from '../helpers/firewall.service'; import { FirewallService } from '../helpers/firewall.service';
import { sortArray } from '../../helpers/utils.service'; import { sortArray } from '../../helpers/utils.service';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-firewall-rules', selector: 'app-firewall-rules',
@ -33,8 +35,12 @@ export class FirewallRulesComponent implements OnInit, OnDestroy
private readonly instancesService: InstancesService, private readonly instancesService: InstancesService,
private readonly modalService: BsModalService, private readonly modalService: BsModalService,
private readonly toastr: ToastrService, 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 // Configure FuseJs
this.fuseJsOptions = { this.fuseJsOptions = {
includeScore: false, includeScore: false,

View File

@ -43,12 +43,12 @@
</div> </div>
</div> </div>
<div class="overflow-auto"> <div class="overflow-auto flex-grow-1 mt-1">
<div class="container my-4"> <div class="container my-2">
<accordion [isAnimated]="false" [closeOthers]="false"> <accordion [isAnimated]="false" [closeOthers]="false">
<accordion-group *ngFor="let vlan of listItems" (isOpenChange)="getNetworks($event, vlan)"> <accordion-group *ngFor="let vlan of listItems" (isOpenChange)="getNetworks($event, vlan)">
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading <div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
tooltip="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"> <h4 class="mb-0">
<span class="text-info me-2">{{ vlan.name }}</span> <span class="text-info me-2">{{ vlan.name }}</span>
<span class="vlan-id"> <span class="vlan-id">
@ -133,17 +133,29 @@
</div> </div>
<div class="card-footer px-2 d-flex justify-content-between align-items-center"> <div class="card-footer px-2 d-flex justify-content-between align-items-center">
<button class="btn btn-sm btn-link text-info px-1" tooltip="Rename this virtual network" container="body" placement="top" [adaptivePosition]="false" <button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Configure a new network</button>
(click)="showVlanEditor(vlan)">
<fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon>
</button>
<button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Add a network</button> <div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body">
<button class="btn btn-link text-info" dropdownToggle
<button class="btn btn-sm btn-link text-danger px-1" tooltip="Delete this virtual network" container="body" placement="top" [adaptivePosition]="false" tooltip="Virtual network options" container="body" placement="top" [adaptivePosition]="false">
(click)="deleteVlan(vlan)"> <fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
<fa-icon icon="trash" [fixedWidth]="true" size="sm"></fa-icon> </button>
</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> </div>
</accordion-group> </accordion-group>
</accordion> </accordion>

View File

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

View File

@ -12,6 +12,8 @@ import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { sortArray } from '../../helpers/utils.service'; import { sortArray } from '../../helpers/utils.service';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-networks', selector: 'app-networks',
@ -32,8 +34,12 @@ export class NetworksComponent implements OnInit, OnDestroy
constructor(private readonly networkingService: NetworkingService, constructor(private readonly networkingService: NetworkingService,
private readonly modalService: BsModalService, private readonly modalService: BsModalService,
private readonly toastr: ToastrService, 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(); this.getVlans();
// Configure FuseJs // Configure FuseJs

View File

@ -1,237 +1,248 @@
<div class="overflow-auto h-100"> <div class="d-flex flex-column h-100">
<div class="container h-100 py-3"> <div class="overflow-auto flex-grow-1 d-flex">
<div class="row h-100" id="securitySettings"> <div class="container flex-grow-1 d-flex mb-3">
<div class="col-sm py-2"> <div class="row flex-grow-1">
<fieldset> <div class="col-sm py-2">
<legend> <fieldset>
<span> <legend>
<fa-icon [fixedWidth]="true" icon="key" size="sm"></fa-icon> <span>
{{ 'security.policies' | translate }} <fa-icon [fixedWidth]="true" icon="key" size="sm"></fa-icon>
</span> {{ 'security.policies' | translate }}
</span>
<button class="btn btn-outline-info" container="body" (click)="showPolicyEditor()"> <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>
</button> <fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addPolicy' | translate }}"
</legend> container="body" placement="top" [adaptivePosition]="false">
</fa-icon>
</button>
</legend>
<p class="px-3">Policies are just access rules grouped together</p> <p class="px-3">Policies are just access rules grouped together</p>
<ul class="list-group list-group-flush p-0" id="policies" <ul class="list-group list-group-flush p-0" id="policies" cdkDropList [cdkDropListData]="policies"
cdkDropList [cdkDropListData]="policies" [cdkDropListConnectedTo]="roleDropLists" [cdkDropListConnectedTo]="roleDropLists" cdkDropListSortingDisabled sortingDisabled
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="noReturnPredicate"> [cdkDropListEnterPredicate]="noReturnPredicate">
<li class="list-group-item pr-1" *ngFor="let policy of policies" <li class="list-group-item pr-1" *ngFor="let policy of policies" cdkDrag [cdkDragData]="policy"
cdkDrag [cdkDragData]="policy" cdkDragBoundary="#securitySettings"> cdkDragBoundary="#securitySettings">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<span class="grip" cdkDragHandle> <span class="grip" cdkDragHandle>
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon> <fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
{{ policy.name }} {{ policy.name }}
</span> </span>
<div class="btn-group btn-group-sm float-end" dropdown> <div class="btn-group btn-group-sm float-end" dropdown>
<!--<button class="btn text-info" tooltip="{{ 'security.assignPolicyToRoles' | translate }}" container="body" (click)="assignPolicyToRoles(policy)"> <!--<button class="btn text-info" tooltip="{{ 'security.assignPolicyToRoles' | translate }}" container="body" (click)="assignPolicyToRoles(policy)">
<fa-icon [fixedWidth]="true" icon="tags" size="sm"></fa-icon> <fa-icon [fixedWidth]="true" icon="tags" size="sm"></fa-icon>
</button>--> </button>-->
<button class="btn text-info" dropdownToggle> <button class="btn text-info" dropdownToggle>
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon> <fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
</button> </button>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu"> <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="showPolicyEditor(policy)"> <button class="dropdown-item" (click)="showPolicyEditor(policy)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
{{ 'security.editPolicy' | translate }} {{ 'security.editPolicy' | translate }}
</button> </button>
</li> </li>
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="deletePolicy(policy)"> <button class="dropdown-item" (click)="deletePolicy(policy)">
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon> <fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
{{ 'security.deletePolicy' | translate }} {{ 'security.deletePolicy' | translate }}
</button> </button>
</li> </li>
</ul> </ul>
</div>
</div>
<div *cdkDragPreview>
<fa-icon [fixedWidth]="true" icon="key"></fa-icon>
{{ policy.name }}
</div>
<div *cdkDragPlaceholder>{{ 'security.dropHere' | translate }}</div>
</li>
</ul>
</fieldset>
</div>
<div class="col-sm py-2">
<fieldset>
<legend>
<span>
<fa-icon [fixedWidth]="true" icon="tag" size="sm"></fa-icon>
{{ 'security.roles' | translate }}
</span>
<button class="btn btn-outline-info" container="body" (click)="showRoleEditor()">
{{ 'security.addRole' | translate }}
</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>
<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">
<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>
<div class="grip" cdkDragHandle>
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
{{ role.name }} <sup *ngIf="role.policies.length">({{ role.policies.length }})</sup>
</div> </div>
</div> </div>
<div class="btn-group btn-group-sm float-end" dropdown> <div *cdkDragPreview>
<!--<button class="btn text-info" tooltip="{{ 'security.assignRoleToUsers' | translate }}" container="body" (click)="assignRoleToUsers(role)"> <fa-icon [fixedWidth]="true" icon="key"></fa-icon>
{{ policy.name }}
</div>
<div *cdkDragPlaceholder>{{ 'security.dropHere' | translate }}</div>
</li>
</ul>
</fieldset>
</div>
<div class="col-sm py-2">
<fieldset>
<legend>
<span>
<fa-icon [fixedWidth]="true" icon="tag" size="sm"></fa-icon>
{{ 'security.roles' | translate }}
</span>
<button class="btn btn-outline-info" container="body" (click)="showRoleEditor()">
<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>
<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">
<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" placement="top"
[adaptivePosition]="false"></fa-icon>
<div class="grip" cdkDragHandle>
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
{{ role.name }} <sup *ngIf="role.policies.length">({{ role.policies.length }})</sup>
</div>
</div>
<div class="btn-group btn-group-sm float-end" dropdown>
<!--<button class="btn text-info" tooltip="{{ 'security.assignRoleToUsers' | translate }}" container="body" (click)="assignRoleToUsers(role)">
<fa-icon [fixedWidth]="true" icon="users" size="sm"></fa-icon> <fa-icon [fixedWidth]="true" icon="users" size="sm"></fa-icon>
</button>--> </button>-->
<button class="btn text-info" dropdownToggle> <button class="btn text-info" dropdownToggle>
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon> <fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
</button> </button>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu"> <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="showRoleEditor(role)"> <button class="dropdown-item" (click)="showRoleEditor(role)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
{{ 'security.editRole' | translate }} {{ 'security.editRole' | translate }}
</button> </button>
</li> </li>
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
<li role="menuitem"> <li role="menuitem">
<button class="dropdown-item" (click)="deleteRole(role)"> <button class="dropdown-item" (click)="deleteRole(role)">
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon> <fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
{{ 'security.deleteRole' | translate }} {{ 'security.deleteRole' | translate }}
</button> </button>
</li>
</ul>
</div>
</div>
<div *cdkDragPreview>
<fa-icon [fixedWidth]="true" icon="tag"></fa-icon>
{{ role.name }}
</div>
<div *cdkDragPlaceholder>{{ 'security.dropHere' | translate }}</div>
<div [collapse]="role.collapsed" [isAnimated]="true">
<ul class="list-group list-group-flush pl-5 mt-2">
<li class="list-group-item pl-1 pr-0" *ngFor="let policy of role.policies">
<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" placement="top" [adaptivePosition]="false"
(click)="removePolicyFromRole(policy, role)">
<fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon>
</button>
</div>
</li> </li>
</ul> </ul>
</div> </div>
</div> </li>
</ul>
</fieldset>
</div>
<div class="col-sm py-2">
<fieldset>
<legend>
<span>
<fa-icon [fixedWidth]="true" icon="user" size="sm"></fa-icon>
{{ 'security.users' | translate }}
</span>
<div *cdkDragPreview> <button class="btn btn-outline-info" container="body" (click)="showUserEditor()">
<fa-icon [fixedWidth]="true" icon="tag"></fa-icon> <span class="d-none d-lg-inline-block">{{ 'security.addUser' | translate }}</span>
{{ role.name }} <fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addUser' | translate }}"
</div> container="body" placement="top" [adaptivePosition]="false">
</fa-icon>
</button>
</legend>
<div *cdkDragPlaceholder>{{ 'security.dropHere' | translate }}</div> <p class="px-3">To assign roles drag them over a user</p>
<div [collapse]="role.collapsed" [isAnimated]="true"> <ul class="list-group list-group-flush p-0">
<ul class="list-group list-group-flush pl-5 mt-2"> <li class="list-group-item px-1" *ngFor="let user of users; let i = index" id="{{ 'user' + i }}"
<li class="list-group-item pl-1 pr-0" *ngFor="let policy of role.policies"> cdkDropList [cdkDropListData]="user" cdkDropListSortingDisabled sortingDisabled
<fa-icon [fixedWidth]="true" icon="key" size="sm"></fa-icon> [cdkDropListEnterPredicate]="usersEnterPredicate" (cdkDropListDropped)="drop($event)">
{{ policy.name }} <div class="d-flex justify-content-between align-items-center">
<div class="btn-group btn-group-sm float-end"> <div>
<button class="btn text-danger" tooltip="{{ 'security.removeRolePolicy' | translate }}" container="body" <fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="user.collapsed ? 0 : 90" class="px-2 py-1"
(click)="removePolicyFromRole(policy, role)"> [ngClass]="user.roles.length ? 'text-secondary' : 'text-muted'"
<fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon> (click)="user.roles.length && user.collapsed = !user.collapsed"
</button> tooltip="{{ 'security.toggleRoles' | translate }}" container="body" placement="top"
</div> [adaptivePosition]="false"></fa-icon>
</li>
</ul>
</div>
</li>
</ul>
</fieldset>
</div>
<div class="col-sm py-2">
<fieldset>
<legend>
<span>
<fa-icon [fixedWidth]="true" icon="user" size="sm"></fa-icon>
{{ 'security.users' | translate }}
</span>
<button class="btn btn-outline-info" container="body" (click)="showUserEditor()"> {{ user.login }} <sup *ngIf="user.roles?.length">({{ user.roles.length }})</sup>
{{ 'security.addUser' | translate }} </div>
</button>
</legend>
<p class="px-3">To assign roles drag them over a user</p> <div class="btn-group btn-group-sm float-end" dropdown>
<button class="btn text-info" dropdownToggle>
<ul class="list-group list-group-flush p-0"> <fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
<li class="list-group-item px-1" *ngFor="let user of users; let i = index" id="{{ 'user' + i }}" </button>
cdkDropList [cdkDropListData]="user" <ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="usersEnterPredicate" <li role="menuitem">
(cdkDropListDropped)="drop($event)"> <button class="dropdown-item" (click)="showUserEditor(user)">
<div class="d-flex justify-content-between align-items-center"> <fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
<div> {{ 'security.editUser' | translate }}
<fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="user.collapsed ? 0 : 90" </button>
class="px-2 py-1" [ngClass]="user.roles.length ? 'text-secondary' : 'text-muted'" </li>
(click)="user.roles.length && user.collapsed = !user.collapsed" <li role="menuitem">
tooltip="{{ 'security.toggleRoles' | translate }}" container="body"></fa-icon> <button class="dropdown-item" (click)="showUserEditor(user, true)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
{{ user.login }} <sup *ngIf="user.roles?.length">({{ user.roles.length }})</sup> {{ 'security.changePassword' | translate }}
</button>
</li>
<li class="dropdown-divider"></li>
<li role="menuitem">
<button class="dropdown-item">
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
{{ 'security.deleteUser' | translate }}
</button>
</li>
</ul>
</div>
</div> </div>
<div class="btn-group btn-group-sm float-end" dropdown> <div *cdkDragPreview>
<button class="btn text-info" dropdownToggle> <fa-icon [fixedWidth]="true" icon="user"></fa-icon>
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon> {{ user.login }}
</button> </div>
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
<li role="menuitem"> <div *cdkDragPlaceholder>{{ 'security.dropHere' | translate }}</div>
<button class="dropdown-item" (click)="showUserEditor(user)">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <div [collapse]="user.collapsed" [isAnimated]="true">
{{ 'security.editUser' | translate }} <ul class="list-group list-group-flush pl-5 mt-2">
</button> <li class="list-group-item pl-1 pr-0" *ngFor="let role of user.roles">
</li> <fa-icon [fixedWidth]="true" icon="tag" size="sm"></fa-icon>
<li role="menuitem"> {{ role.name }}
<button class="dropdown-item" (click)="showUserEditor(user, true)"> <div class="btn-group btn-group-sm float-end">
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon> <button class="btn text-danger" tooltip="{{ 'security.removeUserRole' | translate }}"
{{ 'security.changePassword' | translate }} container="body" (click)="removeRoleFromUser(role, user)">
</button> <fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon>
</li> </button>
<li class="dropdown-divider"></li> </div>
<li role="menuitem">
<button class="dropdown-item">
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
{{ 'security.deleteUser' | translate }}
</button>
</li> </li>
</ul> </ul>
</div> </div>
</div> </li>
</ul>
<div *cdkDragPreview> </fieldset>
<fa-icon [fixedWidth]="true" icon="user"></fa-icon> </div>
{{ user.login }}
</div>
<div *cdkDragPlaceholder>{{ 'security.dropHere' | translate }}</div>
<div [collapse]="user.collapsed" [isAnimated]="true">
<ul class="list-group list-group-flush pl-5 mt-2">
<li class="list-group-item pl-1 pr-0" *ngFor="let role of user.roles">
<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)">
<fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon>
</button>
</div>
</li>
</ul>
</div>
</li>
</ul>
</fieldset>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,3 +1,9 @@
:host
{
overflow: hidden;
flex-grow: 1;
}
fieldset fieldset
{ {
background-color: rgba(16,21,39, .5); 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 { PromptDialogComponent } from '../components/prompt-dialog/prompt-dialog.component';
import { RolePolicy } from './models/role-policy'; import { RolePolicy } from './models/role-policy';
import { RoleUser } from './models/role-user'; import { RoleUser } from './models/role-user';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-security', selector: 'app-security',
@ -33,8 +35,12 @@ export class SecurityComponent implements OnInit, OnDestroy
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
constructor(private readonly securityService: SecurityService, constructor(private readonly securityService: SecurityService,
private readonly modalService: BsModalService, 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({ forkJoin({
users: securityService.getUsers(), users: securityService.getUsers(),
roles: securityService.getRoles(), roles: securityService.getRoles(),

View File

@ -65,7 +65,7 @@
</div> </div>
</div> </div>
<div class="col-sm-9"> <!-- <div class="col-sm-9">
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control" id="address" formControlName="address" placeholder="Address"> <input type="text" class="form-control" id="address" formControlName="address" placeholder="Address">
<label for="address">Address</label> <label for="address">Address</label>
@ -95,7 +95,7 @@
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country"> <input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
<label for="country">Country</label> <label for="country">Country</label>
</div> </div>
</div> </div> -->
</div> </div>
<div class="d-flex justify-content-end align-items-center mt-5"> <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 { AutofocusDirective } from './directives/autofocus.directive';
import { InlineEditorComponent } from './components/inline-editor/inline-editor.component'; import { InlineEditorComponent } from './components/inline-editor/inline-editor.component';
import { ImagesComponent } from './catalog/images/images.component';
import { PackagesComponent } from './catalog/packages/packages.component'; import { PackagesComponent } from './catalog/packages/packages.component';
import { FileSizePipe } from './pipes/file-size.pipe'; import { FileSizePipe } from './pipes/file-size.pipe';
import { AutosizeModule } from 'ngx-autosize'; import { AutosizeModule } from 'ngx-autosize';
@ -54,7 +53,6 @@ import { LazyLoadDirective } from './directives/lazy-load.directive';
AutofocusDirective, AutofocusDirective,
//HasPermissionDirective, //HasPermissionDirective,
InlineEditorComponent, InlineEditorComponent,
ImagesComponent,
PackagesComponent, PackagesComponent,
FileSizePipe, FileSizePipe,
AlphaOnlyDirective, AlphaOnlyDirective,
@ -118,7 +116,6 @@ import { LazyLoadDirective } from './directives/lazy-load.directive';
AutofocusDirective, AutofocusDirective,
TimeagoModule, TimeagoModule,
NgxDatatableModule, NgxDatatableModule,
ImagesComponent,
PackagesComponent, PackagesComponent,
FileSizePipe, FileSizePipe,
InlineEditorComponent, InlineEditorComponent,

View File

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

View File

@ -1,3 +1,9 @@
:host
{
flex-grow: 1;
overflow : hidden;
}
.table-responsive .table-responsive
{ {
background-color: rgba(16, 21, 39, 0.75); background-color: rgba(16, 21, 39, 0.75);
@ -36,4 +42,9 @@
color: #3d5e8e; 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 Fuse from 'fuse.js';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms'; import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
import { distinctUntilChanged, first, takeUntil, debounceTime, filter, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, first, takeUntil, debounceTime, filter, switchMap } from 'rxjs/operators';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-volumes', selector: 'app-volumes',
@ -37,8 +39,11 @@ export class VolumesComponent implements OnInit, OnDestroy
private readonly instancesService: InstancesService, private readonly instancesService: InstancesService,
private readonly modalService: BsModalService, private readonly modalService: BsModalService,
private readonly toastr: ToastrService, 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 // Configure FuseJs
this.fuseJsOptions = { this.fuseJsOptions = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Manta</title> <title>Joyent</title>
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@ -11,7 +11,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <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> <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 rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Mukta&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Mukta&display=swap" rel="stylesheet">
</head> </head>
<body> <body>

View File

@ -13,6 +13,7 @@ body
color: #3d5e8e; color: #3d5e8e;
font-family: 'Mukta', sans-serif; font-family: 'Mukta', sans-serif;
line-height: 1; line-height: 1;
-webkit-font-smoothing: antialiased;
} }
body, div, virtual-scroller body, div, virtual-scroller
@ -450,7 +451,7 @@ body, div, virtual-scroller
{ {
+ .panel + .panel
{ {
margin-top: 2rem; margin-top: 1.5rem;
} }
.card .card
@ -609,9 +610,6 @@ virtual-scroller
flex-wrap: wrap; flex-wrap: wrap;
justify-content: start; justify-content: start;
margin-top: -1rem; 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; --bs-gutter-y: 2rem;
position: relative; position: relative;
@ -621,7 +619,8 @@ virtual-scroller
max-width: 100%; max-width: 100%;
padding-right: calc(var(--bs-gutter-x)/ 2); padding-right: calc(var(--bs-gutter-x)/ 2);
padding-left: 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 accordion
{ {
.panel-heading .panel-heading
@ -673,7 +680,7 @@ accordion
font-family: "Bebas Neue", sans-serif; font-family: "Bebas Neue", sans-serif;
font-size: 1.2rem; font-size: 1.2rem;
z-index: 1020; z-index: 1020;
max-height: 50vh; max-height: 60vh;
overflow: auto; overflow: auto;
} }
@ -720,14 +727,17 @@ accordion
} }
} }
.form-control .form-control, .form-select
{ {
border-color: #11182b; background-color: #11182b;
background: #11182b;
box-shadow: 0 0 0 1px rgb(0 231 255 / 75%) inset;
padding: .5rem .75rem .375rem; 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 .form-switch
{ {
display: inline-flex; display: inline-flex;
@ -928,3 +938,14 @@ accordion
{ {
overflow: hidden; overflow: hidden;
} }
.list-group-item
{
border: none;
padding: .25rem 1rem;
}
.machine-brand b
{
text-transform: uppercase;
}