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..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..c71dbce8fd1 --- /dev/null +++ b/gravitee-apim-console-webui/src/management/integrations/create-integration/create-integration.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 { 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 integration with too short name', async () => { + await componentHarness.clickOnSubmit(); + httpTestingController.expectNone(`${CONSTANTS_TESTING.env.v2BaseURL}/integrations`); + }); + + it('should not submit integration with too long name', async () => { + await componentHarness.setName('test01234567890123456789012345678901234567890123456789'); + 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..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..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); + } +}