From 86f0de0c97f612078dc915cf239268bc27d59167 Mon Sep 17 00:00:00 2001 From: pes-spyro-soft-com Date: Thu, 14 Mar 2024 14:59:30 +0100 Subject: [PATCH] feat(integrations): create integration --- .../src/assets/integrations-banner.png | Bin 0 -> 39881 bytes .../gio-side-nav/gio-side-nav.component.ts | 6 + .../integrations/integration.fixture.ts | 34 ++++ .../create-integration.component.html | 105 ++++++++++++ .../create-integration.component.scss | 89 ++++++++++ .../create-integration.component.spec.ts | 154 ++++++++++++++++++ .../create-integration.component.ts | 77 +++++++++ .../create-integration.harness.ts | 44 +++++ .../integrations-routing.module.ts | 38 +++++ .../integrations/integrations.component.html | 107 ++++++++++++ .../integrations/integrations.component.scss | 87 ++++++++++ .../integrations.component.spec.ts | 136 ++++++++++++++++ .../integrations/integrations.component.ts | 79 +++++++++ .../integrations/integrations.harness.ts | 41 +++++ .../integrations/integrations.model.ts | 38 +++++ .../integrations/integrations.module.ts | 60 +++++++ .../management/management-routing.module.ts | 4 + .../services-ngx/integrations.service.spec.ts | 73 +++++++++ .../src/services-ngx/integrations.service.ts | 42 +++++ 19 files changed, 1214 insertions(+) create mode 100644 gravitee-apim-console-webui/src/assets/integrations-banner.png create mode 100644 gravitee-apim-console-webui/src/entities/integrations/integration.fixture.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.html create mode 100644 gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.scss create mode 100644 gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.spec.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.harness.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations-routing.module.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.component.html create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.component.scss create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.component.spec.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.component.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.harness.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.model.ts create mode 100644 gravitee-apim-console-webui/src/management/integrations/integrations.module.ts create mode 100644 gravitee-apim-console-webui/src/services-ngx/integrations.service.spec.ts create mode 100644 gravitee-apim-console-webui/src/services-ngx/integrations.service.ts diff --git a/gravitee-apim-console-webui/src/assets/integrations-banner.png b/gravitee-apim-console-webui/src/assets/integrations-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..0b9c8902144c1b6d836a4601864c74dceb7ee96d GIT binary patch literal 39881 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

h^IN{t`2pi+uX~!fh-VNhIQZNk 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 Ojgg&a3~J0>7v3Ex$`uFxeylF$u=Rm0x9rtN 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 Mt+j(y1;{_)zoQ#OdH?_b literal 0 HcmV?d00001 diff --git a/gravitee-apim-console-webui/src/components/gio-side-nav/gio-side-nav.component.ts b/gravitee-apim-console-webui/src/components/gio-side-nav/gio-side-nav.component.ts index b2fe9abc1cf..04b7124fb95 100644 --- a/gravitee-apim-console-webui/src/components/gio-side-nav/gio-side-nav.component.ts +++ b/gravitee-apim-console-webui/src/components/gio-side-nav/gio-side-nav.component.ts @@ -125,6 +125,12 @@ export class GioSideNavComponent implements OnInit, OnDestroy { displayName: 'APIs', category: 'Apis', }, + { + icon: 'gio:box', + routerLink: './integrations', + displayName: 'Integrations', + category: 'Integrations', + }, { icon: 'gio:multi-window', routerLink: './applications', diff --git a/gravitee-apim-console-webui/src/entities/integrations/integration.fixture.ts b/gravitee-apim-console-webui/src/entities/integrations/integration.fixture.ts new file mode 100644 index 00000000000..057cc19e9cb --- /dev/null +++ b/gravitee-apim-console-webui/src/entities/integrations/integration.fixture.ts @@ -0,0 +1,34 @@ +/* + * 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 { Integration } from '../../management/integrations/integrations.model'; + +export function fakeIntegration(attribute?: Partial) { + const base: Integration = { + id: 'test_id', + name: 'test_name', + description: 'test_description', + provider: 'test_provider', + owner: 'test_owner', + status: 'test_status', + agent: 'test_agent', + }; + + return { + ...base, + ...attribute, + }; +} diff --git a/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.html b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.html new file mode 100644 index 00000000000..7723f10210c --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.html @@ -0,0 +1,105 @@ + + +

+ + + + +
+
+

Create Integration

+
+ +
+ +
+
+ +
+
+ + +
+
General Information
+

Enter the general information for this new integration.

+
+
+ + + Name + Integration name is required. + Integration name has to be less than 50 characters long. + Integration name has to be more than 1 characters long. + +
+
+ + + Description + {{ input.value.length }}/250 + +
+
+
+
+ +
+ + + +
+
+
+
+
diff --git a/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.scss b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.scss new file mode 100644 index 00000000000..c3c698086bc --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.scss @@ -0,0 +1,89 @@ +/* + * 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. + */ + +@use 'sass:map'; +@use '@angular/material' as mat; +@use '@gravitee/ui-particles-angular' as gio; +@use '../../../scss/gio-layout' as gio-layout; + +$typography: map.get(gio.$mat-theme, typography); + +:host { + @include gio-layout.gio-responsive-content-container; +} + +.page-header { + margin-bottom: 24px; + + &__page-title { + @include mat.typography-level($typography, headline-6); + } + + &__description { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); + @include mat.typography-level($typography, body-2); + } +} + +.card-header { + display: flex; + justify-content: space-between; + padding-bottom: 24px; + + &__title { + @include mat.typography-level($typography, subtitle-1); + display: flex; + flex-direction: column; + justify-content: center; + + h3 { + margin: 0; + } + } +} + +.form { + &__body { + display: flex; + justify-content: center; + padding: 24px 0; + + .mat-mdc-card { + width: 600px; + border: 1px solid mat.get-color-from-palette(gio.$mat-dove-palette, 'darker10'); + box-shadow: 0 2px 4px 0 mat.get-color-from-palette(gio.$mat-dove-palette, 'darker20'); + } + } + + &__actions { + display: flex; + justify-content: space-between; + padding-top: 24px; + } + + .form-field { + width: 100%; + } +} + +textarea { + resize: none; +} + +.info, +.hint { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); +} diff --git a/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.spec.ts b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.spec.ts new file mode 100644 index 00000000000..5008e521089 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.spec.ts @@ -0,0 +1,154 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { InteractivityChecker } from '@angular/cdk/a11y'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatErrorHarness } from '@angular/material/form-field/testing'; + +import { CreateIntegrationComponent } from './create-integration.component'; +import { CreateIntegrationHarness } from './create-integration.harness'; + +import { IntegrationsModule } from '../integrations.module'; +import { CONSTANTS_TESTING, GioTestingModule } from '../../../shared/testing'; +import { SnackBarService } from '../../../services-ngx/snack-bar.service'; +import { CreateIntegrationPayload } from '../integrations.model'; + +describe('CreateIntegrationComponent', () => { + let fixture: ComponentFixture; + let componentHarness: CreateIntegrationHarness; + let httpTestingController: HttpTestingController; + + const fakeSnackBarService = { + error: jest.fn(), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CreateIntegrationComponent], + imports: [GioTestingModule, IntegrationsModule, BrowserAnimationsModule, NoopAnimationsModule], + providers: [ + { + provide: SnackBarService, + useValue: fakeSnackBarService, + }, + ], + }) + .overrideProvider(InteractivityChecker, { + useValue: { + isFocusable: () => true, // This traps focus checks and so avoid warnings when dealing with + isTabbable: () => true, // This traps focus checks and so avoid warnings when dealing with + }, + }) + .compileComponents(); + }); + + beforeEach(async () => { + fixture = TestBed.createComponent(CreateIntegrationComponent); + httpTestingController = TestBed.inject(HttpTestingController); + componentHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, CreateIntegrationHarness); + fixture.detectChanges(); + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + describe('form', () => { + it('should not submit integration with too short name', async () => { + await componentHarness.setName(''); + await componentHarness.setDescription('Some description'); + fixture.detectChanges(); + + const error: MatErrorHarness = await componentHarness.matErrorMessage(); + expect(await error.getText()).toEqual('Integration name is required.'); + + await componentHarness.clickOnSubmit(); + httpTestingController.expectNone(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + }); + + it('should not submit integration with too long name', async () => { + await componentHarness.setName('test too long name 01234567890123456789012345678901234567890123456789'); + await componentHarness.setDescription('Test Description'); + fixture.detectChanges(); + + const error: MatErrorHarness = await componentHarness.matErrorMessage(); + expect(await error.getText()).toEqual('Integration name has to be less than 50 characters long.'); + + await componentHarness.clickOnSubmit(); + httpTestingController.expectNone(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + }); + + it('should not submit integration with too long description', async () => { + await componentHarness.setName('test0'); + await componentHarness.setDescription( + 'TOO long description: loa hdvoiah dfopivioa fdo[ivu[au f[09vu a[09eu v9[ua09efu 0v9u e09fv u09qw uef09v uq0w9duf v0 qu0efdu 0vwu df09vu 0wduf09v wu0dfu v0 wud0fv uqw0 uf90v uw9efuv9wu efvu wqpefuvqwu e0fu v0wu ef0vu w0euf 0vqwu 0efu v0qwuef uvqw uefvru wfeuvwufvu w0 ufev', + ); + await componentHarness.clickOnSubmit(); + httpTestingController.expectNone(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + }); + + it('should create integration with valid name', async () => { + const expectedPayload: CreateIntegrationPayload = { + name: 'TEST123', + description: '', + provider: 'AWS', + }; + await componentHarness.setName('TEST123'); + await componentHarness.clickOnSubmit(); + expectIntegrationPostRequest(expectedPayload); + }); + + it('should create integration with description', async () => { + const expectedPayload: CreateIntegrationPayload = { + name: 'TEST123', + description: 'Test Description', + provider: 'AWS', + }; + await componentHarness.setName('TEST123'); + await componentHarness.setDescription('Test Description'); + await componentHarness.clickOnSubmit(); + + expectIntegrationPostRequest(expectedPayload); + }); + + it('should handle error with message', async () => { + await componentHarness.setName('TEST123'); + await componentHarness.clickOnSubmit(); + + expectIntegrationWithError(); + + fixture.detectChanges(); + + expect(fakeSnackBarService.error).toHaveBeenCalledWith('An error occurred. Integration not created'); + }); + }); + + function expectIntegrationPostRequest(payload: CreateIntegrationPayload): void { + const req = httpTestingController.expectOne(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + req.flush([]); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(payload); + } + + function expectIntegrationWithError(): void { + const req = httpTestingController.expectOne(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + expect(req.request.method).toEqual('POST'); + req.flush({}, { status: 400, statusText: 'Bad Request' }); + } +}); diff --git a/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.ts b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.ts new file mode 100644 index 00000000000..ea0a41d1421 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.ts @@ -0,0 +1,77 @@ +/* + * 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, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormBuilder, Validators } from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { catchError, tap } from 'rxjs/operators'; +import { EMPTY } from 'rxjs'; + +import { CreateIntegrationPayload } from '../integrations.model'; +import { IntegrationsService } from '../../../services-ngx/integrations.service'; +import { SnackBarService } from '../../../services-ngx/snack-bar.service'; + +@Component({ + selector: 'app-create-integration', + templateUrl: './create-integration.component.html', + styleUrls: ['./create-integration.component.scss'], +}) +export class CreateIntegrationComponent { + public isLoading = false; + private destroyRef = inject(DestroyRef); + + public informationForm = this.formBuilder.group({ + name: ['', [Validators.required, Validators.maxLength(50), Validators.minLength(1)]], + description: ['', Validators.maxLength(250)], + }); + + constructor( + private integrationsService: IntegrationsService, + private formBuilder: FormBuilder, + private readonly router: Router, + private activatedRoute: ActivatedRoute, + private snackBarService: SnackBarService, + ) {} + + public onSubmit(): void { + const payload: CreateIntegrationPayload = { + name: this.informationForm.controls.name.getRawValue(), + description: this.informationForm.controls.description.getRawValue(), + provider: 'AWS', + }; + + this.isLoading = true; + this.integrationsService + .createIntegration(payload) + .pipe( + tap(() => { + this.isLoading = false; + this.snackBarService.success(`Integration ${payload.name} created successfully`); + }), + catchError((_) => { + this.isLoading = false; + this.snackBarService.error('An error occurred. Integration not created'); + return EMPTY; + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => { + this.isLoading = false; + this.router.navigate(['..'], { relativeTo: this.activatedRoute }); + }); + } +} diff --git a/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.harness.ts b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.harness.ts new file mode 100644 index 00000000000..5e9f94dae2c --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.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 { ComponentHarness } from '@angular/cdk/testing'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { MatErrorHarness } from '@angular/material/form-field/testing'; + +export class CreateIntegrationHarness extends ComponentHarness { + public static readonly hostSelector = 'app-create-integration'; + + private nameInputLocator = this.locatorFor(MatInputHarness.with({ selector: '[data-testid=create-integration-name-input]' })); + private descriptionTextAreaLocator = this.locatorFor(MatInputHarness.with({ selector: '[data-testid=create-integration-description]' })); + private submitButtonLocator = this.locatorFor(MatButtonHarness.with({ selector: '[data-testid=create-integration-submit-button]' })); + + public async setName(name: string) { + return this.nameInputLocator().then((input: MatInputHarness) => input.setValue(name)); + } + + public async setDescription(description: string) { + return this.descriptionTextAreaLocator().then((input: MatInputHarness) => input.setValue(description)); + } + + public async clickOnSubmit() { + return this.submitButtonLocator().then(async (button: MatButtonHarness) => button.click()); + } + + public async matErrorMessage(): Promise { + return this.locatorFor(MatErrorHarness)(); + } +} diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations-routing.module.ts b/gravitee-apim-console-webui/src/management/integrations/integrations-routing.module.ts new file mode 100644 index 00000000000..f6412e24154 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations-routing.module.ts @@ -0,0 +1,38 @@ +/* + * 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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { IntegrationsComponent } from './integrations.component'; +import { CreateIntegrationComponent } from './create-integration/create-integration.component'; + +const routes: Routes = [ + { + path: '', + component: IntegrationsComponent, + }, + { + path: 'new', + component: CreateIntegrationComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class IntegrationsRoutingModule {} diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.component.html b/gravitee-apim-console-webui/src/management/integrations/integrations.component.html new file mode 100644 index 00000000000..005d7f61afd --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.html @@ -0,0 +1,107 @@ + + +
+ + + +
+
+

Integrations

+
+
+ +
+
+ + +
+
+ +
+
+

No integrations yet

+

Create an integration to start importing APIs and event streams from a 3rd-party provider.

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Table with Integrations +
Name{{ integration.name }}Owner{{ integration.owner || '' }}Provider{{ integration.provider }}Agent{{ integration.agent || '' }}Action + +
+ {{ 'Loading...' }} +
+
+
+
+
diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.component.scss b/gravitee-apim-console-webui/src/management/integrations/integrations.component.scss new file mode 100644 index 00000000000..491cdfabab5 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.scss @@ -0,0 +1,87 @@ +/* + * 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. + */ + +@use 'sass:map'; +@use '@angular/material' as mat; +@use '@gravitee/ui-particles-angular' as gio; +@use '../../scss/gio-layout' as gio-layout; + +$typography: map.get(gio.$mat-theme, typography); + +:host { + @include gio-layout.gio-responsive-content-container; +} + +.page-header { + margin-bottom: 24px; + + &__page-title { + @include mat.typography-level($typography, headline-6); + } + + &__description { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); + @include mat.typography-level($typography, body-2); + } +} + +.card-header { + display: flex; + justify-content: space-between; + padding-bottom: 24px; + + &__title { + @include mat.typography-level($typography, subtitle-1); + display: flex; + flex-direction: column; + justify-content: center; + + h3 { + margin: 0; + } + } +} + +.no-integrations { + &__img { + display: flex; + justify-content: center; + padding: 36px 0 24px 0; + + .banner { + width: 327px; + } + } + + &__message { + display: flex; + flex-direction: column; + padding: 12px 0 22px 0; + + .header { + @include mat.typography-level($typography, 'headline-6'); + align-self: center; + } + + .description { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); + @include mat.typography-level($typography, body-2); + width: 343px; + align-self: center; + text-align: center; + } + } +} diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.component.spec.ts b/gravitee-apim-console-webui/src/management/integrations/integrations.component.spec.ts new file mode 100644 index 00000000000..c11b55717d5 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.spec.ts @@ -0,0 +1,136 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { InteractivityChecker } from '@angular/cdk/a11y'; +import { HttpTestingController, TestRequest } from '@angular/common/http/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TestElement } from '@angular/cdk/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { MatPaginatorHarness } from '@angular/material/paginator/testing'; + +import { IntegrationsComponent } from './integrations.component'; +import { IntegrationsHarness } from './integrations.harness'; +import { Integration, IntegrationResponse } from './integrations.model'; +import { IntegrationsModule } from './integrations.module'; + +import { CONSTANTS_TESTING, GioTestingModule } from '../../shared/testing'; +import { fakeIntegration } from '../../entities/integrations/integration.fixture'; + +describe('IntegrationsComponent', () => { + let fixture: ComponentFixture; + let componentHarness: IntegrationsHarness; + let httpTestingController: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [IntegrationsComponent], + imports: [IntegrationsModule, GioTestingModule, BrowserAnimationsModule, NoopAnimationsModule, RouterTestingModule], + }) + .overrideProvider(InteractivityChecker, { + useValue: { + isFocusable: () => true, // This traps focus checks and so avoid warnings when dealing with + isTabbable: () => true, // This traps focus checks and so avoid warnings when dealing with + }, + }) + .compileComponents(); + }); + + beforeEach(async () => { + fixture = TestBed.createComponent(IntegrationsComponent); + httpTestingController = TestBed.inject(HttpTestingController); + componentHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, IntegrationsHarness); + fixture.componentInstance.filters = { + pagination: { index: 1, size: 10 }, + searchTerm: '', + }; + fixture.detectChanges(); + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + describe('table', () => { + it('should display correct number of rows', async () => { + const fakeIntegrations: Integration[] = [fakeIntegration(), fakeIntegration(), fakeIntegration()]; + const fakeIntegrationResponse: IntegrationResponse = { + data: fakeIntegrations, + pagination: {}, + }; + + expectIntegrationGetRequest(fakeIntegrationResponse); + + const rows = await componentHarness.rowsNumber(); + expect(rows).toEqual(fakeIntegrations.length); + }); + }); + + describe('pagination', () => { + it('should request proper url', async () => { + const fakeIntegrations: Integration[] = [fakeIntegration(), fakeIntegration(), fakeIntegration(), fakeIntegration()]; + const fakeIntegrationResponse: IntegrationResponse = { + data: fakeIntegrations, + pagination: {}, + }; + expectIntegrationGetRequest(fakeIntegrationResponse, 1, 10); + const pagination: MatPaginatorHarness = await componentHarness.getPagination(); + + await pagination.setPageSize(5); + expectIntegrationGetRequest(fakeIntegrationResponse, 1, 5); + pagination.getPageSize().then((value) => { + expect(value).toEqual(5); + }); + + await pagination.setPageSize(25); + expectIntegrationGetRequest(fakeIntegrationResponse, 1, 25); + pagination.getPageSize().then((value) => { + expect(value).toEqual(25); + }); + }); + }); + + describe('banner', () => { + it('should be visible when no integrations', async () => { + const fakeIntegrationResponse: IntegrationResponse = { + data: [], + pagination: {}, + }; + expectIntegrationGetRequest(fakeIntegrationResponse); + const banner: TestElement = await componentHarness.getBanner(); + expect(banner).toBeTruthy(); + }); + + it('should be hidden when integration are present', async () => { + const fakeIntegrationResponse: IntegrationResponse = { + data: [fakeIntegration()], + pagination: {}, + }; + expectIntegrationGetRequest(fakeIntegrationResponse); + const banner: TestElement = await componentHarness.getBanner(); + expect(banner).toBeFalsy(); + }); + }); + + function expectIntegrationGetRequest(fakeIntegrations: IntegrationResponse, page: number = 1, size: number = 10): void { + const req: TestRequest = httpTestingController.expectOne( + `${CONSTANTS_TESTING.env.v2BaseURL}/integrations/?page=${page}&perPage=${size}`, + ); + req.flush(fakeIntegrations); + expect(req.request.method).toEqual('GET'); + } +}); diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.component.ts b/gravitee-apim-console-webui/src/management/integrations/integrations.component.ts new file mode 100644 index 00000000000..965ab3184a5 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.ts @@ -0,0 +1,79 @@ +/* + * 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, inject, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { catchError, distinctUntilChanged, switchMap } from 'rxjs/operators'; +import { BehaviorSubject, EMPTY } from 'rxjs'; +import { isEqual } from 'lodash'; + +import { Integration, IntegrationResponse } from './integrations.model'; + +import { IntegrationsService } from '../../services-ngx/integrations.service'; +import { SnackBarService } from '../../services-ngx/snack-bar.service'; +import { GioTableWrapperFilters } from '../../shared/components/gio-table-wrapper/gio-table-wrapper.component'; + +@Component({ + selector: 'app-integrations', + templateUrl: './integrations.component.html', + styleUrls: ['./integrations.component.scss'], +}) +export class IntegrationsComponent implements OnInit { + private destroyRef: DestroyRef = inject(DestroyRef); + public isLoading: boolean = false; + public integrations: Integration[] = []; + public displayedColumns: string[] = ['name', 'owner', 'provider', 'agent', 'action']; + + public filters: GioTableWrapperFilters = { + pagination: { index: 1, size: 10 }, + searchTerm: '', + }; + public nbTotalInstances = this.integrations.length; + + private filters$ = new BehaviorSubject(this.filters); + + constructor( + private integrationsService: IntegrationsService, + private snackBarService: SnackBarService, + ) {} + + ngOnInit(): void { + this.filters$ + .pipe( + distinctUntilChanged(isEqual), + switchMap((filters: GioTableWrapperFilters) => { + this.isLoading = true; + return this.integrationsService.getIntegrations(filters.pagination.index, filters.pagination.size); + }), + catchError((_) => { + this.isLoading = false; + this.snackBarService.error('Something went wrong!'); + return EMPTY; + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((response: IntegrationResponse) => { + this.nbTotalInstances = response.pagination.totalCount; + this.integrations = response.data; + this.isLoading = false; + }); + } + + onFiltersChanged(filters: GioTableWrapperFilters) { + this.filters = { ...this.filters, ...filters }; + this.filters$.next(this.filters); + } +} diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.harness.ts b/gravitee-apim-console-webui/src/management/integrations/integrations.harness.ts new file mode 100644 index 00000000000..66705d10c86 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.harness.ts @@ -0,0 +1,41 @@ +/* + * 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 { ComponentHarness, TestElement } from '@angular/cdk/testing'; +import { MatRowHarness, MatTableHarness } from '@angular/material/table/testing'; +import { MatPaginatorHarness } from '@angular/material/paginator/testing'; + +export class IntegrationsHarness extends ComponentHarness { + public static readonly hostSelector = 'app-integrations'; + + private getTable = this.locatorForOptional(MatTableHarness); + private getBannerLocator = this.locatorForOptional('.no-integrations'); + private getPaginationLocator = this.locatorForOptional(MatPaginatorHarness); + + public rowsNumber = async (): Promise => { + return this.getTable() + .then((table: MatTableHarness) => table.getRows()) + .then((rows: MatRowHarness[]) => rows.length); + }; + + public getBanner = async (): Promise => { + return await this.getBannerLocator(); + }; + + public getPagination = async (): Promise => { + return await this.getPaginationLocator(); + }; +} diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.model.ts b/gravitee-apim-console-webui/src/management/integrations/integrations.model.ts new file mode 100644 index 00000000000..794a8496960 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.model.ts @@ -0,0 +1,38 @@ +/* + * 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 { Pagination } from '../../entities/management-api-v2'; + +export interface IntegrationResponse { + data: Integration[]; + pagination: Pagination; +} + +export interface Integration { + id: string; + name: string; + provider: string; + description: string; + owner?: string; + status?: string; + agent?: string; +} + +export interface CreateIntegrationPayload { + name: string; + description: string; + provider: string; +} diff --git a/gravitee-apim-console-webui/src/management/integrations/integrations.module.ts b/gravitee-apim-console-webui/src/management/integrations/integrations.module.ts new file mode 100644 index 00000000000..3d445e965d5 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.module.ts @@ -0,0 +1,60 @@ +/* + * 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatCard, MatCardContent, MatCardHeader } from '@angular/material/card'; +import { MatIcon } from '@angular/material/icon'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { MatError, MatFormField, MatHint, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatTableModule } from '@angular/material/table'; + +import { IntegrationsComponent } from './integrations.component'; +import { CreateIntegrationComponent } from './create-integration/create-integration.component'; +import { IntegrationsRoutingModule } from './integrations-routing.module'; + +import { GioTableWrapperModule } from '../../shared/components/gio-table-wrapper/gio-table-wrapper.module'; + +@NgModule({ + declarations: [IntegrationsComponent, CreateIntegrationComponent], + imports: [ + CommonModule, + ReactiveFormsModule, + MatCard, + MatCardHeader, + MatCardContent, + MatIcon, + MatButton, + MatError, + MatFormField, + MatHint, + MatInput, + MatLabel, + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatIconButton, + MatTooltip, + MatTableModule, + GioTableWrapperModule, + IntegrationsRoutingModule, + ], +}) +export class IntegrationsModule {} diff --git a/gravitee-apim-console-webui/src/management/management-routing.module.ts b/gravitee-apim-console-webui/src/management/management-routing.module.ts index ee91f44873b..a2b65ce2781 100644 --- a/gravitee-apim-console-webui/src/management/management-routing.module.ts +++ b/gravitee-apim-console-webui/src/management/management-routing.module.ts @@ -47,6 +47,10 @@ const managementRoutes: Routes = [ path: 'apis', loadChildren: () => import('./api/apis.module').then((m) => m.ApisModule), }, + { + path: 'integrations', + loadChildren: () => import('./integrations/integrations.module').then((m) => m.IntegrationsModule), + }, { path: 'settings', loadChildren: () => import('./settings/settings.module').then((m) => m.SettingsModule), diff --git a/gravitee-apim-console-webui/src/services-ngx/integrations.service.spec.ts b/gravitee-apim-console-webui/src/services-ngx/integrations.service.spec.ts new file mode 100644 index 00000000000..9c59ebeb7fe --- /dev/null +++ b/gravitee-apim-console-webui/src/services-ngx/integrations.service.spec.ts @@ -0,0 +1,73 @@ +/* + * 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 { TestBed } from '@angular/core/testing'; +import { HttpTestingController } from '@angular/common/http/testing'; + +import { IntegrationsService } from './integrations.service'; + +import { CONSTANTS_TESTING, GioTestingModule } from '../shared/testing'; +import { CreateIntegrationPayload, Integration } from '../management/integrations/integrations.model'; + +describe('IntegrationsService', () => { + const url = `${CONSTANTS_TESTING.env.v2BaseURL}/integrations`; + let service: IntegrationsService; + let httpTestingController: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [GioTestingModule], + }); + service = TestBed.inject(IntegrationsService); + + httpTestingController = TestBed.inject(HttpTestingController); + service = TestBed.inject(IntegrationsService); + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + describe('get integrations', () => { + it('should call API', () => { + const fakeData: Integration[] = null; + + service.getIntegrations(1, 10).subscribe((res) => { + expect(res).toMatchObject(fakeData); + }); + + httpTestingController.expectOne({ method: 'GET', url: url + '/?page=1&perPage=10' }).flush(fakeData); + }); + }); + + describe('create', () => { + it('should call API', (done) => { + const fakeData: CreateIntegrationPayload = { + name: 'test_name', + description: 'test_description', + provider: 'test_provider', + }; + + service.createIntegration(fakeData).subscribe(() => { + done(); + }); + + const req = httpTestingController.expectOne({ method: 'POST', url: url }); + req.flush(null); + expect(req.request.body).toEqual(fakeData); + }); + }); +}); diff --git a/gravitee-apim-console-webui/src/services-ngx/integrations.service.ts b/gravitee-apim-console-webui/src/services-ngx/integrations.service.ts new file mode 100644 index 00000000000..ee807c2cd4f --- /dev/null +++ b/gravitee-apim-console-webui/src/services-ngx/integrations.service.ts @@ -0,0 +1,42 @@ +/* + * 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 { Inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { CreateIntegrationPayload, Integration, IntegrationResponse } from '../management/integrations/integrations.model'; +import { Constants } from '../entities/Constants'; + +@Injectable({ + providedIn: 'root', +}) +export class IntegrationsService { + private url: string = `${this.constants.env.v2BaseURL}/integrations`; + + constructor( + private readonly httpClient: HttpClient, + @Inject(Constants) private readonly constants: Constants, + ) {} + + getIntegrations(page, size): Observable { + return this.httpClient.get(`${this.url}/?page=${page}&perPage=${size}`); + } + + createIntegration(payload: CreateIntegrationPayload): Observable { + return this.httpClient.post(this.url, payload); + } +}