Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Resources page with routing and list of resource types #788

Merged
merged 10 commits into from
Nov 28, 2024
2 changes: 2 additions & 0 deletions AMW_angular/io/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { deploymentRoutes } from './deployment/deployment-routes';
import { settingsRoutes } from './settings/settings.routes';
import { deploymentsRoutes } from './deployments/deployments.routes';
import { serversRoute } from './servers/servers.route';
import { resourcesRoute } from './resources/resources.route';

export const routes: Routes = [
// default route only, the rest is done in module routing
{ path: '', component: DeploymentsComponent },

...appsRoutes,
...serversRoute,
...resourcesRoute,
...settingsRoutes,
...auditviewRoutes,
...deploymentRoutes,
Expand Down
2 changes: 1 addition & 1 deletion AMW_angular/io/src/app/navigation/navigation.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
</li>

<li class="nav-item">
<a class="nav-link" href="/AMW_web/pages/resourceList.xhtml">Resources</a>
<a class="nav-link" href="#/resources">Resources</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#/deployments/">Deploy</a>
Expand Down
2 changes: 2 additions & 0 deletions AMW_angular/io/src/app/resource/resource-type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface ResourceType {
id: number;
name: string;
hasChildren: boolean;
children: ResourceType[];
}
12 changes: 0 additions & 12 deletions AMW_angular/io/src/app/resource/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Resource } from './resource';
import { ResourceType } from './resource-type';
import { Release } from './release';
import { Relation } from './relation';
import { Property } from './property';
Expand Down Expand Up @@ -66,17 +65,6 @@ export class ResourceService extends BaseService {
);
}

getAllResourceTypes(): Observable<ResourceType[]> {
return this.http
.get<ResourceType[]>(`${this.getBaseUrl()}/resources/resourceTypes`, {
headers: this.getHeaders(),
})
.pipe(
map((resources) => resources.map(toResource)),
catchError(this.handleError),
);
}

getByType(type: string): Observable<Resource[]> {
return this.http
.get<Resource[]>(`${this.getBaseUrl()}/resources?type=${type}`, {
Expand Down
43 changes: 43 additions & 0 deletions AMW_angular/io/src/app/resources/resource-types.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, startWith, Subject } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import { shareReplay, switchMap } from 'rxjs/operators';
import { BaseService } from '../base/base.service';
import { ResourceType } from '../resource/resource-type';

@Injectable({ providedIn: 'root' })
export class ResourceTypesService extends BaseService {
private reload$ = new Subject<ResourceType[]>();

private predefinedResourceTypes$ = this.getPredefinedResourceTypes();

private rootResourceTypes$ = this.reload$.pipe(
startWith(null),
switchMap(() => this.getRootResourceTypes()),
shareReplay(1),
);

predefinedResourceTypes = toSignal(this.predefinedResourceTypes$, { initialValue: [] as ResourceType[] });
rootResourceTypes = toSignal(this.rootResourceTypes$, { initialValue: [] as ResourceType[] });

constructor(private http: HttpClient) {
super();
}

getAllResourceTypes(): Observable<ResourceType[]> {
return this.http.get<ResourceType[]>(`${this.getBaseUrl()}/resources/resourceTypes`);
}

getPredefinedResourceTypes(): Observable<ResourceType[]> {
return this.http.get<ResourceType[]>(`${this.getBaseUrl()}/resources/predefinedResourceTypes`);
}

getRootResourceTypes(): Observable<ResourceType[]> {
return this.http.get<ResourceType[]>(`${this.getBaseUrl()}/resources/rootResourceTypes`);
}

refreshData() {
this.reload$.next([]);
}
}
31 changes: 31 additions & 0 deletions AMW_angular/io/src/app/resources/resources-page.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<app-loading-indicator [isLoading]="isLoading()"></app-loading-indicator>
<app-page>
<div class="page-title">Resources</div>
<div class="page-content">
@if (permissions().canViewResourceTypes) {
<nav class="nav flex-column">
@for (resourceType of predefinedResourceTypes(); track resourceType.name) {
<a>{{ resourceType.name }}</a>
}
</nav>
<nav class="nav flex-column">
@for (resourceType of rootResourceTypes(); track resourceType.name) {
<a (click)="toggleChildren(resourceType)">
{{ resourceType.name }}
@if (resourceType.hasChildren) {
<span>{{ expandedResourceTypeId === resourceType.id ? '-' : '+' }}</span>
}
</a>
@if (resourceType.hasChildren && expandedResourceTypeId === resourceType.id) {
<ul class="subnav">
@for (child of resourceType.children; track child.name) {
<div class="grid-item">
<a>{{ child.name }}</a>
</div>
}
</ul>
} }
</nav>
}
</div>
</app-page>
24 changes: 24 additions & 0 deletions AMW_angular/io/src/app/resources/resources-page.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResourcesPageComponent } from './resources-page.component';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';

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

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

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

it('should create', () => {
expect(component).toBeTruthy();
});
});
37 changes: 37 additions & 0 deletions AMW_angular/io/src/app/resources/resources-page.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, computed, inject, Signal, signal } from '@angular/core';
import { AuthService } from '../auth/auth.service';
import { PageComponent } from '../layout/page/page.component';
import { LoadingIndicatorComponent } from '../shared/elements/loading-indicator.component';
import { ResourceTypesService } from './resource-types.service';
import { ResourceType } from '../resource/resource-type';

@Component({
selector: 'app-resources-page',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [PageComponent, LoadingIndicatorComponent],
templateUrl: './resources-page.component.html',
})
export class ResourcesPageComponent {
private authService = inject(AuthService);
private resourceTypesService = inject(ResourceTypesService);

predefinedResourceTypes: Signal<ResourceType[]> = this.resourceTypesService.predefinedResourceTypes;
rootResourceTypes: Signal<ResourceType[]> = this.resourceTypesService.rootResourceTypes;
isLoading = signal(false);
expandedResourceTypeId: number | null = null;

permissions = computed(() => {
if (this.authService.restrictions().length > 0) {
return {
canViewResourceTypes: this.authService.hasPermission('RES_TYPE_LIST_TAB', 'ALL'),
};
} else {
return { canViewResourceTypes: false };
}
});

toggleChildren(resourceType: ResourceType): void {
this.expandedResourceTypeId = this.expandedResourceTypeId === resourceType.id ? null : resourceType.id;
}
}
3 changes: 3 additions & 0 deletions AMW_angular/io/src/app/resources/resources.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ResourcesPageComponent } from './resources-page.component';

export const resourcesRoute = [{ path: 'resources', component: ResourcesPageComponent }];
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Restriction } from './restriction';
import { Tag } from './tag';
import { EnvironmentService } from '../../deployment/environment.service';
import { ResourceService } from '../../resource/resource.service';
import { ResourceTypesService } from '../../resources/resource-types.service';
import { Environment } from 'src/app/deployment/environment';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
Expand All @@ -27,6 +28,7 @@ describe('PermissionComponent without any params (default: type Role)', () => {
let permissionService: PermissionService;
let environmentService: EnvironmentService;
let resourceService: ResourceService;
let resourceTypesService: ResourceTypesService;

const mockRoute: any = { snapshot: {} };
mockRoute.params = new Subject<any>();
Expand All @@ -50,6 +52,7 @@ describe('PermissionComponent without any params (default: type Role)', () => {
EnvironmentService,
PermissionService,
ResourceService,
ResourceTypesService,
{ provide: ActivatedRoute, useValue: mockRoute },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
Expand All @@ -63,6 +66,7 @@ describe('PermissionComponent without any params (default: type Role)', () => {
permissionService = TestBed.inject(PermissionService);
environmentService = TestBed.inject(EnvironmentService);
resourceService = TestBed.inject(ResourceService);
resourceTypesService = TestBed.inject(ResourceTypesService);
});

it('should have default data', () => {
Expand All @@ -80,7 +84,7 @@ describe('PermissionComponent without any params (default: type Role)', () => {
} as Environment,
]);
expect(component.resourceGroups).toEqual([]);
expect(component.resourceTypes).toEqual([{ id: null, name: null }]);
expect(component.resourceTypes).toEqual([{ id: null, name: null, hasChildren: false, children: [] }]);
expect(component.restrictionType).toEqual('role');
});

Expand All @@ -98,7 +102,7 @@ describe('PermissionComponent without any params (default: type Role)', () => {
spyOn(permissionService, 'getAllPermissionEnumValues').and.returnValue(of(permissions));
spyOn(environmentService, 'getAllIncludingGroups').and.returnValue(of(environments));
spyOn(resourceService, 'getAllResourceGroups').and.callThrough();
spyOn(resourceService, 'getAllResourceTypes').and.callThrough();
spyOn(resourceTypesService, 'getAllResourceTypes').and.callThrough();
// when
component.ngOnInit();
mockRoute.params.next({ restrictionType: 'role' });
Expand All @@ -109,7 +113,7 @@ describe('PermissionComponent without any params (default: type Role)', () => {
expect(permissionService.getAllPermissionEnumValues).toHaveBeenCalled();
expect(environmentService.getAllIncludingGroups).toHaveBeenCalled();
expect(resourceService.getAllResourceGroups).toHaveBeenCalled();
expect(resourceService.getAllResourceTypes).toHaveBeenCalled();
expect(resourceTypesService.getAllResourceTypes).toHaveBeenCalled();
expect(component.permissions).toEqual(permissions);
expect(component.restriction).toBeNull();
expect(component.groupedEnvironments['All']).toContain({
Expand Down Expand Up @@ -402,6 +406,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => {
let permissionService: PermissionService;
let environmentService: EnvironmentService;
let resourceService: ResourceService;
let resourceTypesService: ResourceTypesService;

const mockRoute: any = { snapshot: {} };
mockRoute.params = new Subject<any>();
Expand All @@ -424,6 +429,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => {
EnvironmentService,
PermissionService,
ResourceService,
ResourceTypesService,
{ provide: ActivatedRoute, useValue: mockRoute },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
Expand All @@ -436,6 +442,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => {
permissionService = TestBed.inject(PermissionService);
environmentService = TestBed.inject(EnvironmentService);
resourceService = TestBed.inject(ResourceService);
resourceTypesService = TestBed.inject(ResourceTypesService);
});

it('should invoke some services on ngOnInt', () => {
Expand All @@ -452,7 +459,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => {
spyOn(permissionService, 'getAllPermissionEnumValues').and.returnValue(of(permissions));
spyOn(environmentService, 'getAllIncludingGroups').and.returnValue(of(environments));
spyOn(resourceService, 'getAllResourceGroups').and.callThrough();
spyOn(resourceService, 'getAllResourceTypes').and.callThrough();
spyOn(resourceTypesService, 'getAllResourceTypes').and.callThrough();
spyOn(permissionService, 'getAllUserRestrictionNames').and.callThrough();
// when
component.ngOnInit();
Expand All @@ -463,7 +470,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => {
expect(permissionService.getAllPermissionEnumValues).toHaveBeenCalled();
expect(environmentService.getAllIncludingGroups).toHaveBeenCalled();
expect(resourceService.getAllResourceGroups).toHaveBeenCalled();
expect(resourceService.getAllResourceTypes).toHaveBeenCalled();
expect(resourceTypesService.getAllResourceTypes).toHaveBeenCalled();
expect(permissionService.getAllUserRestrictionNames).toHaveBeenCalled();
expect(component.permissions).toEqual(permissions);
expect(component.restriction).toBeNull();
Expand Down Expand Up @@ -502,6 +509,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => {
let permissionService: PermissionService;
let environmentService: EnvironmentService;
let resourceService: ResourceService;
let resourceTypesService: ResourceTypesService;

const mockRoute: any = { snapshot: {} };
mockRoute.params = new Subject<any>();
Expand All @@ -524,6 +532,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => {
EnvironmentService,
PermissionService,
ResourceService,
ResourceTypesService,
{ provide: ActivatedRoute, useValue: mockRoute },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
Expand All @@ -536,6 +545,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => {
permissionService = TestBed.inject(PermissionService);
environmentService = TestBed.inject(EnvironmentService);
resourceService = TestBed.inject(ResourceService);
resourceTypesService = TestBed.inject(ResourceTypesService);
});
it('should invoke some services on ngOnInt', () => {
// given
Expand Down Expand Up @@ -565,7 +575,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => {
spyOn(permissionService, 'getOwnUserAndRoleRestrictions').and.returnValue(of(restrictions));
spyOn(environmentService, 'getAllIncludingGroups').and.returnValue(of(environments));
spyOn(resourceService, 'getAllResourceGroups').and.callThrough();
spyOn(resourceService, 'getAllResourceTypes').and.callThrough();
spyOn(resourceTypesService, 'getAllResourceTypes').and.callThrough();
// when
component.ngOnInit();
mockRoute.params.next({ actingUser: 'testUser' });
Expand All @@ -578,7 +588,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => {
expect(component.userNames).not.toContain('testUser');
expect(environmentService.getAllIncludingGroups).toHaveBeenCalled();
expect(resourceService.getAllResourceGroups).toHaveBeenCalled();
expect(resourceService.getAllResourceTypes).toHaveBeenCalled();
expect(resourceTypesService.getAllResourceTypes).toHaveBeenCalled();
expect(permissionService.getOwnUserAndRoleRestrictions).toHaveBeenCalled();
expect(component.assignableRestrictions).toEqual(restrictions);
expect(component.assignablePermissions).toEqual(permissions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap';
import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component';
import { ButtonComponent } from '../../shared/button/button.component';
import { ResourceTypesService } from '../../resources/resource-types.service';

@Component({
selector: 'app-permission',
Expand Down Expand Up @@ -62,7 +63,7 @@ export class PermissionComponent implements OnInit {
Global: [],
};
resourceGroups: Resource[] = [];
resourceTypes: ResourceType[] = [{ id: null, name: null }];
resourceTypes: ResourceType[] = [{ id: null, name: null, hasChildren: false, children: [] }];

defaultNavItem: string = 'Roles';
// role | user
Expand Down Expand Up @@ -90,6 +91,7 @@ export class PermissionComponent implements OnInit {
private permissionService: PermissionService,
private environmentService: EnvironmentService,
private resourceService: ResourceService,
private resourceTypesService: ResourceTypesService,
private activatedRoute: ActivatedRoute,
private location: Location,
) {
Expand Down Expand Up @@ -392,7 +394,7 @@ export class PermissionComponent implements OnInit {

private getAllResourceTypes() {
this.isLoading = true;
this.resourceService.getAllResourceTypes().subscribe({
this.resourceTypesService.getAllResourceTypes().subscribe({
next: (r) => (this.resourceTypes = this.resourceTypes.concat(r)),
error: (e) => (this.errorMessage = e),
complete: () => (this.isLoading = false),
Expand Down
Loading