Skip to content

Commit 36d34b0

Browse files
feat(console): update V4 APIs from import modal
1 parent e2afb79 commit 36d34b0

File tree

9 files changed

+195
-16
lines changed

9 files changed

+195
-16
lines changed

gravitee-apim-console-webui/src/management/api/general-info/api-general-info.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@
210210
*gioPermission="{ anyOf: ['api-definition-c'] }"
211211
mat-button
212212
class="details-card__actions_btn"
213-
[disabled]="isKubernetesOrigin || api.definitionVersion === 'V4' || api.definitionVersion === 'V1'"
213+
[disabled]="isKubernetesOrigin || api.definitionVersion === 'V1'"
214214
(click)="importApi()"
215215
>
216216
<mat-icon svgIcon="gio:download" />

gravitee-apim-console-webui/src/management/api/general-info/api-general-info.component.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { CategoryService } from '../../../services-ngx/category.service';
5151
import { PolicyService } from '../../../services-ngx/policy.service';
5252
import { SnackBarService } from '../../../services-ngx/snack-bar.service';
5353
import { GioApiImportDialogComponent, GioApiImportDialogData } from '../component/gio-api-import-dialog/gio-api-import-dialog.component';
54+
import { ApiImportV4Component, ApiImportV4DialogData } from '../import-v4/api-import-v4.component';
5455
import { GioPermissionService } from '../../../shared/components/gio-permission/gio-permission.service';
5556
import { ApiV2Service } from '../../../services-ngx/api-v2.service';
5657
import { Api, ApiType, ApiV2, ApiV4, UpdateApi, UpdateApiV2, UpdateApiV4 } from '../../../entities/management-api-v2';
@@ -394,6 +395,28 @@ export class ApiGeneralInfoComponent implements OnInit, OnDestroy {
394395
}
395396

396397
importApi() {
398+
if (this.api.definitionVersion === 'V4') {
399+
this.matDialog
400+
.open<ApiImportV4Component, ApiImportV4DialogData>(ApiImportV4Component, {
401+
data: { apiId: this.apiId },
402+
width: '700px',
403+
maxHeight: '90vh',
404+
autoFocus: 'dialog',
405+
role: 'dialog',
406+
id: 'importApiV4Dialog',
407+
})
408+
.afterClosed()
409+
.pipe(
410+
filter(apiId => !!apiId),
411+
tap(() => {
412+
this.refresh$.next();
413+
}),
414+
takeUntil(this.unsubscribe$),
415+
)
416+
.subscribe();
417+
return;
418+
}
419+
397420
this.policyService
398421
.listSwaggerPolicies()
399422
.pipe(

gravitee-apim-console-webui/src/management/api/import-v4/api-import-v4.component.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
-->
1818
<mat-card class="import">
1919
<mat-card-header>
20-
<mat-card-title>Import API</mat-card-title>
20+
<mat-card-title>{{ isUpdateMode ? 'Update API' : 'Import API' }}</mat-card-title>
2121
</mat-card-header>
2222
<mat-card-content>
2323
<form [formGroup]="form">
@@ -96,13 +96,17 @@ <h3>Options</h3>
9696
mat-raised-button
9797
color="primary"
9898
class="import__save-button"
99-
aria-label="Import API"
99+
[attr.aria-label]="isUpdateMode ? 'Update API' : 'Import API'"
100100
[disabled]="form.invalid || !importType"
101101
(click)="import()"
102102
>
103-
Import API
103+
{{ isUpdateMode ? 'Update API' : 'Import API' }}
104104
</button>
105-
<a mat-button mat-raised-button routerLink="../.." aria-label="Cancel">Cancel</a>
105+
@if (isUpdateMode) {
106+
<button mat-button mat-raised-button aria-label="Cancel" (click)="cancel()">Cancel</button>
107+
} @else {
108+
<a mat-button mat-raised-button routerLink="../.." aria-label="Cancel">Cancel</a>
109+
}
106110
</div>
107111
</form>
108112
</mat-card-content>

gravitee-apim-console-webui/src/management/api/import-v4/api-import-v4.component.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
}
77

88
.import {
9+
// When rendered inside a MatDialog the panel is constrained to maxHeight.
10+
// Ensure the card fills that height and scrolls rather than overflowing.
11+
max-height: 100%;
12+
overflow-y: auto;
13+
914
&__row {
1015
margin-top: 24px;
1116
}

gravitee-apim-console-webui/src/management/api/import-v4/api-import-v4.component.spec.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
1717
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
1818
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
1919
import { HttpTestingController } from '@angular/common/http/testing';
20+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
2021

21-
import { ApiImportV4Component } from './api-import-v4.component';
22+
import { ApiImportV4Component, ApiImportV4DialogData } from './api-import-v4.component';
2223
import { ApiImportV4Harness } from './api-import-v4.harness';
2324

2425
import { CONSTANTS_TESTING, GioTestingModule } from '../../../shared/testing';
@@ -140,3 +141,99 @@ describe('ImportV4Component', () => {
140141
.flush(policies);
141142
}
142143
});
144+
145+
describe('ImportV4Component - Update mode (opened as dialog)', () => {
146+
const API_ID = 'test-api-id';
147+
let fixture: ComponentFixture<ApiImportV4Component>;
148+
let componentHarness: ApiImportV4Harness;
149+
let httpTestingController: HttpTestingController;
150+
let dialogRef: MatDialogRef<ApiImportV4Component>;
151+
152+
beforeEach(async () => {
153+
await TestBed.configureTestingModule({
154+
imports: [NoopAnimationsModule, ApiImportV4Component, GioTestingModule],
155+
providers: [
156+
{ provide: MAT_DIALOG_DATA, useValue: { apiId: API_ID } as ApiImportV4DialogData },
157+
{ provide: MatDialogRef, useValue: { close: jest.fn() } },
158+
],
159+
}).compileComponents();
160+
161+
fixture = TestBed.createComponent(ApiImportV4Component);
162+
componentHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, ApiImportV4Harness);
163+
httpTestingController = TestBed.inject(HttpTestingController);
164+
dialogRef = TestBed.inject(MatDialogRef);
165+
fixture.detectChanges();
166+
httpTestingController
167+
.expectOne({
168+
url: `${CONSTANTS_TESTING.org.v2BaseURL}/plugins/policies`,
169+
method: 'GET',
170+
})
171+
.flush([fakePolicyPlugin({ id: 'oas-validation' })]);
172+
});
173+
174+
it('should show Update API button text in update mode', async () => {
175+
const saveButton = await componentHarness.getSaveButtonText();
176+
expect(saveButton).toContain('Update API');
177+
});
178+
179+
it('should call PUT endpoint when updating V4 API from Gravitee definition', async () => {
180+
const apiV4 = fakeApiV4({ definitionVersion: 'V4' });
181+
const importDefinition = JSON.stringify({ api: apiV4 });
182+
183+
await componentHarness.selectFormat('gravitee');
184+
await componentHarness.selectSource('local');
185+
await componentHarness.pickFiles([new File([importDefinition], 'gravitee-api-definition.json', { type: 'application/json' })]);
186+
expect(await componentHarness.isSaveDisabled()).toBeFalsy();
187+
188+
await componentHarness.save();
189+
const req = httpTestingController.expectOne({
190+
method: 'PUT',
191+
url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/${API_ID}/_import/definition`,
192+
});
193+
req.flush(fakeApiV4({ id: API_ID, definitionVersion: 'V4' }));
194+
195+
expect(dialogRef.close).toHaveBeenCalledWith(API_ID);
196+
});
197+
198+
it('should PUT OpenAPI update with withDocumentation and withOASValidationPolicy flags', async () => {
199+
await componentHarness.selectFormat('openapi');
200+
await componentHarness.selectSource('local');
201+
await componentHarness.pickFiles([new File(['openapi: 3.1.0'], 'openapi.yml', { type: 'application/x-yaml' })]);
202+
expect(await componentHarness.isSaveDisabled()).toBeFalsy();
203+
204+
await componentHarness.save();
205+
const req = httpTestingController.expectOne({
206+
method: 'PUT',
207+
url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/${API_ID}/_import/swagger`,
208+
});
209+
expect(req.request.body).toMatchObject({
210+
payload: 'openapi: 3.1.0',
211+
withDocumentation: true,
212+
withOASValidationPolicy: true,
213+
});
214+
req.flush(fakeApiV4({ id: API_ID, definitionVersion: 'V4' }));
215+
});
216+
217+
it('should send withDocumentation false on OpenAPI update when documentation toggle is off', async () => {
218+
await componentHarness.selectFormat('openapi');
219+
await componentHarness.selectSource('local');
220+
await componentHarness.toggleDocumentationImport();
221+
await componentHarness.pickFiles([new File(['openapi: 3.1.0'], 'openapi.yml', { type: 'application/x-yaml' })]);
222+
223+
await componentHarness.save();
224+
const req = httpTestingController.expectOne({
225+
method: 'PUT',
226+
url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/${API_ID}/_import/swagger`,
227+
});
228+
expect(req.request.body).toMatchObject({
229+
withDocumentation: false,
230+
withOASValidationPolicy: true,
231+
});
232+
req.flush(fakeApiV4({ id: API_ID, definitionVersion: 'V4' }));
233+
});
234+
235+
it('should close dialog when cancel is clicked', async () => {
236+
await componentHarness.cancel();
237+
expect(dialogRef.close).toHaveBeenCalledWith();
238+
});
239+
});

gravitee-apim-console-webui/src/management/api/import-v4/api-import-v4.component.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ import { EMPTY, Observable, Subject } from 'rxjs';
2424
import { catchError, map, takeUntil, tap } from 'rxjs/operators';
2525
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
2626
import { MatSlideToggle } from '@angular/material/slide-toggle';
27+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
2728

2829
import { ApiImportFilePickerComponent } from '../component/api-import-file-picker/api-import-file-picker.component';
2930
import { ApiV2Service } from '../../../services-ngx/api-v2.service';
3031
import { SnackBarService } from '../../../services-ngx/snack-bar.service';
3132
import { ApiV4 } from '../../../entities/management-api-v2';
3233
import { PolicyV2Service } from '../../../services-ngx/policy-v2.service';
3334

35+
export type ApiImportV4DialogData = {
36+
apiId: string;
37+
};
38+
3439
@Component({
3540
selector: 'api-import-v4',
3641
imports: [
@@ -61,6 +66,15 @@ export class ApiImportV4Component implements OnInit {
6166
private importFileContent: string;
6267
private unsubscribe$: Subject<void> = new Subject<void>();
6368

69+
/** Set when the component is opened as a MatDialog for update mode. */
70+
protected dialogData: ApiImportV4DialogData | null = inject(MAT_DIALOG_DATA, { optional: true });
71+
protected dialogRef: MatDialogRef<ApiImportV4Component> | null = inject(MatDialogRef, { optional: true });
72+
73+
/** True when opened as a dialog to update an existing V4 API. */
74+
get isUpdateMode(): boolean {
75+
return !!this.dialogData?.apiId;
76+
}
77+
6478
protected importType: string;
6579
protected formats = [
6680
{ value: 'gravitee', label: 'Gravitee definition', icon: 'gio:gravitee' },
@@ -104,29 +118,46 @@ export class ApiImportV4Component implements OnInit {
104118
this.form.updateValueAndValidity();
105119
}
106120

121+
protected cancel(): void {
122+
if (this.dialogRef) {
123+
this.dialogRef.close();
124+
}
125+
}
126+
107127
protected import() {
108128
let result: Observable<ApiV4>;
129+
109130
if (this.form.controls.source.value === 'local' && this.form.controls.format.value === 'gravitee' && this.importType === 'MAPI_V2') {
110-
result = this.apiV2Service.import(this.importFileContent);
131+
result = this.isUpdateMode
132+
? this.apiV2Service.importUpdate(this.dialogData.apiId, this.importFileContent)
133+
: this.apiV2Service.import(this.importFileContent);
111134
} else if (
112135
this.form.controls.source.value === 'local' &&
113136
this.form.controls.format.value === 'openapi' &&
114137
this.importType === 'SWAGGER'
115138
) {
116-
result = this.apiV2Service.importSwaggerApi({
139+
const descriptor = {
117140
payload: this.importFileContent,
118141
withDocumentation: this.form.value.withDocumentation,
119142
withOASValidationPolicy: this.form.value.withOASValidationPolicy,
120-
});
143+
};
144+
result = this.isUpdateMode
145+
? this.apiV2Service.importSwaggerApiUpdate(this.dialogData.apiId, descriptor)
146+
: this.apiV2Service.importSwaggerApi(descriptor);
121147
} else {
122148
this.snackBarService.error('Unsupported type for V4 API import');
149+
return;
123150
}
124151

125152
result
126153
.pipe(
127-
tap(createdApi => {
128-
this.snackBarService.success('API imported successfully');
129-
this.router.navigate([`../../${createdApi.id}`], { relativeTo: this.activatedRoute });
154+
tap(api => {
155+
this.snackBarService.success(this.isUpdateMode ? 'API updated successfully' : 'API imported successfully');
156+
if (this.dialogRef) {
157+
this.dialogRef.close(api.id);
158+
} else {
159+
this.router.navigate([`../../${api.id}`], { relativeTo: this.activatedRoute });
160+
}
130161
}),
131162
catchError(({ error }) => {
132163
this.snackBarService.error(error.message ?? 'An error occurred while importing the API');

gravitee-apim-console-webui/src/management/api/import-v4/api-import-v4.harness.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class ApiImportV4Harness extends ComponentHarness {
2626
private getFormatSelectGroup = this.locatorFor(GioFormSelectionInlineHarness.with({ selector: '[formControlName="format"]' }));
2727
private getSourceSelectGroup = this.locatorFor(GioFormSelectionInlineHarness.with({ selector: '[formControlName="source"]' }));
2828
private getFilePicker = this.locatorFor(GioFormFilePickerInputHarness);
29-
private getSaveButton = this.locatorFor(MatButtonHarness.with({ selector: '[aria-label="Import API"]' }));
29+
private getSaveButton = this.locatorFor(MatButtonHarness.with({ selector: '.import__save-button' }));
3030
private getCancelButton = this.locatorFor(MatButtonHarness.with({ selector: '[aria-label="Cancel"]' }));
3131
private getFormatErrorBanner = this.locatorForOptional(DivHarness.with({ selector: '.banner' }));
3232
private getImportDocumentationToggle = this.locatorFor(MatSlideToggleHarness.with({ selector: '[formControlName="withDocumentation"]' }));
@@ -42,6 +42,10 @@ export class ApiImportV4Harness extends ComponentHarness {
4242
return this.getSaveButton().then(btn => btn.isDisabled());
4343
}
4444

45+
public async getSaveButtonText() {
46+
return this.getSaveButton().then(btn => btn.getText());
47+
}
48+
4549
public async cancel() {
4650
return this.getCancelButton().then(btn => btn.click());
4751
}

gravitee-apim-console-webui/src/management/application/details/general/application-general.module.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { MatCardModule } from '@angular/material/card';
3333
import { MatFormFieldModule } from '@angular/material/form-field';
3434
import { MatInputModule } from '@angular/material/input';
35-
import { MatOptionModule } from '@angular/material/core';
35+
import { MatOptionModule, MatNativeDateModule } from '@angular/material/core';
3636
import { MatSelectModule } from '@angular/material/select';
3737
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
3838
import { ReactiveFormsModule } from '@angular/forms';
@@ -43,7 +43,6 @@ import { MatTooltipModule } from '@angular/material/tooltip';
4343
import { MatTableModule } from '@angular/material/table';
4444
import { MatDialogModule } from '@angular/material/dialog';
4545
import { MatDatepickerModule } from '@angular/material/datepicker';
46-
import { MatNativeDateModule } from '@angular/material/core';
4746

4847
import { ApplicationGeneralComponent } from './application-general.component';
4948
import { AddCertificateDialogComponent } from './add-certificate-dialog/add-certificate-dialog.component';

gravitee-apim-console-webui/src/services-ngx/api-v2.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,22 @@ export class ApiV2Service {
140140
);
141141
}
142142

143-
import(importApi: any): Observable<ApiV4> {
143+
import(importApi: string): Observable<ApiV4> {
144144
return this.http.post<ApiV4>(`${this.constants.env.v2BaseURL}/apis/_import/definition`, importApi, {
145145
headers: {
146146
'Content-Type': 'application/json',
147147
},
148148
});
149149
}
150150

151+
importUpdate(apiId: string, importApi: string): Observable<ApiV4> {
152+
return this.http.put<ApiV4>(`${this.constants.env.v2BaseURL}/apis/${apiId}/_import/definition`, importApi, {
153+
headers: {
154+
'Content-Type': 'application/json',
155+
},
156+
});
157+
}
158+
151159
importSwaggerApi(descriptor: ImportSwaggerDescriptor) {
152160
return this.http.post<ApiV4>(`${this.constants.env.v2BaseURL}/apis/_import/swagger`, descriptor, {
153161
headers: {
@@ -156,6 +164,14 @@ export class ApiV2Service {
156164
});
157165
}
158166

167+
importSwaggerApiUpdate(apiId: string, descriptor: ImportSwaggerDescriptor) {
168+
return this.http.put<ApiV4>(`${this.constants.env.v2BaseURL}/apis/${apiId}/_import/swagger`, descriptor, {
169+
headers: {
170+
'Content-Type': 'application/json',
171+
},
172+
});
173+
}
174+
159175
exportCRD(apiId: string): Observable<Blob> {
160176
return this.http.get(`${this.constants.env.v2BaseURL}/apis/${apiId}/_export/crd`, {
161177
responseType: 'blob',

0 commit comments

Comments
 (0)