From 066ec2b96fbf3af3a5facaf5b91558a1b40fe2aa Mon Sep 17 00:00:00 2001 From: Dragos Date: Mon, 26 Apr 2021 15:34:17 +0300 Subject: [PATCH] fixed issue found during tests --- .../account-editor.component.html | 12 +- app/src/app/account/account.component.html | 97 ++-- app/src/app/account/account.component.scss | 39 +- app/src/app/account/account.component.ts | 19 +- app/src/app/app.component.html | 6 +- app/src/app/catalog/catalog.component.scss | 5 + app/src/app/catalog/catalog.module.ts | 47 +- .../app/catalog/helpers/catalog.service.ts | 9 - .../app/catalog/images/images.component.html | 220 ++++++++- .../app/catalog/images/images.component.scss | 32 ++ .../catalog/images/images.component.spec.ts | 10 +- .../app/catalog/images/images.component.ts | 213 ++++++++- .../nav-menu/nav-menu.component.html | 4 +- app/src/app/helpers/auth-guard.service.ts | 2 +- .../instance-history.component.html | 6 +- .../instance-info.component.html | 48 +- .../instance-info.component.scss | 12 + .../instance-info/instance-info.component.ts | 38 +- .../instance-networks.component.ts | 35 +- .../instance-snapshots.component.ts | 62 ++- .../instance-wizard.component.ts | 24 +- .../app/instances/instances.component.html | 261 ++++++----- .../app/instances/instances.component.scss | 177 ++++---- app/src/app/instances/instances.component.ts | 64 ++- app/src/app/instances/models/instance.ts | 8 +- .../firewall-rules.component.html | 4 +- .../firewall-rules.component.scss | 11 + .../firewall-rules.component.ts | 8 +- .../networks/networks.component.html | 38 +- .../networks/networks.component.scss | 12 +- .../networking/networks/networks.component.ts | 8 +- app/src/app/security/security.component.html | 429 +++++++++--------- app/src/app/security/security.component.scss | 6 + app/src/app/security/security.component.ts | 8 +- .../user-editor/user-editor.component.html | 4 +- app/src/app/shared.module.ts | 3 - app/src/app/volumes/volumes.component.html | 9 +- app/src/app/volumes/volumes.component.scss | 11 + app/src/app/volumes/volumes.component.ts | 7 +- app/src/app/volumes/volumes.module.ts | 2 +- app/src/assets/i18n/account/en.json | 5 + app/src/assets/i18n/catalog/en.json | 4 +- app/src/assets/i18n/dashboard/en.json | 15 +- app/src/assets/i18n/en.json | 18 +- app/src/assets/i18n/networking/en.json | 9 +- app/src/assets/i18n/security/en.json | 1 + app/src/assets/i18n/volumes/en.json | 1 + app/src/index.html | 3 +- app/src/styles/styles.scss | 41 +- 49 files changed, 1384 insertions(+), 723 deletions(-) diff --git a/app/src/app/account/account-editor/account-editor.component.html b/app/src/app/account/account-editor/account-editor.component.html index 6eec434..aa25995 100644 --- a/app/src/app/account/account-editor/account-editor.component.html +++ b/app/src/app/account/account-editor/account-editor.component.html @@ -27,7 +27,7 @@ -
+
diff --git a/app/src/app/account/account.component.html b/app/src/app/account/account.component.html index 5419808..83ab178 100644 --- a/app/src/app/account/account.component.html +++ b/app/src/app/account/account.component.html @@ -1,55 +1,60 @@ -
-
-
-
-
-

My profile

+
+
+
- +
+
+
+ + {{ 'account.myProfile' | translate }} + + + + +
    +
  • + Name: {{ userInfo.firstName }} {{ userInfo.lastName }} +
  • +
  • + Username: {{ userInfo.login }} +
  • +
  • + Email: {{ userInfo.email }} +
  • +
  • + Phone: {{ userInfo.phone }} +
  • +
  • + Container Name Service: + + {{ userInfo.triton_cns_enabled ? 'enabled' : 'disabled' }} + +
  • +
+
-
-
    -
  • - Name: {{ userInfo.firstName }} {{ userInfo.lastName }} -
  • -
  • - Username: {{ userInfo.login }} -
  • -
  • - Email: {{ userInfo.email }} -
  • -
  • - Phone: {{ userInfo.phone }} -
  • -
  • - Container Name Service: - - {{ userInfo.triton_cns_enabled ? 'enabled' : 'disabled' }} - -
  • -
-
-
-
+
+
+ + {{ 'account.myKeys' | translate }} -
-
-
-

My SSH keys

+ + - +
    +
  1. + {{ userKey.name }}: {{ userKey.fingerprint }} +
  2. +
+
- -
-
    -
  1. - {{ userKey.name }}: {{ userKey.fingerprint }} -
  2. -
-
-
+
\ No newline at end of file diff --git a/app/src/app/account/account.component.scss b/app/src/app/account/account.component.scss index f18e2bf..a8971c9 100644 --- a/app/src/app/account/account.component.scss +++ b/app/src/app/account/account.component.scss @@ -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; } } + diff --git a/app/src/app/account/account.component.ts b/app/src/app/account/account.component.ts index 15d996b..9797498 100644 --- a/app/src/app/account/account.component.ts +++ b/app/src/app/account/account.component.ts @@ -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) - // }); } // ---------------------------------------------------------------------------------------------------------------- diff --git a/app/src/app/app.component.html b/app/src/app/app.component.html index fd3199b..b839cc5 100644 --- a/app/src/app/app.component.html +++ b/app/src/app/app.component.html @@ -46,10 +46,8 @@ -
-
- -
+
+
diff --git a/app/src/app/catalog/catalog.component.scss b/app/src/app/catalog/catalog.component.scss index e69de29..3ba4971 100644 --- a/app/src/app/catalog/catalog.component.scss +++ b/app/src/app/catalog/catalog.component.scss @@ -0,0 +1,5 @@ +:host +{ + flex-grow: 1; + overflow: hidden; +} \ No newline at end of file diff --git a/app/src/app/catalog/catalog.module.ts b/app/src/app/catalog/catalog.module.ts index e09f0b2..09dbd1f 100644 --- a/app/src/app/catalog/catalog.module.ts +++ b/app/src/app/catalog/catalog.module.ts @@ -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 ] }) diff --git a/app/src/app/catalog/helpers/catalog.service.ts b/app/src/app/catalog/helpers/catalog.service.ts index 69dddac..770ddbe 100644 --- a/app/src/app/catalog/helpers/catalog.service.ts +++ b/app/src/app/catalog/helpers/catalog.service.ts @@ -59,15 +59,6 @@ export class CatalogService return this.httpClient.get(`/api/my/images?${allStates ? 'state=all' : ''}`); } - // ---------------------------------------------------------------------------------------------------------------- - @Cacheable({ - cacheBusterObserver: imagesCacheBuster$ - }) - getCustomImages(ownerId: string): Observable - { - return this.httpClient.get(`/api/my/images?$state=all&owner=${ownerId}`); - } - // ---------------------------------------------------------------------------------------------------------------- @Cacheable({ cacheBusterObserver: imagesCacheBuster$ diff --git a/app/src/app/catalog/images/images.component.html b/app/src/app/catalog/images/images.component.html index 68a8319..b3671b2 100644 --- a/app/src/app/catalog/images/images.component.html +++ b/app/src/app/catalog/images/images.component.html @@ -1,20 +1,200 @@ - - - - - - - - - - - - - - - - - -
NameDescriptionOS
{{ image.name }}{{ image.description }}{{ image.os }} - -
+
+
+
+ + + +
+ + +
+ +
+ + +
+
+
+ +
+ Loading... +
+
+ +
+
+ + +
+

My images

+ + +
+ +
+

+ You don't have any custom images yet +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionOSTypeBrandPublish dateStatus
+ {{ image.name }} + +
{{ image.description }}
+
+ {{ image.os }} + + {{ image.type }} + + {{ image.requirements.brand }} + + {{ image.published_at ? (image.published_at | timeago) : '' }} + + {{ image.state }} + +
+ + +
+
+ No images match your search criteria +
+
+
+ + +
+
+
+
diff --git a/app/src/app/catalog/images/images.component.scss b/app/src/app/catalog/images/images.component.scss index e69de29..459498c 100644 --- a/app/src/app/catalog/images/images.component.scss +++ b/app/src/app/catalog/images/images.component.scss @@ -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; + } +} diff --git a/app/src/app/catalog/images/images.component.spec.ts b/app/src/app/catalog/images/images.component.spec.ts index d98752c..afec9cd 100644 --- a/app/src/app/catalog/images/images.component.spec.ts +++ b/app/src/app/catalog/images/images.component.spec.ts @@ -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; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(async(() => { + TestBed.configureTestingModule({ declarations: [ ImagesComponent ] }) .compileComponents(); - }); + })); beforeEach(() => { fixture = TestBed.createComponent(ImagesComponent); diff --git a/app/src/app/catalog/images/images.component.ts b/app/src/app/catalog/images/images.component.ts index a1ab987..903124e 100644 --- a/app/src/app/catalog/images/images.component.ts +++ b/app/src/app/catalog/images/images.component.ts @@ -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(); + } + } diff --git a/app/src/app/components/nav-menu/nav-menu.component.html b/app/src/app/components/nav-menu/nav-menu.component.html index 869a13e..a654baf 100644 --- a/app/src/app/components/nav-menu/nav-menu.component.html +++ b/app/src/app/components/nav-menu/nav-menu.component.html @@ -40,9 +40,9 @@ --> + {{ keyValue.key }} +
-
- -
- +
+ +
+ + +
  • { 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() { diff --git a/app/src/app/instances/instance-networks/instance-networks.component.ts b/app/src/app/instances/instance-networks/instance-networks.component.ts index c0bb2dc..c87b2cb 100644 --- a/app/src/app/instances/instance-networks/instance-networks.component.ts +++ b/app/src/app/instances/instance-networks/instance-networks.component.ts @@ -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(); }); } diff --git a/app/src/app/instances/instance-snapshots/instance-snapshots.component.ts b/app/src/app/instances/instance-snapshots/instance-snapshots.component.ts index 33d8e24..c7e96f1 100644 --- a/app/src/app/instances/instance-snapshots/instance-snapshots.component.ts +++ b/app/src/app/instances/instance-snapshots/instance-snapshots.component.ts @@ -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); } // ---------------------------------------------------------------------------------------------------------------- diff --git a/app/src/app/instances/instance-wizard/instance-wizard.component.ts b/app/src/app/instances/instance-wizard/instance-wizard.component.ts index a3f938b..755cfb8 100644 --- a/app/src/app/instances/instance-wizard/instance-wizard.component.ts +++ b/app/src/app/instances/instance-wizard/instance-wizard.component.ts @@ -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; diff --git a/app/src/app/instances/instances.component.html b/app/src/app/instances/instances.component.html index 0a55524..3f032f4 100644 --- a/app/src/app/instances/instances.component.html +++ b/app/src/app/instances/instances.component.html @@ -1,28 +1,29 @@
    -
    +
    - + -
    - -
    -
    -
    -
    +
    -
    -
    +
    +

    - No machine matches your filters + {{ 'dashboard.list.noResults' | translate }}

    + [parentScroll]="scroller.window.document.getElementById('scrollingBlock')" [scrollThrottlingTime]="250">
    + [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)">
    -
    +
    {{ instance.name }}
    - @@ -168,50 +183,69 @@
    - + Info
    - +
    - + Network
    + (load)="setInstanceNetworks(instance, $event)" (processing)="instance.working = true" + (finishedProcessing)="instance.working = false" + (instanceReboot)="watchInstanceState(instance)" + (instanceStateUpdate)="updateInstance(instance, $event)">
    - - + + + + + Migrations + +
    + +
    +
    + Volumes
      -
    • +
    • @@ -224,28 +258,6 @@
    - - - - Snapshots - -
    - - -
    -
    - - - - Migrations - -
    - -
    -
    @@ -260,29 +272,70 @@
    - - + +
    + + +
    - + - + - +
    + +
    +
    + + +
    +
    @@ -291,32 +344,32 @@
  • @@ -324,7 +377,7 @@
  • @@ -332,8 +385,8 @@
  • - + \ No newline at end of file diff --git a/app/src/app/instances/instances.component.scss b/app/src/app/instances/instances.component.scss index 300efb6..ecaa84e 100644 --- a/app/src/app/instances/instances.component.scss +++ b/app/src/app/instances/instances.component.scss @@ -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; +} diff --git a/app/src/app/instances/instances.component.ts b/app/src/app/instances/instances.component.ts index f8e2304..1a1346a 100644 --- a/app/src/app/instances/instances.component.ts +++ b/app/src/app/instances/instances.component.ts @@ -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 `Between ${formattedValue}`; + return `Between ${formattedValue}`; case LabelType.High: - return `and ${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) { diff --git a/app/src/app/instances/models/instance.ts b/app/src/app/instances/models/instance.ts index d373ea9..0a6cb26 100644 --- a/app/src/app/instances/models/instance.ts +++ b/app/src/app/instances/models/instance.ts @@ -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[]; diff --git a/app/src/app/networking/firewall-rules/firewall-rules.component.html b/app/src/app/networking/firewall-rules/firewall-rules.component.html index 52394a6..cfe0e3e 100644 --- a/app/src/app/networking/firewall-rules/firewall-rules.component.html +++ b/app/src/app/networking/firewall-rules/firewall-rules.component.html @@ -51,8 +51,8 @@
    -
    -
    +
    +

    There are no firewall rules yet. diff --git a/app/src/app/networking/firewall-rules/firewall-rules.component.scss b/app/src/app/networking/firewall-rules/firewall-rules.component.scss index 6fb03b3..76ed63c 100644 --- a/app/src/app/networking/firewall-rules/firewall-rules.component.scss +++ b/app/src/app/networking/firewall-rules/firewall-rules.component.scss @@ -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; diff --git a/app/src/app/networking/firewall-rules/firewall-rules.component.ts b/app/src/app/networking/firewall-rules/firewall-rules.component.ts index b83f4ea..302b442 100644 --- a/app/src/app/networking/firewall-rules/firewall-rules.component.ts +++ b/app/src/app/networking/firewall-rules/firewall-rules.component.ts @@ -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, diff --git a/app/src/app/networking/networks/networks.component.html b/app/src/app/networking/networks/networks.component.html index 25ffbb6..959d3d9 100644 --- a/app/src/app/networking/networks/networks.component.html +++ b/app/src/app/networking/networks/networks.component.html @@ -43,12 +43,12 @@

    -
    -
    +
    +
    + tooltip="Show or hide this VLAN's networks" placement="top" container="body">

    {{ vlan.name }} @@ -133,17 +133,29 @@

    diff --git a/app/src/app/networking/networks/networks.component.scss b/app/src/app/networking/networks/networks.component.scss index d2b650e..b7b2d2d 100644 --- a/app/src/app/networking/networks/networks.component.scss +++ b/app/src/app/networking/networks/networks.component.scss @@ -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; diff --git a/app/src/app/networking/networks/networks.component.ts b/app/src/app/networking/networks/networks.component.ts index bfe303c..a432fe8 100644 --- a/app/src/app/networking/networks/networks.component.ts +++ b/app/src/app/networking/networks/networks.component.ts @@ -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 diff --git a/app/src/app/security/security.component.html b/app/src/app/security/security.component.html index 1c1dd7e..2a528e8 100644 --- a/app/src/app/security/security.component.html +++ b/app/src/app/security/security.component.html @@ -1,237 +1,248 @@ -
    -
    -
    -
    -
    - - - - {{ 'security.policies' | translate }} - +
    +
    +
    +
    +
    +
    + + + + {{ 'security.policies' | translate }} + - - + + -

    Policies are just access rules grouped together

    +

    Policies are just access rules grouped together

    -
      -
    • -
      - - - {{ policy.name }} - +
        +
      • +
        + + + {{ policy.name }} + -
        - - - -
        -
        - -
        - - {{ policy.name }} -
        - -
        {{ 'security.dropHere' | translate }}
        -
      • -
      -
    -
    -
    -
    - - - - {{ 'security.roles' | translate }} - - - - - -

    To assign policies drag them over a role

    - -
      -
    • -
      -
      - - -
      - - {{ role.name }} ({{ role.policies.length }}) + +
      -
      - - -
      +
      + +
      + + {{ role.name }} +
      + +
      {{ 'security.dropHere' | translate }}
      + +
      +
        +
      • + + {{ policy.name }} +
        + +
      -
    +
  • + + +
    +
    +
    + + + + {{ 'security.users' | translate }} + -
    - - {{ role.name }} -
    + +
    -
    {{ 'security.dropHere' | translate }}
    +

    To assign roles drag them over a user

    -
    -
      -
    • - - {{ policy.name }} -
      - -
      -
    • -
    -
    - - -
    -
    -
    -
    - - - - {{ 'security.users' | translate }} - +
      +
    • +
      +
      + - - + {{ user.login }} ({{ user.roles.length }}) +
      -

      To assign roles drag them over a user

      - -
        -
      • -
        -
        - - - {{ user.login }} ({{ user.roles.length }}) +
        + + +
        -
        - -
        +
      • +
      -
      - - {{ user.login }} -
      - -
      {{ 'security.dropHere' | translate }}
      - -
      -
        -
      • - - {{ role.name }} -
        - -
        -
      • -
      -
      -
    • -
    - -
    + +
    - + \ No newline at end of file diff --git a/app/src/app/security/security.component.scss b/app/src/app/security/security.component.scss index 9d056db..f8e8b31 100644 --- a/app/src/app/security/security.component.scss +++ b/app/src/app/security/security.component.scss @@ -1,3 +1,9 @@ +:host +{ + overflow: hidden; + flex-grow: 1; +} + fieldset { background-color: rgba(16,21,39, .5); diff --git a/app/src/app/security/security.component.ts b/app/src/app/security/security.component.ts index e915aac..f2ede5b 100644 --- a/app/src/app/security/security.component.ts +++ b/app/src/app/security/security.component.ts @@ -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(), diff --git a/app/src/app/security/user-editor/user-editor.component.html b/app/src/app/security/user-editor/user-editor.component.html index 666c679..51d6040 100644 --- a/app/src/app/security/user-editor/user-editor.component.html +++ b/app/src/app/security/user-editor/user-editor.component.html @@ -65,7 +65,7 @@ -
    +
    diff --git a/app/src/app/shared.module.ts b/app/src/app/shared.module.ts index 54fd056..50a550e 100644 --- a/app/src/app/shared.module.ts +++ b/app/src/app/shared.module.ts @@ -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, diff --git a/app/src/app/volumes/volumes.component.html b/app/src/app/volumes/volumes.component.html index 28e3744..eabb41b 100644 --- a/app/src/app/volumes/volumes.component.html +++ b/app/src/app/volumes/volumes.component.html @@ -43,8 +43,8 @@
    -
    -
    +
    +

    There are no volumes yet. @@ -62,7 +62,7 @@ - {{ volume.name }} + {{ volume.name }} {{ volume.size * 1024 * 1024 | fileSize}} @@ -93,6 +93,9 @@

    + + diff --git a/app/src/app/volumes/volumes.component.scss b/app/src/app/volumes/volumes.component.scss index 1340341..a8643f8 100644 --- a/app/src/app/volumes/volumes.component.scss +++ b/app/src/app/volumes/volumes.component.scss @@ -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; + } } diff --git a/app/src/app/volumes/volumes.component.ts b/app/src/app/volumes/volumes.component.ts index 925f9ee..a61df52 100644 --- a/app/src/app/volumes/volumes.component.ts +++ b/app/src/app/volumes/volumes.component.ts @@ -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 = { diff --git a/app/src/app/volumes/volumes.module.ts b/app/src/app/volumes/volumes.module.ts index f2ce581..a80a4fb 100644 --- a/app/src/app/volumes/volumes.module.ts +++ b/app/src/app/volumes/volumes.module.ts @@ -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, diff --git a/app/src/assets/i18n/account/en.json b/app/src/assets/i18n/account/en.json index c7d8899..a4fd338 100644 --- a/app/src/assets/i18n/account/en.json +++ b/app/src/assets/i18n/account/en.json @@ -1,5 +1,10 @@ { "account": { + "title": "Account", + "myProfile": "My profile", + "updateProfile": "Update profile", + "myKeys": "My keys", + "addKey": "Add key" } } diff --git a/app/src/assets/i18n/catalog/en.json b/app/src/assets/i18n/catalog/en.json index f370121..9f899b2 100644 --- a/app/src/assets/i18n/catalog/en.json +++ b/app/src/assets/i18n/catalog/en.json @@ -7,9 +7,9 @@ "memory": "Memory", "disk": "Storage" }, - "custom": + "images": { - + "title": "Images" } } } diff --git a/app/src/assets/i18n/dashboard/en.json b/app/src/assets/i18n/dashboard/en.json index 04f819f..f361a44 100644 --- a/app/src/assets/i18n/dashboard/en.json +++ b/app/src/assets/i18n/dashboard/en.json @@ -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": "{brand} - Infrastructure container", + "virtualMachine": "{brand} - Virtual machine", "stateRunning": "Running", "stateStopping": "Stopping", "stateProvisioning": "Provisioning", diff --git a/app/src/assets/i18n/en.json b/app/src/assets/i18n/en.json index 2ae47e9..c59f397 100644 --- a/app/src/assets/i18n/en.json +++ b/app/src/assets/i18n/en.json @@ -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": "" } }, diff --git a/app/src/assets/i18n/networking/en.json b/app/src/assets/i18n/networking/en.json index 190a2f6..c924baf 100644 --- a/app/src/assets/i18n/networking/en.json +++ b/app/src/assets/i18n/networking/en.json @@ -1,6 +1,13 @@ { "networking": { - + "networks": + { + "title": "Networks" + }, + "firewall": + { + "title": "Firewall rules" + } } } diff --git a/app/src/assets/i18n/security/en.json b/app/src/assets/i18n/security/en.json index 240e93c..c1476fa 100644 --- a/app/src/assets/i18n/security/en.json +++ b/app/src/assets/i18n/security/en.json @@ -1,6 +1,7 @@ { "security": { + "title": "Security", "users": "Users", "roles": "Roles", "policies": "Policies", diff --git a/app/src/assets/i18n/volumes/en.json b/app/src/assets/i18n/volumes/en.json index fa9cd8b..3a46166 100644 --- a/app/src/assets/i18n/volumes/en.json +++ b/app/src/assets/i18n/volumes/en.json @@ -1,5 +1,6 @@ { "volumes": { + "title": "Volumes" } } diff --git a/app/src/index.html b/app/src/index.html index da6e24b..fb163b6 100644 --- a/app/src/index.html +++ b/app/src/index.html @@ -2,7 +2,7 @@ - Manta + Joyent @@ -11,7 +11,6 @@ - diff --git a/app/src/styles/styles.scss b/app/src/styles/styles.scss index 86acb9c..5c64b02 100644 --- a/app/src/styles/styles.scss +++ b/app/src/styles/styles.scss @@ -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; +} \ No newline at end of file