diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 525ea473ea9619994d2809f548bcee4767e6ff8f..90a63db2bdc441892231c4fe8d228dba7477e08a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,7 +28,7 @@ sonar: build_and_push_latest_image: stage: build - image: korvoj/docker-git:26.1.3 + image: korvoj/docker-git:26.1.4 only: - develop variables: @@ -55,7 +55,7 @@ build_and_push_alfa_image: build_and_push_release_image: stage: build - image: korvoj/docker-git:26.1.3 + image: korvoj/docker-git:26.1.4 only: - tags script: diff --git a/src/app/app.module.ts b/src/app/app.module.ts index be39d339e116fb2a4cf03099c6c8666ea24ace06..98aa012017e5bbee7e7bc954c489c4c7161efe85 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -34,6 +34,7 @@ import {SplitButtonModule} from 'primeng/splitbutton'; import {MenuModule} from 'primeng/menu'; import { AdminLeftMenuComponent } from './shared/admin-left-menu/admin-left-menu.component'; import {AccordionModule} from 'primeng/accordion'; +import { RecaptchaVisibilityService } from './service/recaptcha-visibility.service'; export function appConfigFactory(config: AppConfigService) { return function create() { @@ -98,6 +99,7 @@ export const jwtOptionsFactory = (appConfig: AppConfigService) => ({ providers: [ AuthGuard, AuthService, + RecaptchaVisibilityService, AppConfigService, provideZxvbnServiceForPSM(), { diff --git a/src/app/appmarket/admin/configuration/configuration.routes.ts b/src/app/appmarket/admin/configuration/configuration.routes.ts index adde5168ce4badc1f068931ff7978c8122ecdbbf..c69d5a34517777869646484e7c22b62c5ecfa1fb 100644 --- a/src/app/appmarket/admin/configuration/configuration.routes.ts +++ b/src/app/appmarket/admin/configuration/configuration.routes.ts @@ -2,8 +2,14 @@ import {Route} from "@angular/router"; import {AuthGuard} from "../../../auth/auth.guard"; import {RoleGuard} from "../../../auth/role.guard"; import {ConfigurationDetailsComponent} from "./index"; +import { WebhookListComponent } from "../webhook/webhook-list/webhook-list.component"; +import { WebhookDetailsComponent } from "../webhook/webhook-details/webhook-details.component"; export const ConfigurationRoutes: Route[] = [ {path: 'configuration', component: ConfigurationDetailsComponent, canActivate: [AuthGuard, RoleGuard], + data:{roles: ['ROLE_SYSTEM_ADMIN']} }, + {path: 'webhooks', component: WebhookListComponent, canActivate: [AuthGuard, RoleGuard], + data:{roles: ['ROLE_SYSTEM_ADMIN']} }, + {path: 'webhooks/:id', component: WebhookDetailsComponent, canActivate: [AuthGuard, RoleGuard], data:{roles: ['ROLE_SYSTEM_ADMIN']} } ]; diff --git a/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.css b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.html b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f2fb671831fa7abf2a18ef727bde77e0c878e631 --- /dev/null +++ b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.html @@ -0,0 +1,81 @@ +<div class=""> + <div class="background-section"> + <h4 style="font-size:15px; font-weight: bold">{{ 'WEBHOOKS.TITLE' | translate }}</h4> + + <div class="panel-body"> + <form *ngIf="webhook" (submit)="submit()" class="form-horizontal" #webhookForm="ngForm"> + <div class="panel-default panel-heading">{{ 'CLUSTERS.GENERAL' | translate }} </div> + <div class="panel-body"> + + <div class="form-group"> + <label for="clusterId" class="col-sm-2 control-label">{{ 'WEBHOOKS.ID' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterId" name="clusterId" + [(ngModel)]="webhook.id" [disabled]="true"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterName" class="col-sm-2 control-label">{{ 'WEBHOOKS.NAME' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterName" name="clusterName" + [(ngModel)]="webhook.name" [disabled]="false"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="targetUrl" class="col-sm-2 control-label">{{ 'WEBHOOKS.TARGEt_URL' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="targetUrl" name="targetUrl" + [(ngModel)]="webhook.targetUrl" [disabled]="false"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterDescription" class="col-sm-2 control-label">{{ 'WEBHOOKS.TYPE' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterDescription" name="clusterDescription" + placeholder="{{webhook.eventType | translate}}" [disabled]="true"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="token" class="col-sm-2 control-label">{{ 'WEBHOOKS.TOKEN' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="token" name="token" + [(ngModel)]="webhook.tokenValue" [disabled]="false"> + </div> + </div> + </div> + + + <div class="form-group"> + <label for="auth" class="col-sm-2 control-label">{{ 'WEBHOOKS.AUTH' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="auth" name="auth" + [(ngModel)]="webhook.authorizationHeader" [disabled]="false"> + </div> + </div> + </div> + + <div class="flex justify-content-end"> + <button [disabled]="!webhookForm.form.valid" type="submit" class="btn btn-primary" + type="submit">{{ 'PORTAL_CONFIGURATION.SUBMIT_BUTTON' | translate }}</button> + </div> + + + </div> + </form> + </div> +</div> +</div> \ No newline at end of file diff --git a/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.spec.ts b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c7b2ae1615edbe524fb76d39254f9acd4ea8367 --- /dev/null +++ b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.spec.ts @@ -0,0 +1,64 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { WebhookDetailsComponent } from './webhook-details.component'; +import { WebhookService } from '../../../../service/webhook.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Webhook } from '../../../../model/webhook'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { FormsModule } from '@angular/forms'; + +class MockWebhookService { + getOne = jasmine.createSpy().and.returnValue(of({ id: 1, name: 'Test', eventType: 'DOMAIN_CREATION', targetUrl: 'http://test' })); + update = jasmine.createSpy().and.returnValue(of({ id: 1, name: 'Updated', eventType: 'DOMAIN_CREATION', targetUrl: 'http://test' })); +} + +class MockActivatedRoute { + params = of({ id: 1 }); +} + +class MockRouter {} + +describe('WebhookDetailsComponent', () => { + let component: WebhookDetailsComponent; + let fixture: ComponentFixture<WebhookDetailsComponent>; + let service: MockWebhookService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [WebhookDetailsComponent], + imports: [ + FormsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + }), + ], + providers: [ + { provide: WebhookService, useClass: MockWebhookService }, + { provide: ActivatedRoute, useClass: MockActivatedRoute }, + { provide: Router, useClass: MockRouter } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(WebhookDetailsComponent); + component = fixture.componentInstance; + service = TestBed.inject(WebhookService) as any; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize and fetch webhook by id', () => { + expect(component.webhooksId).toBe(1); + expect(service.getOne).toHaveBeenCalledWith(1); + expect(component.webhook).toBeDefined(); + expect(component.webhook.id).toBe(1); + }); + +}); diff --git a/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.ts b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef3ae89e81d00a607683d2c4178c02861fd2ba6f --- /dev/null +++ b/src/app/appmarket/admin/webhook/webhook-details/webhook-details.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from '@angular/core'; +import { WebhookService } from '../../../../service/webhook.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Webhook } from '../../../../model/webhook'; +import { BaseComponent } from '../../../../shared/common/basecomponent/base.component'; + +@Component({ + selector: 'app-webhook-details', + templateUrl: './webhook-details.component.html', + styleUrl: './webhook-details.component.css' +}) +export class WebhookDetailsComponent extends BaseComponent implements OnInit { + + public webhooksId: number; + public webhook: Webhook; + + constructor(private service: WebhookService, + public router: Router, + private route: ActivatedRoute) { + super(); + } + + + ngOnInit(): void { + this.route.params.subscribe(params => { + this.webhooksId = +params['id']; + + this.service.getOne(this.webhooksId).subscribe(result => { + console.log(result); + this.webhook = result; + } ) + }) + } + + + public submit(): void { + console.log(this.webhook); + this.service.update(this.webhook).subscribe(result => { + console.log(result); + this.webhook = result; + }); + } +} diff --git a/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.css b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.html b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8158c52a0252fe096e268b2f7036dfda7f05a424 --- /dev/null +++ b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.html @@ -0,0 +1,112 @@ +<div style="display: flex; align-items: center; margin-top:20px"> + <div style="margin-right:20px"> + <div > + <button class="btn btn-primary" (click)="openModal()">{{'WEBHOOKS.NEW' | translate}}</button> + </div> + </div> + <div class="flex" style="margin-right:20px"> + + </div> +</div> + + <h4 class="header">{{ 'WEBHOOKS.TITLE' | translate }}</h4> + +<div class="background-section"> + <p-table + [value]="webkooks" + [paginator]="true" + [rows]="maxItemsOnPage" + [rowsPerPageOptions]="[15, 20, 25, 30, 50]" + [responsiveLayout]="'scroll'"> + <ng-template pTemplate="header"> + <tr> + <th pSortableColumn="id" id="id"> {{ 'WEBHOOKS.ID' | translate }} + <p-sortIcon field="id"></p-sortIcon> + </th> + <th pSortableColumn="name" id="name"> {{ 'WEBHOOKS.NAME' | translate }} + <p-sortIcon field="name"></p-sortIcon> + </th> + <th pSortableColumn="url" id="url"> {{ 'WEBHOOKS.TARGET_URL' | translate }} + <p-sortIcon field="url"></p-sortIcon> + </th> + <th pSortableColumn="type" id="type"> {{ 'WEBHOOKS.TYPE' | translate }} + <p-sortIcon field="type"></p-sortIcon> + </th> + <th></th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-webhook> + <tr> + <td [routerLink]="[webhook.id]">{{webhook.id}}</td> + <td>{{webhook.name}}</td> + <td>{{('WEBHOOKS.' + webhook.eventType.toString().toUpperCase() ) | translate}}</td> + <td>{{webhook.targetUrl}}</td> + + <td class="text-right"> + <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle" aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> + </a> + <ul class="dropdown-menu pull-right-drop" > + <li><a [routerLink]="[ webhook.id]" class=""> + {{ 'WEBHOOKS.DETAILS' | translate }}</a> + </li> + </ul> + </span> + </td> + </tr> + </ng-template> + </p-table> + +</div> + + +<nmaas-modal > + <div class="nmaas-modal-header">{{'WEBHOOKS.TITLE_SHORT' | translate}}</div> + <div class="nmaas-modal-body"> + <div class="flex flex-column"> + <div class="mt-4"> + <label for="name">{{'WEBHOOKS.NAME' | translate}}</label> + <input id="name" type="text" class="form-control" [(ngModel)]="addedWebhook.name" [ngModelOptions]="{standalone: true}"> + </div> + + <div class="mt-4"> + <label for="desc">{{'WEBHOOKS.TARGET_URL' | translate}}</label> + <input id="desc" type="text" class="form-control" [(ngModel)]="addedWebhook.targetUrl" [ngModelOptions]="{standalone: true}"> + </div> + <div class="mt-4"> + <label for="contactEmail">{{'WEBHOOKS.TYPE' | translate}}</label> + <select id="domain" #typeSelect class="form-control" (change)="onTypeSelect(typeSelect.value)" > + <option *ngFor="let t of type" [value]="t.name" + >{{t.name | translate}} + </option> + </select> + </div> + + + <div class="mt-4"> + <label for="assignDomain">Auth required</label> + <input class="ml-2" type="checkbox" [(ngModel)]="authRequired" id="authRequired"/> + </div> + + <div *ngIf="authRequired" class="mt-4"> + <label for="desc">{{'WEBHOOKS.TOKEN' | translate}}</label> + <input id="desc" type="text" class="form-control" [(ngModel)]="addedWebhook.tokenValue" [ngModelOptions]="{standalone: true}"> + </div> + + <div *ngIf="authRequired" class="mt-4"> + <label for="desc">{{'WEBHOOKS.AUTH' | translate}}</label> + <input id="desc" type="text" class="form-control" [(ngModel)]="addedWebhook.authorizationHeader" [ngModelOptions]="{standalone: true}"> + </div> + + + </div> + </div> + <div class="nmaas-modal-footer"> + <button type="button" class="btn btn-primary" [disabled]=" addedWebhook?.name === null "(click)="closeModalAndSaveWebhook()" + pTooltip="Upload file is required before save" showDelay="2000" >{{'CLUSTERS.SAVE' | translate}}</button> + <button type="button" class="btn btn-secondary" + (click)="this.modal.hide()">{{'UNDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> + + </div> + </nmaas-modal> diff --git a/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.spec.ts b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fc3ab0d77a4ec175f96ac65c1e3db2412292bb0 --- /dev/null +++ b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.spec.ts @@ -0,0 +1,93 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { WebhookListComponent } from './webhook-list.component'; +import { WebhookService } from '../../../../service/webhook.service'; +import { of } from 'rxjs'; +import { ModalComponent } from '../../../../shared'; +import { Webhook, WebhookType } from '../../../../model/webhook'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +class MockWebhookService { + getAll = jasmine.createSpy().and.returnValue(of([{ id: 1, name: 'Test', eventType: 'DOMAIN_CREATION', targetUrl: 'http://test' }])); + create = jasmine.createSpy().and.returnValue(of({})); +} + +class MockModalComponent { + show = jasmine.createSpy(); + hide = jasmine.createSpy(); +} + +describe('WebhookListComponent', () => { + let component: WebhookListComponent; + let fixture: ComponentFixture<WebhookListComponent>; + let service: MockWebhookService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [WebhookListComponent], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + }), + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: WebhookService, useClass: MockWebhookService } + ] + }) + .overrideComponent(WebhookListComponent, { + set: { + providers: [], + } + }) + .compileComponents(); + + fixture = TestBed.createComponent(WebhookListComponent); + component = fixture.componentInstance; + service = TestBed.inject(WebhookService) as any; + component.modal = new MockModalComponent() as any; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have a defined component instance', () => { + expect(component).toBeDefined(); + }); + + it('should render the component', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled).toBeTruthy(); + }); + + it('should refresh list and set webkooks', () => { + component.refreshList(); + expect(service.getAll).toHaveBeenCalled(); + fixture.detectChanges(); + expect(component.webkooks.length).toBeGreaterThan(0); + }); + + it('should open modal and set default event type', () => { + component.openModal(); + expect(component.addedWebhook.eventType).toBe(WebhookType.DOMAIN_CREATION); + expect(component.modal.show).toHaveBeenCalled(); + }); + + it('should call service.create and hide modal on closeModalAndSaveWebhook', () => { + component.addedWebhook = { name: 'Test', eventType: 'DOMAIN_CREATION', targetUrl: 'http://test' } as Webhook; + component.closeModalAndSaveWebhook(); + expect(service.create).toHaveBeenCalledWith(component.addedWebhook); + expect(component.modal.hide).toHaveBeenCalled(); + }); + + it('should set addedWebhook.eventType on type select', () => { + component.onTypeSelect('USER_ASSIGNMENT'); + expect(component.addedWebhook.eventType).toBe('USER_ASSIGNMENT'); + }); + +}); diff --git a/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.ts b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..63500136dd4095e6f6474f4bb86338b56d05322c --- /dev/null +++ b/src/app/appmarket/admin/webhook/webhook-list/webhook-list.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Webhook, WebhookType } from '../../../../model/webhook'; +import { ModalComponent } from '../../../../shared'; +import { WebhookService } from '../../../../service/webhook.service'; + +@Component({ + selector: 'app-webhook-list', + templateUrl: './webhook-list.component.html', + styleUrl: './webhook-list.component.css' +}) +export class WebhookListComponent implements OnInit { + + public webkooks: Webhook[] = []; + + public addedWebhook: Webhook = new Webhook(); + public maxItemsOnPage = 15; + + public authRequired: boolean = false; + + public type =[ + { name: "DOMAIN_CREATION", value: "DOMAIN_CREATION" }, + { name: "APPLICATION_DEPLOYMENT", value: "APPLICATION_DEPLOYMENT" }, + { name: "USER_ASSIGNMENT", value: "USER_ASSIGNMENT" }, + { name: "DOMAIN_GROUP_CHANGE", value: "DOMAIN_GROUP_CHANGE" } + ] + + @ViewChild(ModalComponent, { static: true }) + public modal: ModalComponent; + + + constructor(private service: WebhookService) { + } + + ngOnInit() { + this.refreshList(); + } + + public refreshList() { + this.service.getAll().subscribe(result => { + this.webkooks = result; + }) + } + + onTypeSelect(event: any) { + console.log(event); + this.addedWebhook.eventType = event; + } + + public openModal() { + this.addedWebhook.eventType = WebhookType.DOMAIN_CREATION + this.modal.show(); + } + + public closeModalAndSaveWebhook() { + this.service.create(this.addedWebhook).subscribe(result => { + this.modal.hide(); + this.refreshList(); + }); + + } + +} diff --git a/src/app/appmarket/appmarket.module.ts b/src/app/appmarket/appmarket.module.ts index 1539264532ea45f31fb58cb059887338c5cecf57..ac973b85d8a36ae4f9d123d28f24a4473091244b 100644 --- a/src/app/appmarket/appmarket.module.ts +++ b/src/app/appmarket/appmarket.module.ts @@ -51,6 +51,9 @@ import { InputSwitchModule } from 'primeng/inputswitch'; import { OverlayPanelModule } from 'primeng/overlaypanel'; import { SidebarModule } from 'primeng/sidebar'; import { ProgressBarModule } from 'primeng/progressbar'; +import { WebhookDetailsComponent } from './admin/webhook/webhook-details/webhook-details.component'; +import { WebhookListComponent } from './admin/webhook/webhook-list/webhook-list.component'; +import { WebhookService } from '../service/webhook.service'; @@ -67,7 +70,9 @@ import { ProgressBarModule } from 'primeng/progressbar'; BulkViewComponent, BulkAppListComponent, BulkListComponent, - BulkSearchPipe + BulkSearchPipe, + WebhookDetailsComponent, + WebhookListComponent ], imports: [ FormsModule, @@ -101,7 +106,7 @@ import { ProgressBarModule } from 'primeng/progressbar'; InputSwitchModule, OverlayPanelModule, SidebarModule, - ProgressBarModule + ProgressBarModule, ], exports: [ AppMarketComponent, @@ -116,6 +121,7 @@ import { ProgressBarModule } from 'primeng/progressbar'; ClusterService, SortService, SessionService, + WebhookService ], schemas: [ NO_ERRORS_SCHEMA, diff --git a/src/app/model/cluster-manager.ts b/src/app/model/cluster-manager.ts index 754b5332eb386b3659d8c031c93a678eebc48d22..f37b9747cb8cc223d1ee1d4d8b5f142cf582b270 100644 --- a/src/app/model/cluster-manager.ts +++ b/src/app/model/cluster-manager.ts @@ -15,4 +15,6 @@ export class ClusterManager { public externalNetworks: ClusterExtNetwork[] public domainNames: string[]; public state : string; + public currentStateSince: Date; + public contactEmail: string; } \ No newline at end of file diff --git a/src/app/model/webhook.ts b/src/app/model/webhook.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fd1c0a0bf4622c404a62fd69d9d27f18950fd2b --- /dev/null +++ b/src/app/model/webhook.ts @@ -0,0 +1,16 @@ +export class Webhook { + + public id: number = undefined; + public name: string = undefined; + public targetUrl: string = undefined; + public tokenValue: string = undefined; + public authorizationHeader: string = undefined; + public eventType: WebhookType = undefined; +} + +export enum WebhookType { + DOMAIN_CREATION = "DOMAIN_CREATION", + APPLICATION_DEPLOYMENT = "APPLICATION_DEPLOYMENT", + USER_ASSIGNMENT = "USER_ASSIGNMENT", + DOMAIN_GROUP_CHANGE = "DOMAIN_GROUP_CHANGE", +} \ No newline at end of file diff --git a/src/app/service/domain.service.ts b/src/app/service/domain.service.ts index 621496caa8a8f09d8660674a01cc7a163a59dfa9..7f6a3e2c51420794fc1317ac2e88ddabfdfdc7d8 100644 --- a/src/app/service/domain.service.ts +++ b/src/app/service/domain.service.ts @@ -19,12 +19,15 @@ export class DomainService extends GenericDataService { protected url: string; + protected urlGroups: string; + private updateRequiredFlag: boolean; constructor(http: HttpClient, appConfig: AppConfigService) { super(http, appConfig); this.updateRequiredFlag = false; this.url = this.appConfig.getApiUrl() + '/domains'; + this.urlGroups = this.appConfig.getApiUrl() + '/groups'; } public getGlobalDomainId(): number { @@ -93,35 +96,35 @@ export class DomainService extends GenericDataService { // GROUPS public getAllDomainGroups(): Observable<DomainGroup[]> { - return this.get<DomainGroup[]>(this.url + '/group'); + return this.get<DomainGroup[]>(this.urlGroups); } public getDomainGroup(domainGroupId: number): Observable<DomainGroup> { - return this.get<DomainGroup>(this.url + '/group/' + domainGroupId); + return this.get<DomainGroup>(this.urlGroups + '/' + domainGroupId); } public deleteDomainGroup(domainGroupId: number): Observable<void> { - return this.delete<void>(this.url + '/group/' + domainGroupId); + return this.delete<void>(this.urlGroups + '/' + domainGroupId); } public addDomainsToGroup(groupCodeName: string, domainIds: number[]): Observable<DomainGroup> { - return this.post(this.url + '/group/' + groupCodeName, domainIds); + return this.post(this.urlGroups + '/' + groupCodeName, domainIds); } public deleteDomainFromGroup(groupId: number, domainId: number): Observable<DomainGroup> { - return this.patch(this.url + '/group/' + groupId, domainId); + return this.patch(this.urlGroups + '/' + groupId, domainId); } public createDomainGroup(domainGroup: DomainGroup): Observable<Id> { - return this.post(this.url + '/group', domainGroup); + return this.post(this.urlGroups , domainGroup); } public updateDomainGroup(domainGroup: DomainGroup, id: number): Observable<Id> { - return this.put(this.url + '/group/' + id, domainGroup); + return this.put(this.urlGroups+ "/" + id, domainGroup); } public updateDomainGroupManagers(managers: User[], id: number): Observable<DomainGroup> { - return this.put(this.url + '/group/members/' + id, managers); + return this.put(this.urlGroups + '/' + id + "/members", managers); } public getAnnotations(): Observable<DomainAnnotation[]> { diff --git a/src/app/service/recaptcha-visibility.service.ts b/src/app/service/recaptcha-visibility.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a29d3a8ff8d69e3b72ef379e7c4d799a6de2677 --- /dev/null +++ b/src/app/service/recaptcha-visibility.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class RecaptchaVisibilityService { + showBadge(): void { + const badge = document.querySelector('.grecaptcha-badge') as HTMLElement; + if (badge) { + badge.style.visibility = 'visible'; + } + } + + hideBadge(): void { + const badge = document.querySelector('.grecaptcha-badge') as HTMLElement; + if (badge) { + badge.style.visibility = 'hidden'; + } + } +} diff --git a/src/app/service/webhook.service.ts b/src/app/service/webhook.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6dc30fc662bcf28a658225cc108bdf627f8a9d71 --- /dev/null +++ b/src/app/service/webhook.service.ts @@ -0,0 +1,38 @@ +import { HttpClient } from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {BehaviorSubject, Observable} from 'rxjs'; +import { AppConfigService } from './appconfig.service'; +import { GenericDataService } from './genericdata.service'; +import { Webhook } from '../model/webhook'; +import { Id } from '../model'; + +@Injectable() +export class WebhookService extends GenericDataService { + + + protected url: string; + + constructor(http: HttpClient, appConfig: AppConfigService) { + super(http, appConfig); + this.url = this.appConfig.getApiUrl() + '/webhooks'; + } + + + public getAll() : Observable<Webhook[]> { + return this.get<Webhook[]>(this.url); + } + + public create(webhook: Webhook) { + return this.post<Webhook, Id>(this.url, webhook); + } + + public getOne(id: number) { + return this.get<Webhook>(this.url + '/' + id); + } + + + public update(webhook: Webhook) { + return this.put<Webhook, Webhook>(this.url + '/' + webhook.id, webhook); + } + +} diff --git a/src/app/shared/about/about.component.ts b/src/app/shared/about/about.component.ts index c458c63c2cbf6669c972af2381517fd75a009701..dc297fd738d3337c36c1946eda0e5c20bb0f3878 100644 --- a/src/app/shared/about/about.component.ts +++ b/src/app/shared/about/about.component.ts @@ -1,23 +1,30 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {AppConfigService, ChangelogService} from '../../service'; import {GitInfo} from '../../model/gitinfo'; +import { RecaptchaVisibilityService } from '../../service/recaptcha-visibility.service'; @Component({ selector: 'app-about', templateUrl: './about.component.html', styleUrls: ['./about.component.css'] }) -export class AboutComponent implements OnInit { +export class AboutComponent implements OnInit, OnDestroy { public gitInfo: GitInfo; constructor(private changelogService: ChangelogService, - private appConfigService: AppConfigService) { + private appConfigService: AppConfigService, + private readonly recaptcha: RecaptchaVisibilityService) { } ngOnInit() { + this.recaptcha.showBadge(); if (this.appConfigService.getShowGitInfo()) { this.changelogService.getGitInfo().subscribe(info => this.gitInfo = info); } } + + ngOnDestroy(): void { + this.recaptcha.hideBadge(); + } } diff --git a/src/app/shared/admin/clusters/manager/manager.component.html b/src/app/shared/admin/clusters/manager/manager.component.html index 0fd942b39e5429ac00e97a38056af73c1fc57250..fbced4c7777792ce92c21edf168687483bd954cd 100644 --- a/src/app/shared/admin/clusters/manager/manager.component.html +++ b/src/app/shared/admin/clusters/manager/manager.component.html @@ -59,7 +59,7 @@ </a> <ul class="dropdown-menu pull-right-drop" > <li><a [routerLink]="[ cluster.id]" class=""> - {{ 'CLUSTERS.CONFIGURATION' | translate }}</a> + {{ 'CLUSTERS.DETAILS' | translate }}</a> </li> </ul> </span> @@ -81,17 +81,30 @@ </div> <div class="mt-4"> - <label for="name">{{'CLUSTERS.DESCRIPTION' | translate}}</label> - <input id="name" type="text" class="form-control" [(ngModel)]="addedCluster.description" [ngModelOptions]="{standalone: true}"> + <label for="desc">{{'CLUSTERS.DESCRIPTION' | translate}}</label> + <input id="desc" type="text" class="form-control" [(ngModel)]="addedCluster.description" [ngModelOptions]="{standalone: true}"> </div> + <div class="mt-4"> + <label for="contactEmail">{{'CLUSTERS.CONTACT_MAIL' | translate}}</label> + <input id="contactEmail" type="text" class="form-control" [(ngModel)]="addedCluster.contactEmail" [ngModelOptions]="{standalone: true}"> + </div> + <div class="mt-4"> + + <div> + <label for="assignDomain">Assign to domain? </label> + <input type="checkbox" [(ngModel)]="assignedDomain" (change)="onDomainChange($event)" id="assignDomain" /> + </div> + <div class="mt-4" *ngIf="assignedDomain"> <label for="name">{{'CLUSTERS.DOMAIN' | translate}}</label> - <select id="domain" #domainSelect class="form-control" (change)="onDomainSelection(domainSelect.value)"> + <select id="domain" #domainSelect class="form-control" (change)="onDomainSelection(domainSelect.value)" plaaceholder="testtesttest" > <option *ngFor="let domain of domains" [value]="domain.name" >{{domain.name}} </option> </select> + </div> + </div> @@ -106,7 +119,7 @@ <div class="nmaas-modal-footer"> <button type="button" class="btn btn-secondary" (click)="this.modal.hide()">{{'UNDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> - <button type="button" class="btn btn-primary" [disabled]="updatedFile === null || addedCluster?.name === null || addedCluster?.description === null"(click)="closeModalAndSaveCluster()" + <button type="button" class="btn btn-primary" [disabled]="updatedFile === null || addedCluster?.name === null || addedCluster?.description === null"(click)="closeModalAndSaveCluster()" pTooltip="Upload file is required before save" showDelay="2000" >{{'CLUSTERS.SAVE' | translate}}</button> diff --git a/src/app/shared/admin/clusters/manager/manager.component.ts b/src/app/shared/admin/clusters/manager/manager.component.ts index 75113f3aee51cc7221c4027fa089606ea8f8404c..1e8c8938bca5d9ab01c66b444b5aad8142e7e4c7 100644 --- a/src/app/shared/admin/clusters/manager/manager.component.ts +++ b/src/app/shared/admin/clusters/manager/manager.component.ts @@ -14,8 +14,9 @@ export class ClusterManagerComponent { public clusters: ClusterManager[] = []; public addedCluster: ClusterManager = new ClusterManager(); - public updatedFile : File = null; + public updatedFile: File = null; public maxItemsOnPage = 15; + public assignedDomain: boolean = false; public searchValue = ''; filteredClusters: ClusterManager[] = []; @@ -34,7 +35,7 @@ export class ClusterManagerComponent { public saveFile(event: any) { console.log(event); - this.updatedFile =event.files[0]; + this.updatedFile = event.files[0]; } public getAllClusters() { @@ -58,10 +59,23 @@ export class ClusterManagerComponent { } - public onDomainSelection(event: any) { - console.log(event); - this.addedCluster.domainNames = [event] - } +public onDomainSelection(event: any) { + + console.log(event); + this.addedCluster.domainNames = [event] +} + +public openModal() { + if (this.domains.length > 0) { + this.addedCluster.domainNames = [this.domains[0].name]; + } + this.modal.show(); +} + +public onDomainChange(event: any) { +console.log(event); +} + filterClusters() { const value = this.searchValue?.toLowerCase() || ''; this.filteredClusters = this.clusters.filter(cluster => @@ -70,5 +84,4 @@ export class ClusterManagerComponent { cluster.id?.toString().includes(value) ); } - } diff --git a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html index 28edc9a564b5b7dc72d04895c8c721df681298a3..9f7c872af6f0fe143d19d6bf09102cd51986309c 100644 --- a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html +++ b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html @@ -70,6 +70,16 @@ </div> </div> + <div class="form-group"> + <label for="state" class="col-sm-2 control-label">{{ 'CLUSTERS.STATE_SINCE' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="state" name="state" + [ngModel]="formatDate(cluster.currentStateSince)" [disabled]="true"> + </div> + </div> + </div> + <div class="form-group"> <label for="clusterCreationDate" class="col-sm-2 control-label">{{ 'CLUSTERS.CREATION_DATE' | translate }}</label> diff --git a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts index eef78c6c0a8abdad85584608c13238ef30aa22eb..b9b928804c25a5b8ba4817fc5bab13369cef47cf 100644 --- a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts +++ b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts @@ -22,6 +22,8 @@ describe('ClusterManagerDetailsComponent', () => { id: 1, name: 'Test Cluster', state: "UP", + currentStateSince: new Date('2025-01-01'), + contactEmail: "test@test.test", description: 'Test Description', externalNetworks: [], creationDate: new Date('2025-01-01'), @@ -171,6 +173,8 @@ describe('ClusterManagerDetailsComponent', () => { id: 1, name: 'Test Cluster', state:"UP", + currentStateSince: new Date('2025-01-01'), + contactEmail: "test@test.test", description: 'Test Description', externalNetworks: [], creationDate: new Date('2025-01-01'), diff --git a/src/app/shared/left-menu/left-menu.component.html b/src/app/shared/left-menu/left-menu.component.html index 35fb1012e9652c3a94fea38b6afcbb7f6cc3945b..e6d42129afd36bf4e7eac24996e0cdd34bb8d01e 100644 --- a/src/app/shared/left-menu/left-menu.component.html +++ b/src/app/shared/left-menu/left-menu.component.html @@ -119,9 +119,17 @@ </span> </a> </li> + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/webhooks']"> + <i class="pi pi-share-alt" style="margin-right:10px; font-size: 18px" title="Settings"></i> + <span *ngIf="!isCollapsed"> + {{ 'WEBHOOKS.TITLE_SHORT' | translate }} + </span> + </a> + </li> <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> <a style="display: flex; align-items: center;" [routerLink]="['/admin/manage/clusters']"> - <i class="pi pi-cog" style="margin-right:10px; font-size: 18px"></i> + <i class="pi pi-cloud" style="margin-right:10px; font-size: 18px"></i> <span *ngIf="!isCollapsed"> {{ 'CLUSTERS.CONFIGURATION' | translate }} </span> @@ -129,7 +137,7 @@ </li> <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> <a style="display: flex; align-items: center;" [routerLink]="['/admin/languages']"> - <i class="pi pi-tags" style="margin-right:10px; font-size: 18px" title=" {{'NAVBAR.LANGUAGES' | translate }}"></i> + <i class="pi pi-language" style="margin-right:10px; font-size: 18px" title=" {{'NAVBAR.LANGUAGES' | translate }}"></i> <span *ngIf="!isCollapsed"> {{'NAVBAR.LANGUAGES' | translate }} </span> diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 20ffb98d661d7af399933fb60a6111bbcaf9bf74..167728209e92bacbd66f831a2f55b99259d32bf6 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -72,6 +72,7 @@ import { BrowserModule } from '@angular/platform-browser'; import {ChartModule} from 'primeng/chart'; import { RolesExcludedDirective } from '../directive/roles-exluded.directive'; import { FileUploadModule } from 'primeng/fileupload'; +import { RecaptchaVisibilityService } from '../service/recaptcha-visibility.service'; @@ -155,6 +156,7 @@ import { FileUploadModule } from 'primeng/fileupload'; PasswordValidator, UserDataService, NotificationService, + RecaptchaVisibilityService, AppConfigService, DatePipe, { diff --git a/src/app/welcome/welcome.component.ts b/src/app/welcome/welcome.component.ts index 6e8a863fd7e15aad123a0844796c5ecf508c537b..29d846e93716cd6e71a8cdcc0cd706b4bafbe2b1 100644 --- a/src/app/welcome/welcome.component.ts +++ b/src/app/welcome/welcome.component.ts @@ -1,14 +1,15 @@ import {AppConfigService} from '../service/appconfig.service'; -import {AfterContentChecked, AfterViewChecked, Component, OnInit,} from '@angular/core'; +import {AfterContentChecked, AfterViewChecked, Component, OnDestroy, OnInit,} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {ServiceUnavailableService} from '../service-unavailable/service-unavailable.service'; +import { RecaptchaVisibilityService } from '../service/recaptcha-visibility.service'; @Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.css'] }) -export class WelcomeComponent implements OnInit, AfterViewChecked, AfterContentChecked { +export class WelcomeComponent implements OnInit, AfterViewChecked, AfterContentChecked, OnDestroy { private height = 0; @@ -19,7 +20,8 @@ export class WelcomeComponent implements OnInit, AfterViewChecked, AfterContentC constructor(private appConfig: AppConfigService, public router: Router, private serviceHealth: ServiceUnavailableService, - private readonly route: ActivatedRoute) { + private readonly route: ActivatedRoute, + private readonly recaptcha: RecaptchaVisibilityService) { this.route.queryParams.subscribe(params => { console.log(params) if (params.logout !== undefined) { @@ -38,14 +40,21 @@ export class WelcomeComponent implements OnInit, AfterViewChecked, AfterContentC this.landingProfile = this.appConfig.getLandingProfile(); console.log("Landing profile = ", this.landingProfile) + this.recaptcha.showBadge(); } ngAfterContentChecked() { // this.onResize(); + this.recaptcha.showBadge(); } ngAfterViewChecked() { // this.onResize(); + this.recaptcha.showBadge(); + } + + ngOnDestroy(): void { + this.recaptcha.hideBadge(); } public onCloseBanner() { diff --git a/src/styles.css b/src/styles.css index a5f7a031d68219d6eb25f01f4d7ae8da9b245e5c..db65041caea971378fc49f73f4c65e48d62c3875 100644 --- a/src/styles.css +++ b/src/styles.css @@ -241,3 +241,8 @@ body .p-datatable .p-sortable-column.p-highlight{ body .p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon{ color:var(--primary-button-color); } + +.grecaptcha-badge { + visibility: hidden; + bottom: 45px !important; +} \ No newline at end of file