fixed issue found during tests
This commit is contained in:
parent
20ee57102e
commit
066ec2b96f
@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<!--<div class="col-sm-10">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="address" formControlName="address" placeholder="Address">
|
||||
<label for="address">Address</label>
|
||||
@ -42,8 +42,8 @@
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="city" formControlName="city" placeholder="City">
|
||||
<label for="city">City</label>
|
||||
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
|
||||
<label for="country">Country</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
@ -54,10 +54,10 @@
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
|
||||
<label for="country">Country</label>
|
||||
<input type="text" class="form-control" id="city" formControlName="city" placeholder="City">
|
||||
<label for="city">City</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="form-floating">
|
||||
|
@ -1,55 +1,60 @@
|
||||
<div *ngIf="userInfo" class="pt-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4>My profile</h4>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div class="overflow-auto flex-grow-1">
|
||||
<div class="container mb-3">
|
||||
|
||||
<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 class="card-body">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{ 'account.myKeys' | translate }}
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4>My SSH keys</h4>
|
||||
<button class="btn btn-outline-info" container="body" (click)="addSshKey()">
|
||||
{{ 'account.addKey' | translate }}
|
||||
</button>
|
||||
</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 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>
|
@ -1,7 +1,7 @@
|
||||
ul, ol
|
||||
:host
|
||||
{
|
||||
background-color: rgba(16, 21, 39, .75);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
h4
|
||||
@ -11,10 +11,10 @@ h4
|
||||
|
||||
.list-group-item
|
||||
{
|
||||
background: none;
|
||||
padding: 1rem;
|
||||
border-color: rgb(61, 94, 142, .25);
|
||||
background-color: transparent;
|
||||
border-color: #354164;
|
||||
color: #5a8cd8;
|
||||
padding: .5rem 1rem;
|
||||
|
||||
b
|
||||
{
|
||||
@ -22,21 +22,32 @@ h4
|
||||
}
|
||||
}
|
||||
|
||||
.card
|
||||
fieldset
|
||||
{
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
background-color: rgba(16, 21, 39, 0.5);
|
||||
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
|
||||
transition: box-shadow 0.15s ease-out;
|
||||
background-color: rgba(16,21,39, .5);
|
||||
border-radius: .3rem;
|
||||
height: 100%;
|
||||
transition: all 0.3s cubic-bezier(0.46, 0.03, 0.52, 0.96);
|
||||
height: 100%;
|
||||
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
|
||||
|
||||
&:hover
|
||||
{
|
||||
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgba(18, 203, 240, .4), 0 0 10px 3px #0e162a;
|
||||
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgb(18 203 240 / 40%), 0 0 10px 3px #0e162a;
|
||||
}
|
||||
|
||||
.card-body
|
||||
legend
|
||||
{
|
||||
padding: 0;
|
||||
font-family: 'Bebas Neue', sans-serif;
|
||||
line-height: 1.2;
|
||||
color: #3d5e8e;
|
||||
padding: .75rem .5rem .75rem 1rem;
|
||||
position: relative;
|
||||
background-color: rgba(16,21,39, .5);
|
||||
border-radius: .3rem .3rem 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ import { BsModalService } from 'ngx-bootstrap/modal';
|
||||
import { AccountEditorComponent } from './account-editor/account-editor.component';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { SshKeyEditorComponent } from './ssh-key-editor/ssh-key-editor.component';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
@ -26,8 +28,12 @@ export class AccountComponent implements OnInit, OnDestroy
|
||||
constructor(private readonly accountService: AccountService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly toastr: ToastrService)
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
translationService.get('account.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
|
||||
|
||||
//accountService.getUsers().subscribe(x => console.log(x));
|
||||
|
||||
accountService.getUserLimits().subscribe(x => console.log(x));
|
||||
@ -64,17 +70,6 @@ export class AccountComponent implements OnInit, OnDestroy
|
||||
};
|
||||
|
||||
const modalRef = this.modalService.show(SshKeyEditorComponent, modalConfig);
|
||||
|
||||
|
||||
modalRef.content.save.pipe(first()).subscribe(x => this.userKeys = [...this.userKeys, x]);
|
||||
// this.accountService.addKey('test',
|
||||
// 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAzf7Cbu8tPvxgwG3MhXK959F7TtsSCQQXb3jSPAJtQT+CltA+OYLod/ojclfQfnutIHUpqq6PsCD/nhxiF2JYkKWve7olJV6akvXQOGNLqRdXTcEouUhevLAQV3sB+YNvjr5FRpspNK8prAn7UU4vyZhCKBT8VAgwkio3u8eR/26XDNow1C9NXC6P+2BYWjjKbJCI41XpLFIzsmHBw+XZox+IbVg8mcVsWfdhEHRDyxM1HgvOKU9vkCwigmww9nsIatSQuM0jCtohQRkddc2DlfKieBmpeC/VqNoWE77iei/nVOcgIaLjwwevdCGHhwtSBmkE+W14JCwFbzl0yThL2w== rsa-key-20210314',
|
||||
// 'ba:04:55:94:64:24:75:a4:b2:60:e5:bf:77:19:df:34')
|
||||
// .subscribe(response => this.userKeys = [...this.userKeys, response],
|
||||
// err =>
|
||||
// {
|
||||
// this.toastr.error(err.error.message)
|
||||
// });
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
|
@ -46,10 +46,8 @@
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="no-overflow flex-grow-1">
|
||||
<div class="h-100">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<div class="no-overflow flex-grow-1 d-flex flex-column">
|
||||
<router-outlet></router-outlet>
|
||||
</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 { CatalogComponent } from './catalog.component';
|
||||
import { CustomImagesComponent } from './custom-images/custom-images.component';
|
||||
import { DockerImagesComponent } from './docker-images/docker-images.component';
|
||||
import { DockerRegistryComponent } from './docker-registry/docker-registry.component';
|
||||
import { DockerImageEditorComponent } from './docker-image-editor/docker-image-editor.component';
|
||||
import { DockerRegistryEditorComponent } from './docker-registry-editor/docker-registry-editor.component';
|
||||
import { ImagesComponent } from './images/images.component';
|
||||
import { CustomImageEditorComponent } from './custom-image-editor/custom-image-editor.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CatalogComponent,
|
||||
CustomImagesComponent,
|
||||
DockerImagesComponent,
|
||||
DockerRegistryComponent,
|
||||
DockerImageEditorComponent,
|
||||
DockerRegistryEditorComponent
|
||||
ImagesComponent
|
||||
],
|
||||
imports: [
|
||||
SharedModule,
|
||||
@ -35,38 +27,19 @@ import { CustomImageEditorComponent } from './custom-image-editor/custom-image-e
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'custom-images'
|
||||
redirectTo: 'images'
|
||||
},
|
||||
{
|
||||
path: 'custom-images',
|
||||
component: CustomImagesComponent,
|
||||
path: 'images',
|
||||
component: ImagesComponent,
|
||||
data:
|
||||
{
|
||||
title: 'catalog.customImages.title',
|
||||
subTitle: 'catalog.customImages.subTitle',
|
||||
title: 'catalog.images.title',
|
||||
subTitle: 'catalog.images.subTitle',
|
||||
icon: 'layer-group'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'docker-images',
|
||||
component: DockerImagesComponent,
|
||||
data:
|
||||
{
|
||||
title: 'catalog.dockerImages.title',
|
||||
subTitle: 'catalog.dockerImages.subTitle',
|
||||
icon: ['fab', 'docker']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'docker-registry',
|
||||
component: DockerRegistryComponent,
|
||||
data:
|
||||
{
|
||||
title: 'catalog.dockerRegistry.title',
|
||||
subTitle: 'catalog.dockerRegistry.subTitle',
|
||||
icon: ['fab', 'docker']
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
TranslateModule.forChild({
|
||||
@ -83,8 +56,6 @@ import { CustomImageEditorComponent } from './custom-image-editor/custom-image-e
|
||||
})
|
||||
],
|
||||
entryComponents: [
|
||||
DockerImageEditorComponent,
|
||||
DockerRegistryEditorComponent,
|
||||
CustomImageEditorComponent
|
||||
]
|
||||
})
|
||||
|
@ -59,15 +59,6 @@ export class CatalogService
|
||||
return this.httpClient.get<CatalogImage[]>(`/api/my/images?${allStates ? 'state=all' : ''}`);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@Cacheable({
|
||||
cacheBusterObserver: imagesCacheBuster$
|
||||
})
|
||||
getCustomImages(ownerId: string): Observable<CatalogImage[]>
|
||||
{
|
||||
return this.httpClient.get<CatalogImage[]>(`/api/my/images?$state=all&owner=${ownerId}`);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@Cacheable({
|
||||
cacheBusterObserver: imagesCacheBuster$
|
||||
|
@ -1,20 +1,200 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>OS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let image of images">
|
||||
<td>{{ image.name }}</td>
|
||||
<td>{{ image.description }}</td>
|
||||
<td>{{ image.os }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" (click)="select.emit(image)">select</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div class="container text-center mt-1" [formGroup]="editorForm">
|
||||
<div class="btn-toolbar pt-2">
|
||||
<span class="d-none d-sm-block flex-grow-1"></span>
|
||||
|
||||
<ng-container *ngIf="images && images.length">
|
||||
<div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
|
||||
<input type="text" class="form-control" placeholder="Search by name..." formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$">
|
||||
<button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
|
||||
tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
|
||||
<fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom left">
|
||||
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
|
||||
Sort by
|
||||
<b *ngIf="editorForm.get('sortProperty').value === 'name'">name</b>
|
||||
<b *ngIf="editorForm.get('sortProperty').value === 'description'">description</b>
|
||||
<b *ngIf="editorForm.get('sortProperty').value === 'os'">operating system</b>
|
||||
<b *ngIf="editorForm.get('sortProperty').value === 'type'">type</b>
|
||||
<b *ngIf="editorForm.get('sortProperty').value === 'state'">status</b>
|
||||
</button>
|
||||
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'" (click)="setSortProperty('name')">
|
||||
Name
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'description'" (click)="setSortProperty('description')">
|
||||
Description
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'" (click)="setSortProperty('os')">
|
||||
Operating system
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'type'" (click)="setSortProperty('type')">
|
||||
Type
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'status'" (click)="setSortProperty('status')">
|
||||
Status
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="spinner-border text-center text-info text-faded" role="status" *ngIf="loadingIndicator">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto flex-grow-1 my-3">
|
||||
<div class="container my-2">
|
||||
<accordion [isAnimated]="false" [closeOthers]="false">
|
||||
<accordion-group [isOpen]="myImagesExpanded">
|
||||
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
|
||||
tooltip="Show or hide my images" placement="top" container="body">
|
||||
<h4 class="text-info text-uppercase mb-0">My images</h4>
|
||||
|
||||
<fa-icon icon="angle-right" [fixedWidth]="true" [rotate]="myImagesExpanded ? 90 : 0" class="text-info"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" *ngIf="!loadingIndicator">
|
||||
<p *ngIf="!images.length" class="text-center text-info text-faded p-3 mb-0">
|
||||
You don't have any custom images yet
|
||||
</p>
|
||||
|
||||
<table class="table table-hover" *ngIf="myImages.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th 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';
|
||||
|
||||
describe('ImagesComponent', () => {
|
||||
describe('CustomImagesComponent', () => {
|
||||
let component: ImagesComponent;
|
||||
let fixture: ComponentFixture<ImagesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ImagesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImagesComponent);
|
||||
|
@ -1,37 +1,222 @@
|
||||
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ColumnMode, SelectionType } from '@swimlane/ngx-datatable';
|
||||
import { CatalogService } from '../helpers/catalog.service';
|
||||
import { AuthService } from '../../helpers/auth.service';
|
||||
import { debounceTime, distinctUntilChanged, filter, first, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CatalogImage } from '../models/image';
|
||||
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
|
||||
import Fuse from 'fuse.js';
|
||||
import { sortArray } from '../../helpers/utils.service';
|
||||
import { BsModalService } from 'ngx-bootstrap/modal';
|
||||
import { ConfirmationDialogComponent } from '../../components/confirmation-dialog/confirmation-dialog.component';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-images',
|
||||
templateUrl: './images.component.html',
|
||||
styleUrls: ['./images.component.scss']
|
||||
})
|
||||
export class ImagesComponent implements OnInit
|
||||
export class ImagesComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@Output()
|
||||
select = new EventEmitter();
|
||||
|
||||
images: any[];
|
||||
myImages: CatalogImage[] = [];
|
||||
myListItems: CatalogImage[] = [];
|
||||
images: CatalogImage[] = [];
|
||||
listItems: CatalogImage[] = [];
|
||||
editorForm: FormGroup;
|
||||
loadingIndicator = true;
|
||||
selectionType = SelectionType;
|
||||
columnMode = ColumnMode;
|
||||
myImagesExpanded = true;
|
||||
otherImagesExpanded = true;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
private readonly fuseJsOptions: {};
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
constructor(private readonly catalogService: CatalogService)
|
||||
constructor(private readonly catalogService: CatalogService,
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
catalogService.getImages().subscribe(x =>
|
||||
{
|
||||
this.images = x;
|
||||
translationService.get('catalog.images.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${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
|
||||
{
|
||||
this.getCustomImages();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
ngOnDestroy()
|
||||
{
|
||||
this.destroy$.next();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -40,9 +40,9 @@
|
||||
</li>-->
|
||||
<li class="dropdown-divider"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="['./catalog/custom-images']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
|
||||
<a class="nav-link" [routerLink]="['./catalog/images']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">
|
||||
<fa-icon [fixedWidth]="true" icon="layer-group"></fa-icon>
|
||||
{{ 'navbar.menu.customImages' | translate }}
|
||||
{{ 'navbar.menu.images' | translate }}
|
||||
</a>
|
||||
</li>
|
||||
<!--<li class="nav-item">
|
||||
|
@ -36,7 +36,7 @@ export class AuthGuardService implements CanActivate, CanLoad
|
||||
if (this.tokenService.accessTokenUpdated$.getValue())
|
||||
return true;
|
||||
|
||||
this.router.navigate(['/unauthorized'], { state: { data: route.data } });
|
||||
//this.router.navigate(['/unauthorized'], { state: { data: route.data } });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
<th>Action</th>
|
||||
<th>Time</th>
|
||||
<th>Finished</th>
|
||||
<th></th>
|
||||
<th>User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -35,9 +35,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<fa-icon icon="user" [fixedWidth]="true" size="sm"
|
||||
[tooltip]="info.caller.keyId" placement="top" container="body" [adaptivePosition]="false">
|
||||
</fa-icon>
|
||||
<div class="text-truncate">{{ info.caller.keyId }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -1,32 +1,36 @@
|
||||
<ul class="list-group list-group-flush list-info">
|
||||
<li class="list-group-item text-uppercase ps-0" tooltip="Machine ID" container="body" placement="top" [adaptivePosition]="false">
|
||||
<fa-icon icon="fingerprint" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
<b class="ms-1">{{ instance.id }}</b>
|
||||
<li class="dropdown-header">Machine identifier</li>
|
||||
<li class="list-group-item text-uppercase ps-0">
|
||||
<b>{{ instance.id }}</b>
|
||||
</li>
|
||||
|
||||
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
|
||||
*ngFor="let keyValue of instance.dnsList | keyvalue; let index = index">
|
||||
<div class="text-truncate">
|
||||
<fa-icon icon="link" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
<span class="ms-1" [ngClass]="keyValue.value[0] === instance.id || keyValue.value[0] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
|
||||
{{ keyValue.value[0] }}
|
||||
</span>
|
||||
<ng-container *ngIf="instance.dnsList">
|
||||
<li class="dropdown-header">DNS list</li>
|
||||
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
|
||||
*ngFor="let keyValue of instance.dnsList | keyvalue; let index = index">
|
||||
<div class="text-truncate text-info text-faded" [tooltip]="keyValue.key" container="body" placement="top" [adaptivePosition]="false">
|
||||
<!--<span class="ms-1" [ngClass]="keyValue.value[0] === instance.id || keyValue.value[0] === instance.name.toLowerCase() ? 'highlight' : 'text-info text-faded'">
|
||||
{{ keyValue.value[0] }}
|
||||
</span>
|
||||
|
||||
<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] }}
|
||||
</span>
|
||||
<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] }}
|
||||
</span>
|
||||
|
||||
{{ keyValue.value[2] }}
|
||||
</div>
|
||||
{{ keyValue.value[2] }}-->
|
||||
{{ keyValue.key }}
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm" *ngxClipboardIfSupported
|
||||
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)">
|
||||
<fa-icon icon="clipboard" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<div class="btn-group btn-group-sm" *ngxClipboardIfSupported
|
||||
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)">
|
||||
<fa-icon icon="clipboard" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ng-container>
|
||||
|
||||
<li class="dropdown-header">Deletion protection</li>
|
||||
<li class="list-group-item ps-0 pb-0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="dp{{ instance.id }}" [(ngModel)]="instance.deletion_protection"
|
||||
|
@ -24,4 +24,16 @@
|
||||
{
|
||||
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 { CatalogService } from '../../catalog/helpers/catalog.service';
|
||||
import { empty, forkJoin, Observable, of, Subject } from 'rxjs';
|
||||
import { empty, forkJoin, Observable, of, Subject, ReplaySubject } from 'rxjs';
|
||||
import { delay, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { InstancesService } from '../helpers/instances.service';
|
||||
import { BsModalService } from 'ngx-bootstrap/modal';
|
||||
@ -12,20 +12,19 @@ import { Instance } from '../models/instance';
|
||||
templateUrl: './instance-info.component.html',
|
||||
styleUrls: ['./instance-info.component.scss']
|
||||
})
|
||||
export class InstanceInfoComponent implements OnInit, OnDestroy
|
||||
export class InstanceInfoComponent implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
@Input()
|
||||
instance: Instance;
|
||||
|
||||
@Input()
|
||||
set loadInfo(value: boolean)
|
||||
{
|
||||
if (!this.finishedLoading && value && this.instance)
|
||||
this.getInfo();
|
||||
}
|
||||
loadInfo: boolean;
|
||||
|
||||
@Output()
|
||||
beforeLoad = new EventEmitter();
|
||||
processing = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
finishedProcessing = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
load = new EventEmitter();
|
||||
@ -34,6 +33,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
|
||||
|
||||
private finishedLoading: boolean;
|
||||
private destroy$ = new Subject();
|
||||
private onChanges$ = new ReplaySubject();
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
constructor(private readonly instancesService: InstancesService,
|
||||
@ -46,18 +46,18 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
toggleDeletionProtection(event, instance: Instance)
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
this.instancesService.toggleDeletionProtection(instance.id, event.target.checked)
|
||||
.subscribe(() =>
|
||||
{
|
||||
this.toastr.info(`The deletion protection for machine "${instance.name}" is now ${event.target.checked ? 'enabled' : 'disabled'}`);
|
||||
this.load.emit();
|
||||
this.finishedProcessing.emit();
|
||||
},
|
||||
err =>
|
||||
{
|
||||
this.toastr.error(`Machine "${instance.name}" error: ${err.error.message}`);
|
||||
this.load.emit();
|
||||
this.finishedProcessing.emit();
|
||||
});
|
||||
}
|
||||
|
||||
@ -85,6 +85,7 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
|
||||
|
||||
this.loading = false;
|
||||
this.finishedLoading = true;
|
||||
this.load.emit(dnsList);
|
||||
},
|
||||
err =>
|
||||
{
|
||||
@ -109,8 +110,21 @@ export class InstanceInfoComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
ngOnInit(): void
|
||||
{
|
||||
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
|
||||
{
|
||||
if (!this.finishedLoading && this.loadInfo && !this.instance?.infoLoaded)
|
||||
this.getInfo();
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
ngOnChanges(changes: SimpleChanges): void
|
||||
{
|
||||
// Since we can't control if ngOnChanges is executed before ngOnInit, we need this trick
|
||||
this.onChanges$.next(changes);
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
ngOnDestroy()
|
||||
{
|
||||
|
@ -24,7 +24,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
loadNetworks: boolean;
|
||||
|
||||
@Output()
|
||||
beforeLoad = new EventEmitter();
|
||||
processing = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
finishedProcessing = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
load = new EventEmitter();
|
||||
@ -62,14 +65,16 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
}, err =>
|
||||
{
|
||||
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}`);
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
private getNetworks()
|
||||
{
|
||||
if (this.finishedLoading) return;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const observables = this.nics.map(x => this.networkingService.getNetwork(x.network));
|
||||
@ -83,8 +88,9 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
nic.networkName = nic.networkDetails ? nic.networkDetails.name : '';
|
||||
}
|
||||
|
||||
this.finishedLoading = true;
|
||||
this.loading = false;
|
||||
this.finishedLoading = true;
|
||||
this.load.emit(this.nics);
|
||||
},
|
||||
err =>
|
||||
{
|
||||
@ -113,7 +119,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
first(),
|
||||
tap(() =>
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
this.toastr.info(`Connecting machine "${this.instance.name}" to the "${network.name}" network...`);
|
||||
}),
|
||||
@ -160,8 +166,10 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
nic.networkName = nic.networkDetails?.name || '';
|
||||
}
|
||||
|
||||
this.load.emit(this.nics);
|
||||
|
||||
this.toastr.info(`The machine "${this.instance.name}" has been connected to the "${network.name}" network`);
|
||||
this.load.emit();
|
||||
this.finishedProcessing.emit();
|
||||
},
|
||||
err =>
|
||||
{
|
||||
@ -171,7 +179,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
|
||||
this.toastr.error(`Failed to connect machine "${this.instance.name}" to the "${network.name}" network ${errorDetails}`);
|
||||
this.load.emit();
|
||||
this.finishedProcessing.emit();
|
||||
});
|
||||
}
|
||||
|
||||
@ -196,7 +204,7 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
first(),
|
||||
tap(() =>
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
this.toastr.info(`Removing network interface "${nic.mac.toUpperCase()}" from machine "${this.instance.name}"...`);
|
||||
}),
|
||||
@ -238,13 +246,15 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
found.primary = networkInterface.primary;
|
||||
}
|
||||
|
||||
this.load.emit();
|
||||
this.finishedProcessing.emit();
|
||||
|
||||
this.load.emit(this.nics);
|
||||
|
||||
this.toastr.info(`The network interface has been removed from machine "${this.instance.name}"`);
|
||||
}, err =>
|
||||
{
|
||||
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
||||
this.load.emit();
|
||||
this.finishedProcessing.emit();
|
||||
});
|
||||
}
|
||||
|
||||
@ -277,9 +287,12 @@ export class InstanceNetworksComponent implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
this.nics = this.instance?.nics || [];
|
||||
|
||||
if (this.instance.networksLoaded)
|
||||
this.finishedLoading = true;
|
||||
|
||||
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
|
||||
{
|
||||
if (!this.finishedLoading && this.loadNetworks && this.instance)
|
||||
if (!this.finishedLoading && this.loadNetworks && !this.instance?.networksLoaded)
|
||||
this.getNetworks();
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, OnChanges, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CatalogService } from '../../catalog/helpers/catalog.service';
|
||||
import { InstancesService } from '../helpers/instances.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { delay, first, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import Fuse from 'fuse.js';
|
||||
import { BsModalService } from 'ngx-bootstrap/modal';
|
||||
@ -15,20 +15,19 @@ import { SnapshotsService } from '../helpers/snapshots.service';
|
||||
templateUrl: './instance-snapshots.component.html',
|
||||
styleUrls: ['./instance-snapshots.component.scss']
|
||||
})
|
||||
export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
export class InstanceSnapshotsComponent implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
@Input()
|
||||
instance: any;
|
||||
|
||||
@Input()
|
||||
set loadSnapshots(value: boolean)
|
||||
{
|
||||
if (value && this.instance && !this.snapshots)
|
||||
this.getSnapshots();
|
||||
}
|
||||
loadSnapshots: boolean;
|
||||
|
||||
@Output()
|
||||
beforeLoad = new EventEmitter();
|
||||
processing = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
processingFinished = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
load = new EventEmitter();
|
||||
@ -37,13 +36,16 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
instanceStateUpdate = new EventEmitter();
|
||||
|
||||
loadingSnapshots: boolean;
|
||||
snapshotsLoaded: boolean;
|
||||
filteredSnapshots: any[];
|
||||
snapshotName: string;
|
||||
_searchTerm: string;
|
||||
shouldSearch: boolean;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
private onChanges$ = new ReplaySubject();
|
||||
private snapshots: any[];
|
||||
private finishedLoading: boolean
|
||||
private readonly fuseJsOptions: {};
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@ -69,7 +71,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
createSnapshot()
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
this.snapshots = this.snapshots || [];
|
||||
|
||||
@ -95,7 +97,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
if (index >= 0)
|
||||
this.snapshots[index] = x;
|
||||
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
this.toastr.info(`A new snapshot "${snapshotName}" has been created for machine "${this.instance.name}"`);
|
||||
},
|
||||
err =>
|
||||
@ -106,7 +108,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
if (index >= 0)
|
||||
this.snapshots.splice(index, 1);
|
||||
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
||||
});
|
||||
}
|
||||
@ -117,7 +119,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
this.confirmRestore(snapshot)
|
||||
.subscribe(() =>
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
snapshot.working = true;
|
||||
|
||||
@ -135,7 +137,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
err =>
|
||||
{
|
||||
snapshot.working = false;
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
|
||||
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
||||
});
|
||||
@ -166,7 +168,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
private startMachineFromSnapshot(snapshot: Snapshot)
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
this.toastr.info(`Restoring machine "${this.instance.name}" from "${snapshot.name}" snapshot`);
|
||||
|
||||
@ -182,14 +184,14 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
{
|
||||
snapshot.working = false;
|
||||
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
|
||||
this.toastr.info(`The machine "${this.instance.name}" has been started from the "${snapshot.name}" snapshot`);
|
||||
}, err =>
|
||||
{
|
||||
snapshot.working = false;
|
||||
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
|
||||
this.toastr.error(`Machine "${this.instance.name}" error: ${err.error.message}`);
|
||||
});
|
||||
@ -214,7 +216,7 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
|
||||
modalRef.content.confirm.pipe(first()).subscribe(() =>
|
||||
{
|
||||
this.beforeLoad.emit();
|
||||
this.processing.emit();
|
||||
|
||||
this.snapshotsService.deleteSnapshot(this.instance.id, snapshot.name)
|
||||
.subscribe(() =>
|
||||
@ -223,12 +225,12 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
if (index >= 0)
|
||||
this.snapshots.splice(index, 1);
|
||||
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
|
||||
this.toastr.info(`The "${snapshot.name}" snapshot has been deleted`);
|
||||
}, err =>
|
||||
{
|
||||
this.load.emit();
|
||||
this.processingFinished.emit();
|
||||
|
||||
this.toastr.error(`The "${snapshot.name}" snapshot couldn't be deleted: ${err.error.message}`);
|
||||
});
|
||||
@ -238,6 +240,8 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
private getSnapshots()
|
||||
{
|
||||
if (this.snapshotsLoaded) return
|
||||
|
||||
this.loadingSnapshots = true;
|
||||
|
||||
// Get the list of snapshots
|
||||
@ -246,7 +250,10 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
{
|
||||
this.snapshots = x;
|
||||
this.filteredSnapshots = x;
|
||||
|
||||
this.loadingSnapshots = false;
|
||||
this.snapshotsLoaded = true;
|
||||
this.load.emit(x);
|
||||
},
|
||||
err =>
|
||||
{
|
||||
@ -288,6 +295,21 @@ export class InstanceSnapshotsComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
ngOnInit(): void
|
||||
{
|
||||
this.snapshots = this.instance?.snapshots;
|
||||
this.filteredSnapshots = this.snapshots;
|
||||
|
||||
this.onChanges$.pipe(takeUntil(this.destroy$)).subscribe(() =>
|
||||
{
|
||||
if (!this.finishedLoading && this.loadSnapshots && !this.instance?.snapshotsLoaded)
|
||||
this.getSnapshots();
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
ngOnChanges(changes: SimpleChanges): void
|
||||
{
|
||||
// Since we can't control if ngOnChanges is executed before ngOnInit, we need this trick
|
||||
this.onChanges$.next(changes);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
|
@ -11,7 +11,7 @@ import { CatalogService } from '../../catalog/helpers/catalog.service';
|
||||
import { NetworkingService } from '../../networking/helpers/networking.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { VolumesService } from '../../volumes/helpers/volumes.service';
|
||||
import { VolumeResponse } from '../../volumes/models/volume';
|
||||
import { AuthService } from '../../helpers/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-instance-wizard',
|
||||
@ -51,12 +51,14 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
kvmRequired: boolean;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
private userId: string;
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
constructor(private readonly modalRef: BsModalRef,
|
||||
private readonly router: Router,
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly fileSizePipe: FileSizePipe,
|
||||
private readonly authService: AuthService,
|
||||
private readonly instancesService: InstancesService,
|
||||
private readonly catalogService: CatalogService,
|
||||
private readonly networkingService: NetworkingService,
|
||||
@ -93,6 +95,10 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
description: 'Tag and create your machine'
|
||||
}
|
||||
];
|
||||
|
||||
authService.userInfoUpdated$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(x => this.userId = x.id);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@ -163,7 +169,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
if (imageType === 1)
|
||||
{
|
||||
for (const image of this.images)
|
||||
if (['zvol'].includes(image.type))
|
||||
if (['lx-dataset', 'zone-dataset'].includes(image.type) && image.owner !== this.userId)
|
||||
{
|
||||
operatingSystems[image.os] = true;
|
||||
imageList.push(image);
|
||||
@ -172,7 +178,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
else if (imageType === 2)
|
||||
{
|
||||
for (const image of this.images)
|
||||
if (['lx-dataset', 'zone-dataset'].includes(image.type))
|
||||
if (['zvol'].includes(image.type) && image.owner !== this.userId)
|
||||
{
|
||||
operatingSystems[image.os] = true;
|
||||
imageList.push(image);
|
||||
@ -181,7 +187,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
else if (imageType === 3)
|
||||
{
|
||||
for (const image of this.images)
|
||||
if (!['zvol', 'lx-dataset', 'zone-dataset'].includes(image.type))
|
||||
if (image.owner === this.userId)
|
||||
{
|
||||
operatingSystems[image.os] = true;
|
||||
imageList.push(image);
|
||||
@ -213,7 +219,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
if (imageType === 1)
|
||||
{
|
||||
for (const image of this.images)
|
||||
if (['zvol'].includes(image.type) && (!imageOs || imageOs === image.os))
|
||||
if (['lx-dataset', 'zone-dataset'].includes(image.type) && (!imageOs || imageOs === image.os) && image.owner !== this.userId)
|
||||
{
|
||||
operatingSystems[image.os] = true;
|
||||
imageList.push(image);
|
||||
@ -222,7 +228,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
else if (imageType === 2)
|
||||
{
|
||||
for (const image of this.images)
|
||||
if (['lx-dataset', 'zone-dataset'].includes(image.type) && (!imageOs || imageOs === image.os))
|
||||
if (['zvol'].includes(image.type) && (!imageOs || imageOs === image.os) && image.owner !== this.userId)
|
||||
{
|
||||
operatingSystems[image.os] = true;
|
||||
imageList.push(image);
|
||||
@ -231,7 +237,7 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
else if (imageType === 3)
|
||||
{
|
||||
for (const image of this.images)
|
||||
if (!['zvol', 'lx-dataset', 'zone-dataset'].includes(image.type) && (!imageOs || imageOs === image.os))
|
||||
if (image.owner === this.userId)
|
||||
{
|
||||
operatingSystems[image.os] = true;
|
||||
imageList.push(image);
|
||||
@ -552,9 +558,9 @@ export class InstanceWizardComponent implements OnInit, OnDestroy
|
||||
|
||||
if (this.instance)
|
||||
{
|
||||
if (this.instance.type === 'virtualmachine')
|
||||
if (this.instance.type === 'smartmachine')
|
||||
this.imageType = 1;
|
||||
else if (this.instance.type === 'smartmachine')
|
||||
else if (this.instance.type === 'virtualmachine')
|
||||
this.imageType = 2;
|
||||
|
||||
this.preselectedPackage = this.instance.package;
|
||||
|
@ -1,28 +1,29 @@
|
||||
<div class="d-flex flex-column h-100" [formGroup]="editorForm">
|
||||
<div class="container text-center mt-1">
|
||||
<div class="btn-toolbar pt-2">
|
||||
<div class="btn-group flex-grow-1 flex-grow-sm-0">
|
||||
<div class="btn-group mb-3 mb-lg-0 w-lg-auto w-100">
|
||||
<button class="btn btn-lg btn-info" (click)="createMachine()" [disabled]="loadingIndicator">
|
||||
Create a new machine
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="d-none d-sm-block flex-grow-1"></span>
|
||||
<span class="d-none d-lg-block flex-grow-1"></span>
|
||||
|
||||
<ng-container *ngIf="instances && instances.length">
|
||||
<div class="input-group input-group-pill flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
|
||||
<input type="text" class="form-control" placeholder="Search..."
|
||||
formControlName="searchTerm" appAlphaOnly="^[A-Za-z0-9_-]+$"
|
||||
tooltip="Search by name, tag, metadata, operating system or brand" placement="top" container="body" [adaptivePosition]="false">
|
||||
<button class="btn btn-outline-info" type="button" (click)="clearSearch()" [disabled]="!editorForm.get('searchTerm').value"
|
||||
tooltip="Clear search" container="body" placement="top" [adaptivePosition]="false">
|
||||
<div class="input-group input-group-pill me-lg-3 mb-3 mb-lg-0 w-lg-auto w-100">
|
||||
<input type="text" class="form-control" placeholder="Search..." formControlName="searchTerm"
|
||||
appAlphaOnly="^[A-Za-z0-9_-]+$" tooltip="Search by name, tag, metadata, operating system or brand"
|
||||
placement="top" container="body" [adaptivePosition]="false">
|
||||
<button class="btn btn-outline-info" type="button" (click)="clearSearch()"
|
||||
[disabled]="!editorForm.get('searchTerm').value" tooltip="Clear search" container="body" placement="top"
|
||||
[adaptivePosition]="false">
|
||||
<fa-icon icon="times" size="sm" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group flex-grow-1 flex-grow-sm-0 me-sm-3 w-sm-auto w-100">
|
||||
<button class="btn btn-outline-info dropdown-toggle" [disabled]="loadingIndicator"
|
||||
[popover]="filtersTemplate" [outsideClick]="true" container="body" placement="bottom right" containerClass="menu-popover">
|
||||
<div class="btn-group me-lg-3 mb-3 mb-lg-0 w-lg-auto w-100">
|
||||
<button class="btn btn-outline-info dropdown-toggle" [disabled]="loadingIndicator" [popover]="filtersTemplate"
|
||||
[outsideClick]="true" container="body" placement="bottom right" containerClass="menu-popover">
|
||||
Showing {{ listItems.length }} / {{ instances.length }}
|
||||
<ng-container *ngIf="runningInstanceCount && stoppedInstanceCount">
|
||||
<span class="badge rounded-pill bg-success text-dark">{{ runningInstanceCount }} running</span>
|
||||
@ -37,33 +38,38 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group flex-grow-1 flex-grow-sm-0 w-sm-auto w-100" dropdown placement="bottom right">
|
||||
<div class="btn-group w-lg-auto w-100" dropdown placement="bottom right">
|
||||
<button class="btn btn-outline-info dropdown-toggle" dropdownToggle>
|
||||
Sort by <b>{{ editorForm.get('sortProperty').value }}</b>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'" (click)="setSortProperty('name')">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'name'"
|
||||
(click)="setSortProperty('name')">
|
||||
Name
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'" (click)="setSortProperty('os')">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'os'"
|
||||
(click)="setSortProperty('os')">
|
||||
Operating system
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'brand'" (click)="setSortProperty('brand')">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'brand'"
|
||||
(click)="setSortProperty('brand')">
|
||||
Brand
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'image'" (click)="setSortProperty('image')">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'image'"
|
||||
(click)="setSortProperty('image')">
|
||||
Image
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'state'" (click)="setSortProperty('state')">
|
||||
<button class="dropdown-item" [class.active]="editorForm.get('sortProperty').value === 'state'"
|
||||
(click)="setSortProperty('state')">
|
||||
State
|
||||
</button>
|
||||
</li>
|
||||
@ -77,34 +83,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto my-2" id="scrollingBlock">
|
||||
<div class="container">
|
||||
<div class="overflow-auto flex-grow-1 mt-3" id="scrollingBlock">
|
||||
<div class="container py-2">
|
||||
<h2 *ngIf="listItems && listItems.length === 0 && instances && instances.length > 0" class="text-uppercase">
|
||||
No machine matches your filters
|
||||
{{ 'dashboard.list.noResults' | translate }}
|
||||
</h2>
|
||||
|
||||
<virtual-scroller #scroller [items]="listItems" bufferAmount="2" class="instances"
|
||||
[parentScroll]="scroller.window.document.getElementById('scrollingBlock')" [scrollThrottlingTime]="250">
|
||||
[parentScroll]="scroller.window.document.getElementById('scrollingBlock')" [scrollThrottlingTime]="250">
|
||||
<div *ngFor="let instance of scroller.viewPortItems; trackBy: trackByFunction; let index = index"
|
||||
[ngClass]="showMachineDetails ? 'col-lg-6 col-12 full-details' : 'col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12'"
|
||||
lazyLoad [lazyLoadDelay]="lazyLoadDelay" [container]="scroller.element.nativeElement.getElementsByClassName('scrollable-content')[0]"
|
||||
(canLoad)="instance.loading = false" (unload)="instance.loading = true" (load)="loadInstanceDetails(instance)">
|
||||
[ngClass]="showMachineDetails ? 'col-12 full-details' : 'col-xl-2 col-lg-3 col-md-4 col-sm-6 col-12'"
|
||||
[class.col-lg-6]="editorForm.get('fullDetailsTwoColumns').value" lazyLoad [lazyLoadDelay]="lazyLoadDelay"
|
||||
[container]="scroller.element.nativeElement.getElementsByClassName('scrollable-content')[0]"
|
||||
(canLoad)="instance.loading = false" (unload)="instance.loading = true"
|
||||
(load)="loadInstanceDetails(instance)">
|
||||
<fieldset class="card" [disabled]="instance.working">
|
||||
<div class="row g-0">
|
||||
<div class="card-info" [ngClass]="showMachineDetails ? 'col-lg-4' : 'col'">
|
||||
<div>
|
||||
<h5 class="card-title text-truncate" [tooltip]="instance.name" container="body" placement="top" [adaptivePosition]="false">
|
||||
<h5 class="card-title text-truncate" [tooltip]="instance.name" container="body" placement="top"
|
||||
[adaptivePosition]="false">
|
||||
{{ instance.name }}
|
||||
</h5>
|
||||
|
||||
<div *ngIf="!instance.loading && instance.imageDetails" class="text-truncate small text-info text-faded mb-1"
|
||||
[tooltip]="instance.imageDetails.description" container="body" placement="top" [adaptivePosition]="false">
|
||||
<div *ngIf="!instance.loading && instance.imageDetails"
|
||||
class="text-truncate small text-info text-faded mb-1" [tooltip]="instance.imageDetails.description"
|
||||
container="body" placement="top" [adaptivePosition]="false">
|
||||
{{ instance.imageDetails.name }}, v{{ instance.imageDetails.version }}
|
||||
</div>
|
||||
|
||||
<button *ngIf="!instance.loading" class="btn btn-outline-info w-100 d-flex justify-content-around align-items-center text-truncate"
|
||||
tooltip="Change specifications" container="body" placement="top" [adaptivePosition]="false"
|
||||
(click)="resizeMachine(instance)" [disabled]="instance.brand === 'kvm'">
|
||||
<button *ngIf="!instance.loading"
|
||||
class="btn btn-outline-info w-100 d-flex justify-content-around align-items-center text-truncate"
|
||||
tooltip="Change specifications" container="body" placement="top" [adaptivePosition]="false"
|
||||
(click)="resizeMachine(instance)" [disabled]="instance.brand === 'kvm'">
|
||||
|
||||
<!--<span class="text-uppercase text-truncate">{{ instance.packageDetails.name }}</span>-->
|
||||
<span class="px-1">
|
||||
@ -127,37 +138,41 @@
|
||||
|
||||
<div>
|
||||
<div class="small text-truncate my-2">
|
||||
<ng-container *ngIf="instance.type === 'virtualmachine'">
|
||||
<fa-icon icon="server" size="sm"></fa-icon>
|
||||
<b class="text-uppercase ms-1">{{ instance.brand }}</b> - infrastructure container
|
||||
</ng-container>
|
||||
<ng-container *ngIf="instance.type === 'smartmachine'">
|
||||
<fa-icon icon="desktop" size="sm"></fa-icon>
|
||||
<b class="text-uppercase ms-1">{{ instance.brand }}</b> - virtual machine
|
||||
<fa-icon icon="server" size="sm" class="me-1"></fa-icon>
|
||||
<span class="machine-brand" innerHtml="{{ 'dashboard.listItem.infrastructureContainer' | translate: { brand: instance.brand } }}"></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="instance.type === 'virtualmachine'">
|
||||
<fa-icon icon="desktop" size="sm" class="me-1"></fa-icon>
|
||||
<span class="machine-brand" innerHtml="{{ 'dashboard.listItem.virtualMachine' | translate: { brand: instance.brand } }}"></span>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-nowrap justify-content-between align-items-center">
|
||||
<a href="javascript:void(0)" class="badge text-uppercase"
|
||||
[class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'"
|
||||
[class.bg-danger]="instance.state === 'stopped'"
|
||||
[class.bg-success]="instance.state === 'running'"
|
||||
(click)="showMachineHistory(instance)" tooltip="Show machine history" container="body" placement="top" [adaptivePosition]="false">
|
||||
[class.bg-light]="instance.state !== 'running' && instance.state !== 'stopped'"
|
||||
[class.bg-danger]="instance.state === 'stopped'" [class.bg-success]="instance.state === 'running'"
|
||||
(click)="showMachineHistory(instance)" tooltip="{{ 'dashboard.listItem.history' | translate }}"
|
||||
container="body" placement="top" [adaptivePosition]="false">
|
||||
{{ instance.state }}
|
||||
</a>
|
||||
|
||||
<div class="btn-group btn-group-sm" dropdown placement="bottom right" *ngIf="!instance.loading">
|
||||
<button class="btn btn-link text-warning" (click)="restartMachine(instance)" *ngIf="instance.state === 'running'">
|
||||
<fa-icon icon="power-off" [fixedWidth]="true" size="sm" tooltip="Restart this machine" container="body" placement="top" [adaptivePosition]="false"></fa-icon>
|
||||
<button class="btn btn-link text-warning" (click)="restartMachine(instance)"
|
||||
*ngIf="instance.state === 'running'">
|
||||
<fa-icon icon="power-off" [fixedWidth]="true" size="sm" tooltip="Restart this machine"
|
||||
container="body" placement="top" [adaptivePosition]="false"></fa-icon>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link text-success" (click)="startMachine(instance)" *ngIf="instance.state === 'stopped'">
|
||||
<fa-icon icon="play" [fixedWidth]="true" size="sm" tooltip="Start this machine" container="body" placement="top" [adaptivePosition]="false"></fa-icon>
|
||||
<button class="btn btn-link text-success" (click)="startMachine(instance)"
|
||||
*ngIf="instance.state === 'stopped'">
|
||||
<fa-icon icon="play" [fixedWidth]="true" size="sm" tooltip="Start this machine" container="body"
|
||||
placement="top" [adaptivePosition]="false"></fa-icon>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link text-info"
|
||||
[popover]="instanceContextMenu" [popoverContext]="{ instance: instance }"
|
||||
placement="bottom left" containerClass="menu-dropdown" [outsideClick]="true">
|
||||
<button class="btn btn-link text-info" [popover]="instanceContextMenu"
|
||||
[popoverContext]="{ instance: instance }" placement="bottom left" containerClass="menu-dropdown"
|
||||
[outsideClick]="true">
|
||||
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -168,50 +183,69 @@
|
||||
<div class="col mt-sm-0 mt-3 no-overflow-sm" *ngIf="showMachineDetails">
|
||||
<div class="card-header p-0 h-100">
|
||||
<tabset class="dashboard-tabs" *ngIf="!instance.loading">
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-info">
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
|
||||
id="{{ instance.id }}-info">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="info-circle" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Info</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<app-instance-info [instance]="instance" [loadInfo]="true"
|
||||
(beforeLoad)="instance.working = true" (load)="instance.working = false">
|
||||
<app-instance-info [instance]="instance" [loadInfo]="instance.shouldLoadInfo"
|
||||
(load)="setInstanceInfo(instance, $event)" (processing)="instance.working = true"
|
||||
(finishedProcessing)="instance.working = false">
|
||||
</app-instance-info>
|
||||
</div>
|
||||
</tab>
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-networks">
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
|
||||
id="{{ instance.id }}-networks">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="network-wired" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Network</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<app-instance-networks [instance]="instance" [loadNetworks]="instance.shouldLoadNetworks"
|
||||
(beforeLoad)="instance.working = true" (load)="instance.working = false"
|
||||
(instanceReboot)="watchInstanceState(instance)" (instanceStateUpdate)="updateInstance(instance, $event)">
|
||||
(load)="setInstanceNetworks(instance, $event)" (processing)="instance.working = true"
|
||||
(finishedProcessing)="instance.working = false"
|
||||
(instanceReboot)="watchInstanceState(instance)"
|
||||
(instanceStateUpdate)="updateInstance(instance, $event)">
|
||||
</app-instance-networks>
|
||||
</div>
|
||||
</tab>
|
||||
<!--<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-firewall">
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
|
||||
id="{{ instance.id }}-snapshots" *ngIf="instance.brand !== 'kvm'">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="fire-alt" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Firewall</span>
|
||||
<fa-icon icon="history" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Snapshots</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<app-instance-firewall-rules [instance]="instance" [loadFirewallRules]="instance.shouldLoadFirewallRules"
|
||||
(cloudFirewallChange)="instance.firewall_enabled = $event">
|
||||
</app-instance-firewall-rules>
|
||||
<app-instance-snapshots [instance]="instance" [loadSnapshots]="instance.shouldLoadSnapshots"
|
||||
(load)="setInstanceSnapshots(instance, $event)" (processing)="instance.working = true"
|
||||
(finishedProcessing)="instance.working = false"
|
||||
(instanceStateUpdate)="updateInstance(instance, $event)">
|
||||
</app-instance-snapshots>
|
||||
</div>
|
||||
</tab>-->
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-volumes"
|
||||
*ngIf="instance.volumes && instance.volumes.length">
|
||||
</tab>
|
||||
<tab *ngIf="false" customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
|
||||
id="{{ instance.id }}-migrations">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="coins" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Migrations</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<button class="btn btn-outline-info w-100">Move to another node</button>
|
||||
</div>
|
||||
</tab>
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)"
|
||||
id="{{ instance.id }}-volumes" *ngIf="instance.volumes && instance.volumes.length">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="database" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Volumes</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<ul class="list-group list-group-flush list-info">
|
||||
<li class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
|
||||
*ngFor="let volume of instance.volumes">
|
||||
<li
|
||||
class="list-group-item text-uppercase px-0 dns d-flex justify-content-between align-items-center"
|
||||
*ngFor="let volume of instance.volumes">
|
||||
<div class="text-truncate">
|
||||
<fa-icon icon="database" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
<span class="ms-1">
|
||||
@ -224,28 +258,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
</tab>
|
||||
<tab customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-snapshots"
|
||||
*ngIf="instance.brand !== 'kvm'">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="history" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Snapshots</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<app-instance-snapshots [instance]="instance" [loadSnapshots]="instance.shouldLoadSnapshots"
|
||||
(beforeLoad)="instance.working = true" (load)="instance.working = false"
|
||||
(instanceStateUpdate)="updateInstance(instance, $event)">
|
||||
</app-instance-snapshots>
|
||||
</div>
|
||||
</tab>
|
||||
<tab *ngIf="false" customClass="dashboard-tab" (selectTab)="tabChanged($event, instance)" id="{{ instance.id }}-migrations">
|
||||
<ng-template tabHeading>
|
||||
<fa-icon icon="coins" class="d-sm-none"></fa-icon>
|
||||
<span class="d-none d-sm-inline-block ms-1">Migrations</span>
|
||||
</ng-template>
|
||||
<div class="card-body p-2 h-100">
|
||||
<button class="btn btn-outline-info w-100">Move to another node</button>
|
||||
</div>
|
||||
</tab>
|
||||
</tabset>
|
||||
</div>
|
||||
</div>
|
||||
@ -260,29 +272,70 @@
|
||||
<ng-template #filtersTemplate [formGroup]="editorForm">
|
||||
<fieldset class="filters">
|
||||
<ng-container formGroupName="filters">
|
||||
<div class="dropdown-header">Filter by state</div>
|
||||
<select class="form-control mb-3" formControlName="stateFilter">
|
||||
<option [ngValue]="null">Any state</option>
|
||||
<option *ngFor="let state of instanceStateArray" [value]="state">{{ state }}</option>
|
||||
</select>
|
||||
<div class="dropdown-header">{{ 'dashboard.list.filterByState' | translate }}</div>
|
||||
<div class="btn-group w-100 mb-3" dropdown>
|
||||
<button class="btn btn-state-filter dropdown-toggle d-flex justify-content-between align-items-center"
|
||||
dropdownToggle>
|
||||
<span *ngIf="!editorForm.get(['filters', 'stateFilter']).value">{{ 'dashboard.list.anyState' | translate
|
||||
}}</span>
|
||||
<span *ngIf="editorForm.get(['filters', 'stateFilter']).value === 'running'">{{
|
||||
'dashboard.listItem.stateRunning' | translate }}</span>
|
||||
<span *ngIf="editorForm.get(['filters', 'stateFilter']).value === 'stopped'">{{
|
||||
'dashboard.listItem.stateStopped' | translate }}</span>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-state-filter" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" [class.active]="!editorForm.get(['filters', 'stateFilter']).value"
|
||||
(click)="setStateFilter()">
|
||||
{{ 'dashboard.list.anyState' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item"
|
||||
[class.active]="editorForm.get(['filters', 'stateFilter']).value === 'running'"
|
||||
(click)="setStateFilter('running')">
|
||||
{{ 'dashboard.listItem.stateRunning' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item"
|
||||
[class.active]="editorForm.get(['filters', 'stateFilter']).value === 'stopped'"
|
||||
(click)="setStateFilter('stopped')">
|
||||
{{ 'dashboard.listItem.stateStopped' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-header">Filter by memory</div>
|
||||
<div class="dropdown-header">{{ 'dashboard.list.filterByMemory' | translate }}</div>
|
||||
<ngx-slider class="mb-4" formControlName="memoryFilter" [options]="memoryFilterOptions"></ngx-slider>
|
||||
|
||||
<div class="dropdown-header">Filter by disk size</div>
|
||||
<div class="dropdown-header">{{ 'dashboard.list.filterByDisk' | translate }}</div>
|
||||
<ngx-slider class="mb-3" formControlName="diskFilter" [options]="diskFilterOptions"></ngx-slider>
|
||||
|
||||
<button class="btn btn-outline-dark w-100 mt-3" (click)="clearFilters()">Reset filters</button>
|
||||
<button class="btn btn-outline-dark w-100 mt-3" (click)="clearFilters()">{{ 'dashboard.list.resetFilters' |
|
||||
translate }}</button>
|
||||
</ng-container>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="showMachineDetails" formControlName="showMachineDetails">
|
||||
<label class="form-check-label" for="showMachineDetails">
|
||||
Show machine details
|
||||
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1" *ngIf="editorForm.get('showMachineDetails').disabled"></fa-icon>
|
||||
{{ 'dashboard.list.showDetails' | translate }}
|
||||
<fa-icon icon="spinner" [pulse]="true" size="sm" class="me-1"
|
||||
*ngIf="editorForm.get('showMachineDetails').disabled"></fa-icon>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div [collapse]="!editorForm.get('showMachineDetails').value" [isAnimated]="true">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="fullDetailsTwoColumns"
|
||||
formControlName="fullDetailsTwoColumns">
|
||||
<label class="form-check-label" for="fullDetailsTwoColumns">
|
||||
{{ 'dashboard.list.dualColumns' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</ng-template>
|
||||
|
||||
@ -291,32 +344,32 @@
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="renameMachine(instance)">
|
||||
<fa-icon icon="pen" [fixedWidth]="true"></fa-icon>
|
||||
Rename this machine
|
||||
{{ 'dashboard.listItem.rename' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showTagEditor(instance)">
|
||||
<fa-icon icon="tags" [fixedWidth]="true"></fa-icon>
|
||||
Edit machine tags
|
||||
{{ 'dashboard.listItem.editTags' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showTagEditor(instance, true)">
|
||||
<fa-icon icon="tags" [fixedWidth]="true"></fa-icon>
|
||||
Edit machine metadata
|
||||
{{ 'dashboard.listItem.editMetadata' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="createMachine(instance)">
|
||||
<fa-icon icon="clone" [fixedWidth]="true"></fa-icon>
|
||||
Clone this machine
|
||||
{{ 'dashboard.listItem.clone' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="createImageFromMachine(instance)">
|
||||
<fa-icon icon="layer-group" [fixedWidth]="true"></fa-icon>
|
||||
Create an image from this machine
|
||||
{{ 'dashboard.listItem.createImage' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
@ -324,7 +377,7 @@
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="stopMachine(instance)">
|
||||
<fa-icon icon="stop" [fixedWidth]="true"></fa-icon>
|
||||
Stop this machine
|
||||
{{ 'dashboard.listItem.stop' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
@ -332,8 +385,8 @@
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="deleteMachine(instance)">
|
||||
<fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
|
||||
Delete this machine
|
||||
{{ 'dashboard.listItem.delete' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</ng-template>
|
@ -1,152 +1,169 @@
|
||||
.ips
|
||||
{
|
||||
+ .ips
|
||||
{
|
||||
margin-left: .5rem;
|
||||
:host {
|
||||
flex-direction: column;
|
||||
overflow : hidden;
|
||||
}
|
||||
|
||||
.ips {
|
||||
+.ips {
|
||||
margin-left : .5rem;
|
||||
padding-left: .5rem;
|
||||
border-left: 2px solid #2b3540;
|
||||
border-left : 2px solid #2b3540;
|
||||
}
|
||||
}
|
||||
|
||||
.card
|
||||
{
|
||||
border: 1px solid rgba(0, 0, 0, .5);
|
||||
.card {
|
||||
border : 1px solid rgba(0, 0, 0, .5);
|
||||
background-color: rgba(16, 21, 39, .5);
|
||||
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
|
||||
transition: box-shadow .15s ease-out;
|
||||
box-shadow : 0 0 0 2px #0b2b51, 0 0 2px 4px #0b284b, 0 0 10px 3px #0e162a;
|
||||
transition : box-shadow .15s ease-out;
|
||||
|
||||
&:hover
|
||||
{
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 2px #0b2b51, 0 0 2px 4px rgba(18, 203, 240, .4), 0 0 10px 3px #0e162a;
|
||||
}
|
||||
|
||||
.card-title
|
||||
{
|
||||
color: #ff9c07;
|
||||
.card-title {
|
||||
color : #ff9c07;
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
font-weight : bold;
|
||||
}
|
||||
|
||||
.card-body
|
||||
{
|
||||
.card-body {
|
||||
background-color: rgba(16, 21, 39, .5);
|
||||
max-height: 205px;
|
||||
overflow: auto;
|
||||
padding-top: .7rem !important;
|
||||
max-height : 205px;
|
||||
overflow : auto;
|
||||
padding-top : .7rem !important;
|
||||
}
|
||||
|
||||
.list-group-item
|
||||
{
|
||||
.list-group-item {
|
||||
background: none;
|
||||
|
||||
span
|
||||
{
|
||||
span {
|
||||
color: #8881ff;
|
||||
}
|
||||
|
||||
b, .strong
|
||||
{
|
||||
color: #ff9c07;
|
||||
b,
|
||||
.strong {
|
||||
color : #ff9c07;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-info, .btn-outline-info
|
||||
{
|
||||
font-size: 1rem;
|
||||
.btn-info,
|
||||
.btn-outline-info {
|
||||
font-size : 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-info
|
||||
{
|
||||
.card-info {
|
||||
background-color: rgba(16, 21, 39, .75);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: .25rem .5rem .5rem;
|
||||
height: 170px;
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
justify-content : space-between;
|
||||
padding : .25rem .5rem .5rem;
|
||||
height : 170px;
|
||||
}
|
||||
|
||||
.full-details .card-info
|
||||
{
|
||||
.full-details .card-info {
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px)
|
||||
{
|
||||
.card-info
|
||||
{
|
||||
@media (max-width: 576px) {
|
||||
.card-info {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.no-overflow-sm
|
||||
{
|
||||
.no-overflow-sm {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 992px)
|
||||
{
|
||||
.no-overflow-sm
|
||||
{
|
||||
@media (max-width: 992px) {
|
||||
.no-overflow-sm {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.open .dropdown-toggle
|
||||
{
|
||||
}
|
||||
.open .dropdown-toggle {}
|
||||
|
||||
.filters
|
||||
{
|
||||
.filters {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.form-check-label
|
||||
{
|
||||
.form-check-label {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.btn-toolbar .btn
|
||||
{
|
||||
.badge
|
||||
{
|
||||
padding: 0.1rem 0.35rem 0;
|
||||
border: 1px solid transparent;
|
||||
.btn-toolbar .btn {
|
||||
.badge {
|
||||
padding : 0.1rem 0.35rem 0;
|
||||
border : 1px solid transparent;
|
||||
text-shadow: 0 0 3px rgb(255 255 255 / 25%);
|
||||
|
||||
&:first-letter
|
||||
{
|
||||
&:first-letter {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover
|
||||
{
|
||||
.badge
|
||||
{
|
||||
&:hover {
|
||||
.badge {
|
||||
border-color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flash
|
||||
{
|
||||
from, 50%, to
|
||||
{
|
||||
@keyframes flash {
|
||||
|
||||
from,
|
||||
50%,
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
25%, 75%
|
||||
{
|
||||
25%,
|
||||
75% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.flash
|
||||
{
|
||||
animation-name: flash;
|
||||
animation-duration: calc(.9s * 1.3);
|
||||
.flash {
|
||||
animation-name : flash;
|
||||
animation-duration : calc(.9s * 1.3);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.btn-state-filter:not(.btn-lg):not(.btn-sm) {
|
||||
background-color: #11182b;
|
||||
color : #ff9c07;
|
||||
padding : .5rem 1rem;
|
||||
}
|
||||
|
||||
.open.show .btn-state-filter {
|
||||
border-radius: 1.25rem 1.25rem 0 0;
|
||||
}
|
||||
|
||||
.dropdown-menu.dropdown-menu-state-filter {
|
||||
background : #11182b;
|
||||
color : #ff9c07;
|
||||
width : 100%;
|
||||
padding : 0;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
|
||||
.dropdown-item {
|
||||
color: inherit;
|
||||
|
||||
&.active {
|
||||
color: #0dcaf0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(11, 172, 204, .2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-switch,
|
||||
.form-switch input,
|
||||
.form-switch label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { InstancesService } from './helpers/instances.service';
|
||||
import { BsModalService } from 'ngx-bootstrap/modal';
|
||||
import { debounceTime, delay, distinctUntilChanged, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { InstanceWizardComponent } from './instance-wizard/instance-wizard.component';
|
||||
import { SelectionType, ColumnMode } from '@swimlane/ngx-datatable';
|
||||
import { Instance } from './models/instance';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -21,6 +20,8 @@ import { LabelType, Options } from '@angular-slider/ngx-slider';
|
||||
import { FileSizePipe } from '../pipes/file-size.pipe';
|
||||
import { sortArray } from '../helpers/utils.service';
|
||||
import { VolumesService } from '../volumes/helpers/volumes.service';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-instances',
|
||||
@ -42,6 +43,7 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
canPrepareForLoading: boolean;
|
||||
editorForm: FormGroup;
|
||||
showMachineDetails: boolean;
|
||||
fullDetailsTwoColumns: boolean;
|
||||
runningInstanceCount = 0;
|
||||
stoppedInstanceCount = 0;
|
||||
instanceStateArray: string[] = [];
|
||||
@ -72,8 +74,12 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly fileSizePipe: FileSizePipe)
|
||||
private readonly fileSizePipe: FileSizePipe,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
translationService.get('dashboard.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
|
||||
|
||||
this.lazyLoadDelay = this.minimumLazyLoadDelay;
|
||||
|
||||
// Configure FuseJs
|
||||
@ -93,6 +99,7 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
};
|
||||
|
||||
this.showMachineDetails = !!JSON.parse(localStorage.getItem('showMachineDetails') || '0');
|
||||
this.fullDetailsTwoColumns = !!JSON.parse(localStorage.getItem('fullDetailsTwoColumns') || '1');
|
||||
|
||||
this.createForm();
|
||||
}
|
||||
@ -108,9 +115,9 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
switch (label)
|
||||
{
|
||||
case LabelType.Low:
|
||||
return `<b>Between</b> ${formattedValue}`;
|
||||
return `Between ${formattedValue}`;
|
||||
case LabelType.High:
|
||||
return `<b>and</b> ${formattedValue}`;
|
||||
return `and ${formattedValue}`;
|
||||
default:
|
||||
return formattedValue;
|
||||
}
|
||||
@ -187,7 +194,8 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
imageFilter: [], // instances provisioned with a certain image
|
||||
}),
|
||||
filtersActive: [false],
|
||||
showMachineDetails: [this.showMachineDetails]
|
||||
showMachineDetails: [this.showMachineDetails],
|
||||
fullDetailsTwoColumns: [{ value: this.fullDetailsTwoColumns, disabled: !this.showMachineDetails }]
|
||||
});
|
||||
|
||||
this.editorForm.get('searchTerm').valueChanges
|
||||
@ -232,7 +240,15 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
// Store this setting in the local storage
|
||||
localStorage.setItem('showMachineDetails', JSON.stringify(showMachineDetails));
|
||||
|
||||
setTimeout(() => this.editorForm.get('showMachineDetails').enable(), 300);
|
||||
setTimeout(() =>
|
||||
{
|
||||
this.editorForm.get('showMachineDetails').enable();
|
||||
|
||||
if (showMachineDetails)
|
||||
this.editorForm.get('fullDetailsTwoColumns').enable();
|
||||
else
|
||||
this.editorForm.get('fullDetailsTwoColumns').disable();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
@ -291,6 +307,12 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
this.editorForm.get('sortProperty').setValue(propertyName);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
setStateFilter(state?: string)
|
||||
{
|
||||
this.editorForm.get(['filters', 'stateFilter']).setValue(state);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
clearSearch()
|
||||
{
|
||||
@ -700,6 +722,7 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
};
|
||||
|
||||
const modalRef = this.modalService.show(InstanceHistoryComponent, modalConfig);
|
||||
modalRef.setClass('modal-lg');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
@ -711,8 +734,6 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
instance.shouldLoadSnapshots = this.editorForm.get('showMachineDetails').value;
|
||||
else if (event.id.endsWith('networks'))
|
||||
instance.shouldLoadNetworks = this.editorForm.get('showMachineDetails').value;
|
||||
else if (event.id.endsWith('firewall'))
|
||||
instance.shouldLoadFirewallRules = this.editorForm.get('showMachineDetails').value;
|
||||
else if (event.id.endsWith('volumes'))
|
||||
{
|
||||
//instance.shouldLoadVolumes = this.editorForm.get('showMachineDetails').value;
|
||||
@ -782,6 +803,33 @@ export class InstancesComponent implements OnInit, OnDestroy
|
||||
instance.state = updates.state;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
setInstanceInfo(instance: Instance, dnsList)
|
||||
{
|
||||
// Update the instance as a result of the info panel's "load" event. We do this because the intances are (un)loaded
|
||||
// from the viewport as the user scrolls through the page, to optimize memory consumption.
|
||||
instance.dnsList = dnsList;
|
||||
instance.infoLoaded = true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
setInstanceNetworks(instance: Instance, nics)
|
||||
{
|
||||
// Update the instance as a result of the networks panel's "load" event. We do this because the intances are (un)loaded
|
||||
// from the viewport as the user scrolls through the page, to optimize memory consumption.
|
||||
instance.nics = nics;
|
||||
instance.networksLoaded = true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
setInstanceSnapshot(instance: Instance, snapshots)
|
||||
{
|
||||
// Update the instance as a result of the snapshots panel's "load" event. We do this because the intances are (un)loaded
|
||||
// from the viewport as the user scrolls through the page, to optimize memory consumption.
|
||||
instance.snapshots = snapshots;
|
||||
instance.snapshotsLoaded = true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
private fillInInstanceDetails(instance: Instance)
|
||||
{
|
||||
|
@ -32,18 +32,20 @@ export class Instance extends InstanceRequest
|
||||
imageDetails: CatalogImage;
|
||||
packageDetails: CatalogPackage;
|
||||
dns_names: string[];
|
||||
dnsList: {};
|
||||
dnsList: any;
|
||||
memory: number;
|
||||
type: string;
|
||||
state: string;
|
||||
snapshots: any[];
|
||||
|
||||
loading: boolean;
|
||||
working: boolean;
|
||||
shouldLoadInfo: boolean;
|
||||
infoLoaded: boolean;
|
||||
shouldLoadNetworks: boolean;
|
||||
shouldLoadSecurity: boolean;
|
||||
networksLoaded: boolean;
|
||||
shouldLoadSnapshots: boolean;
|
||||
shouldLoadFirewallRules: boolean;
|
||||
snapshotsLoaded: boolean;
|
||||
volumesEnabled: boolean;
|
||||
metadataKeys: string[];
|
||||
tagKeys: string[];
|
||||
|
@ -51,8 +51,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto">
|
||||
<div class="container my-4">
|
||||
<div class="overflow-auto flex-grow-1 my-3">
|
||||
<div class="container my-2">
|
||||
<div class="table-responsive" *ngIf="!loadingIndicator">
|
||||
<p *ngIf="!firewallRules.length" class="text-center text-info text-faded p-3 mb-0">
|
||||
There are no firewall rules yet.
|
||||
|
@ -1,3 +1,9 @@
|
||||
:host
|
||||
{
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.table-responsive
|
||||
{
|
||||
background-color: rgba(16, 21, 39, 0.75);
|
||||
@ -32,6 +38,11 @@
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.text-transform-none
|
||||
{
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.inline-list-item + .inline-list-item
|
||||
{
|
||||
padding-left: .25rem;
|
||||
|
@ -11,6 +11,8 @@ import { ConfirmationDialogComponent } from '../../components/confirmation-dialo
|
||||
import { InstancesService } from '../../instances/helpers/instances.service';
|
||||
import { FirewallService } from '../helpers/firewall.service';
|
||||
import { sortArray } from '../../helpers/utils.service';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-firewall-rules',
|
||||
@ -33,8 +35,12 @@ export class FirewallRulesComponent implements OnInit, OnDestroy
|
||||
private readonly instancesService: InstancesService,
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly fb: FormBuilder)
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
translationService.get('networking.firewall.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
|
||||
|
||||
// Configure FuseJs
|
||||
this.fuseJsOptions = {
|
||||
includeScore: false,
|
||||
|
@ -43,12 +43,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto">
|
||||
<div class="container my-4">
|
||||
<div class="overflow-auto flex-grow-1 mt-1">
|
||||
<div class="container my-2">
|
||||
<accordion [isAnimated]="false" [closeOthers]="false">
|
||||
<accordion-group *ngFor="let vlan of listItems" (isOpenChange)="getNetworks($event, vlan)">
|
||||
<div class="d-flex justify-content-between align-items-center sticky-top" accordion-heading
|
||||
tooltip="Click to expand this virtual network and show all its networks" placement="top" container="body">
|
||||
tooltip="Show or hide this VLAN's networks" placement="top" container="body">
|
||||
<h4 class="mb-0">
|
||||
<span class="text-info me-2">{{ vlan.name }}</span>
|
||||
<span class="vlan-id">
|
||||
@ -133,17 +133,29 @@
|
||||
</div>
|
||||
|
||||
<div class="card-footer px-2 d-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-sm btn-link text-info px-1" tooltip="Rename this virtual network" container="body" placement="top" [adaptivePosition]="false"
|
||||
(click)="showVlanEditor(vlan)">
|
||||
<fa-icon icon="pen" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
<button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Configure a new network</button>
|
||||
|
||||
<button class="btn btn-outline-info" (click)="showNetworkEditor(vlan)">Add a network</button>
|
||||
|
||||
<button class="btn btn-sm btn-link text-danger px-1" tooltip="Delete this virtual network" container="body" placement="top" [adaptivePosition]="false"
|
||||
(click)="deleteVlan(vlan)">
|
||||
<fa-icon icon="trash" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
<div class="btn-group btn-group-sm" dropdown placement="bottom right" container="body">
|
||||
<button class="btn btn-link text-info" dropdownToggle
|
||||
tooltip="Virtual network options" container="body" placement="top" [adaptivePosition]="false">
|
||||
<fa-icon icon="ellipsis-v" [fixedWidth]="true" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul id="dropdown-split" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="button-split">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showVlanEditor(vlan)">
|
||||
<fa-icon icon="pen" [fixedWidth]="true"></fa-icon>
|
||||
Rename this virtual network
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="deleteVlan(vlan)">
|
||||
<fa-icon icon="trash" [fixedWidth]="true"></fa-icon>
|
||||
Delete this virtual network
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</accordion-group>
|
||||
</accordion>
|
||||
|
@ -1,14 +1,14 @@
|
||||
:host
|
||||
{
|
||||
flex-grow: 1;
|
||||
overflow : hidden;
|
||||
}
|
||||
|
||||
.vlan-id
|
||||
{
|
||||
color: #3d5e8e;
|
||||
}
|
||||
|
||||
h4, .network-name
|
||||
{
|
||||
text-transform: uppercase;
|
||||
color: #8881ff;
|
||||
}
|
||||
|
||||
.network-name
|
||||
{
|
||||
color: #ff9c07;
|
||||
|
@ -12,6 +12,8 @@ import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '
|
||||
import Fuse from 'fuse.js';
|
||||
import { Subject } from 'rxjs';
|
||||
import { sortArray } from '../../helpers/utils.service';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-networks',
|
||||
@ -32,8 +34,12 @@ export class NetworksComponent implements OnInit, OnDestroy
|
||||
constructor(private readonly networkingService: NetworkingService,
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly fb: FormBuilder)
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
translationService.get('networking.networks.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
|
||||
|
||||
this.getVlans();
|
||||
|
||||
// Configure FuseJs
|
||||
|
@ -1,237 +1,248 @@
|
||||
<div class="overflow-auto h-100">
|
||||
<div class="container h-100 py-3">
|
||||
<div class="row h-100" id="securitySettings">
|
||||
<div class="col-sm py-2">
|
||||
<fieldset>
|
||||
<legend>
|
||||
<span>
|
||||
<fa-icon [fixedWidth]="true" icon="key" size="sm"></fa-icon>
|
||||
{{ 'security.policies' | translate }}
|
||||
</span>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div class="overflow-auto flex-grow-1 d-flex">
|
||||
<div class="container flex-grow-1 d-flex mb-3">
|
||||
<div class="row flex-grow-1">
|
||||
<div class="col-sm py-2">
|
||||
<fieldset>
|
||||
<legend>
|
||||
<span>
|
||||
<fa-icon [fixedWidth]="true" icon="key" size="sm"></fa-icon>
|
||||
{{ 'security.policies' | translate }}
|
||||
</span>
|
||||
|
||||
<button class="btn btn-outline-info" container="body" (click)="showPolicyEditor()">
|
||||
{{ 'security.addPolicy' | translate }}
|
||||
</button>
|
||||
</legend>
|
||||
<button class="btn btn-outline-info" container="body" (click)="showPolicyEditor()">
|
||||
<span class="d-none d-lg-inline-block">{{ 'security.addPolicy' | translate }}</span>
|
||||
<fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addPolicy' | translate }}"
|
||||
container="body" placement="top" [adaptivePosition]="false">
|
||||
</fa-icon>
|
||||
</button>
|
||||
</legend>
|
||||
|
||||
<p class="px-3">Policies are just access rules grouped together</p>
|
||||
<p class="px-3">Policies are just access rules grouped together</p>
|
||||
|
||||
<ul class="list-group list-group-flush p-0" id="policies"
|
||||
cdkDropList [cdkDropListData]="policies" [cdkDropListConnectedTo]="roleDropLists"
|
||||
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="noReturnPredicate">
|
||||
<li class="list-group-item pr-1" *ngFor="let policy of policies"
|
||||
cdkDrag [cdkDragData]="policy" cdkDragBoundary="#securitySettings">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="grip" cdkDragHandle>
|
||||
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
|
||||
{{ policy.name }}
|
||||
</span>
|
||||
<ul class="list-group list-group-flush p-0" id="policies" cdkDropList [cdkDropListData]="policies"
|
||||
[cdkDropListConnectedTo]="roleDropLists" cdkDropListSortingDisabled sortingDisabled
|
||||
[cdkDropListEnterPredicate]="noReturnPredicate">
|
||||
<li class="list-group-item pr-1" *ngFor="let policy of policies" cdkDrag [cdkDragData]="policy"
|
||||
cdkDragBoundary="#securitySettings">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="grip" cdkDragHandle>
|
||||
<fa-icon [fixedWidth]="true" icon="arrows-alt" size="sm"></fa-icon>
|
||||
{{ policy.name }}
|
||||
</span>
|
||||
|
||||
<div class="btn-group btn-group-sm float-end" dropdown>
|
||||
<!--<button class="btn text-info" tooltip="{{ 'security.assignPolicyToRoles' | translate }}" container="body" (click)="assignPolicyToRoles(policy)">
|
||||
<div class="btn-group btn-group-sm float-end" dropdown>
|
||||
<!--<button class="btn text-info" tooltip="{{ 'security.assignPolicyToRoles' | translate }}" container="body" (click)="assignPolicyToRoles(policy)">
|
||||
<fa-icon [fixedWidth]="true" icon="tags" size="sm"></fa-icon>
|
||||
</button>-->
|
||||
<button class="btn text-info" dropdownToggle>
|
||||
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showPolicyEditor(policy)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ 'security.editPolicy' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="deletePolicy(policy)">
|
||||
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
|
||||
{{ 'security.deletePolicy' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
</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>
|
||||
<button class="btn text-info" dropdownToggle>
|
||||
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showPolicyEditor(policy)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ 'security.editPolicy' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="deletePolicy(policy)">
|
||||
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
|
||||
{{ 'security.deletePolicy' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</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)">
|
||||
<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()">
|
||||
<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>
|
||||
</button>-->
|
||||
<button class="btn text-info" dropdownToggle>
|
||||
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showRoleEditor(role)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ 'security.editRole' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="deleteRole(role)">
|
||||
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
|
||||
{{ 'security.deleteRole' | translate }}
|
||||
</button>
|
||||
<button class="btn text-info" dropdownToggle>
|
||||
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showRoleEditor(role)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ 'security.editRole' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="deleteRole(role)">
|
||||
<fa-icon [fixedWidth]="true" icon="trash"></fa-icon>
|
||||
{{ 'security.deleteRole' | translate }}
|
||||
</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>
|
||||
</ul>
|
||||
</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>
|
||||
<fa-icon [fixedWidth]="true" icon="tag"></fa-icon>
|
||||
{{ role.name }}
|
||||
</div>
|
||||
<button class="btn btn-outline-info" container="body" (click)="showUserEditor()">
|
||||
<span class="d-none d-lg-inline-block">{{ 'security.addUser' | translate }}</span>
|
||||
<fa-icon icon="plus" class="d-lg-none d-inline-block" tooltip="{{ 'security.addUser' | translate }}"
|
||||
container="body" placement="top" [adaptivePosition]="false">
|
||||
</fa-icon>
|
||||
</button>
|
||||
</legend>
|
||||
|
||||
<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 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"
|
||||
(click)="removePolicyFromRole(policy, role)">
|
||||
<fa-icon [fixedWidth]="true" icon="trash" size="sm"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
<ul class="list-group list-group-flush p-0">
|
||||
<li class="list-group-item px-1" *ngFor="let user of users; let i = index" id="{{ 'user' + i }}"
|
||||
cdkDropList [cdkDropListData]="user" cdkDropListSortingDisabled sortingDisabled
|
||||
[cdkDropListEnterPredicate]="usersEnterPredicate" (cdkDropListDropped)="drop($event)">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="user.collapsed ? 0 : 90" class="px-2 py-1"
|
||||
[ngClass]="user.roles.length ? 'text-secondary' : 'text-muted'"
|
||||
(click)="user.roles.length && user.collapsed = !user.collapsed"
|
||||
tooltip="{{ 'security.toggleRoles' | translate }}" container="body" placement="top"
|
||||
[adaptivePosition]="false"></fa-icon>
|
||||
|
||||
<button class="btn btn-outline-info" container="body" (click)="showUserEditor()">
|
||||
{{ 'security.addUser' | translate }}
|
||||
</button>
|
||||
</legend>
|
||||
{{ user.login }} <sup *ngIf="user.roles?.length">({{ user.roles.length }})</sup>
|
||||
</div>
|
||||
|
||||
<p class="px-3">To assign roles drag them over a user</p>
|
||||
|
||||
<ul class="list-group list-group-flush p-0">
|
||||
<li class="list-group-item px-1" *ngFor="let user of users; let i = index" id="{{ 'user' + i }}"
|
||||
cdkDropList [cdkDropListData]="user"
|
||||
cdkDropListSortingDisabled sortingDisabled [cdkDropListEnterPredicate]="usersEnterPredicate"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<fa-icon [fixedWidth]="true" icon="angle-right" [rotate]="user.collapsed ? 0 : 90"
|
||||
class="px-2 py-1" [ngClass]="user.roles.length ? 'text-secondary' : 'text-muted'"
|
||||
(click)="user.roles.length && user.collapsed = !user.collapsed"
|
||||
tooltip="{{ 'security.toggleRoles' | translate }}" container="body"></fa-icon>
|
||||
|
||||
{{ user.login }} <sup *ngIf="user.roles?.length">({{ user.roles.length }})</sup>
|
||||
<div class="btn-group btn-group-sm float-end" dropdown>
|
||||
<button class="btn text-info" dropdownToggle>
|
||||
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showUserEditor(user)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ 'security.editUser' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showUserEditor(user, true)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ '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 class="btn-group btn-group-sm float-end" dropdown>
|
||||
<button class="btn text-info" dropdownToggle>
|
||||
<fa-icon [fixedWidth]="true" icon="ellipsis-v" size="sm"></fa-icon>
|
||||
</button>
|
||||
<ul *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showUserEditor(user)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ 'security.editUser' | translate }}
|
||||
</button>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<button class="dropdown-item" (click)="showUserEditor(user, true)">
|
||||
<fa-icon [fixedWidth]="true" icon="pen-nib"></fa-icon>
|
||||
{{ '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>
|
||||
<div *cdkDragPreview>
|
||||
<fa-icon [fixedWidth]="true" icon="user"></fa-icon>
|
||||
{{ 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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div *cdkDragPreview>
|
||||
<fa-icon [fixedWidth]="true" icon="user"></fa-icon>
|
||||
{{ 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>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,3 +1,9 @@
|
||||
:host
|
||||
{
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
fieldset
|
||||
{
|
||||
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 { RolePolicy } from './models/role-policy';
|
||||
import { RoleUser } from './models/role-user';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-security',
|
||||
@ -33,8 +35,12 @@ export class SecurityComponent implements OnInit, OnDestroy
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
constructor(private readonly securityService: SecurityService,
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly toastr: ToastrService)
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
translationService.get('security.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
|
||||
|
||||
forkJoin({
|
||||
users: securityService.getUsers(),
|
||||
roles: securityService.getRoles(),
|
||||
|
@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-9">
|
||||
<!-- <div class="col-sm-9">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="address" formControlName="address" placeholder="Address">
|
||||
<label for="address">Address</label>
|
||||
@ -95,7 +95,7 @@
|
||||
<input type="text" class="form-control" id="country" formControlName="country" placeholder="Country">
|
||||
<label for="country">Country</label>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end align-items-center mt-5">
|
||||
|
@ -39,7 +39,6 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
|
||||
import { AutofocusDirective } from './directives/autofocus.directive';
|
||||
|
||||
import { InlineEditorComponent } from './components/inline-editor/inline-editor.component';
|
||||
import { ImagesComponent } from './catalog/images/images.component';
|
||||
import { PackagesComponent } from './catalog/packages/packages.component';
|
||||
import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
import { AutosizeModule } from 'ngx-autosize';
|
||||
@ -54,7 +53,6 @@ import { LazyLoadDirective } from './directives/lazy-load.directive';
|
||||
AutofocusDirective,
|
||||
//HasPermissionDirective,
|
||||
InlineEditorComponent,
|
||||
ImagesComponent,
|
||||
PackagesComponent,
|
||||
FileSizePipe,
|
||||
AlphaOnlyDirective,
|
||||
@ -118,7 +116,6 @@ import { LazyLoadDirective } from './directives/lazy-load.directive';
|
||||
AutofocusDirective,
|
||||
TimeagoModule,
|
||||
NgxDatatableModule,
|
||||
ImagesComponent,
|
||||
PackagesComponent,
|
||||
FileSizePipe,
|
||||
InlineEditorComponent,
|
||||
|
@ -43,8 +43,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto">
|
||||
<div class="container my-4">
|
||||
<div class="overflow-auto flex-grow-1 my-3">
|
||||
<div class="container my-2">
|
||||
<div class="table-responsive" *ngIf="!loadingIndicator">
|
||||
<p *ngIf="!volumes.length" class="text-center text-info text-faded p-3 mb-0">
|
||||
There are no volumes yet.
|
||||
@ -62,7 +62,7 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let volume of listItems">
|
||||
<td>
|
||||
<b class="text-uppercase">{{ volume.name }}</b>
|
||||
<b>{{ volume.name }}</b>
|
||||
</td>
|
||||
<td>
|
||||
{{ volume.size * 1024 * 1024 | fileSize}}
|
||||
@ -93,6 +93,9 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<fa-icon icon="lock" class="in-use text-danger" *ngIf="volume.refs && volume.refs.length"
|
||||
tooltip="In use by one or more machines" placement="top" [adaptivePosition]="false"></fa-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -1,3 +1,9 @@
|
||||
:host
|
||||
{
|
||||
flex-grow: 1;
|
||||
overflow : hidden;
|
||||
}
|
||||
|
||||
.table-responsive
|
||||
{
|
||||
background-color: rgba(16, 21, 39, 0.75);
|
||||
@ -36,4 +42,9 @@
|
||||
color: #3d5e8e;
|
||||
}
|
||||
}
|
||||
|
||||
.in-use
|
||||
{
|
||||
padding: 0 .75rem;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import { sortArray } from '../helpers/utils.service';
|
||||
import Fuse from 'fuse.js';
|
||||
import { FormGroup, FormBuilder, Validators, AbstractControl, FormArray } from '@angular/forms';
|
||||
import { distinctUntilChanged, first, takeUntil, debounceTime, filter, switchMap } from 'rxjs/operators';
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-volumes',
|
||||
@ -37,8 +39,11 @@ export class VolumesComponent implements OnInit, OnDestroy
|
||||
private readonly instancesService: InstancesService,
|
||||
private readonly modalService: BsModalService,
|
||||
private readonly toastr: ToastrService,
|
||||
private readonly fb: FormBuilder)
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly titleService: Title,
|
||||
private readonly translationService: TranslateService)
|
||||
{
|
||||
translationService.get('volumes.title').pipe(first()).subscribe(x => titleService.setTitle(`Joyent - ${x}`));
|
||||
|
||||
// Configure FuseJs
|
||||
this.fuseJsOptions = {
|
||||
|
@ -34,7 +34,7 @@ import { VolumeEditorComponent } from './volume-editor/volume-editor.component';
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
//useClass: WebpackTranslateLoader
|
||||
useFactory: () => new WebpackTranslateLoader('networking')
|
||||
useFactory: () => new WebpackTranslateLoader('volumes')
|
||||
},
|
||||
compiler: {
|
||||
provide: TranslateCompiler,
|
||||
|
@ -1,5 +1,10 @@
|
||||
{
|
||||
"account":
|
||||
{
|
||||
"title": "Account",
|
||||
"myProfile": "My profile",
|
||||
"updateProfile": "Update profile",
|
||||
"myKeys": "My keys",
|
||||
"addKey": "Add key"
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@
|
||||
"memory": "Memory",
|
||||
"disk": "Storage"
|
||||
},
|
||||
"custom":
|
||||
"images":
|
||||
{
|
||||
|
||||
"title": "Images"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"dashboard":
|
||||
{
|
||||
"title": "Dashboard",
|
||||
"general":
|
||||
{
|
||||
"save": "Save changes",
|
||||
@ -15,23 +16,25 @@
|
||||
"noResults": "No machine matches your filters",
|
||||
"filters": "Showing {filteredCount} (out of {totalCount}: {totalRunning} running, {totalStopped} stopped)",
|
||||
"filterByState": "Filter by state",
|
||||
"anyState": "Any state",
|
||||
"filterByMemory": "Filter by memory",
|
||||
"filterByDisk": "Filter by disk size",
|
||||
"between": "Between",
|
||||
"and": "and",
|
||||
"resetFilters": "Reset filters",
|
||||
"showDetails": "Show machine details",
|
||||
"sortBy": "Sort by {}",
|
||||
"dualColumns": "2 columns on large displays",
|
||||
"sortBy": "Sort by {sortProperty}",
|
||||
"sortByName": "Name",
|
||||
"sortByOs": "Operating system",
|
||||
"sortByBrand": "Brand",
|
||||
"sortByImage": "Image",
|
||||
"sortByState": "State",
|
||||
"between": "Between",
|
||||
"and": "and"
|
||||
"sortByState": "State"
|
||||
},
|
||||
"listItem":
|
||||
{
|
||||
"infrastructureContainer": "{type} - insfrastructure container",
|
||||
"virtualMachine": "{type} - virtual machine",
|
||||
"infrastructureContainer": "<b>{brand}</b> - Infrastructure container",
|
||||
"virtualMachine": "<b>{brand}</b> - Virtual machine",
|
||||
"stateRunning": "Running",
|
||||
"stateStopping": "Stopping",
|
||||
"stateProvisioning": "Provisioning",
|
||||
|
@ -6,9 +6,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"fileManager": "File Manager",
|
||||
"volumes": "Volumes",
|
||||
"customImages": "Custom Images",
|
||||
"dockerImages": "Docker Images",
|
||||
"dockerRegistry": "Docker Registry",
|
||||
"images": "Images",
|
||||
"networks": "Networks",
|
||||
"virtualNetworks": "Virtual Networks",
|
||||
"firewallRules": "Firewall rules",
|
||||
@ -23,19 +21,9 @@
|
||||
},
|
||||
"catalog":
|
||||
{
|
||||
"customImages":
|
||||
"images":
|
||||
{
|
||||
"title": "Custom Images",
|
||||
"subTitle": ""
|
||||
},
|
||||
"dockerImages":
|
||||
{
|
||||
"title": "Docker Images",
|
||||
"subTitle": ""
|
||||
},
|
||||
"dockerRegistry":
|
||||
{
|
||||
"title": "Docker Registry",
|
||||
"title": "Images",
|
||||
"subTitle": ""
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,13 @@
|
||||
{
|
||||
"networking":
|
||||
{
|
||||
|
||||
"networks":
|
||||
{
|
||||
"title": "Networks"
|
||||
},
|
||||
"firewall":
|
||||
{
|
||||
"title": "Firewall rules"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"security":
|
||||
{
|
||||
"title": "Security",
|
||||
"users": "Users",
|
||||
"roles": "Roles",
|
||||
"policies": "Policies",
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"volumes":
|
||||
{
|
||||
"title": "Volumes"
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Manta</title>
|
||||
<title>Joyent</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@ -11,7 +11,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<style type="text/css">.spinner{font-size:10px;text-indent:-9999em;width:10em;height:10em;border-radius:50%;background:#0dcaf0;background:linear-gradient(to right,#0dcaf0 10%,rgba(17,124,238,0) 42%);position:relative;animation:load3 1.4s infinite linear;transform:translateZ(0) translate(-50%,-50%);transform-origin:0 0;position:absolute;left:50%;top:50%}.spinner:before{width:50%;height:50%;background:#0dcaf0;border-radius:100% 0 0 0;position:absolute;top:0;left:0;content:''}.spinner:after{background:#090b17;width:75%;height:75%;border-radius:50%;content:'';margin:auto;position:absolute;top:0;left:0;bottom:0;right:0}@keyframes load3{0%{transform:rotate(0) translateZ(0) translate(-50%,-50%)}100%{transform:rotate(360deg) translateZ(0) translate(-50%,-50%)}}</style>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Mukta&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -13,6 +13,7 @@ body
|
||||
color: #3d5e8e;
|
||||
font-family: 'Mukta', sans-serif;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body, div, virtual-scroller
|
||||
@ -450,7 +451,7 @@ body, div, virtual-scroller
|
||||
{
|
||||
+ .panel
|
||||
{
|
||||
margin-top: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.card
|
||||
@ -609,9 +610,6 @@ virtual-scroller
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
margin-top: -1rem;
|
||||
/* margin-top: calc(var(--bs-gutter-y) * -1);
|
||||
margin-right: calc(var(--bs-gutter-x)/ -2);
|
||||
margin-left: calc(var(--bs-gutter-x)/ -2); */
|
||||
--bs-gutter-y: 2rem;
|
||||
position: relative;
|
||||
|
||||
@ -621,7 +619,8 @@ virtual-scroller
|
||||
max-width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x)/ 2);
|
||||
padding-left: calc(var(--bs-gutter-x)/ 2);
|
||||
margin-top: var(--bs-gutter-y);
|
||||
padding-bottom: calc(var(--bs-gutter-y) / 2);
|
||||
margin-top: calc(var(--bs-gutter-y) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -652,6 +651,14 @@ virtual-scroller
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px)
|
||||
{
|
||||
.w-lg-auto
|
||||
{
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
accordion
|
||||
{
|
||||
.panel-heading
|
||||
@ -673,7 +680,7 @@ accordion
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 1.2rem;
|
||||
z-index: 1020;
|
||||
max-height: 50vh;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -720,14 +727,17 @@ accordion
|
||||
}
|
||||
}
|
||||
|
||||
.form-control
|
||||
.form-control, .form-select
|
||||
{
|
||||
border-color: #11182b;
|
||||
background: #11182b;
|
||||
box-shadow: 0 0 0 1px rgb(0 231 255 / 75%) inset;
|
||||
background-color: #11182b;
|
||||
padding: .5rem .75rem .375rem;
|
||||
}
|
||||
|
||||
.form-select
|
||||
{
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ff9c07' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.form-switch
|
||||
{
|
||||
display: inline-flex;
|
||||
@ -928,3 +938,14 @@ accordion
|
||||
{
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-group-item
|
||||
{
|
||||
border: none;
|
||||
padding: .25rem 1rem;
|
||||
}
|
||||
|
||||
.machine-brand b
|
||||
{
|
||||
text-transform: uppercase;
|
||||
}
|
Reference in New Issue
Block a user