diff --git a/AMW_angular/io/src/app/core/amw-constants.ts b/AMW_angular/io/src/app/core/amw-constants.ts index 12697a286..27357536b 100644 --- a/AMW_angular/io/src/app/core/amw-constants.ts +++ b/AMW_angular/io/src/app/core/amw-constants.ts @@ -1,4 +1,9 @@ export const AMW_LOGOUT_URL = 'amw.logoutUrl'; +export const ENVIRONMENT = { + AMW_VM_DETAILS_URL: 'amw.vmDetailUrl', + AMW_VM_URL_PARAM: 'amw.vmUrlParam', +}; + // used for date-fns export const DATE_TIME_FORMAT = 'dd.MM.yyyy HH:mm'; export const DATE_FORMAT = 'dd.MM.yyyy'; diff --git a/AMW_angular/io/src/app/servers/server.ts b/AMW_angular/io/src/app/servers/server.ts new file mode 100644 index 000000000..6acfdd9e7 --- /dev/null +++ b/AMW_angular/io/src/app/servers/server.ts @@ -0,0 +1,15 @@ +export type Server = { + host: string; + appServer: string; + appServerRelease: string; + runtime: string; + node: string; + nodeRelease: string; + environment: string; + appServerId: number; + nodeId: number; + environmentId: number; + domain: string; + domainId: string; + definedOnNode: boolean; +}; diff --git a/AMW_angular/io/src/app/servers/servers-list/servers-list.component.spec.ts b/AMW_angular/io/src/app/servers/servers-list/servers-list.component.spec.ts new file mode 100644 index 000000000..bb04a30cf --- /dev/null +++ b/AMW_angular/io/src/app/servers/servers-list/servers-list.component.spec.ts @@ -0,0 +1,68 @@ +import { ServersListComponent } from './servers-list.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentRef } from '@angular/core'; +import { Server } from '../server'; + +describe(ServersListComponent.name, () => { + let component: ServersListComponent; + let componentRef: ComponentRef; + let fixture: ComponentFixture; + + const servers: Server[] = [ + { + host: 'host1', + appServer: 'Application Server 1', + appServerRelease: 'multiple', + runtime: 'multiple runtimes', + node: 'node1', + nodeRelease: '1.1', + environment: 'A', + appServerId: 1, + nodeId: 2, + environmentId: 3, + domain: 'domain 1', + domainId: '1', + definedOnNode: true, + }, + { + host: 'host2', + appServer: 'Application Server 2', + appServerRelease: 'multiple', + runtime: 'multiple runtimes', + node: 'node2', + nodeRelease: '1.2', + environment: 'B', + appServerId: 2, + nodeId: 3, + environmentId: 4, + domain: 'domain 2', + domainId: '2', + definedOnNode: false, + }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ServersListComponent], + providers: [], + }).compileComponents(); + + fixture = TestBed.createComponent(ServersListComponent); + + component = fixture.componentInstance; + componentRef = fixture.componentRef; + componentRef.setInput('servers', servers); + componentRef.setInput('canReadAppServer', true); + componentRef.setInput('canReadResources', false); + componentRef.setInput('linkToHostUrl', '/link/to/host'); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeDefined(); + expect(component.servers().length).toBe(2); + expect(component.canReadResources()).toBeFalse(); + expect(component.canReadAppServer()).toBeTrue(); + expect(component.linkToHostUrl()).toEqual('/link/to/host'); + }); +}); diff --git a/AMW_angular/io/src/app/servers/servers-list/servers-list.component.ts b/AMW_angular/io/src/app/servers/servers-list/servers-list.component.ts new file mode 100644 index 000000000..48b4d9549 --- /dev/null +++ b/AMW_angular/io/src/app/servers/servers-list/servers-list.component.ts @@ -0,0 +1,64 @@ +import { ChangeDetectionStrategy, Component, inject, input, Signal } from '@angular/core'; +import { AppsListComponent } from '../../apps/apps-list/apps-list-component'; +import { Server } from '../server'; + +@Component({ + selector: 'app-servers-list', + standalone: true, + imports: [AppsListComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: `
+ @if (servers() && servers().length > 0) { + + + + + + + + + + + + + + @for (server of servers(); track server; let even = $even) { + + + + + + + + + + } + +
HostEnvAppServerAppServer ReleaseRuntimeNodeNode Release
+ {{ server.host }} + {{ server.environment }} + @if(canReadAppServer()) { + {{ server.appServer }} + } @else { + {{ server.appServer }} } + {{ server.appServerRelease }}{{ server.runtime }} + @if (canReadResources()) { + {{ + server.node + }} + } @else { + {{ server.node }} + } + {{ server.nodeRelease }}
+ } +
`, +}) +export class ServersListComponent { + servers = input.required(); + canReadAppServer = input.required(); + canReadResources = input.required(); + linkToHostUrl = input.required(); +} diff --git a/AMW_angular/io/src/app/servers/servers-page.component.ts b/AMW_angular/io/src/app/servers/servers-page.component.ts index 5becdf70e..3566354d0 100644 --- a/AMW_angular/io/src/app/servers/servers-page.component.ts +++ b/AMW_angular/io/src/app/servers/servers-page.component.ts @@ -1,33 +1,59 @@ -import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, Signal, signal } from '@angular/core'; import { PageComponent } from '../layout/page/page.component'; import { LoadingIndicatorComponent } from '../shared/elements/loading-indicator.component'; import { AuthService } from '../auth/auth.service'; +import { ServersListComponent } from './servers-list/servers-list.component'; +import { ServersService } from './servers.service'; +import { ConfigurationService } from '../shared/service/configuration.service'; +import { ENVIRONMENT } from '../core/amw-constants'; +import { Config, pluck } from '../shared/configuration'; @Component({ selector: 'app-servers-page', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [PageComponent, LoadingIndicatorComponent], + imports: [PageComponent, LoadingIndicatorComponent, ServersListComponent], template: `
Servers
- {{ permissions() }} -
`, + `, }) export class ServersPageComponent { private authService = inject(AuthService); + private serversService = inject(ServersService); + private configurationService = inject(ConfigurationService); isLoading = signal(false); + servers = this.serversService.servers; + configuration: Signal = this.configurationService.configuration; + + linkToHostUrl = computed(() => { + if (!this.configuration()) return; + const config = this.configuration(); + const vmDetailUrl = pluck(ENVIRONMENT.AMW_VM_DETAILS_URL, config); + const vmUrlParam = pluck(ENVIRONMENT.AMW_VM_URL_PARAM, config); + return `${vmDetailUrl}?${vmUrlParam}=`; + }); + permissions = computed(() => { if (this.authService.restrictions().length > 0) { return { - canViewSomething: true, + canReadAppServer: this.authService.hasResourcePermission('RESOURCE', 'CREATE', 'APPLICATION'), + canReadResources: this.authService.hasPermission('RESOURCE', 'READ'), }; } else { - return { canViewSomething: false }; + return { + canReadAppServer: false, + canReadResources: false, + }; } }); } diff --git a/AMW_angular/io/src/app/servers/servers.service.ts b/AMW_angular/io/src/app/servers/servers.service.ts new file mode 100644 index 000000000..724e2956e --- /dev/null +++ b/AMW_angular/io/src/app/servers/servers.service.ts @@ -0,0 +1,22 @@ +import { BaseService } from '../base/base.service'; +import { catchError, map } from 'rxjs/operators'; +import { inject, Injectable, Signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Server } from './server'; +import { toSignal } from '@angular/core/rxjs-interop'; + +@Injectable({ providedIn: 'root' }) +export class ServersService extends BaseService { + private http = inject(HttpClient); + private serversUrl = `${this.getBaseUrl()}/servers`; + + private servers$ = this.http + .get(`${this.serversUrl}`, { + headers: this.getHeaders(), + observe: 'response', + }) + .pipe(catchError(this.handleError)) + .pipe(map((response) => response.body)); + + servers: Signal = toSignal(this.servers$); +} diff --git a/AMW_angular/io/src/app/shared/configuration.ts b/AMW_angular/io/src/app/shared/configuration.ts new file mode 100644 index 000000000..b49287caf --- /dev/null +++ b/AMW_angular/io/src/app/shared/configuration.ts @@ -0,0 +1,5 @@ +export type Config = { key: { value: string; env: string }; value: string; defaultValue: string }; + +export function pluck(key: string, config: Config[]): string { + return config.filter((c) => c.key.value === key).map((c) => c.value)[0]; +} diff --git a/AMW_angular/io/src/app/shared/service/configuration.service.ts b/AMW_angular/io/src/app/shared/service/configuration.service.ts new file mode 100644 index 000000000..0d72e9c00 --- /dev/null +++ b/AMW_angular/io/src/app/shared/service/configuration.service.ts @@ -0,0 +1,23 @@ +import { BaseService } from '../../base/base.service'; +import { inject, Injectable, Signal } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { catchError, map } from 'rxjs/operators'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { Observable } from 'rxjs'; +import { Config } from '../configuration'; + +@Injectable({ providedIn: 'root' }) +export class ConfigurationService extends BaseService { + private http = inject(HttpClient); + private settingsUrl = `${this.getBaseUrl()}/settings`; + + private configuration$: Observable = this.http + .get(`${this.settingsUrl}`, { + headers: this.getHeaders(), + observe: 'response', + }) + .pipe(catchError(this.handleError)) + .pipe(map((response: HttpResponse): Config[] => response.body)); + + configuration: Signal = toSignal(this.configuration$); +} diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/boundary/GetServersUseCase.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/boundary/GetServersUseCase.java new file mode 100644 index 000000000..978b7ab9e --- /dev/null +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/boundary/GetServersUseCase.java @@ -0,0 +1,8 @@ +package ch.puzzle.itc.mobiliar.business.server.boundary; + +import java.util.List; + +public interface GetServersUseCase { + + List all(); +} diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/boundary/Server.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/boundary/Server.java new file mode 100644 index 000000000..eccc49deb --- /dev/null +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/boundary/Server.java @@ -0,0 +1,37 @@ +package ch.puzzle.itc.mobiliar.business.server.boundary; + +import ch.puzzle.itc.mobiliar.business.server.entity.ServerTuple; +import lombok.Getter; + +@Getter +public class Server { + private final String host; + private final String appServer; + private final String appServerRelease; + private final String runtime; + private final String node; + private final String nodeRelease; + private final String environment; + private final Integer appServerId; + private final Integer nodeId; + private final Integer environmentId; + private final String domain; + private final Integer domainId; + private final boolean definedOnNode; + + public Server(ServerTuple serverTuple) { + this.host = serverTuple.getHost(); + this.appServer = serverTuple.getAppServer(); + this.appServerRelease = serverTuple.getAppServerRelease(); + this.runtime = serverTuple.getRuntime(); + this.node = serverTuple.getNode(); + this.nodeRelease = serverTuple.getNodeRelease(); + this.environment = serverTuple.getEnvironment(); + this.appServerId = serverTuple.getAppServerId(); + this.nodeId = serverTuple.getNodeId(); + this.environmentId = serverTuple.getEnvironmentId(); + this.domain = serverTuple.getDomain(); + this.domainId = serverTuple.getDomainId(); + this.definedOnNode = serverTuple.isDefinedOnNode(); + } +} diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/control/GetServersService.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/control/GetServersService.java new file mode 100644 index 000000000..4e647129e --- /dev/null +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/server/control/GetServersService.java @@ -0,0 +1,23 @@ +package ch.puzzle.itc.mobiliar.business.server.control; + +import ch.puzzle.itc.mobiliar.business.server.boundary.GetServersUseCase; +import ch.puzzle.itc.mobiliar.business.server.boundary.Server; +import ch.puzzle.itc.mobiliar.business.server.boundary.ServerView; + +import javax.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; + +public class GetServersService implements GetServersUseCase { + + @Inject + private ServerView serverView; + + @Override + public List all() { + return serverView.getServers(null, null, null, null, null, true) + .stream() + .map(Server::new) + .collect(Collectors.toList()); + } +} diff --git a/AMW_business/src/test/resources/integration-test/testdb/amwFileDbIntegrationEmpty.mv.db b/AMW_business/src/test/resources/integration-test/testdb/amwFileDbIntegrationEmpty.mv.db index bd1f9d182..480ab1bd2 100644 Binary files a/AMW_business/src/test/resources/integration-test/testdb/amwFileDbIntegrationEmpty.mv.db and b/AMW_business/src/test/resources/integration-test/testdb/amwFileDbIntegrationEmpty.mv.db differ diff --git a/AMW_e2e/cypress/e2e/servers/servers.cy.js b/AMW_e2e/cypress/e2e/servers/servers.cy.js index 0a4f0c470..666093690 100644 --- a/AMW_e2e/cypress/e2e/servers/servers.cy.js +++ b/AMW_e2e/cypress/e2e/servers/servers.cy.js @@ -1,10 +1,13 @@ describe("Servers Page", () => { it("should navigate to the servers page", () => { cy.visit("AMW_angular/#/servers", { - username: "admin", - password: "admin", + auth: { + username: "admin", + password: "admin", + }, }); cy.get('[data-cy="page-title"]').contains("Servers"); + cy.get("tbody > tr > :nth-child(3)").contains("testapplicationserver"); }); }); diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java index 459ecfe8f..0fd601d5c 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java @@ -34,6 +34,7 @@ import ch.mobi.itc.mobiliar.rest.properties.PropertyTypesRest; import ch.mobi.itc.mobiliar.rest.resources.*; import ch.mobi.itc.mobiliar.rest.releases.ReleasesRest; +import ch.mobi.itc.mobiliar.rest.servers.ServersRest; import ch.mobi.itc.mobiliar.rest.settings.SettingsRest; import ch.mobi.itc.mobiliar.rest.tags.boundary.TagsRest; @@ -77,6 +78,7 @@ private void addRestResourceClasses(Set> resources) { resources.add(TagsRest.class); resources.add(PropertyTypesRest.class); resources.add(FunctionsRest.class); + resources.add(ServersRest.class); // writers resources.add(DeploymentDtoCsvBodyWriter.class); diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/servers/ServersRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/servers/ServersRest.java new file mode 100644 index 000000000..86a6c0ba7 --- /dev/null +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/servers/ServersRest.java @@ -0,0 +1,33 @@ +package ch.mobi.itc.mobiliar.rest.servers; + +import ch.puzzle.itc.mobiliar.business.server.boundary.GetServersUseCase; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import javax.ejb.Stateless; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.Response.Status.OK; + +@Stateless +@Path("/servers") +@Api(value = "/servers") +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +public class ServersRest { + + @Inject + GetServersUseCase getServersUseCase; + + @GET + @ApiOperation("Get servers") + public Response getServers() { + return Response.status(OK).entity(getServersUseCase.all()).build(); + } +}