Skip to content

Commit

Permalink
Feature/942 add new resource (#802)
Browse files Browse the repository at this point in the history
  • Loading branch information
BeriBoss authored Dec 5, 2024
1 parent 2390527 commit 67cf1c5
Show file tree
Hide file tree
Showing 20 changed files with 290 additions and 44 deletions.
1 change: 1 addition & 0 deletions AMW_angular/io/src/app/resource/resource-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface ResourceType {
name: string;
hasChildren: boolean;
children: ResourceType[];
isApplication: boolean;
}
13 changes: 11 additions & 2 deletions AMW_angular/io/src/app/resource/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ interface Named {
export class ResourceService extends BaseService {
private resourceType$: Subject<ResourceType> = new Subject<ResourceType>();

private resourceGroupListForType: Observable<Resource[]> = this.resourceType$.pipe(
private resourceGroupListForType$: Observable<Resource[]> = this.resourceType$.pipe(
switchMap((resourceType: ResourceType) => this.getGroupsForType(resourceType)),
startWith(null),
shareReplay(1),
);

resourceGroupListForTypeSignal = toSignal(this.resourceGroupListForType, { initialValue: [] as Resource[] });
resourceGroupListForType = toSignal(this.resourceGroupListForType$, { initialValue: [] as Resource[] });

constructor(private http: HttpClient) {
super();
Expand All @@ -46,6 +46,14 @@ export class ResourceService extends BaseService {
);
}

createResourceForResourceType(resource: any) {
return this.http
.post<Resource>(`${this.getBaseUrl()}/resources`, resource, {
headers: this.getHeaders(),
})
.pipe(catchError(this.handleError));
}

getResourceName(resourceId: number): Observable<Named> {
return this.http
.get<Named>(`${this.getBaseUrl()}/resources/name/${resourceId}`, {
Expand All @@ -64,6 +72,7 @@ export class ResourceService extends BaseService {
catchError(this.handleError),
);
}

get(resourceGroupName: string): Observable<Resource> {
return this.http
.get(`${this.getBaseUrl()}/resources/${resourceGroupName}`, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="container">
<app-modal-header [title]="getTitle()" (cancel)="cancel()"></app-modal-header>
<div class="modal-body">
<div class="needs-validation mb-3">
<label for="resourceName" class="form-label">Resource name</label>
<input type="text" class="form-control" id="resourceName"
[(ngModel)]="resourceName" required
pattern="^[a-zA-Z0-9_\-]+$"/>
<div class="invalid-feedback">
The name must not contain any other than alphanumerical and "_" / "-" characters.
</div>
</div>
<div class="mb-3">
<label class="form-label">Release</label>
<ng-select id="selectRelease"
[clearable]="false"
[(ngModel)]="selectedReleaseName"
(change)="setSelectedRelease($event)">
@for (release of releases; track release.id) {
<ng-option [value]="release.name">{{ release.name }}</ng-option>
}
</ng-select>
</div>
</div>
<div class="modal-footer">
<app-button [variant]="'light'" (click)="cancel()">Cancel</app-button>
<app-button [variant]="'primary'"
[disabled]="!isValidForm()"
(click)="save()">Save
</app-button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ResourceAddComponent } from './resource-add.component';

describe('ResourceAddComponent', () => {
let component: ResourceAddComponent;
let fixture: ComponentFixture<ResourceAddComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ResourceAddComponent],
providers: [NgbActiveModal],
}).compileComponents();

fixture = TestBed.createComponent(ResourceAddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output } from '@angular/core';
import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { FormsModule } from '@angular/forms';
import { ModalHeaderComponent } from '../../shared/modal-header/modal-header.component';
import { ButtonComponent } from '../../shared/button/button.component';
import { ResourceType } from '../../resource/resource-type';
import { Release } from '../../settings/releases/release';
import { NgSelectModule } from '@ng-select/ng-select';

@Component({
selector: 'app-resource-add',
standalone: true,
imports: [FormsModule, ModalHeaderComponent, ButtonComponent, NgbTypeahead, NgSelectModule],
templateUrl: './resource-add.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResourceAddComponent {
activeModal = inject(NgbActiveModal);
@Input() resourceType: ResourceType;
@Input() releases: Release[];
@Input() selectedReleaseName: Release;
resourceName: string;
@Output() saveResource: EventEmitter<any> = new EventEmitter<any>();

getTitle() {
if (!this.resourceType) return;
return this.resourceType.name ? `Create new instance for ${this.resourceType.name}` : `Create new instance`;
}

save() {
let forms: NodeListOf<Element> = document.querySelectorAll('.needs-validation');
if (this.isValidForm()) {
const resourceToAdd = {
name: this.resourceName,
type: this.resourceType.name,
releaseName: this.selectedReleaseName,
};
this.saveResource.emit(resourceToAdd);
this.activeModal.close();
} else {
forms[0].classList.add('was-validated');
}
}

cancel() {
this.activeModal.close();
}

setSelectedRelease($event: any) {
this.selectedReleaseName = $event;
}

isValidForm() {
const REGEXP_ALPHANUMERIC_WITH_UNDERSCORE_HYPHEN = /^[a-zA-Z0-9_-]+$/;
return this.resourceName ? REGEXP_ALPHANUMERIC_WITH_UNDERSCORE_HYPHEN.test(this.resourceName) : false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
<div class="container">
@if (resourceType()){
<h1>{{resourceType().name}}</h1>
@if (resourceType() && permissions().canReadResources) {
<h1>{{ resourceType().name }}</h1>
@if (permissions().canCreateResource && !resourceType().isApplication) {
<div class="row my-3">
<app-button [variant]="'primary'" (click)="addResource()">
<app-icon icon="plus-circle"></app-icon>
New Resource
</app-button
>
</div>
}
<table class="table table-sm table-striped">
<thead>
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ResourcesListComponent } from './resources-list.component';
import { ComponentRef } from '@angular/core';
import { Resource } from '../../resource/resource';
import { ResourceType } from '../../resource/resource-type';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';

describe('ResourcesListComponent', () => {
let component: ResourcesListComponent;
Expand All @@ -13,14 +15,15 @@ describe('ResourcesListComponent', () => {
name: 'type',
hasChildren: false,
children: [],
isApplication: false,
};

const resourceGroupsOfResourceType: Resource[] = [];

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ResourcesListComponent],
providers: [],
providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()],
}).compileComponents();

fixture = TestBed.createComponent(ResourcesListComponent);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Component, input } from '@angular/core';
import { Component, computed, inject, input, output } from '@angular/core';
import { Resource } from '../../resource/resource';
import { ResourceType } from '../../resource/resource-type';
import { ButtonComponent } from '../../shared/button/button.component';
import { IconComponent } from '../../shared/icon/icon.component';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ResourceAddComponent } from '../resource-add/resource-add.component';
import { Release } from '../../settings/releases/release';
import { AuthService } from '../../auth/auth.service';

@Component({
selector: 'app-resources-list',
Expand All @@ -11,6 +17,35 @@ import { IconComponent } from '../../shared/icon/icon.component';
imports: [ButtonComponent, IconComponent],
})
export class ResourcesListComponent {
private modalService = inject(NgbModal);
private authService = inject(AuthService);
private destroy$ = new Subject<void>();
resourceType = input.required<ResourceType>();
resourceGroupList = input<Resource[]>();
resourceGroupList = input.required<Resource[]>();
releases = input.required<Release[]>();
resourceToAdd = output<any>();

permissions = computed(() => {
if (this.authService.restrictions().length > 0) {
return {
canReadResources: this.authService.hasPermission('RESOURCE', 'READ'),
canCreateResource: this.authService.hasPermission('RESOURCE', 'CREATE'),
};
} else {
return {
canReadResources: false,
canCreateResource: false,
};
}
});

addResource() {
const modalRef: NgbModalRef = this.modalService.open(ResourceAddComponent);
modalRef.componentInstance.resourceType = this.resourceType();
modalRef.componentInstance.releases = this.releases();
modalRef.componentInstance.selectedReleaseName = this.releases()[0].name;
modalRef.componentInstance.saveResource
.pipe(takeUntil(this.destroy$))
.subscribe((resource: any) => this.resourceToAdd.emit(resource));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
}
</div>
<div class="col-10">
<app-resources-list [resourceType]="this.selectedResourceTypeOrDefault()" [resourceGroupList]="resourceGroupListForTypeSignal()"></app-resources-list>
<app-resources-list [resourceType]="selectedResourceTypeOrDefault()"
[resourceGroupList]="resourceGroupListForType()"
[releases]="releases()"
(resourceToAdd)="addResource($event)"></app-resources-list>
</div>
</div>
</div>
Expand Down
23 changes: 22 additions & 1 deletion AMW_angular/io/src/app/resources/resources-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { ResourceType } from '../resource/resource-type';
import { ResourcesListComponent } from './resources-list/resources-list.component';
import { ResourceService } from '../resource/resource.service';
import { Resource } from '../resource/resource';
import { ReleasesService } from '../settings/releases/releases.service';
import { Release } from '../settings/releases/release';
import { ToastService } from '../shared/elements/toast/toast.service';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
selector: 'app-resources-page',
Expand All @@ -19,10 +24,15 @@ export class ResourcesPageComponent {
private authService = inject(AuthService);
private resourceTypesService = inject(ResourceTypesService);
private resourceService = inject(ResourceService);
private releaseService = inject(ReleasesService);
private toastService = inject(ToastService);
private error$ = new BehaviorSubject<string>('');
private destroy$ = new Subject<void>();

predefinedResourceTypes: Signal<ResourceType[]> = this.resourceTypesService.predefinedResourceTypes;
rootResourceTypes: Signal<ResourceType[]> = this.resourceTypesService.rootResourceTypes;
resourceGroupListForTypeSignal: Signal<Resource[]> = this.resourceService.resourceGroupListForTypeSignal;
resourceGroupListForType: Signal<Resource[]> = this.resourceService.resourceGroupListForType;
releases: Signal<Release[]> = this.releaseService.allReleases;
isLoading = signal(false);
expandedResourceTypeId: number | null = null;
selectedResourceType: WritableSignal<ResourceType | null> = signal(null);
Expand Down Expand Up @@ -51,4 +61,15 @@ export class ResourcesPageComponent {
this.expandedResourceTypeId = this.expandedResourceTypeId === resourceType.id ? null : resourceType.id;
this.selectedResourceType.set(resourceType);
}

addResource(resource: any) {
this.resourceService
.createResourceForResourceType(resource)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: () => this.toastService.success('Resource saved successfully.'),
error: (e) => this.error$.next(e),
complete: () => this.resourceService.setTypeForResourceGroupList(this.selectedResourceTypeOrDefault()), // refresh data of the selected resource type
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ describe('PermissionComponent without any params (default: type Role)', () => {
} as Environment,
]);
expect(component.resourceGroups).toEqual([]);
expect(component.resourceTypes).toEqual([{ id: null, name: null, hasChildren: false, children: [] }]);
expect(component.resourceTypes).toEqual([
{ id: null, name: null, hasChildren: false, children: [], isApplication: false },
]);
expect(component.restrictionType).toEqual('role');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class PermissionComponent implements OnInit {
Global: [],
};
resourceGroups: Resource[] = [];
resourceTypes: ResourceType[] = [{ id: null, name: null, hasChildren: false, children: [] }];
resourceTypes: ResourceType[] = [{ id: null, name: null, hasChildren: false, children: [], isApplication: false }];

defaultNavItem: string = 'Roles';
// role | user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,21 @@ describe('RestrictionAddComponent', () => {
name: 'APP',
hasChildren: false,
children: [],
isApplication: false,
},
{
id: 2,
name: 'AS',
hasChildren: false,
children: [],
isApplication: false,
},
{
id: 3,
name: 'FOO',
hasChildren: false,
children: [],
isApplication: false,
},
];
restrictionComponent.selectedPermissionNames = ['NEO'];
Expand Down Expand Up @@ -282,18 +285,21 @@ describe('RestrictionAddComponent', () => {
name: 'APP',
hasChildren: false,
children: [],
isApplication: false,
},
{
id: 2,
name: 'AS',
hasChildren: false,
children: [],
isApplication: false,
},
{
id: 3,
name: 'FOO',
hasChildren: false,
children: [],
isApplication: false,
},
];
restrictionComponent.selectedPermissionNames = ['NEO'];
Expand Down Expand Up @@ -341,18 +347,21 @@ describe('RestrictionAddComponent', () => {
name: 'APP',
hasChildren: false,
children: [],
isApplication: false,
},
{
id: 2,
name: 'AS',
hasChildren: false,
children: [],
isApplication: false,
},
{
id: 3,
name: 'FOO',
hasChildren: false,
children: [],
isApplication: false,
},
];
restrictionComponent.selectedPermissionNames = ['NEO'];
Expand Down
Loading

0 comments on commit 67cf1c5

Please sign in to comment.