fixed issue found during tests
This commit is contained in:
parent
20ee57102e
commit
066ec2b96f
@ -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">
|
||||||
|
@ -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>
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------------------------------
|
||||||
|
@ -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>
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
:host
|
||||||
|
{
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
@ -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
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -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$
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------------------------------
|
||||||
|
@ -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;
|
||||||
|
@ -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,7 +385,7 @@
|
|||||||
<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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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[];
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -1,236 +1,247 @@
|
|||||||
<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>
|
||||||
|
@ -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);
|
||||||
|
@ -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(),
|
||||||
|
@ -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">
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
@ -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,
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"account":
|
"account":
|
||||||
{
|
{
|
||||||
|
"title": "Account",
|
||||||
|
"myProfile": "My profile",
|
||||||
|
"updateProfile": "Update profile",
|
||||||
|
"myKeys": "My keys",
|
||||||
|
"addKey": "Add key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"disk": "Storage"
|
"disk": "Storage"
|
||||||
},
|
},
|
||||||
"custom":
|
"images":
|
||||||
{
|
{
|
||||||
|
"title": "Images"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"networking":
|
"networking":
|
||||||
{
|
{
|
||||||
|
"networks":
|
||||||
|
{
|
||||||
|
"title": "Networks"
|
||||||
|
},
|
||||||
|
"firewall":
|
||||||
|
{
|
||||||
|
"title": "Firewall rules"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"security":
|
"security":
|
||||||
{
|
{
|
||||||
|
"title": "Security",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
"policies": "Policies",
|
"policies": "Policies",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"volumes":
|
"volumes":
|
||||||
{
|
{
|
||||||
|
"title": "Volumes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
Reference in New Issue
Block a user