mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-01-02 19:16:44 +00:00
Add resources pools management interface
This commit is contained in:
parent
b106f31b36
commit
6b5b784658
@ -69,6 +69,9 @@ import { GroupResolver } from "./resolvers/group.resolver";
|
|||||||
import { GroupRoleResolver } from "./resolvers/group-role.resolver";
|
import { GroupRoleResolver } from "./resolvers/group-role.resolver";
|
||||||
import { RoleDetailComponent } from "./components/role-management/role-detail/role-detail.component";
|
import { RoleDetailComponent } from "./components/role-management/role-detail/role-detail.component";
|
||||||
import { RoleDetailResolver } from "./resolvers/role-detail.resolver";
|
import { RoleDetailResolver } from "./resolvers/role-detail.resolver";
|
||||||
|
import {ResourcePoolsManagementComponent} from "@components/resource-pools-management/resource-pools-management.component";
|
||||||
|
import {ResourcePoolDetailsComponent} from "@components/resource-pool-details/resource-pool-details.component";
|
||||||
|
import {ResourcePoolsResolver} from "@resolvers/resource-pools.resolver";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -99,6 +102,15 @@ const routes: Routes = [
|
|||||||
groups: UserGroupsResolver,
|
groups: UserGroupsResolver,
|
||||||
controller: ControllerResolve},
|
controller: ControllerResolve},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'controller/:controller_id/management/resourcePools/:pool_id',
|
||||||
|
component: ResourcePoolDetailsComponent,
|
||||||
|
canActivate: [LoginGuard],
|
||||||
|
resolve: {
|
||||||
|
pool: ResourcePoolsResolver,
|
||||||
|
controller: ControllerResolve
|
||||||
|
}
|
||||||
|
},
|
||||||
{ path: 'installed-software', component: InstalledSoftwareComponent },
|
{ path: 'installed-software', component: InstalledSoftwareComponent },
|
||||||
{ path: 'controller/:controller_id/systemstatus', component: SystemStatusComponent, canActivate: [LoginGuard] },
|
{ path: 'controller/:controller_id/systemstatus', component: SystemStatusComponent, canActivate: [LoginGuard] },
|
||||||
|
|
||||||
@ -231,6 +243,10 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
component: RoleManagementComponent
|
component: RoleManagementComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "resourcePools",
|
||||||
|
component: ResourcePoolsManagementComponent
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -311,6 +311,12 @@ import { ConfirmationDeleteAllProjectsComponent } from './components/projects/co
|
|||||||
import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component';
|
import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component';
|
||||||
import { PrivilegeComponent } from './components/role-management/role-detail/privilege/privilege.component';
|
import { PrivilegeComponent } from './components/role-management/role-detail/privilege/privilege.component';
|
||||||
import { GroupPrivilegesPipe } from './components/role-management/role-detail/privilege/group-privileges.pipe';
|
import { GroupPrivilegesPipe } from './components/role-management/role-detail/privilege/group-privileges.pipe';
|
||||||
|
import { ResourcePoolsManagementComponent } from './components/resource-pools-management/resource-pools-management.component';
|
||||||
|
import { AddResourcePoolDialogComponent } from './components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component';
|
||||||
|
import { DeleteResourcePoolComponent } from './components/resource-pools-management/delete-resource-pool/delete-resource-pool.component';
|
||||||
|
import { ResourcePoolsFilterPipe } from './components/resource-pools-management/resource-pools-filter.pipe';
|
||||||
|
import { ResourcePoolDetailsComponent } from './components/resource-pool-details/resource-pool-details.component';
|
||||||
|
import { DeleteResourceConfirmationDialogComponent } from './components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -535,6 +541,12 @@ import { GroupPrivilegesPipe } from './components/role-management/role-detail/pr
|
|||||||
ProjectMapLockConfirmationDialogComponent,
|
ProjectMapLockConfirmationDialogComponent,
|
||||||
PrivilegeComponent,
|
PrivilegeComponent,
|
||||||
GroupPrivilegesPipe,
|
GroupPrivilegesPipe,
|
||||||
|
ResourcePoolsManagementComponent,
|
||||||
|
AddResourcePoolDialogComponent,
|
||||||
|
DeleteResourcePoolComponent,
|
||||||
|
ResourcePoolsFilterPipe,
|
||||||
|
ResourcePoolDetailsComponent,
|
||||||
|
DeleteResourceConfirmationDialogComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -23,7 +23,7 @@ import {ControllerService} from "@services/controller.service";
|
|||||||
export class ManagementComponent implements OnInit {
|
export class ManagementComponent implements OnInit {
|
||||||
|
|
||||||
controller: Controller;
|
controller: Controller;
|
||||||
links = ['users', 'groups', 'roles'];
|
links = ['users', 'groups', 'roles', 'resourcePools'];
|
||||||
activeLink: string = this.links[0];
|
activeLink: string = this.links[0];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<div class="title">delete resource {{data.resource_type}}/{{data.name}} ?</div>
|
||||||
|
<div class="button">
|
||||||
|
<button mat-raised-button color="warn" (click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="dialogRef.close(data)">Update</button>
|
||||||
|
</div>
|
@ -0,0 +1,15 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
margin: 30px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DeleteResourceConfirmationDialogComponent } from './delete-resource-confirmation-dialog.component';
|
||||||
|
|
||||||
|
describe('DeleteResourceConfirmationDialogComponent', () => {
|
||||||
|
let component: DeleteResourceConfirmationDialogComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteResourceConfirmationDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DeleteResourceConfirmationDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DeleteResourceConfirmationDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {DIALOG_DATA} from "@angular/cdk/dialog";
|
||||||
|
import {Resource} from "@models/resourcePools/Resource";
|
||||||
|
import {MatDialogRef} from "@angular/material/dialog";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-delete-resource-confirmation-dialog',
|
||||||
|
templateUrl: './delete-resource-confirmation-dialog.component.html',
|
||||||
|
styleUrls: ['./delete-resource-confirmation-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class DeleteResourceConfirmationDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(@Inject(DIALOG_DATA) public data: Resource,
|
||||||
|
public dialogRef: MatDialogRef<DeleteResourceConfirmationDialogComponent>,) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
<div class="content">
|
||||||
|
<div class="default-header">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<a
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="back to resource pools management"
|
||||||
|
mattooltipclass="custom-tooltip"
|
||||||
|
[routerLink]="['/controller', controller.id, 'management', 'resourcePools']">
|
||||||
|
<mat-icon aria-label="back to resource pools management">keyboard_arrow_left</mat-icon>
|
||||||
|
</a>
|
||||||
|
<h1 class="col">role {{pool.name}} details</h1>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="details">
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>pool name:</mat-label>
|
||||||
|
<input matInput type="text" [(ngModel)]="pool.name">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>creation date: {{pool.created_at}}</div>
|
||||||
|
<div>last update date: {{pool.updated_at}}</div>
|
||||||
|
<div>uuid: {{pool.resource_pool_id}}</div>
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary">
|
||||||
|
update role
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-divider [vertical]="true"></mat-divider>
|
||||||
|
<div class="resources">
|
||||||
|
<div class="addResource">
|
||||||
|
<div >
|
||||||
|
<input type="text"
|
||||||
|
placeholder="add project to resource pool"
|
||||||
|
matInput
|
||||||
|
[formControl]="addResourceFormControl"
|
||||||
|
[matAutocomplete]="auto">
|
||||||
|
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
|
||||||
|
<mat-option *ngFor="let option of addResourceFilteredOptions | async" [value]="option">
|
||||||
|
{{option}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button mat-button>
|
||||||
|
<mat-icon (click)="addResource()">add</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ownedResources" *ngFor="let resource of pool.resources">
|
||||||
|
<div>{{resource.name}}</div>
|
||||||
|
<div>
|
||||||
|
<button mat-button>
|
||||||
|
<mat-icon (click)="deleteResource(resource)">delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,67 @@
|
|||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
width: 30vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privilege {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-left: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
padding-left: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
.header > div {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resources {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ownedResources {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.addResource {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ResourcePoolDetailsComponent } from './resource-pool-details.component';
|
||||||
|
|
||||||
|
describe('ResourcePoolDetailsComponent', () => {
|
||||||
|
let component: ResourcePoolDetailsComponent;
|
||||||
|
let fixture: ComponentFixture<ResourcePoolDetailsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ResourcePoolDetailsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ResourcePoolDetailsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,122 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {FormControl, UntypedFormControl, UntypedFormGroup} from "@angular/forms";
|
||||||
|
import {ToasterService} from "@services/toaster.service";
|
||||||
|
import {ActivatedRoute} from "@angular/router";
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
import {ResourcePoolsService} from "@services/resource-pools.service";
|
||||||
|
import {ProjectService} from "@services/project.service";
|
||||||
|
import {filter, map, startWith, switchMap} from "rxjs/operators";
|
||||||
|
import {Project} from "@models/project";
|
||||||
|
import {BehaviorSubject, Observable, of} from "rxjs";
|
||||||
|
import {Resource} from "@models/resourcePools/Resource";
|
||||||
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
|
import {
|
||||||
|
DeleteResourceConfirmationDialogComponent
|
||||||
|
} from "@components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-resource-pool-details',
|
||||||
|
templateUrl: './resource-pool-details.component.html',
|
||||||
|
styleUrls: ['./resource-pool-details.component.scss']
|
||||||
|
})
|
||||||
|
export class ResourcePoolDetailsComponent implements OnInit {
|
||||||
|
|
||||||
|
controller: Controller;
|
||||||
|
editPoolForm: UntypedFormGroup;
|
||||||
|
pool: ResourcePool;
|
||||||
|
addResourceFormControl = new FormControl('');
|
||||||
|
addResourceFilteredOptions: Observable<string[]>;
|
||||||
|
projects: Project[] = [];
|
||||||
|
|
||||||
|
constructor(private toastService: ToasterService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private resourcePoolsService: ResourcePoolsService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
this.editPoolForm = new UntypedFormGroup({
|
||||||
|
poolname: new UntypedFormControl(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.data.subscribe((d: { controller: Controller; pool: ResourcePool }) => {
|
||||||
|
this.controller = d.controller;
|
||||||
|
this.pool = d.pool;
|
||||||
|
|
||||||
|
this.refresh();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate() {
|
||||||
|
this.resourcePoolsService.update(this.controller, this.pool)
|
||||||
|
.subscribe((pool: ResourcePool) => {
|
||||||
|
this.toastService.success(`pool ${pool.name}, updated`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addResource() {
|
||||||
|
const selected = this.addResourceFormControl.value;
|
||||||
|
const project = this.projects.filter( p => p.name.includes(selected));
|
||||||
|
if(project.length === 1) {
|
||||||
|
this.resourcePoolsService.addResource(this.controller,this.pool, project[0])
|
||||||
|
.subscribe(() => {
|
||||||
|
this.toastService.success(`project : ${project[0].name}, added to pool: ${this.pool.name}`);
|
||||||
|
this.refresh();
|
||||||
|
this.addResourceFormControl.setValue('');
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(project.length === 0) {
|
||||||
|
this.toastService.error(`cannot found related project with string: ${selected}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(project.length > 1) {
|
||||||
|
this.toastService.error(`${project.length} match ${selected}, please be more accurate`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteResource(resource: Resource) {
|
||||||
|
this.dialog.open(DeleteResourceConfirmationDialogComponent, {data: resource})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((resource: Resource) => {
|
||||||
|
if(resource) {
|
||||||
|
this.resourcePoolsService
|
||||||
|
.deleteResource(this.controller, resource, this.pool)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.refresh()
|
||||||
|
this.toastService.success(`resource ${resource.name} delete from pool ${this.pool.name}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private refresh() {
|
||||||
|
this.resourcePoolsService
|
||||||
|
.get(this.controller, this.pool.resource_pool_id)
|
||||||
|
.subscribe((pool) => {
|
||||||
|
this.pool = pool;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resourcePoolsService
|
||||||
|
.getFreeResources(this.controller)
|
||||||
|
.subscribe((projects: Project[]) => {
|
||||||
|
this.projects = projects;
|
||||||
|
|
||||||
|
this.addResourceFilteredOptions = this.addResourceFormControl.valueChanges.pipe(
|
||||||
|
startWith(''),
|
||||||
|
map((value: string) => {
|
||||||
|
return this.projects
|
||||||
|
.filter((project: Project) => project.name.toLowerCase().includes(value.toLowerCase() || ''))
|
||||||
|
.map((project: Project) => project.name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
import { UntypedFormControl } from '@angular/forms';
|
||||||
|
import { timer } from 'rxjs';
|
||||||
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { Controller } from "@models/controller";
|
||||||
|
import {ResourcePoolsService} from "@services/resource-pools.service";
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
|
||||||
|
export const poolNameAsyncValidator = (controller: Controller, resourcePoolsService: ResourcePoolsService) => {
|
||||||
|
return (control: UntypedFormControl) => {
|
||||||
|
return timer(500).pipe(
|
||||||
|
switchMap(() => resourcePoolsService.getAll(controller)),
|
||||||
|
map((response: ResourcePool[]) => {
|
||||||
|
return (response.find((n) => n.name === control.value) ? { projectExist: true } : null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PoolNameValidator {
|
||||||
|
get(poolName) {
|
||||||
|
const pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/);
|
||||||
|
|
||||||
|
if (!pattern.test(poolName.value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { invalidName: true };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<h1 mat-dialog-title>Create new pool</h1>
|
||||||
|
<form [formGroup]="poolNameForm" class="file-name-form">
|
||||||
|
<mat-form-field class="file-name-form-field">
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
(keydown)="onKeyDown($event)"
|
||||||
|
type="text"
|
||||||
|
formControlName="poolName"
|
||||||
|
[ngClass]="{ 'is-invalid': form.poolName?.errors }"
|
||||||
|
placeholder="Please enter a pool name"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="form.poolName?.touched && form.poolName?.errors && form.poolName?.errors.required"
|
||||||
|
>Pool name is required</mat-error
|
||||||
|
>
|
||||||
|
<mat-error *ngIf="form.poolName?.errors && form.poolName?.errors.invalidName"
|
||||||
|
>Pool name is incorrect</mat-error
|
||||||
|
>
|
||||||
|
<mat-error *ngIf="form.poolName?.errors && form.poolName?.errors.projectExist"
|
||||||
|
>Pool with this name exists</mat-error
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
|
||||||
|
<button mat-button (click)="onAddClick()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Add Pool
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
.file-name-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-snackbar {
|
||||||
|
background: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userList {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users {
|
||||||
|
display: flex;
|
||||||
|
height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-div {
|
||||||
|
float: right;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AddResourcePoolDialogComponent } from './add-resource-pool-dialog.component';
|
||||||
|
|
||||||
|
describe('AddResourcePoolDialogComponent', () => {
|
||||||
|
let component: AddResourcePoolDialogComponent;
|
||||||
|
let fixture: ComponentFixture<AddResourcePoolDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ AddResourcePoolDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AddResourcePoolDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,67 @@
|
|||||||
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators} from "@angular/forms";
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||||
|
import {PoolNameValidator} from "@components/resource-pools-management/add-resource-pool-dialog/PoolNameValidator";
|
||||||
|
import {ResourcePoolsService} from "@services/resource-pools.service";
|
||||||
|
import {ToasterService} from "@services/toaster.service";
|
||||||
|
import {poolNameAsyncValidator} from "@components/resource-pools-management/add-resource-pool-dialog/PoolNameAsyncValidator";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-add-resource-pool-dialog',
|
||||||
|
templateUrl: './add-resource-pool-dialog.component.html',
|
||||||
|
styleUrls: ['./add-resource-pool-dialog.component.scss'],
|
||||||
|
providers: [PoolNameValidator]
|
||||||
|
})
|
||||||
|
export class AddResourcePoolDialogComponent implements OnInit {
|
||||||
|
poolNameForm: UntypedFormGroup;
|
||||||
|
controller: Controller;
|
||||||
|
|
||||||
|
constructor(private dialogRef: MatDialogRef<AddResourcePoolDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { controller: Controller },
|
||||||
|
private formBuilder: UntypedFormBuilder,
|
||||||
|
private poolNameValidator: PoolNameValidator,
|
||||||
|
private resourcePoolsService: ResourcePoolsService,
|
||||||
|
private toasterService: ToasterService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.controller = this.data.controller;
|
||||||
|
this.poolNameForm = this.formBuilder.group({
|
||||||
|
poolName: new UntypedFormControl(
|
||||||
|
null,
|
||||||
|
[Validators.required, this.poolNameValidator.get],
|
||||||
|
[poolNameAsyncValidator(this.data.controller, this.resourcePoolsService)]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
this.onAddClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this.poolNameForm.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddClick() {
|
||||||
|
if (this.poolNameForm.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const poolName = this.poolNameForm.controls['poolName'].value;
|
||||||
|
|
||||||
|
this.resourcePoolsService.add(this.controller, poolName)
|
||||||
|
.subscribe((pool) => {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}, (error) => {
|
||||||
|
this.toasterService.error(`An error occur while trying to create new pool ${poolName}`);
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoClick() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<h1 mat-dialog-title>Are you sure to delete pools named: </h1>
|
||||||
|
<p *ngFor="let pool of data.pools">{{pool.name}}</p>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onCancel()" color="accent">No, cancel</button>
|
||||||
|
<button mat-button (click)="onDelete()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Yes, delete!
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,6 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DeleteResourcePoolComponent } from './delete-resource-pool.component';
|
||||||
|
|
||||||
|
describe('DeleteResourcePoolComponent', () => {
|
||||||
|
let component: DeleteResourcePoolComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteResourcePoolComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DeleteResourcePoolComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DeleteResourcePoolComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Software Name : GNS3 Web UI
|
||||||
|
* Version: 3
|
||||||
|
* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This software is distributed under the GPL-3.0 or any later version,
|
||||||
|
* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* or see the "LICENSE" file for more details.
|
||||||
|
*
|
||||||
|
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-delete-resource-pool',
|
||||||
|
templateUrl: './delete-resource-pool.component.html',
|
||||||
|
styleUrls: ['./delete-resource-pool.component.scss']
|
||||||
|
})
|
||||||
|
export class DeleteResourcePoolComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private dialogRef: MatDialogRef<DeleteResourcePoolComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { pools: ResourcePool[] }) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete() {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import { ResourcePoolsFilterPipe } from './resource-pools-filter.pipe';
|
||||||
|
|
||||||
|
describe('ResourcePoolsFilterPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new ResourcePoolsFilterPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'resourcePoolsFilter'
|
||||||
|
})
|
||||||
|
export class ResourcePoolsFilterPipe implements PipeTransform {
|
||||||
|
transform(resourcePool: MatTableDataSource<ResourcePool>, searchText: string): MatTableDataSource<ResourcePool> {
|
||||||
|
if (!searchText) {
|
||||||
|
return resourcePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchText = searchText.trim().toLowerCase();
|
||||||
|
resourcePool.filter = searchText;
|
||||||
|
return resourcePool;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<div class="content" *ngIf="isReady; else loading">
|
||||||
|
<div class="default-header">
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="col">Resource Pools management</h1>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="onDelete(selection.selected)"
|
||||||
|
class="add-ressourcePool-button" [disabled]="selection.selected.length == 0">
|
||||||
|
Delete selected pools
|
||||||
|
</button>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="addResourcePool()" class="add-ressourcePool-button">
|
||||||
|
Add resource pool
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<mat-form-field class="full-width">
|
||||||
|
<input matInput placeholder="Search by name" [(ngModel)]="searchText"
|
||||||
|
[ngModelOptions]="{ standalone: true }"/>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="default-content">
|
||||||
|
<table mat-table [dataSource]="dataSource | resourcePoolsFilter: searchText" class="mat-elevation-z8" matSort
|
||||||
|
#resourcePoolsSort="matSort">
|
||||||
|
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="small-col">
|
||||||
|
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row" class="small-col">
|
||||||
|
<mat-checkbox (click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? selection.toggle(row) : null"
|
||||||
|
[checked]="selection.isSelected(row)">
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name</th>
|
||||||
|
<td mat-cell *matCellDef="let element"><a class="table-link"
|
||||||
|
routerLink="/controller/{{controller.id}}/management/resourcePools/{{element.resource_pool_id}}">{{element.name}}</a>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="created_at">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Creation date</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.created_at}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-container matColumnDef="updated_at">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last update</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.updated_at}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="delete">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<button mat-button (click)="onDelete([element])">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</table>
|
||||||
|
<mat-paginator #resourcePoolsPaginator="matPaginator"
|
||||||
|
[pageSizeOptions]="[5, 10, 20]"
|
||||||
|
showFirstLastButtons
|
||||||
|
aria-label="Select page">
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #loading>
|
||||||
|
<div>
|
||||||
|
<mat-spinner class="loader"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -0,0 +1,26 @@
|
|||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 940px;
|
||||||
|
margin-left: -470px;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-ressourcePool-button {
|
||||||
|
height: 40px;
|
||||||
|
width: 160px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
margin: auto;
|
||||||
|
height: 175px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 175px;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ResourcePoolsManagementComponent } from './resource-pools-management.component';
|
||||||
|
|
||||||
|
describe('ResourcePoolsManagementComponent', () => {
|
||||||
|
let component: ResourcePoolsManagementComponent;
|
||||||
|
let fixture: ComponentFixture<ResourcePoolsManagementComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ResourcePoolsManagementComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ResourcePoolsManagementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,123 @@
|
|||||||
|
import {Component, OnInit, QueryList, ViewChildren} from '@angular/core';
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {MatPaginator} from "@angular/material/paginator";
|
||||||
|
import {MatSort} from "@angular/material/sort";
|
||||||
|
import {SelectionModel} from "@angular/cdk/collections";
|
||||||
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
|
import {ActivatedRoute} from "@angular/router";
|
||||||
|
import {ControllerService} from "@services/controller.service";
|
||||||
|
import {ToasterService} from "@services/toaster.service";
|
||||||
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
|
import {forkJoin} from "rxjs";
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
import {
|
||||||
|
AddResourcePoolDialogComponent
|
||||||
|
} from "@components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component";
|
||||||
|
import {DeleteResourcePoolComponent} from "@components/resource-pools-management/delete-resource-pool/delete-resource-pool.component";
|
||||||
|
import {ResourcePoolsService} from "@services/resource-pools.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-resource-pools-management',
|
||||||
|
templateUrl: './resource-pools-management.component.html',
|
||||||
|
styleUrls: ['./resource-pools-management.component.scss']
|
||||||
|
})
|
||||||
|
export class ResourcePoolsManagementComponent implements OnInit {
|
||||||
|
controller: Controller;
|
||||||
|
|
||||||
|
@ViewChildren('resourcePoolsPaginator') resourcePoolsPaginator: QueryList<MatPaginator>;
|
||||||
|
@ViewChildren('resourcePoolsSort') resourcePoolsSort: QueryList<MatSort>;
|
||||||
|
|
||||||
|
public displayedColumns = ['select', 'name', 'created_at', 'updated_at', 'delete'];
|
||||||
|
selection = new SelectionModel<ResourcePool>(true, []);
|
||||||
|
resourcePools: ResourcePool[];
|
||||||
|
dataSource = new MatTableDataSource<ResourcePool>();
|
||||||
|
searchText: string;
|
||||||
|
isReady = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private controllerService: ControllerService,
|
||||||
|
private toasterService: ToasterService,
|
||||||
|
public resourcePoolsService: ResourcePoolsService,
|
||||||
|
public dialog: MatDialog
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const controllerId = this.route.parent.snapshot.paramMap.get('controller_id');
|
||||||
|
this.controllerService.get(+controllerId).then((controller: Controller) => {
|
||||||
|
this.controller = controller;
|
||||||
|
this.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.resourcePoolsPaginator.changes.subscribe((comps: QueryList <MatPaginator>) =>
|
||||||
|
{
|
||||||
|
this.dataSource.paginator = comps.first;
|
||||||
|
});
|
||||||
|
this.resourcePoolsSort.changes.subscribe((comps: QueryList<MatSort>) => {
|
||||||
|
this.dataSource.sort = comps.first;
|
||||||
|
});
|
||||||
|
this.dataSource.sortingDataAccessor = (item, property) => {
|
||||||
|
switch (property) {
|
||||||
|
case 'name':
|
||||||
|
return item[property] ? item[property].toLowerCase() : '';
|
||||||
|
default:
|
||||||
|
return item[property];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSelected() {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
const numRows = this.resourcePools.length;
|
||||||
|
return numSelected === numRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
masterToggle() {
|
||||||
|
this.isAllSelected() ?
|
||||||
|
this.selection.clear() :
|
||||||
|
this.resourcePools.forEach(row => this.selection.select(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
addResourcePool() {
|
||||||
|
this.dialog
|
||||||
|
.open(AddResourcePoolDialogComponent, {width: '600px', height: '500px', data: {controller: this.controller}})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((added: boolean) => {
|
||||||
|
if (added) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.resourcePoolsService.getAll(this.controller).subscribe((resourcePools: ResourcePool[]) => {
|
||||||
|
this.isReady = true;
|
||||||
|
this.resourcePools = resourcePools;
|
||||||
|
this.dataSource.data = resourcePools;
|
||||||
|
this.selection.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete(resourcePoolToDelete: ResourcePool[]) {
|
||||||
|
this.dialog
|
||||||
|
.open(DeleteResourcePoolComponent, {width: '500px', height: '250px', data: {pools: resourcePoolToDelete}})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((isDeletedConfirm) => {
|
||||||
|
if (isDeletedConfirm) {
|
||||||
|
const observables = resourcePoolToDelete.map((resourcePool: ResourcePool) => this.resourcePoolsService.delete(this.controller, resourcePool.resource_pool_id));
|
||||||
|
forkJoin(observables)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.toasterService.error(`An error occur while trying to delete resource pool`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
7
src/app/models/resourcePools/Resource.ts
Normal file
7
src/app/models/resourcePools/Resource.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export class Resource {
|
||||||
|
resource_id: string;
|
||||||
|
resource_type: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
9
src/app/models/resourcePools/ResourcePool.ts
Normal file
9
src/app/models/resourcePools/ResourcePool.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {Resource} from "@models/resourcePools/Resource";
|
||||||
|
|
||||||
|
export class ResourcePool {
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
resource_pool_id: string;
|
||||||
|
resources?: Resource[];
|
||||||
|
}
|
16
src/app/resolvers/resource-pools.resolver.spec.ts
Normal file
16
src/app/resolvers/resource-pools.resolver.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ResourcePoolsResolver } from './resource-pools.resolver';
|
||||||
|
|
||||||
|
describe('ResourcePoolsResolver', () => {
|
||||||
|
let resolver: ResourcePoolsResolver;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
resolver = TestBed.inject(ResourcePoolsResolver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(resolver).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
36
src/app/resolvers/resource-pools.resolver.ts
Normal file
36
src/app/resolvers/resource-pools.resolver.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Router, Resolve,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
ActivatedRouteSnapshot
|
||||||
|
} from '@angular/router';
|
||||||
|
import {Observable, of, Subscriber} from 'rxjs';
|
||||||
|
import {ControllerService} from "@services/controller.service";
|
||||||
|
import {ResourcePoolsService} from "@services/resource-pools.service";
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ResourcePoolsResolver implements Resolve<ResourcePool> {
|
||||||
|
|
||||||
|
constructor(private controllerService: ControllerService,
|
||||||
|
private resourcePoolsService: ResourcePoolsService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ResourcePool> {
|
||||||
|
return new Observable<ResourcePool>((subscriber: Subscriber<ResourcePool>) => {
|
||||||
|
|
||||||
|
const controllerId = route.paramMap.get('controller_id');
|
||||||
|
const poolId = route.paramMap.get('pool_id');
|
||||||
|
|
||||||
|
this.controllerService.get(+controllerId).then((controller: Controller) => {
|
||||||
|
this.resourcePoolsService.get(controller, poolId).subscribe((resourcePool: ResourcePool) => {
|
||||||
|
subscriber.next(resourcePool);
|
||||||
|
subscriber.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
src/app/services/resource-pools.service.spec.ts
Normal file
16
src/app/services/resource-pools.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ResourcePoolsService } from './resource-pools.service';
|
||||||
|
|
||||||
|
describe('ResourcePoolsService', () => {
|
||||||
|
let service: ResourcePoolsService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ResourcePoolsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
88
src/app/services/resource-pools.service.ts
Normal file
88
src/app/services/resource-pools.service.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Controller} from "@models/controller";
|
||||||
|
import {Observable, of} from "rxjs";
|
||||||
|
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||||
|
import {HttpController} from "@services/http-controller.service";
|
||||||
|
import {Resource} from "@models/resourcePools/Resource";
|
||||||
|
import {filter, map, mergeAll, switchMap, tap} from "rxjs/operators";
|
||||||
|
import {Project} from "@models/project";
|
||||||
|
import {ProjectService} from "@services/project.service";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ResourcePoolsService {
|
||||||
|
|
||||||
|
constructor(private httpController: HttpController,
|
||||||
|
private projectService: ProjectService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(controller: Controller) {
|
||||||
|
return this.httpController.get<ResourcePool[]>(controller, '/pools');
|
||||||
|
}
|
||||||
|
|
||||||
|
get(controller: Controller, poolId: string): Observable<ResourcePool> {
|
||||||
|
return Observable.forkJoin([
|
||||||
|
this.httpController.get<ResourcePool>(controller, `/pools/${poolId}`),
|
||||||
|
this.httpController.get<Resource[]>(controller, `/pools/${poolId}/resources`),
|
||||||
|
]).pipe(map(results => {
|
||||||
|
results[0].resources = results[1];
|
||||||
|
return results[0];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(controller: Controller, uuid: string) {
|
||||||
|
return this.httpController.delete(controller, `/pools/${uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(controller: Controller, newPoolName: string) {
|
||||||
|
return this.httpController.post<{ name: string }>(controller, '/pools', {name: newPoolName});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(controller: Controller, pool: ResourcePool) {
|
||||||
|
return this.httpController.put(controller, `/pools/${pool.resource_pool_id}`, {name: pool.name});
|
||||||
|
}
|
||||||
|
|
||||||
|
addResource(controller: Controller, pool: ResourcePool, project: Project) {
|
||||||
|
return this.httpController.put<string>(controller, `/pools/${pool.resource_pool_id}/resources/${project.project_id}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteResource(controller: Controller, resource: Resource, pool: ResourcePool) {
|
||||||
|
return this.httpController.delete<string>(controller, `/pools/${pool.resource_pool_id}/resources/${resource.resource_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFreeResources(controller: Controller) {
|
||||||
|
return this.projectService
|
||||||
|
.list(controller)
|
||||||
|
.pipe(
|
||||||
|
switchMap((projects) => {
|
||||||
|
return this.getAllNonFreeResources(controller)
|
||||||
|
.pipe(map(resources => resources.map(resource => resource.resource_id)),
|
||||||
|
map(resources_id => projects.filter(project => !resources_id.includes(project.project_id)))
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllNonFreeResources(controller: Controller) {
|
||||||
|
return this.getAll(controller)
|
||||||
|
.pipe(switchMap((resourcesPools) => {
|
||||||
|
return Observable.forkJoin(
|
||||||
|
resourcesPools.map((r) => this.httpController.get<Resource[]>(controller, `/pools/${r.resource_pool_id}/resources`),)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
map((data) => {
|
||||||
|
|
||||||
|
//flatten results
|
||||||
|
|
||||||
|
const output: Resource[] = [];
|
||||||
|
for(const res of data) {
|
||||||
|
for(const r of res) {
|
||||||
|
output.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ export class ToasterService {
|
|||||||
constructor(private snackbar: MatSnackBar, private zone: NgZone) {}
|
constructor(private snackbar: MatSnackBar, private zone: NgZone) {}
|
||||||
|
|
||||||
public error(message: string) {
|
public error(message: string) {
|
||||||
|
console.error(message);
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
this.snackbar.open(message, 'Close', this.snackBarConfigForError);
|
this.snackbar.open(message, 'Close', this.snackBarConfigForError);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user