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 00000000000..0b9c8902144 Binary files /dev/null and b/gravitee-apim-console-webui/src/assets/integrations-banner.png differ 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..8d85b1fe105 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.html @@ -0,0 +1,106 @@ + + +
+ + + + +
+
+

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..7851b38166b --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.scss @@ -0,0 +1,88 @@ +/* + * 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..ffe81add81e --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.component.spec.ts @@ -0,0 +1,127 @@ +/* + * 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'; +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 empty form', async () => { + await componentHarness.clickOnSubmit(); + httpTestingController.expectNone(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + }); + + it('should create integration with valid form', 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..1142d20af25 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.harness.ts @@ -0,0 +1,39 @@ +/* + * 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 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()); + } +} 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..b6a8378a3e5 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.html @@ -0,0 +1,114 @@ + + +
+ + + +
+
+

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..4a700ca917e --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/integrations.component.spec.ts @@ -0,0 +1,140 @@ +/* + * 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..0f3135495bf --- /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..3c4482fd576 --- /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().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..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); + } +}