From 91fbf331562022d049dca4eccbefcbd4c7d51140 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 | 18 +++ .../create-integration.component.html | 107 ++++++++++++++++ .../create-integration.component.scss | 81 ++++++++++++ .../create-integration.component.spec.ts | 78 ++++++++++++ .../create-integration.component.ts | 78 ++++++++++++ .../create-integration.harness.ts | 34 +++++ .../integrations-routing.module.ts | 40 ++++++ .../integrations/integrations.component.html | 115 +++++++++++++++++ .../integrations/integrations.component.scss | 83 ++++++++++++ .../integrations.component.spec.ts | 120 ++++++++++++++++++ .../integrations/integrations.component.ts | 81 ++++++++++++ .../integrations/integrations.harness.ts | 35 +++++ .../integrations/integrations.model.ts | 38 ++++++ .../integrations/integrations.module.ts | 65 ++++++++++ .../management/management-routing.module.ts | 4 + .../services-ngx/integrations.service.spec.ts | 74 +++++++++++ .../src/services-ngx/integrations.service.ts | 48 +++++++ 19 files changed, 1105 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..9149ce87b16 --- /dev/null +++ b/gravitee-apim-console-webui/src/entities/integrations/integration.fixture.ts @@ -0,0 +1,18 @@ +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..e5472604b83 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.html @@ -0,0 +1,107 @@ + + +

+ + + + + +
+
+

Create Integration

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

Enter the general information for this new integration.

+
+
+ + + Name + Name is required + +
+
+ + + 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..2e3ec244fc8 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.scss @@ -0,0 +1,81 @@ +/* + * 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 '@angular/material' as mat; +@use '@gravitee/ui-particles-angular' as gio; +@use '../../../scss/gio-layout' as gio-layout; + +:host { + @include gio-layout.gio-responsive-content-container +} + +.page-header { + margin-bottom: 24px; + + &__description { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); + } +} + +.card-header { + display: flex; + justify-content: space-between; + padding-bottom: 24px; + + &__title { + 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 var(--Dove-Darker10, #E7E8EF); + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.20); + } + } + + &__actions { + display: flex; + justify-content: space-between; + padding-top: 24px; + } + + .form-field { + width: 100%; + } +} + +textarea { + resize: none; +} + +.info { + 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..acc8377fa76 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.spec.ts @@ -0,0 +1,78 @@ +/* + * 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 { CreateIntegrationComponent } from './create-integration.component'; +import { CreateIntegrationHarness } from './create-integration.harness'; + +import { IntegrationsModule } from '../integrations.module'; +import { CONSTANTS_TESTING, GioTestingModule } from '../../../shared/testing'; + +describe('CreateIntegrationComponent', () => { + let fixture: ComponentFixture; + let componentHarness: CreateIntegrationHarness; + let httpTestingController: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CreateIntegrationComponent], + imports: [GioTestingModule, IntegrationsModule, BrowserAnimationsModule, NoopAnimationsModule] + }) + .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 empty form', async () => { + await componentHarness.clickOnSubmit(); + httpTestingController.expectNone(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + }); + + it('should create integration with valid form', async () => { + await componentHarness.setName('TEST123'); + await componentHarness.clickOnSubmit(); + expectIntegrationPostRequest(); + }) + }); + + function expectIntegrationPostRequest(): void { + const req = httpTestingController.expectOne(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + req.flush([]); + expect(req.request.method).toEqual('POST'); + } +}); 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..f031d47b0d0 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.ts @@ -0,0 +1,78 @@ +/* + * 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.minLength(1)], + description: [''] + }); + + 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..2e2403f6538 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.harness.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 { ComponentHarness } from '@angular/cdk/testing'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { MatButtonHarness } from '@angular/material/button/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 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 clickOnSubmit() { + return this.submitButtonLocator().then(async (button: MatButtonHarness) => button.click()); + } +} 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..bb234fa9647 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations-routing.module.ts @@ -0,0 +1,40 @@ +/* + * 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: 'create-integration', + 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..2b61bc05010 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.html @@ -0,0 +1,115 @@ + + +
+ + + +
+
+

Integrations

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

No integrations yet

+

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

+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{ integration.name }}Owner{{ integration.owner || 'Not Implemented' }}Provider{{ integration.provider }}Agent{{ integration.agent || 'Not Implemented' }}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..3a784cfe209 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.scss @@ -0,0 +1,83 @@ +/* + * 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 '@angular/material' as mat; +@use '@gravitee/ui-particles-angular' as gio; +@use '../../scss/gio-layout' as gio-layout; + +:host { + @include gio-layout.gio-responsive-content-container +} + +.page-header { + margin-bottom: 24px; + + &__description { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); + } +} + +.card-header { + display: flex; + justify-content: space-between; + padding-bottom: 24px; + + &__title { + 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 36px 0; + gap: 16px; + + .header { + align-self: center; + font-size: 26px; + font-weight: 700; + margin-bottom: 0; + } + + .description { + color: mat.get-color-from-palette(gio.$mat-space-palette, 'lighter40'); + width: 343px; + align-self: center; + font-size: 14px; + font-weight: 400; + text-align: center; + margin-bottom: 0; + } + } +} 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..5cdf59e2bf2 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.spec.ts @@ -0,0 +1,120 @@ +/* + * 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 { 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('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): void { + const req: TestRequest = httpTestingController.expectOne(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations/?page=1&size=10`); + 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..4317390887d --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.ts @@ -0,0 +1,81 @@ +/* + * 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 { + // toDo: Pagination is waiting for backed + this.filters$ + .pipe( + distinctUntilChanged(isEqual), + switchMap((filters: GioTableWrapperFilters) => { + this.isLoading = true; + return this.integrationsService.getIntegrations(filters.pagination.index - 1, filters.pagination.size); + }), + catchError((_) => { + this.isLoading = false; + this.snackBarService.error('An error occurred'); + 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..7bf4de0b834 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.harness.ts @@ -0,0 +1,35 @@ +/* + * 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'; + +export class IntegrationsHarness extends ComponentHarness { + public static readonly hostSelector = 'app-integrations'; + + private getTable = this.locatorForOptional(MatTableHarness); + private getBannerLocator = this.locatorForOptional('.no-integrations'); + + 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(); + } +} 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..6f4140095fa --- /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..26ac18896cc --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.module.ts @@ -0,0 +1,65 @@ +/* + * 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..b6ffbfebf74 --- /dev/null +++ b/gravitee-apim-console-webui/src/services-ngx/integrations.service.spec.ts @@ -0,0 +1,74 @@ +/* + * 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().subscribe((res) => { + expect(res).toMatchObject(fakeData); + }); + + httpTestingController.expectOne({ method: 'GET', url: url + '/?page=1&size=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..ff4715acf8f --- /dev/null +++ b/gravitee-apim-console-webui/src/services-ngx/integrations.service.ts @@ -0,0 +1,48 @@ +/* + * 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?: number, size?: number): Observable { + // toDo: Pagination to check after backend ready: + return this.httpClient.get(`${this.url}/?page=${page || 1}&size=${size || 10}`); + } + + createIntegration(payload: CreateIntegrationPayload): Observable { + return this.httpClient.post(this.url, payload) + } +}