From 2f3a5c15579b398b28dab40006b74d9080593cb6 Mon Sep 17 00:00:00 2001 From: jourdiw Date: Fri, 15 Nov 2024 18:03:12 +0100 Subject: [PATCH 1/8] refactor(console): generalize host validation --- .../gio-form-listeners-tcp-hosts.component.ts | 8 ++++---- .../api-general-info-duplicate-dialog.component.ts | 6 +++--- .../host-async-validator.directive.spec.ts} | 10 +++++----- .../host-async-validator.directive.ts} | 2 +- .../host-sync-validator.directive.spec.ts} | 8 ++++---- .../host-sync-validator.directive.ts} | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) rename gravitee-apim-console-webui/src/shared/validators/{tcp-hosts/tcp-host-async-validator.directive.spec.ts => host/host-async-validator.directive.spec.ts} (91%) rename gravitee-apim-console-webui/src/shared/validators/{tcp-hosts/tcp-host-async-validator.directive.ts => host/host-async-validator.directive.ts} (92%) rename gravitee-apim-console-webui/src/shared/validators/{tcp-hosts/tcp-host-sync-validator.directive.spec.ts => host/host-sync-validator.directive.spec.ts} (84%) rename gravitee-apim-console-webui/src/shared/validators/{tcp-hosts/tcp-host-sync-validator.directive.ts => host/host-sync-validator.directive.ts} (93%) diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-tcp-hosts/gio-form-listeners-tcp-hosts.component.ts b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-tcp-hosts/gio-form-listeners-tcp-hosts.component.ts index 53ac830636f..8a1d2a0e93d 100644 --- a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-tcp-hosts/gio-form-listeners-tcp-hosts.component.ts +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-tcp-hosts/gio-form-listeners-tcp-hosts.component.ts @@ -32,8 +32,8 @@ import { asyncScheduler, Observable, Subject } from 'rxjs'; import { TcpHost } from '../../../../../entities/management-api-v2/api/v4/tcpHost'; import { ApiV2Service } from '../../../../../services-ngx/api-v2.service'; -import { tcpHostSyncValidator } from '../../../../../shared/validators/tcp-hosts/tcp-host-sync-validator.directive'; -import { tcpHostAsyncValidator } from '../../../../../shared/validators/tcp-hosts/tcp-host-async-validator.directive'; +import { hostSyncValidator } from '../../../../../shared/validators/host/host-sync-validator.directive'; +import { hostAsyncValidator } from '../../../../../shared/validators/host/host-async-validator.directive'; @Component({ selector: 'gio-form-listeners-tcp-hosts', @@ -149,8 +149,8 @@ export class GioFormListenersTcpHostsComponent implements OnInit, OnDestroy, Con public newListenerFormGroup(listener: TcpHost) { return new FormGroup({ host: new FormControl(listener.host || '', { - validators: [tcpHostSyncValidator], - asyncValidators: [tcpHostAsyncValidator(this.apiV2Service, this.apiId)], + validators: [hostSyncValidator], + asyncValidators: [hostAsyncValidator(this.apiV2Service, this.apiId)], }), }); } diff --git a/gravitee-apim-console-webui/src/management/api/general-info/api-general-info-duplicate-dialog/api-general-info-duplicate-dialog.component.ts b/gravitee-apim-console-webui/src/management/api/general-info/api-general-info-duplicate-dialog/api-general-info-duplicate-dialog.component.ts index 92f266128db..04345346f35 100644 --- a/gravitee-apim-console-webui/src/management/api/general-info/api-general-info-duplicate-dialog/api-general-info-duplicate-dialog.component.ts +++ b/gravitee-apim-console-webui/src/management/api/general-info/api-general-info-duplicate-dialog/api-general-info-duplicate-dialog.component.ts @@ -24,8 +24,8 @@ import { Api, ApiV2, ApiV4, DuplicateFilteredField, HttpListener, TcpListener } import { ApiService } from '../../../../services-ngx/api.service'; import { SnackBarService } from '../../../../services-ngx/snack-bar.service'; import { ApiV2Service } from '../../../../services-ngx/api-v2.service'; -import { tcpHostSyncValidator } from '../../../../shared/validators/tcp-hosts/tcp-host-sync-validator.directive'; -import { tcpHostAsyncValidator } from '../../../../shared/validators/tcp-hosts/tcp-host-async-validator.directive'; +import { hostSyncValidator } from '../../../../shared/validators/host/host-sync-validator.directive'; +import { hostAsyncValidator } from '../../../../shared/validators/host/host-async-validator.directive'; import { contextPathModePathSyncValidator } from '../../../../shared/validators/context-path/context-path-sync-validator.directive'; import { contextPathAsyncValidator } from '../../../../shared/validators/context-path/context-path-async-validator.directive'; @@ -90,7 +90,7 @@ export class ApiGeneralInfoDuplicateDialogComponent implements OnDestroy { if (dialogData.api.definitionVersion === 'V4') { if (dialogData.api.listeners?.find((listener) => listener.type === 'TCP')) { - this.duplicateApiForm.addControl('host', new FormControl('', [tcpHostSyncValidator], [tcpHostAsyncValidator(this.apiV2Service)])); + this.duplicateApiForm.addControl('host', new FormControl('', [hostSyncValidator], [hostAsyncValidator(this.apiV2Service)])); } else { this.duplicateApiForm.addControl( 'contextPath', diff --git a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-async-validator.directive.spec.ts b/gravitee-apim-console-webui/src/shared/validators/host/host-async-validator.directive.spec.ts similarity index 91% rename from gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-async-validator.directive.spec.ts rename to gravitee-apim-console-webui/src/shared/validators/host/host-async-validator.directive.spec.ts index 3ea2a92666d..83fdf98d48f 100644 --- a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-async-validator.directive.spec.ts +++ b/gravitee-apim-console-webui/src/shared/validators/host/host-async-validator.directive.spec.ts @@ -17,13 +17,13 @@ import { FormControl } from '@angular/forms'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { HttpTestingController } from '@angular/common/http/testing'; -import { tcpHostAsyncValidator } from './tcp-host-async-validator.directive'; +import { hostAsyncValidator } from './host-async-validator.directive'; import { ApiV2Service } from '../../../services-ngx/api-v2.service'; import { CONSTANTS_TESTING, GioTestingModule } from '../../testing'; import { Constants } from '../../../entities/Constants'; -describe('TcpHostAsyncValidator', () => { +describe('HostAsyncValidator', () => { const fakeConstants = CONSTANTS_TESTING; let httpTestingController: HttpTestingController; let apiV2Service: ApiV2Service; @@ -44,7 +44,7 @@ describe('TcpHostAsyncValidator', () => { it('should be invalid host', fakeAsync(async () => { const formControl = new FormControl('', { - asyncValidators: tcpHostAsyncValidator(apiV2Service), + asyncValidators: hostAsyncValidator(apiV2Service), }); formControl.markAsDirty(); formControl.patchValue('already-used-host'); @@ -58,7 +58,7 @@ describe('TcpHostAsyncValidator', () => { it('should be invalid host for api', fakeAsync(async () => { const formControl = new FormControl('', { - asyncValidators: tcpHostAsyncValidator(apiV2Service, 'api-id'), + asyncValidators: hostAsyncValidator(apiV2Service, 'api-id'), }); formControl.markAsDirty(); formControl.patchValue('already-used-host'); @@ -72,7 +72,7 @@ describe('TcpHostAsyncValidator', () => { it('should be valid host', fakeAsync(() => { const formControl = new FormControl('', { - asyncValidators: tcpHostAsyncValidator(apiV2Service), + asyncValidators: hostAsyncValidator(apiV2Service), }); formControl.markAsDirty(); formControl.patchValue('valid-host'); diff --git a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-async-validator.directive.ts b/gravitee-apim-console-webui/src/shared/validators/host/host-async-validator.directive.ts similarity index 92% rename from gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-async-validator.directive.ts rename to gravitee-apim-console-webui/src/shared/validators/host/host-async-validator.directive.ts index bc7ce9fe739..3a401951ed3 100644 --- a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-async-validator.directive.ts +++ b/gravitee-apim-console-webui/src/shared/validators/host/host-async-validator.directive.ts @@ -19,7 +19,7 @@ import { map, switchMap } from 'rxjs/operators'; import { ApiV2Service } from '../../../services-ngx/api-v2.service'; -export function tcpHostAsyncValidator(apiV2Service: ApiV2Service, apiId?: string): AsyncValidatorFn { +export function hostAsyncValidator(apiV2Service: ApiV2Service, apiId?: string): AsyncValidatorFn { return (formControl: FormControl): Observable => { if (formControl && formControl.dirty) { return timer(250).pipe( diff --git a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-sync-validator.directive.spec.ts b/gravitee-apim-console-webui/src/shared/validators/host/host-sync-validator.directive.spec.ts similarity index 84% rename from gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-sync-validator.directive.spec.ts rename to gravitee-apim-console-webui/src/shared/validators/host/host-sync-validator.directive.spec.ts index fb77a4f7d47..6295ddbcb89 100644 --- a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-sync-validator.directive.spec.ts +++ b/gravitee-apim-console-webui/src/shared/validators/host/host-sync-validator.directive.spec.ts @@ -15,9 +15,9 @@ */ import { FormControl } from '@angular/forms'; -import { tcpHostSyncValidator } from './tcp-host-sync-validator.directive'; +import { hostSyncValidator } from './host-sync-validator.directive'; -describe('TcpHostSyncValidator', () => { +describe('HostSyncValidator', () => { it.each` key | message | host ${'format'} | ${`Host is not valid`} | ${'ThisIsALongHostNameWithMoreThan63CharactersWhichIsNotValidInOurCase'} @@ -25,10 +25,10 @@ describe('TcpHostSyncValidator', () => { ${'required'} | ${`Host is required.`} | ${''} ${'required'} | ${`Host is required.`} | ${null} `('should be invalid host: $host because $message', ({ key, message, host }) => { - expect(tcpHostSyncValidator(new FormControl(host))).toEqual({ [key]: message }); + expect(hostSyncValidator(new FormControl(host))).toEqual({ [key]: message }); }); it('should be valid host', () => { - expect(tcpHostSyncValidator(new FormControl('host'))).toBeNull(); + expect(hostSyncValidator(new FormControl('host'))).toBeNull(); }); }); diff --git a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-sync-validator.directive.ts b/gravitee-apim-console-webui/src/shared/validators/host/host-sync-validator.directive.ts similarity index 93% rename from gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-sync-validator.directive.ts rename to gravitee-apim-console-webui/src/shared/validators/host/host-sync-validator.directive.ts index d4f97f2c878..25954b185d1 100644 --- a/gravitee-apim-console-webui/src/shared/validators/tcp-hosts/tcp-host-sync-validator.directive.ts +++ b/gravitee-apim-console-webui/src/shared/validators/host/host-sync-validator.directive.ts @@ -26,7 +26,7 @@ const HOST_PATTERN_REGEX = new RegExp( /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$/, ); -export const tcpHostSyncValidator: ValidatorFn = (formControl: FormControl): ValidationErrors | null => { +export const hostSyncValidator: ValidatorFn = (formControl: FormControl): ValidationErrors | null => { const host = formControl.value || ''; if (isEmpty(host.trim())) { return { required: 'Host is required.' }; From 3a6044034bf12e438994c1b1f0a8053e79934d89 Mon Sep 17 00:00:00 2001 From: jourdiw Date: Fri, 15 Nov 2024 18:03:48 +0100 Subject: [PATCH 2/8] feat(console): create kafka listener configuration control --- .../management-api-v2/api/v4/index.ts | 2 + .../management-api-v2/api/v4/kafkaHost.ts | 21 ++ .../management-api-v2/api/v4/kafkaPort.ts | 21 ++ ...m-listeners-kafka-host-port.component.html | 68 +++++ ...m-listeners-kafka-host-port.component.scss | 12 + ...isteners-kafka-host-port.component.spec.ts | 268 ++++++++++++++++++ ...orm-listeners-kafka-host-port.component.ts | 188 ++++++++++++ ...-form-listeners-kafka-host-port.harness.ts | 44 +++ ...-form-listeners-kafka-host-port.stories.ts | 43 +++ 9 files changed, 667 insertions(+) create mode 100644 gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaHost.ts create mode 100644 gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaPort.ts create mode 100644 gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.html create mode 100644 gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.scss create mode 100644 gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.spec.ts create mode 100644 gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.ts create mode 100644 gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.harness.ts create mode 100644 gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.stories.ts diff --git a/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/index.ts b/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/index.ts index 6b65ec47e3e..1e720e30b38 100644 --- a/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/index.ts +++ b/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/index.ts @@ -32,7 +32,9 @@ export * from './flowExecution'; export * from './flowV4'; export * from './httpListener'; export * from './httpSelector'; +export * from './kafkaHost'; export * from './kafkaListener'; +export * from './kafkaPort'; export * from './listener'; export * from './listenerType'; export * from './loggingContentV4'; diff --git a/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaHost.ts b/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaHost.ts new file mode 100644 index 00000000000..4901067a8bd --- /dev/null +++ b/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaHost.ts @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Used for Native Kafka API listeners + */ +export interface KafkaHost { + host?: string; +} diff --git a/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaPort.ts b/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaPort.ts new file mode 100644 index 00000000000..6e0b771fc58 --- /dev/null +++ b/gravitee-apim-console-webui/src/entities/management-api-v2/api/v4/kafkaPort.ts @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Used for Native Kafka API listeners + */ +export interface KafkaPort { + port?: number; +} diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.html b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.html new file mode 100644 index 00000000000..6df4d9a2cca --- /dev/null +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.html @@ -0,0 +1,68 @@ + + + + + + + + + + +
+ + Host + + @if (hostFormControl.hasError('required')) { + Host is required + } + @if (hostFormControl.hasError('listeners')) { + {{ hostFormControl.getError('listeners') }} + } + @if (hostFormControl.hasError('max')) { + {{ hostFormControl.getError('max') }} + } + @if (hostFormControl.hasError('format')) { + {{ hostFormControl.getError('format') }} + } + +
+
+ + Port + + @if (portFormControl.hasError('required')) { + Port is required + } + +
+ + +
+
+ Host and Port + +
+
+ Host can contain an uppercase letter, number, dot, dash or underscore. Total maximum length is 63 chars. +
+
diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.scss b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.scss new file mode 100644 index 00000000000..00ed66183e3 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.scss @@ -0,0 +1,12 @@ +.kafka-configuration { + display: flex; + width: 100%; + + &__host { + flex: 1 1 100%; + } + + &__port { + flex: 1 1 40%; + } +} diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.spec.ts b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.spec.ts new file mode 100644 index 00000000000..7275cfbeef2 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.spec.ts @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpTestingController } from '@angular/common/http/testing'; + +import { GioFormListenersKafkaHostPortHarness } from './gio-form-listeners-kafka-host-port.harness'; +import { GioFormListenersKafkaHostPortComponent } from './gio-form-listeners-kafka-host-port.component'; + +import { CONSTANTS_TESTING, GioTestingModule } from '../../../../../shared/testing'; +import { Constants } from '../../../../../entities/Constants'; + +@Component({ + template: `
`, +}) +class TestComponent { + public form = new FormGroup({ + kafka: new FormControl({}), + }); +} + +@Component({ + template: `
`, +}) +class TestComponentWithApiId { + public form = new FormGroup({ + kafka: new FormControl({}), + }); +} + +describe('GioFormListenersKafkaHostPortComponent', () => { + const fakeConstants = CONSTANTS_TESTING; + let fixture: ComponentFixture; + let loader: HarnessLoader; + let testComponent: TestComponent; + let httpTestingController: HttpTestingController; + + const KAFKA_CONFIG = { + host: { host: 'host1' }, + port: { port: 1000 }, + }; + + describe('without api id', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [ + GioFormListenersKafkaHostPortComponent, + NoopAnimationsModule, + MatIconTestingModule, + ReactiveFormsModule, + GioTestingModule, + ], + providers: [ + { + provide: Constants, + useValue: fakeConstants, + }, + ], + }); + fixture = TestBed.createComponent(TestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + testComponent = fixture.componentInstance; + httpTestingController = TestBed.inject(HttpTestingController); + fixture.detectChanges(); + }); + + afterEach(() => { + httpTestingController.verify({ ignoreCancelled: true }); + }); + + it('should display host + port', async () => { + testComponent.form.setValue({ kafka: KAFKA_CONFIG }); + + const formHarness = await loader.getHarness(GioFormListenersKafkaHostPortHarness); + + expect(await formHarness.getHostInput().then((host) => host.getValue())).toEqual(KAFKA_CONFIG.host.host); + expect(await formHarness.getPortInput().then((post) => post.getValue())).toEqual(`${KAFKA_CONFIG.port.port}`); + }); + + describe('Validation', () => { + it.each` + reason | isValid | host + ${'Host cannot be empty'} | ${false} | ${''} + ${'Host with only whitespace are considered empty'} | ${false} | ${' '} + ${'Total length should not be greater than 255 chars'} | ${false} | ${'a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host'} + ${'Simple host label should be lower than 63 chars'} | ${false} | ${'ThisIsALongHostNameWithMoreThan63CharactersWhichIsNotValidInOurCase'} + ${'A label within a hostname should not be more than 63 chars long'} | ${false} | ${'host.ThisIsALongHostNameWithMoreThan63CharactersWhichIsNotValidInOurCase.gravitee'} + ${'Can not start with dash'} | ${false} | ${'-simple-host'} + ${'Can not end with dash'} | ${false} | ${'simple-host-'} + ${'Host label can not end with dash'} | ${false} | ${'simple-host-.host'} + ${'Can not start with underscore'} | ${false} | ${'_simple-host'} + ${'Can not end with underscore'} | ${false} | ${'simple-host_'} + ${'Host label can not end with underscore'} | ${false} | ${'simple-host_.host'} + ${'IPv4 should be a valid host'} | ${true} | ${'127.0.0.1'} + ${'Can contain multiple host label'} | ${true} | ${'dev.simple-host.gravitee.io'} + ${'Can contain dash'} | ${true} | ${'simple-host'} + ${'Can contain underscore'} | ${true} | ${'simple_host'} + ${'Can contain dash and underscore'} | ${true} | ${'simple-host_underscored'} + ${'Can contain uppercase, numbers, dash and underscore'} | ${true} | ${'simple1-Host_underscored33'} + `('should validate `$reason`: is valid=$isValid', async ({ isValid, host }) => { + const formHarness = await loader.getHarness(GioFormListenersKafkaHostPortHarness); + const hostInput = await formHarness.getHostInput(); + + expect(await hostInput.getValue()).toEqual(''); + + await hostInput.setValue(host); + expect(await hostInput.host().then((host) => host.hasClass('ng-invalid'))).toEqual(!isValid); + if (isValid) { + expectVerifyHosts([host]); + } + }); + }); + + it('should edit host', async () => { + testComponent.form.setValue({ kafka: KAFKA_CONFIG }); + const formHarness = await loader.getHarness(GioFormListenersKafkaHostPortHarness); + + const hostRowToEdit = await formHarness.getHostInput(); + await hostRowToEdit.setValue('host-modified'); + expectVerifyHosts(['host-modified']); + + expect(testComponent.form.controls.kafka.value).toEqual({ ...KAFKA_CONFIG, host: { host: 'host-modified' } }); + }); + + it('should handle touched & dirty on focus and change value', async () => { + testComponent.form.reset(); + const formHarness = await loader.getHarness(GioFormListenersKafkaHostPortHarness); + + expect(testComponent.form.touched).toEqual(false); + expect(testComponent.form.dirty).toEqual(false); + + await formHarness.getHostInput().then((host) => host.focus()); + + expect(testComponent.form.touched).toEqual(true); + expect(testComponent.form.dirty).toEqual(false); + + await formHarness.getHostInput().then((host) => host.setValue('another-host')); + expectVerifyHosts(['another-host']); + + expect(testComponent.form.touched).toEqual(true); + expect(testComponent.form.dirty).toEqual(true); + }); + }); + + describe('with api id', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponentWithApiId], + imports: [ + GioFormListenersKafkaHostPortComponent, + NoopAnimationsModule, + MatIconTestingModule, + ReactiveFormsModule, + GioTestingModule, + ], + providers: [ + { + provide: Constants, + useValue: fakeConstants, + }, + ], + }); + fixture = TestBed.createComponent(TestComponentWithApiId); + loader = TestbedHarnessEnvironment.loader(fixture); + testComponent = fixture.componentInstance; + httpTestingController = TestBed.inject(HttpTestingController); + fixture.detectChanges(); + }); + + afterEach(() => { + httpTestingController.verify({ ignoreCancelled: true }); + }); + + describe('Validation', () => { + it.each` + reason | isValid | host + ${'Host cannot be empty'} | ${false} | ${''} + ${'Host with only whitespace are considered empty'} | ${false} | ${' '} + ${'Total length should not be greater than 255 chars'} | ${false} | ${'a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host.a-valid-sub-host'} + ${'Simple host label should be lower than 63 chars'} | ${false} | ${'ThisIsALongHostNameWithMoreThan63CharactersWhichIsNotValidInOurCase'} + ${'A label within a hostname should not be more than 63 chars long'} | ${false} | ${'host.ThisIsALongHostNameWithMoreThan63CharactersWhichIsNotValidInOurCase.gravitee'} + ${'Can not start with dash'} | ${false} | ${'-simple-host'} + ${'Can not end with dash'} | ${false} | ${'simple-host-'} + ${'Host label can not end with dash'} | ${false} | ${'simple-host-.host'} + ${'Can not start with underscore'} | ${false} | ${'_simple-host'} + ${'Can not end with underscore'} | ${false} | ${'simple-host_'} + ${'Host label can not end with underscore'} | ${false} | ${'simple-host_.host'} + ${'IPv4 should be a valid host'} | ${true} | ${'127.0.0.1'} + ${'Can contain multiple host label'} | ${true} | ${'dev.simple-host.gravitee.io'} + ${'Can contain dash'} | ${true} | ${'simple-host'} + ${'Can contain underscore'} | ${true} | ${'simple_host'} + ${'Can contain dash and underscore'} | ${true} | ${'simple-host_underscored'} + ${'Can contain uppercase, numbers, dash and underscore'} | ${true} | ${'simple1-Host_underscored33'} + `('should validate `$reason`: is valid=$isValid', async ({ isValid, host }) => { + const formHarness = await loader.getHarness(GioFormListenersKafkaHostPortHarness); + const hostInput = await formHarness.getHostInput(); + + expect(await hostInput.getValue()).toEqual(''); + + await hostInput.setValue(host); + expect(await hostInput.host().then((host) => host.hasClass('ng-invalid'))).toEqual(!isValid); + + if (isValid) { + expectVerifyHosts([host], true); + } + }); + + it('should send the API ID when verifying hosts', async () => { + const formPaths = await loader.getHarness(GioFormListenersKafkaHostPortHarness); + + const host = await formPaths.getHostInput(); + + expectApiVerify(); + httpTestingController.verify({ ignoreCancelled: true }); + + await host.setValue('host'); + expect(await host.host().then((hst) => hst.hasClass('ng-invalid'))).toEqual(false); + + const req = httpTestingController.match({ url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/_verify/hosts`, method: 'POST' }); + req + .filter((r) => !r.cancelled) + .forEach((req) => { + expect(req.request.body.apiId).toEqual('api-id'); + }); + httpTestingController.verify({ ignoreCancelled: true }); + }); + }); + }); + + const expectApiVerify = (inError = false) => { + httpTestingController + .match({ url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/_verify/hosts`, method: 'POST' }) + .filter((r) => !r.cancelled) + .map((c) => c.flush({ ok: !inError, reason: inError ? 'error reason' : '' })); + }; + + function expectVerifyHosts(hosts: string[], withApiId = false) { + const requests = httpTestingController.match({ url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/_verify/hosts`, method: 'POST' }); + hosts.forEach((host, index) => { + const request = requests[index]; + const expectedResult: { apiId?: string; hosts: [string] } = { hosts: [host] }; + if (withApiId) { + expectedResult.apiId = 'api-id'; + } + expect(request.request.body).toEqual(expectedResult); + if (!request.cancelled) request.flush({ ok: true }); + }); + } +}); diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.ts b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.ts new file mode 100644 index 00000000000..0e9d8e753e0 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component.ts @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, DestroyRef, ElementRef, forwardRef, inject, Input, OnInit } from '@angular/core'; +import { + AsyncValidator, + ControlValueAccessor, + FormBuilder, + FormControl, + FormControlStatus, + FormGroup, + NG_ASYNC_VALIDATORS, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { isEmpty } from 'lodash'; +import { combineLatestWith, filter, map, observeOn, startWith, take, tap } from 'rxjs/operators'; +import { FocusMonitor } from '@angular/cdk/a11y'; +import { asyncScheduler, Observable } from 'rxjs'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { GioIconsModule } from '@gravitee/ui-particles-angular'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { KafkaHost, KafkaPort } from '../../../../../entities/management-api-v2'; +import { hostAsyncValidator } from '../../../../../shared/validators/host/host-async-validator.directive'; +import { hostSyncValidator } from '../../../../../shared/validators/host/host-sync-validator.directive'; +import { ApiV2Service } from '../../../../../services-ngx/api-v2.service'; + +export interface KafkaHostPortData { + host?: KafkaHost; + port?: KafkaPort; +} + +@Component({ + selector: 'gio-form-listeners-kafka-host-port', + templateUrl: './gio-form-listeners-kafka-host-port.component.html', + styleUrls: ['../gio-form-listeners.common.scss', './gio-form-listeners-kafka-host-port.component.scss'], + imports: [MatInputModule, MatFormFieldModule, ReactiveFormsModule, MatIconModule, GioIconsModule, MatButtonModule, MatTooltipModule], + standalone: true, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GioFormListenersKafkaHostPortComponent), + multi: true, + }, + { + provide: NG_ASYNC_VALIDATORS, + useExisting: forwardRef(() => GioFormListenersKafkaHostPortComponent), + multi: true, + }, + ], +}) +export class GioFormListenersKafkaHostPortComponent implements OnInit, ControlValueAccessor, AsyncValidator { + @Input() + public apiId?: string; + + public host: KafkaHost; + public port: KafkaPort; + + public hostFormControl: FormControl; + public portFormControl: FormControl; + + public mainForm: FormGroup<{ host: FormControl; port: FormControl }>; + public isDisabled = false; + + private destroyRef = inject(DestroyRef); + + protected _onChange: (_listener: KafkaHostPortData) => void = () => ({}); + + protected _onTouched: () => void = () => ({}); + + constructor( + private readonly fm: FocusMonitor, + private readonly elRef: ElementRef, + private readonly apiV2Service: ApiV2Service, + private readonly fb: FormBuilder, + ) {} + + ngOnInit(): void { + this.hostFormControl = this.fb.control('', { + validators: [hostSyncValidator, Validators.required], + asyncValidators: [hostAsyncValidator(this.apiV2Service, this.apiId)], + }); + this.portFormControl = this.fb.control(0, [Validators.required]); + + this.mainForm = this.fb.group({ + host: this.hostFormControl, + port: this.portFormControl, + }); + + this.mainForm.valueChanges + .pipe( + tap((value) => this._onChange({ host: { host: value.host }, port: { port: value.port } })), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + + this.fm + .monitor(this.elRef.nativeElement, true) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this._onTouched(); + }); + } + + // From ControlValueAccessor interface + public writeValue(data: KafkaHostPortData | null = {}): void { + if (!data || isEmpty(data)) { + return; + } + + this.host = data.host; + this.port = data.port; + this.initForm(); + } + + // From ControlValueAccessor interface + public registerOnChange(fn: (host: KafkaHostPortData | null) => void): void { + this._onChange = fn; + } + + registerOnTouched(onTouched: () => void): void { + this._onTouched = onTouched; + } + + setDisabledState(disabled: boolean): void { + if (disabled) { + this.mainForm.disable(); + } else { + this.mainForm.enable(); + } + } + + public validate(): Observable { + return this.validateHostFormControl().pipe( + combineLatestWith(this.validatePortFormControl()), + map(() => (this.mainForm.controls.host.valid && this.mainForm.controls.port.valid ? null : { invalid: true })), + take(1), + ); + } + + protected getValue(): KafkaHostPortData { + const formData = this.mainForm.getRawValue(); + return { host: { host: formData.host }, port: { port: formData.port } }; + } + + private initForm(): void { + // Reset with current values + this.mainForm.reset({ host: this.host?.host, port: this.port?.port }); + + // Update controls + this.hostFormControl.updateValueAndValidity(); + this.portFormControl.updateValueAndValidity(); + } + + private validateHostFormControl(): Observable { + return this.hostFormControl.statusChanges.pipe( + observeOn(asyncScheduler), + startWith(this.hostFormControl.status), + filter(() => !this.mainForm.controls.host.pending), + ); + } + + private validatePortFormControl(): Observable { + return this.portFormControl.statusChanges.pipe( + startWith(this.portFormControl.status), + filter(() => !this.mainForm.controls.port.pending), + ); + } +} diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.harness.ts b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.harness.ts new file mode 100644 index 00000000000..2bb491062c3 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.harness.ts @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseHarnessFilters, ComponentHarness, HarnessPredicate } from '@angular/cdk/testing'; +import { MatInputHarness } from '@angular/material/input/testing'; + +export class GioFormListenersKafkaHostPortHarness extends ComponentHarness { + public static hostSelector = 'gio-form-listeners-kafka-host-port'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `GioFormListenersContextPathHarness` that meets + * certain criteria. + * + * @param options Options for filtering which input instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + public static with(options: BaseHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(GioFormListenersKafkaHostPortHarness, options); + } + + protected hostInput = this.locatorFor(MatInputHarness.with({ ancestor: '.kafka-configuration__host' })); + protected portInput = this.locatorFor(MatInputHarness.with({ ancestor: '.kafka-configuration__port' })); + + public getHostInput(): Promise { + return this.hostInput(); + } + + public getPortInput(): Promise { + return this.portInput(); + } +} diff --git a/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.stories.ts b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.stories.ts new file mode 100644 index 00000000000..26bf45edc8a --- /dev/null +++ b/gravitee-apim-console-webui/src/management/api/component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.stories.ts @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientModule } from '@angular/common/http'; + +import { GioFormListenersKafkaHostPortComponent } from './gio-form-listeners-kafka-host-port.component'; + +export default { + title: 'API / Listeners / Kafka / Form listeners Kafka host + port', + component: GioFormListenersKafkaHostPortComponent, + decorators: [ + moduleMetadata({ + imports: [BrowserAnimationsModule, GioFormListenersKafkaHostPortComponent, FormsModule, ReactiveFormsModule, HttpClientModule], + providers: [], + }), + ], + argTypes: {}, + render: (args) => ({ + template: ``, + props: args, + }), + args: { + listeners: {}, + }, +} as Meta; + +export const Default: StoryObj = {}; +Default.args = {}; From 063e76dcfe1cf8778285426edcea0a2e7799cb3e Mon Sep 17 00:00:00 2001 From: jourdiw Date: Fri, 15 Nov 2024 18:35:49 +0100 Subject: [PATCH 3/8] fix(console): make summary view values same size as key to align fonts --- .../steps/step-5-summary/step-5-summary.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.scss b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.scss index 7d5813788b5..35137370dcf 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.scss +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.scss @@ -49,6 +49,7 @@ $typography: map.get(gio.$mat-theme, typography); &__value { @include mat.m2-typography-level($typography, subtitle-2); color: mat.m2-get-color-from-palette(gio.$mat-accent-palette, 'darker20'); + line-height: 22px; } &__row { display: flex; From 5155f277c4415261d5fad4230e2871c6a829a2f4 Mon Sep 17 00:00:00 2001 From: jourdiw Date: Fri, 15 Nov 2024 18:36:29 +0100 Subject: [PATCH 4/8] refactor(console): re-word architecture choices in creation workflow --- .../step-2-entrypoints-0-architecture.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html index 0d58db4afd1..d5e2f7fa45a 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html @@ -33,12 +33,12 @@ [class.gio-selection__selected]="form.controls['type'].value === 'PROXY'" > - Proxy upstream Protocol + Proxy Generic Protocol

- This mode puts a proxy in front of upstream services. It passes the response through the gateway with policies on request - and response phases. + Choose this mode if you want to proxy an upstream HTTP or TCP service such as a REST API, SOAP service, WebSocket server or + gRPC service.

Can proxy @@ -57,13 +57,13 @@ -
Introspect Messages from Event-Driven Backend
+
Protocol Mediation

- This mode produces and consumes messages to event-driven services and allows for message transformation in between via - message policies and multiple entrypoints. + This mode serves incoming requests on a generic protocol such as HTTP and translates the request into a specialized backend + protocol such as Kafka or Solace.

Can expose From 487dae067fa3af969bb19c850bdc6f52a8ec8143 Mon Sep 17 00:00:00 2001 From: jourdiw Date: Fri, 15 Nov 2024 18:40:03 +0100 Subject: [PATCH 5/8] feat(console): include kafka choice in architecture list --- .../api-creation-v4-spec-stepper-helper.ts | 6 +- .../creation-v4/models/ApiCreationPayload.ts | 1 + ...-entrypoints-0-architecture.component.html | 24 +++++++ ...-entrypoints-0-architecture.component.scss | 4 ++ ...-2-entrypoints-0-architecture.component.ts | 68 +++++++++++++++++-- 5 files changed, 93 insertions(+), 10 deletions(-) diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/api-creation-v4-spec-stepper-helper.ts b/gravitee-apim-console-webui/src/management/api/creation-v4/api-creation-v4-spec-stepper-helper.ts index 1f293841c72..b492459fa6e 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/api-creation-v4-spec-stepper-helper.ts +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/api-creation-v4-spec-stepper-helper.ts @@ -27,7 +27,7 @@ import { Step3Endpoints2ConfigHarness } from './steps/step-3-endpoints/step-3-en import { Step4Security1PlansHarness } from './steps/step-4-security/step-4-security-1-plans.harness'; import { Step3EndpointListHarness } from './steps/step-3-endpoints/step-3-endpoints-1-list.harness'; -import { ApiType, ConnectorPlugin } from '../../../entities/management-api-v2'; +import { ConnectorPlugin } from '../../../entities/management-api-v2'; export class ApiCreationV4SpecStepperHelper { private ossLicense: License = { tier: 'oss', features: [], packs: [] }; @@ -50,9 +50,9 @@ export class ApiCreationV4SpecStepperHelper { await apiDetails.fillAndValidate(name, version, description); } - async fillAndValidateStep2_0_EntrypointsArchitecture(type: ApiType = 'MESSAGE') { + async fillAndValidateStep2_0_EntrypointsArchitecture(type: 'PROXY' | 'MESSAGE' | 'KAFKA' = 'MESSAGE') { const architecture = await this.harnessLoader.getHarness(Step2Entrypoints0ArchitectureHarness); - if (type === 'MESSAGE') { + if (type === 'MESSAGE' || type === 'KAFKA') { expect(await architecture.isLicenseBannerShown()).toEqual(true); } await architecture.fillAndValidate(type); diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/models/ApiCreationPayload.ts b/gravitee-apim-console-webui/src/management/api/creation-v4/models/ApiCreationPayload.ts index a8744c3f51a..ff4e72ec053 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/models/ApiCreationPayload.ts +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/models/ApiCreationPayload.ts @@ -24,6 +24,7 @@ export type ApiCreationPayload = Partial<{ // Entrypoints type?: ApiType; + selectedNativeType?: 'KAFKA'; paths?: PathV4[]; hosts?: TcpHost[]; selectedEntrypoints?: { diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html index d5e2f7fa45a..ec26e3e91e2 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-0-architecture.component.html @@ -82,6 +82,30 @@ >
+ + + +
+
Kafka Protocol
+ New +
+
+ + +

+ Choose this mode if you want to proxy the native Kafka protocol. In this case, the gateway acts as a Kafka broker to Kafka + clients. +

+
+
+
+ +
+
+
+

Configure common entrypoints fields

+ + +
diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.scss b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.scss index 21c6cc9e92f..c83ddb3e43b 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.scss +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.scss @@ -2,11 +2,13 @@ @use '@gravitee/ui-particles-angular' as gio; .step-2-entrypoints-2-config { - &__listeners-context__title { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; + &__listeners-context { + &__title { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } } } diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.ts b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.ts index 26d36aaa815..f746bba6bab 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.ts +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.component.ts @@ -26,10 +26,12 @@ import { Step3Endpoints1ListComponent } from '../step-3-endpoints/step-3-endpoin import { ApiCreationPayload } from '../../models/ApiCreationPayload'; import { Step3Endpoints2ConfigComponent } from '../step-3-endpoints/step-3-endpoints-2-config.component'; import { ConnectorPluginsV2Service } from '../../../../../services-ngx/connector-plugins-v2.service'; -import { PathV4, Qos } from '../../../../../entities/management-api-v2'; +import { KafkaHost, KafkaPort, PathV4, Qos } from '../../../../../entities/management-api-v2'; import { ApimFeature, UTMTags } from '../../../../../shared/components/gio-license/gio-license-data'; import { RestrictedDomainService } from '../../../../../services-ngx/restricted-domain.service'; import { TcpHost } from '../../../../../entities/management-api-v2/api/v4/tcpHost'; +import { Step5SummaryComponent } from '../step-5-summary/step-5-summary.component'; +import { KafkaHostPortData } from '../../../component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.component'; @Component({ selector: 'step-2-entrypoints-2-config', @@ -45,6 +47,7 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { public entrypointSchemas: Record; public hasHttpListeners: boolean; public hasTcpListeners: boolean; + public hasKafkaListeners: boolean; public enableVirtualHost: boolean; public domainRestrictions: string[] = []; public shouldUpgrade = false; @@ -67,7 +70,9 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { this.apiType = currentStepPayload.type; const paths = currentStepPayload.paths ?? []; - const hosts = currentStepPayload.hosts ?? []; + const tcpHosts = currentStepPayload.hosts ?? []; + const kafkaHost = currentStepPayload.host; + const kafkaPort = currentStepPayload.port; this.restrictedDomainService .get() @@ -81,7 +86,7 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { .subscribe(); this.formGroup = this.formBuilder.group({}); - this.initFormForSyncEntrypoints(currentStepPayload, paths, hosts); + this.initFormForSyncEntrypoints(currentStepPayload, paths, tcpHosts, kafkaHost, kafkaPort); currentStepPayload.selectedEntrypoints.forEach(({ id, configuration, selectedQos }) => { this.formGroup.addControl(`${id}-config`, this.formBuilder.control(configuration ?? {})); @@ -112,7 +117,13 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { }); } - private initFormForSyncEntrypoints(currentStepPayload: ApiCreationPayload, paths: PathV4[], hosts: any) { + private initFormForSyncEntrypoints( + currentStepPayload: ApiCreationPayload, + paths: PathV4[], + tcpHosts: TcpHost[], + kafkaHost: KafkaHost, + kafkaPort: KafkaPort, + ) { this.hasHttpListeners = currentStepPayload.selectedEntrypoints.find((entrypoint) => entrypoint.supportedListenerType === 'HTTP') != null; if (this.hasHttpListeners) { @@ -120,7 +131,13 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { } this.hasTcpListeners = currentStepPayload.selectedEntrypoints.find((entrypoint) => entrypoint.supportedListenerType === 'TCP') != null; if (this.hasTcpListeners) { - this.formGroup.addControl('hosts', this.formBuilder.control(hosts, Validators.required)); + this.formGroup.addControl('hosts', this.formBuilder.control(tcpHosts, Validators.required)); + } + this.hasKafkaListeners = currentStepPayload.selectedEntrypoints.some((entrypoint) => entrypoint.supportedListenerType === 'KAFKA'); + if (this.hasKafkaListeners) { + const initialKafkaControlValue: KafkaHostPortData = { host: kafkaHost, port: kafkaPort }; + + this.formGroup.addControl('kafka', this.formBuilder.control(initialKafkaControlValue, Validators.required)); } this.formGroup.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribe$)).subscribe(() => { @@ -137,20 +154,25 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { save(): void { const pathsValue = this.formGroup.get('paths')?.value ?? []; const hostsValues = this.formGroup.get('hosts')?.value ?? []; + const hostValue = this.formGroup.get('kafka')?.value?.host; + const portValue = this.formGroup.get('kafka')?.value?.port; this.stepService.validStep((previousPayload) => { let paths: PathV4[]; let hosts: TcpHost[]; + let host: KafkaHost; + let port: KafkaPort; if (this.selectedEntrypoints.some((entrypoint) => entrypoint.supportedListenerType === 'TCP')) { - paths = undefined; hosts = hostsValues; + } else if (this.selectedEntrypoints.some((entrypoint) => entrypoint.supportedListenerType === 'KAFKA')) { + host = hostValue; + port = portValue; } else { paths = this.enableVirtualHost ? // Remove host and overrideAccess from virualHost if is not necessary pathsValue.map(({ path, host, overrideAccess }) => ({ path, host, overrideAccess })) : // Clear private properties from gio-listeners-virtual-host component pathsValue.map(({ path }) => ({ path })); - hosts = undefined; } const selectedEntrypoints: ApiCreationPayload['selectedEntrypoints'] = previousPayload.selectedEntrypoints.map((entrypoint) => ({ @@ -159,13 +181,51 @@ export class Step2Entrypoints2ConfigComponent implements OnInit, OnDestroy { selectedQos: this.formGroup.get(`${entrypoint.id}-qos`)?.value, })); - return { ...previousPayload, paths, hosts, selectedEntrypoints }; - }); - // Skip step 3-list if api type is sync - this.stepService.goToNextStep({ - groupNumber: 3, - component: this.apiType === 'MESSAGE' ? Step3Endpoints1ListComponent : Step3Endpoints2ConfigComponent, + // TODO: Incorporate call to get native plugins + return { + ...previousPayload, + paths, + hosts, + host, + port, + selectedEntrypoints, + ...(this.apiType === 'NATIVE' + ? { + selectedEndpoints: [ + { + id: 'native-kafka', + name: 'Native Kafka Endpoint', + deployed: true, + icon: 'gio:kafka', + configuration: {}, + sharedConfiguration: {}, + }, + ], + } + : {}), + }; }); + + switch (this.apiType) { + case 'MESSAGE': + this.stepService.goToNextStep({ + groupNumber: 3, + component: Step3Endpoints1ListComponent, + }); + break; + case 'PROXY': + this.stepService.goToNextStep({ + groupNumber: 3, + component: Step3Endpoints2ConfigComponent, + }); + break; + case 'NATIVE': + this.stepService.goToNextStep({ + groupNumber: 5, + component: Step5SummaryComponent, + }); + break; + } } goBack(): void { diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.harness.ts b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.harness.ts index 638c2f0cc02..ed7a51993d7 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.harness.ts +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-2-entrypoints/step-2-entrypoints-2-config.harness.ts @@ -21,6 +21,7 @@ import { GioFormListenersVirtualHostHarness } from '../../../component/gio-form- import { Qos } from '../../../../../entities/management-api-v2'; import { GioFormQosHarness } from '../../../component/gio-form-qos/gio-form-qos.harness'; import { GioFormListenersTcpHostsHarness } from '../../../component/gio-form-listeners/gio-form-listeners-tcp-hosts/gio-form-listeners-tcp-hosts.harness'; +import { GioFormListenersKafkaHostPortHarness } from '../../../component/gio-form-listeners/gio-form-listeners-kafka/gio-form-listeners-kafka-host-port.harness'; export class Step2Entrypoints2ConfigHarness extends ComponentHarness { static hostSelector = 'step-2-entrypoints-2-config'; @@ -48,6 +49,8 @@ export class Step2Entrypoints2ConfigHarness extends ComponentHarness { protected getQosSelect = (entrypointId: string) => this.locatorFor(GioFormQosHarness.with({ selector: '[ng-reflect-id="' + entrypointId + '"]' })); + protected getKafkaHostPort = this.locatorFor(GioFormListenersKafkaHostPortHarness); + async clickPrevious(): Promise { return this.getPreviousButton().then((elt) => elt.click()); } @@ -125,4 +128,25 @@ export class Step2Entrypoints2ConfigHarness extends ComponentHarness { .then(async (elt) => elt != null && !(await elt.isDisabled())) .catch(() => false); } + + /** + * Kafka Listener configuration + */ + async hasKafkaListenersForm(): Promise { + return this.getKafkaHostPort() + .then((elt) => elt != null) + .catch(() => false); + } + + async fillHost(newHostValue: string): Promise { + return this.getKafkaHostPort() + .then((component) => component.getHostInput()) + .then((hostInput) => hostInput.setValue(newHostValue)); + } + + async fillPort(newPortValue: number): Promise { + return this.getKafkaHostPort() + .then((component) => component.getPortInput()) + .then((portInput) => portInput.setValue(`${newPortValue}`)); + } } From 97a727fe046605df86f36e309452480d74ffb9a2 Mon Sep 17 00:00:00 2001 From: jourdiw Date: Fri, 15 Nov 2024 18:43:36 +0100 Subject: [PATCH 7/8] feat(console): include kafka information in summary --- .../step-5-summary/step-5-summary.component.html | 14 +++++++++++--- .../step-5-summary/step-5-summary.component.ts | 6 +++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.html b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.html index 7b147d86216..a41361f700f 100644 --- a/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.html +++ b/gravitee-apim-console-webui/src/management/api/creation-v4/steps/step-5-summary/step-5-summary.component.html @@ -60,6 +60,14 @@ Host: {{ hosts.join(', ') }}
+
+ Host: + {{ host }} +
+
+ Port: + {{ port }} +
Type: {{ listenerType }} @@ -98,7 +106,7 @@
-
+
@@ -121,7 +129,7 @@ No plans are selected. -
+
@@ -144,7 +152,7 @@ >