From dcde6d9adb80fa037364f9b5f98009ae601aebbd Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Mon, 2 Jun 2025 11:30:44 +0300 Subject: [PATCH 01/13] Add service health checker component --- .../df-api-docs/df-api-docs.component.html | 22 +++++++ .../df-api-docs/df-api-docs.component.scss | 45 +++++++++++++ .../df-api-docs/df-api-docs.component.ts | 66 +++++++++++++++++-- src/assets/i18n/en.json | 6 ++ 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index da274cff..a1a77205 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -32,4 +32,26 @@ +
+
+

{{ 'apiHealthBanner.loading' | transloco }}

+
+
+

{{ 'apiHealthBanner.healthy' | transloco }}

+
+
+

{{ 'apiHealthBanner.unhealthy' | transloco:{ healthError: healthError } }}

+
+
+

{{ 'apiHealthBanner.error' | transloco:{ healthError: healthError } }}

+
+
+
diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss index 45708b99..548597e7 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss @@ -41,3 +41,48 @@ .swagger-ui { margin-top: 16px; } + +.api-health-banner { + padding: 8px 12px; + border-radius: 4px; + border-left-width: 4px; + border-left-style: solid; + + p { + margin: 0; + font-size: 0.9em; + } + + &.status-healthy { + border-left-color: #28a745; // Green + background-color: #e9f5ec; // Light green + color: #155724; // Darker green text + } + + &.status-unhealthy, + &.status-error { + border-left-color: #dc3545; // Red + background-color: #f8d7da; // Light red + color: #721c24; // Darker red text + } + + &.status-loading { + border-left-color: #007bff; // Blue + background-color: #e7f3ff; // Light blue + color: #004085; // Darker blue text + } +} + +// Styles for elements within Swagger UI, piercing encapsulation +:host ::ng-deep { + .swagger-ui { + // This targets the wrapper div for Swagger UI in your component's template + .information-container { + .main { + display: flex; + justify-content: space-between; + flex-direction: row-reverse; + } + } + } +} diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 8d805e37..2638e994 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -25,7 +25,7 @@ import { mapSnakeToCamel, } from 'src/app/shared/utilities/case'; import { DfThemeService } from 'src/app/shared/services/df-theme.service'; -import { AsyncPipe, NgIf, NgFor, SlicePipe } from '@angular/common'; +import { AsyncPipe, NgIf, NgFor, SlicePipe, NgClass } from '@angular/common'; import { environment } from '../../../../environments/environment'; import { ApiKeysService } from '../services/api-keys.service'; import { ApiKeyInfo } from 'src/app/shared/types/api-keys'; @@ -34,10 +34,10 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { DfCurrentServiceService } from 'src/app/shared/services/df-current-service.service'; -import { tap, switchMap, map, distinctUntilChanged } from 'rxjs/operators'; -import { HttpClient } from '@angular/common/http'; +import { tap, switchMap, map, distinctUntilChanged, catchError } from 'rxjs/operators'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; -import { Subscription } from 'rxjs'; +import { Subscription, of } from 'rxjs'; interface ServiceResponse { resource: Array<{ @@ -63,6 +63,7 @@ interface ServiceResponse { NgIf, NgFor, SlicePipe, + NgClass, FontAwesomeModule, ], }) @@ -70,11 +71,15 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { @ViewChild('apiDocumentation', { static: true }) apiDocElement: | ElementRef | undefined; + @ViewChild('healthBannerElement') healthBannerElementRef: ElementRef | undefined; apiDocJson: object; apiKeys: ApiKeyInfo[] = []; faCopy = faCopy; private subscriptions: Subscription[] = []; + healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'error' = 'loading'; + healthError: string | null = null; + serviceName: string | null = null; constructor( private activatedRoute: ActivatedRoute, @@ -90,14 +95,14 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { isDarkMode = this.themeService.darkMode$; ngOnInit(): void { // Get the service name from the route - const serviceName = this.activatedRoute.snapshot.params['name']; + this.serviceName = this.activatedRoute.snapshot.params['name']; // First fetch the service ID by name - if (serviceName) { + if (this.serviceName) { this.subscriptions.push( this.http .get( - `${BASE_URL}/system/service?filter=name=${serviceName}` + `${BASE_URL}/system/service?filter=name=${this.serviceName}` ) .pipe( map(response => response?.resource?.[0]?.id || -1), @@ -109,6 +114,28 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { ) .subscribe() ); + + // Perform health check + this.healthStatus = 'loading'; + this.subscriptions.push( + this.http.get(`${BASE_URL}/${this.serviceName}/_schema`).pipe( + tap(() => { + this.healthStatus = 'healthy'; + this.healthError = null; + }), + catchError((error: HttpErrorResponse) => { + if (error.status >= 200 && error.status < 300) { + // Even if there's an error in parsing, if it's 2xx, consider it healthy for schema check + this.healthStatus = 'healthy'; + this.healthError = null; + } else { + this.healthStatus = 'unhealthy'; + this.healthError = error.message || 'Unknown error'; + } + return of(null); // Continue the stream + }) + ).subscribe() + ); } // Handle the API documentation @@ -165,6 +192,31 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { return req; }, showMutatedRequest: true, + onComplete: () => { + if (this.healthBannerElementRef && this.healthBannerElementRef.nativeElement && this.apiDocElement && this.apiDocElement.nativeElement) { + const swaggerContainer = this.apiDocElement.nativeElement; + const bannerNode = this.healthBannerElementRef.nativeElement; + + // Try to find the information container within Swagger UI + const infoContainer = swaggerContainer.querySelector('.information-container .main'); + + if (infoContainer) { + // Prepend banner to the information container (which typically holds the title) + if (infoContainer.firstChild) { + infoContainer.insertBefore(bannerNode, infoContainer.firstChild); + } else { + infoContainer.appendChild(bannerNode); + } + } else { + // Fallback: Prepend to the main swagger container if .information-container is not found + if (swaggerContainer.firstChild) { + swaggerContainer.insertBefore(bannerNode, swaggerContainer.firstChild); + } else { + swaggerContainer.appendChild(bannerNode); + } + } + } + } }); } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index f8f4399c..244aaeda 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -117,6 +117,12 @@ "python3": "Python3", "nodejs": "Node.js" }, + "apiHealthBanner": { + "loading": "Checking API status...", + "healthy": "API status: Healthy", + "unhealthy": "API status: Unhealthy. {{healthError}}", + "error": "Error checking API status. {{healthError}}" + }, "nav": { "error": { "header": "Error" From 1f4588f1eeca8d9bc5c40d7f6052a3aa77017a41 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Mon, 2 Jun 2025 11:31:09 +0300 Subject: [PATCH 02/13] Used a prittier --- .../df-api-docs/df-api-docs.component.html | 28 +++++--- .../df-api-docs/df-api-docs.component.ts | 65 ++++++++++++------- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index a1a77205..1df757a1 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -32,14 +32,16 @@ -
+

{{ 'apiHealthBanner.loading' | transloco }}

@@ -47,10 +49,16 @@

{{ 'apiHealthBanner.healthy' | transloco }}

-

{{ 'apiHealthBanner.unhealthy' | transloco:{ healthError: healthError } }}

+

+ {{ + 'apiHealthBanner.unhealthy' | transloco: { healthError: healthError } + }} +

-

{{ 'apiHealthBanner.error' | transloco:{ healthError: healthError } }}

+

+ {{ 'apiHealthBanner.error' | transloco: { healthError: healthError } }} +

diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 2638e994..35325fc4 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -34,7 +34,13 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { DfCurrentServiceService } from 'src/app/shared/services/df-current-service.service'; -import { tap, switchMap, map, distinctUntilChanged, catchError } from 'rxjs/operators'; +import { + tap, + switchMap, + map, + distinctUntilChanged, + catchError, +} from 'rxjs/operators'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; import { Subscription, of } from 'rxjs'; @@ -71,7 +77,9 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { @ViewChild('apiDocumentation', { static: true }) apiDocElement: | ElementRef | undefined; - @ViewChild('healthBannerElement') healthBannerElementRef: ElementRef | undefined; + @ViewChild('healthBannerElement') healthBannerElementRef: + | ElementRef + | undefined; apiDocJson: object; apiKeys: ApiKeyInfo[] = []; @@ -118,23 +126,26 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { // Perform health check this.healthStatus = 'loading'; this.subscriptions.push( - this.http.get(`${BASE_URL}/${this.serviceName}/_schema`).pipe( - tap(() => { - this.healthStatus = 'healthy'; - this.healthError = null; - }), - catchError((error: HttpErrorResponse) => { - if (error.status >= 200 && error.status < 300) { - // Even if there's an error in parsing, if it's 2xx, consider it healthy for schema check - this.healthStatus = 'healthy'; + this.http + .get(`${BASE_URL}/${this.serviceName}/_schema`) + .pipe( + tap(() => { + this.healthStatus = 'healthy'; this.healthError = null; - } else { - this.healthStatus = 'unhealthy'; - this.healthError = error.message || 'Unknown error'; - } - return of(null); // Continue the stream - }) - ).subscribe() + }), + catchError((error: HttpErrorResponse) => { + if (error.status >= 200 && error.status < 300) { + // Even if there's an error in parsing, if it's 2xx, consider it healthy for schema check + this.healthStatus = 'healthy'; + this.healthError = null; + } else { + this.healthStatus = 'unhealthy'; + this.healthError = error.message || 'Unknown error'; + } + return of(null); // Continue the stream + }) + ) + .subscribe() ); } @@ -193,12 +204,19 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }, showMutatedRequest: true, onComplete: () => { - if (this.healthBannerElementRef && this.healthBannerElementRef.nativeElement && this.apiDocElement && this.apiDocElement.nativeElement) { + if ( + this.healthBannerElementRef && + this.healthBannerElementRef.nativeElement && + this.apiDocElement && + this.apiDocElement.nativeElement + ) { const swaggerContainer = this.apiDocElement.nativeElement; const bannerNode = this.healthBannerElementRef.nativeElement; // Try to find the information container within Swagger UI - const infoContainer = swaggerContainer.querySelector('.information-container .main'); + const infoContainer = swaggerContainer.querySelector( + '.information-container .main' + ); if (infoContainer) { // Prepend banner to the information container (which typically holds the title) @@ -210,13 +228,16 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { } else { // Fallback: Prepend to the main swagger container if .information-container is not found if (swaggerContainer.firstChild) { - swaggerContainer.insertBefore(bannerNode, swaggerContainer.firstChild); + swaggerContainer.insertBefore( + bannerNode, + swaggerContainer.firstChild + ); } else { swaggerContainer.appendChild(bannerNode); } } } - } + }, }); } From 8d5a2262717f64b600820015b5a8e31f3ece16ef Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 4 Jun 2025 16:37:14 +0300 Subject: [PATCH 03/13] Make a few improvements for health checker component --- .../df-api-docs/df-api-docs.component.html | 16 ++-- .../df-api-docs/df-api-docs.component.scss | 72 ++++++++++++++--- .../df-api-docs/df-api-docs.component.ts | 81 ++++++++++++------- src/assets/i18n/en.json | 6 +- 4 files changed, 128 insertions(+), 47 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index 1df757a1..6b3d7121 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -40,7 +40,7 @@ 'status-loading': healthStatus === 'loading', 'status-healthy': healthStatus === 'healthy', 'status-unhealthy': healthStatus === 'unhealthy', - 'status-error': healthStatus === 'error' + 'status-warning': healthStatus === 'warning' }">

{{ 'apiHealthBanner.loading' | transloco }}

@@ -50,14 +50,18 @@

- {{ - 'apiHealthBanner.unhealthy' | transloco: { healthError: healthError } - }} + {{ 'apiHealthBanner.unhealthyBase' | transloco }} +

+
+
{{ healthError }}
+
-
+

- {{ 'apiHealthBanner.error' | transloco: { healthError: healthError } }} + {{ 'apiHealthBanner.warningDefault' | transloco }}

diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss index 548597e7..136f9269 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss @@ -43,6 +43,8 @@ } .api-health-banner { + display: flex; + align-items: center; padding: 8px 12px; border-radius: 4px; border-left-width: 4px; @@ -54,22 +56,73 @@ } &.status-healthy { - border-left-color: #28a745; // Green - background-color: #e9f5ec; // Light green - color: #155724; // Darker green text + border-left-color: #28a745; + background-color: #e9f5ec; + color: #155724; } &.status-unhealthy, &.status-error { - border-left-color: #dc3545; // Red - background-color: #f8d7da; // Light red - color: #721c24; // Darker red text + border-left-color: #dc3545; + background-color: #f8d7da; + color: #721c24; + } + + &.status-unhealthy { + & > div { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + + & > p { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + .view-details-button { + margin-left: 12px; + flex-shrink: 0; + padding: 2px 8px; + line-height: normal; + font-size: 0.9em; + min-width: auto; + } + } + + .unhealthy-error-details { + margin-top: 0; + padding: 8px 12px; + background-color: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 4px; + width: 100%; + box-sizing: border-box; + max-height: 150px; + overflow-y: auto; + + pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 0.85em; + color: inherit; + } + } + } } &.status-loading { - border-left-color: #007bff; // Blue - background-color: #e7f3ff; // Light blue - color: #004085; // Darker blue text + border-left-color: #007bff; + background-color: #e7f3ff; + color: #004085; + } + + &.status-warning { + border-left-color: #ffc107; + background-color: #fff3cd; + color: #856404; } } @@ -82,6 +135,7 @@ display: flex; justify-content: space-between; flex-direction: row-reverse; + gap: 8px; } } } diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 35325fc4..4112179f 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -53,6 +53,12 @@ interface ServiceResponse { }>; } +interface ApiDocJson { + paths: { + [key: string]: any; + }; +} + @UntilDestroy({ checkProperties: true }) @Component({ selector: 'df-api-docs', @@ -81,13 +87,14 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { | ElementRef | undefined; - apiDocJson: object; + apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; faCopy = faCopy; private subscriptions: Subscription[] = []; - healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'error' = 'loading'; + healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'warning' = 'loading'; healthError: string | null = null; serviceName: string | null = null; + showUnhealthyErrorDetails = false; constructor( private activatedRoute: ActivatedRoute, @@ -122,31 +129,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { ) .subscribe() ); - - // Perform health check - this.healthStatus = 'loading'; - this.subscriptions.push( - this.http - .get(`${BASE_URL}/${this.serviceName}/_schema`) - .pipe( - tap(() => { - this.healthStatus = 'healthy'; - this.healthError = null; - }), - catchError((error: HttpErrorResponse) => { - if (error.status >= 200 && error.status < 300) { - // Even if there's an error in parsing, if it's 2xx, consider it healthy for schema check - this.healthStatus = 'healthy'; - this.healthError = null; - } else { - this.healthStatus = 'unhealthy'; - this.healthError = error.message || 'Unknown error'; - } - return of(null); // Continue the stream - }) - ) - .subscribe() - ); } // Handle the API documentation @@ -154,9 +136,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.activatedRoute.data.subscribe(({ data }) => { if (data) { if ( - data.paths['/']?.get && - data.paths['/']?.get.operationId && - data.paths['/']?.get.operationId === 'getSoapResources' + data.paths['/']?.get?.operationId === 'getSoapResources' ) { this.apiDocJson = { ...data, paths: mapSnakeToCamel(data.paths) }; } else { @@ -184,6 +164,9 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { ngAfterContentInit(): void { const apiDocumentation = this.apiDocJson; + + this.checkApiHealth(); + SwaggerUI({ spec: apiDocumentation, domNode: this.apiDocElement?.nativeElement, @@ -246,6 +229,40 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.subscriptions.forEach(sub => sub.unsubscribe()); } + private checkApiHealth(): void { + if (this.serviceName && this.apiDocJson?.paths['/_schema']?.get) { + // Perform health check + this.performHealthCheck(this.serviceName, `${BASE_URL}/${this.serviceName}/_schema`); + } else { + this.setHealthState('warning'); + } + } + + private setHealthState(status: 'healthy' | 'unhealthy' | 'warning', error: string | null = null): void { + this.healthStatus = status; + this.healthError = error; + } + + private performHealthCheck(serviceName: string | null, url: string): void { + this.healthStatus = 'loading'; + this.healthError = null; + + this.subscriptions.push( + this.http.get(url).pipe( + tap(() => this.setHealthState('healthy')), + catchError((error: HttpErrorResponse) => { + if (error.status >= 200 && error.status < 300) { + this.setHealthState('healthy'); + return of(null); + } + + this.setHealthState('unhealthy', error.message || 'Unknown error'); + return of(null); + }) + ).subscribe() + ); + } + goBackToList(): void { this.currentServiceService.clearCurrentServiceId(); this.router.navigate(['../'], { relativeTo: this.activatedRoute }); @@ -265,4 +282,8 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { duration: 3000, }); } + + toggleUnhealthyErrorDetails(): void { + this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 244aaeda..41a28d60 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -120,8 +120,10 @@ "apiHealthBanner": { "loading": "Checking API status...", "healthy": "API status: Healthy", - "unhealthy": "API status: Unhealthy. {{healthError}}", - "error": "Error checking API status. {{healthError}}" + "unhealthyBase": "API status: Unhealthy.", + "viewDetails": "View Details", + "hideDetails": "Hide Details", + "warningDefault": "Warning: This type of API currently does not support automatic health checks." }, "nav": { "error": { From 96040bd6e4de327fef4d5f00ec02e65576a2ef21 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 4 Jun 2025 16:38:08 +0300 Subject: [PATCH 04/13] used a prittier --- .../df-api-docs/df-api-docs.component.html | 13 ++++++- .../df-api-docs/df-api-docs.component.ts | 39 +++++++++++-------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index 6b3d7121..6caa9b0d 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -51,8 +51,17 @@

{{ 'apiHealthBanner.unhealthyBase' | transloco }} -

diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 4112179f..9c7829b5 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -135,9 +135,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.subscriptions.push( this.activatedRoute.data.subscribe(({ data }) => { if (data) { - if ( - data.paths['/']?.get?.operationId === 'getSoapResources' - ) { + if (data.paths['/']?.get?.operationId === 'getSoapResources') { this.apiDocJson = { ...data, paths: mapSnakeToCamel(data.paths) }; } else { this.apiDocJson = { ...data, paths: mapCamelToSnake(data.paths) }; @@ -232,13 +230,19 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { private checkApiHealth(): void { if (this.serviceName && this.apiDocJson?.paths['/_schema']?.get) { // Perform health check - this.performHealthCheck(this.serviceName, `${BASE_URL}/${this.serviceName}/_schema`); + this.performHealthCheck( + this.serviceName, + `${BASE_URL}/${this.serviceName}/_schema` + ); } else { this.setHealthState('warning'); } } - private setHealthState(status: 'healthy' | 'unhealthy' | 'warning', error: string | null = null): void { + private setHealthState( + status: 'healthy' | 'unhealthy' | 'warning', + error: string | null = null + ): void { this.healthStatus = status; this.healthError = error; } @@ -248,18 +252,21 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.healthError = null; this.subscriptions.push( - this.http.get(url).pipe( - tap(() => this.setHealthState('healthy')), - catchError((error: HttpErrorResponse) => { - if (error.status >= 200 && error.status < 300) { - this.setHealthState('healthy'); - return of(null); - } + this.http + .get(url) + .pipe( + tap(() => this.setHealthState('healthy')), + catchError((error: HttpErrorResponse) => { + if (error.status >= 200 && error.status < 300) { + this.setHealthState('healthy'); + return of(null); + } - this.setHealthState('unhealthy', error.message || 'Unknown error'); - return of(null); - }) - ).subscribe() + this.setHealthState('unhealthy', error.message || 'Unknown error'); + return of(null); + }) + ) + .subscribe() ); } From 1d57f99415689f96e77327c445d521a5a2aac067 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 5 Jun 2025 15:50:05 +0300 Subject: [PATCH 05/13] A few more improvements for health check banner functionality --- .../df-api-docs/df-api-docs.component.ts | 108 +++++++++++++++--- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 9c7829b5..f14ebef7 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -43,7 +43,7 @@ import { } from 'rxjs/operators'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; -import { Subscription, of } from 'rxjs'; +import { Subscription, of, forkJoin } from 'rxjs'; interface ServiceResponse { resource: Array<{ @@ -54,9 +54,22 @@ interface ServiceResponse { } interface ApiDocJson { + info: { + description: string | undefined; + title: string; + version: string | undefined; + group: string; + }; paths: { [key: string]: any; }; + [key: string]: any; +} + +interface HealthCheckResult { + endpoint: string; + success?: boolean; + error?: string; } @UntilDestroy({ checkProperties: true }) @@ -95,6 +108,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { healthError: string | null = null; serviceName: string | null = null; showUnhealthyErrorDetails = false; + private static SUPPORTED_SERVICE_TYPE_GROUPS = ['Database', 'File']; constructor( private activatedRoute: ActivatedRoute, @@ -162,7 +176,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { ngAfterContentInit(): void { const apiDocumentation = this.apiDocJson; - this.checkApiHealth(); SwaggerUI({ @@ -228,17 +241,64 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { } private checkApiHealth(): void { - if (this.serviceName && this.apiDocJson?.paths['/_schema']?.get) { + let endpointsToValidate = this.getApiEnpointsToValidate(); + if (this.serviceName && endpointsToValidate.length > 0) { // Perform health check - this.performHealthCheck( - this.serviceName, - `${BASE_URL}/${this.serviceName}/_schema` - ); + this.performHealthCheck(endpointsToValidate); } else { this.setHealthState('warning'); } } + private getApiEnpointsToValidate(): string[] { + const endpointsToValidate: string[] = []; + + if (!this.apiDocJson || !this.apiDocJson.info) { + return []; // Not enough info to determine endpoints + } + + if ( + DfApiDocsComponent.SUPPORTED_SERVICE_TYPE_GROUPS.includes( + this.apiDocJson.info.group + ) + ) { + for (const path in this.apiDocJson.paths) { + if ( + !Object.prototype.hasOwnProperty.call(this.apiDocJson.paths, path) + ) { + continue; + } + const methods = this.apiDocJson.paths[path]; + + for (const method in methods) { + if (!Object.prototype.hasOwnProperty.call(methods, method)) { + continue; + } + if (method.toLowerCase() === 'get') { + const operation = methods[method]; + const parameters: Array<{ + name: string; + in: string; + required?: boolean; + }> = operation.parameters || []; + + const hasPathParameters = path.includes('{'); + + const hasRequiredQueryParameters = parameters.some( + param => param && param.in === 'query' && param.required === true + ); + + if (!hasPathParameters && !hasRequiredQueryParameters) { + endpointsToValidate.push(path); + } + } + } + } + } + + return endpointsToValidate; + } + private setHealthState( status: 'healthy' | 'unhealthy' | 'warning', error: string | null = null @@ -247,23 +307,39 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.healthError = error; } - private performHealthCheck(serviceName: string | null, url: string): void { + private performHealthCheck(endpoints: string[]): void { this.healthStatus = 'loading'; this.healthError = null; - this.subscriptions.push( + const healthCheckRequests = endpoints.map(endpoint => this.http - .get(url) + .get(`${BASE_URL}/${this.serviceName}${endpoint}`, { + responseType: 'text', + }) .pipe( - tap(() => this.setHealthState('healthy')), + map(() => ({ endpoint, success: true }) as HealthCheckResult), catchError((error: HttpErrorResponse) => { - if (error.status >= 200 && error.status < 300) { + return of({ + endpoint, + error: error.message || error.error?.message || 'Unknown error', + } as HealthCheckResult); + }) + ) + ); + + this.subscriptions.push( + forkJoin(healthCheckRequests) + .pipe( + tap((results: HealthCheckResult[]) => { + const errors = results.filter(result => result.error); + if (errors.length > 0) { + this.setHealthState( + 'unhealthy', + errors.map(e => `${e.endpoint}: ${e.error}`).join(', \n\n') + ); + } else { this.setHealthState('healthy'); - return of(null); } - - this.setHealthState('unhealthy', error.message || 'Unknown error'); - return of(null); }) ) .subscribe() From 796dd1d4ef999526bb079ed97de7dce4b90f919f Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 5 Jun 2025 16:32:36 +0300 Subject: [PATCH 06/13] Add system and user service groups into supported types --- .../df-api-docs/df-api-docs.component.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index f14ebef7..657800c2 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -108,7 +108,12 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { healthError: string | null = null; serviceName: string | null = null; showUnhealthyErrorDetails = false; - private static SUPPORTED_SERVICE_TYPE_GROUPS = ['Database', 'File']; + private static SUPPORTED_SERVICE_TYPE_GROUPS = [ + 'Database', + 'File', + 'User', + 'System', + ]; constructor( private activatedRoute: ActivatedRoute, @@ -274,6 +279,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { if (!Object.prototype.hasOwnProperty.call(methods, method)) { continue; } + if (method.toLowerCase() === 'get') { const operation = methods[method]; const parameters: Array<{ @@ -283,12 +289,21 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }> = operation.parameters || []; const hasPathParameters = path.includes('{'); - const hasRequiredQueryParameters = parameters.some( param => param && param.in === 'query' && param.required === true ); - if (!hasPathParameters && !hasRequiredQueryParameters) { + const isSystemServiceRelatedPath = + (path.includes('/admin') || path.includes('/service_report')) && + this.apiDocJson.info.group === 'System'; + + const isNonRootAdmin = !this.userDataService.userData?.isRootAdmin; + + if ( + !hasPathParameters && + !hasRequiredQueryParameters && + !(isNonRootAdmin && isSystemServiceRelatedPath) + ) { endpointsToValidate.push(path); } } From e463b37d5478969a52e164fe33bb9ef9b3810ff4 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 5 Jun 2025 17:07:30 +0300 Subject: [PATCH 07/13] Revert "Add system and user service groups into supported types" This reverts commit 796dd1d4ef999526bb079ed97de7dce4b90f919f. --- .../df-api-docs/df-api-docs.component.ts | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 657800c2..f14ebef7 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -108,12 +108,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { healthError: string | null = null; serviceName: string | null = null; showUnhealthyErrorDetails = false; - private static SUPPORTED_SERVICE_TYPE_GROUPS = [ - 'Database', - 'File', - 'User', - 'System', - ]; + private static SUPPORTED_SERVICE_TYPE_GROUPS = ['Database', 'File']; constructor( private activatedRoute: ActivatedRoute, @@ -279,7 +274,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { if (!Object.prototype.hasOwnProperty.call(methods, method)) { continue; } - if (method.toLowerCase() === 'get') { const operation = methods[method]; const parameters: Array<{ @@ -289,21 +283,12 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }> = operation.parameters || []; const hasPathParameters = path.includes('{'); + const hasRequiredQueryParameters = parameters.some( param => param && param.in === 'query' && param.required === true ); - const isSystemServiceRelatedPath = - (path.includes('/admin') || path.includes('/service_report')) && - this.apiDocJson.info.group === 'System'; - - const isNonRootAdmin = !this.userDataService.userData?.isRootAdmin; - - if ( - !hasPathParameters && - !hasRequiredQueryParameters && - !(isNonRootAdmin && isSystemServiceRelatedPath) - ) { + if (!hasPathParameters && !hasRequiredQueryParameters) { endpointsToValidate.push(path); } } From 26c51f14751db5eeb7c39735ba3c403cfe7c9914 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 5 Jun 2025 17:49:58 +0300 Subject: [PATCH 08/13] Get rid of dynemic endpoint fetching functionality to reduce requests --- .../df-api-docs/df-api-docs.component.ts | 95 ++++--------------- 1 file changed, 19 insertions(+), 76 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index f14ebef7..519a9ef3 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -108,7 +108,11 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { healthError: string | null = null; serviceName: string | null = null; showUnhealthyErrorDetails = false; - private static SUPPORTED_SERVICE_TYPE_GROUPS = ['Database', 'File']; + // Mapping of service types to their corresponding endpoints, probably would be better to move to the back-end + healthCheckEndpointsMap: { [key: string]: string[] } = { + Database: ['/_schema', '/_table'], + File: ['/'], + }; constructor( private activatedRoute: ActivatedRoute, @@ -241,64 +245,16 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { } private checkApiHealth(): void { - let endpointsToValidate = this.getApiEnpointsToValidate(); - if (this.serviceName && endpointsToValidate.length > 0) { + let endpointsToValidate = + this.healthCheckEndpointsMap[this.apiDocJson.info.group]; + if (this.serviceName && endpointsToValidate) { // Perform health check - this.performHealthCheck(endpointsToValidate); + this.performHealthCheck(endpointsToValidate[0]); } else { this.setHealthState('warning'); } } - private getApiEnpointsToValidate(): string[] { - const endpointsToValidate: string[] = []; - - if (!this.apiDocJson || !this.apiDocJson.info) { - return []; // Not enough info to determine endpoints - } - - if ( - DfApiDocsComponent.SUPPORTED_SERVICE_TYPE_GROUPS.includes( - this.apiDocJson.info.group - ) - ) { - for (const path in this.apiDocJson.paths) { - if ( - !Object.prototype.hasOwnProperty.call(this.apiDocJson.paths, path) - ) { - continue; - } - const methods = this.apiDocJson.paths[path]; - - for (const method in methods) { - if (!Object.prototype.hasOwnProperty.call(methods, method)) { - continue; - } - if (method.toLowerCase() === 'get') { - const operation = methods[method]; - const parameters: Array<{ - name: string; - in: string; - required?: boolean; - }> = operation.parameters || []; - - const hasPathParameters = path.includes('{'); - - const hasRequiredQueryParameters = parameters.some( - param => param && param.in === 'query' && param.required === true - ); - - if (!hasPathParameters && !hasRequiredQueryParameters) { - endpointsToValidate.push(path); - } - } - } - } - } - - return endpointsToValidate; - } - private setHealthState( status: 'healthy' | 'unhealthy' | 'warning', error: string | null = null @@ -307,39 +263,26 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.healthError = error; } - private performHealthCheck(endpoints: string[]): void { + private performHealthCheck(endpoint: string): void { this.healthStatus = 'loading'; this.healthError = null; - const healthCheckRequests = endpoints.map(endpoint => + this.subscriptions.push( this.http .get(`${BASE_URL}/${this.serviceName}${endpoint}`, { responseType: 'text', }) .pipe( - map(() => ({ endpoint, success: true }) as HealthCheckResult), + tap(() => this.setHealthState('healthy')), catchError((error: HttpErrorResponse) => { - return of({ - endpoint, - error: error.message || error.error?.message || 'Unknown error', - } as HealthCheckResult); - }) - ) - ); + this.setHealthState( + 'unhealthy', + `${endpoint}: ${ + error.message || error.error.message || 'Unknown error' + }` + ); - this.subscriptions.push( - forkJoin(healthCheckRequests) - .pipe( - tap((results: HealthCheckResult[]) => { - const errors = results.filter(result => result.error); - if (errors.length > 0) { - this.setHealthState( - 'unhealthy', - errors.map(e => `${e.endpoint}: ${e.error}`).join(', \n\n') - ); - } else { - this.setHealthState('healthy'); - } + return of(null); }) ) .subscribe() From 4882f67ad4d76c8b045d41f008b9fc9010b4f728 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Fri, 6 Jun 2025 12:46:44 +0300 Subject: [PATCH 09/13] Add some basic and simple pre-generated curls for supported for health check api types --- .../df-api-docs/df-api-docs.component.html | 119 ++++++++++++------ .../df-api-docs/df-api-docs.component.scss | 24 +++- .../df-api-docs/df-api-docs.component.ts | 94 ++++++++++---- src/assets/i18n/en.json | 4 + 4 files changed, 176 insertions(+), 65 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index 6caa9b0d..45d8ed42 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -32,47 +32,88 @@
-
-
-

{{ 'apiHealthBanner.loading' | transloco }}

-
-
-

{{ 'apiHealthBanner.healthy' | transloco }}

-
-
-

- {{ 'apiHealthBanner.unhealthyBase' | transloco }} - -

-
-
{{ healthError }}
+
+
+
+

{{ 'apiHealthBanner.loading' | transloco }}

+
+
+

{{ 'apiHealthBanner.healthy' | transloco }}

+
+
+

+ {{ 'apiHealthBanner.unhealthyBase' | transloco }} + +

+
+
{{ healthError }}
+
+
+
+

+ {{ 'apiHealthBanner.warningDefault' | transloco }} +

-
-

- {{ 'apiHealthBanner.warningDefault' | transloco }} -

-
+ + + + + + {{ 'apiBasicCurlCommands.title' | transloco }} + + + + +
+ + +
{{ command.text }}
+
+ + + +
+
+

+ {{ 'apiBasicCurlCommands.noCommands' | transloco }} +

+
+
+
diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss index 136f9269..d2f0639d 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss @@ -134,9 +134,31 @@ .main { display: flex; justify-content: space-between; - flex-direction: row-reverse; + flex-wrap: wrap; gap: 8px; } } } } + +.curl-command-text { + white-space: pre; + font-family: monospace; + font-size: 0.9em; + margin: 0; + color: var(--df-script-editor-text-color); + overflow-x: auto; +} + +.custom-swagger-content-wrapper { + width: 100%; +} + +.curl-commands-container { + display: flex; + flex-direction: column; + gap: 8px; + .actions-container { + padding: 0px 8px; + } +} diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 519a9ef3..b216f3cc 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -31,6 +31,10 @@ import { ApiKeysService } from '../services/api-keys.service'; import { ApiKeyInfo } from 'src/app/shared/types/api-keys'; import { Clipboard } from '@angular/cdk/clipboard'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatListModule } from '@angular/material/list'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatCardModule } from '@angular/material/card'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { DfCurrentServiceService } from 'src/app/shared/services/df-current-service.service'; @@ -90,12 +94,18 @@ interface HealthCheckResult { SlicePipe, NgClass, FontAwesomeModule, + MatListModule, + MatTooltipModule, + MatExpansionModule, + MatCardModule, ], }) export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { @ViewChild('apiDocumentation', { static: true }) apiDocElement: | ElementRef | undefined; + @ViewChild('swaggerInjectedContentContainer') + swaggerInjectedContentContainerRef: ElementRef | undefined; @ViewChild('healthBannerElement') healthBannerElementRef: | ElementRef | undefined; @@ -103,6 +113,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; faCopy = faCopy; + curlCommands: { text: string }[] = []; // Will be populated dynamically private subscriptions: Subscription[] = []; healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'warning' = 'loading'; healthError: string | null = null; @@ -202,38 +213,27 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }, showMutatedRequest: true, onComplete: () => { + this.prepareCurlCommands(); + if ( - this.healthBannerElementRef && - this.healthBannerElementRef.nativeElement && this.apiDocElement && - this.apiDocElement.nativeElement + this.apiDocElement.nativeElement && + this.swaggerInjectedContentContainerRef && + this.swaggerInjectedContentContainerRef.nativeElement ) { const swaggerContainer = this.apiDocElement.nativeElement; - const bannerNode = this.healthBannerElementRef.nativeElement; + const customContentNode = + this.swaggerInjectedContentContainerRef.nativeElement; - // Try to find the information container within Swagger UI const infoContainer = swaggerContainer.querySelector( '.information-container .main' ); - if (infoContainer) { - // Prepend banner to the information container (which typically holds the title) - if (infoContainer.firstChild) { - infoContainer.insertBefore(bannerNode, infoContainer.firstChild); - } else { - infoContainer.appendChild(bannerNode); - } - } else { - // Fallback: Prepend to the main swagger container if .information-container is not found - if (swaggerContainer.firstChild) { - swaggerContainer.insertBefore( - bannerNode, - swaggerContainer.firstChild - ); - } else { - swaggerContainer.appendChild(bannerNode); - } - } + this.injectCustomContent( + swaggerContainer, + infoContainer, + customContentNode + ); } }, }); @@ -304,12 +304,56 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { copyApiKey(key: string) { this.clipboard.copy(key); - this.snackBar.open('API Key copied to clipboard', 'Close', { - duration: 3000, + this.snackBar.open('API Key copied to clipboard!', 'Close', { + duration: 2000, }); } + copyCurlCommand(commandText: string) { + this.clipboard.copy(commandText); + } + toggleUnhealthyErrorDetails(): void { this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; } + + private prepareCurlCommands(): void { + this.curlCommands = []; + if (!this.serviceName || !this.apiDocJson?.info?.group) { + return; + } + + const endpoints = this.healthCheckEndpointsMap[this.apiDocJson.info.group]; + if (endpoints && endpoints.length > 0) { + endpoints.forEach(endpoint => { + const sessionToken = this.userDataService.token + ? this.userDataService.token + : 'YOUR_SESSION_TOKEN'; + + console.log(`${BASE_URL}/${this.serviceName}${endpoint}`); + + const command = `curl -X 'GET' '${window.location.origin}${BASE_URL}/${this.serviceName}${endpoint}' -H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; + this.curlCommands.push({ text: command }); + }); + } + } + + private injectCustomContent( + swaggerContainer: HTMLElement, + infoContainer: HTMLElement | null, + customContentNode: HTMLElement + ): void { + if (infoContainer) { + infoContainer.appendChild(customContentNode); + } else { + if (swaggerContainer.firstChild) { + swaggerContainer.insertBefore( + customContentNode, + swaggerContainer.firstChild + ); + } else { + swaggerContainer.appendChild(customContentNode); + } + } + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 41a28d60..524ca228 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -125,6 +125,10 @@ "hideDetails": "Hide Details", "warningDefault": "Warning: This type of API currently does not support automatic health checks." }, + "apiBasicCurlCommands": { + "title": "Test api yourself with this simple curl commads here:", + "copyTooltip": "Copy" + }, "nav": { "error": { "header": "Error" From f6480a039a0ce9136a175fcd7f7d1a3321cca75a Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Fri, 6 Jun 2025 12:53:46 +0300 Subject: [PATCH 10/13] Do not display dropdown if no curl commands generated --- src/app/adf-api-docs/df-api-docs/df-api-docs.component.html | 5 +---- src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index 45d8ed42..a8711347 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -76,7 +76,7 @@
- + @@ -108,9 +108,6 @@
-

- {{ 'apiBasicCurlCommands.noCommands' | transloco }} -

diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index b216f3cc..70788022 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -329,9 +329,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { const sessionToken = this.userDataService.token ? this.userDataService.token : 'YOUR_SESSION_TOKEN'; - - console.log(`${BASE_URL}/${this.serviceName}${endpoint}`); - const command = `curl -X 'GET' '${window.location.origin}${BASE_URL}/${this.serviceName}${endpoint}' -H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; this.curlCommands.push({ text: command }); }); From 9049d0fca7a2d1675a274d4b4bdcff07eb6d6b30 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Mon, 9 Jun 2025 15:19:25 +0300 Subject: [PATCH 11/13] Make a few improvements for quickstart dropdown component + move quickstart dropdown to its own component --- .../df-api-docs/df-api-docs.component.html | 39 +----- .../df-api-docs/df-api-docs.component.scss | 20 +-- .../df-api-docs/df-api-docs.component.ts | 77 +++++------ .../df-curl-command.component.html | 52 ++++++++ .../df-curl-command.component.scss | 26 ++++ .../df-curl-command.component.ts | 126 ++++++++++++++++++ src/app/shared/types/files.ts | 21 +++ src/assets/i18n/en.json | 6 +- src/dark-style.scss | 3 +- 9 files changed, 268 insertions(+), 102 deletions(-) create mode 100644 src/app/adf-api-docs/df-curl-command/df-curl-command.component.html create mode 100644 src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss create mode 100644 src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index a8711347..396bd261 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -76,41 +76,10 @@
- - - - - {{ 'apiBasicCurlCommands.title' | transloco }} - - - - -
- - -
{{ command.text }}
-
- - - -
-
-
-
-
+
diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss index d2f0639d..4351fb83 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss @@ -141,24 +141,6 @@ } } -.curl-command-text { - white-space: pre; - font-family: monospace; - font-size: 0.9em; - margin: 0; - color: var(--df-script-editor-text-color); - overflow-x: auto; -} - .custom-swagger-content-wrapper { width: 100%; -} - -.curl-commands-container { - display: flex; - flex-direction: column; - gap: 8px; - .actions-container { - padding: 0px 8px; - } -} +} \ No newline at end of file diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 70788022..aa447b87 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -48,6 +48,8 @@ import { import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; import { Subscription, of, forkJoin } from 'rxjs'; +import { DfCurlCommandComponent } from '../df-curl-command/df-curl-command.component'; +import { ApiDocJson } from '../../shared/types/files'; interface ServiceResponse { resource: Array<{ @@ -57,19 +59,6 @@ interface ServiceResponse { }>; } -interface ApiDocJson { - info: { - description: string | undefined; - title: string; - version: string | undefined; - group: string; - }; - paths: { - [key: string]: any; - }; - [key: string]: any; -} - interface HealthCheckResult { endpoint: string; success?: boolean; @@ -98,6 +87,7 @@ interface HealthCheckResult { MatTooltipModule, MatExpansionModule, MatCardModule, + DfCurlCommandComponent, ], }) export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { @@ -113,16 +103,37 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; faCopy = faCopy; - curlCommands: { text: string }[] = []; // Will be populated dynamically + private subscriptions: Subscription[] = []; healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'warning' = 'loading'; healthError: string | null = null; serviceName: string | null = null; showUnhealthyErrorDetails = false; // Mapping of service types to their corresponding endpoints, probably would be better to move to the back-end - healthCheckEndpointsMap: { [key: string]: string[] } = { - Database: ['/_schema', '/_table'], - File: ['/'], + healthCheckEndpointsInfo: { + [key: string]: { endpoint: string; title: string; description: string }[]; + } = { + Database: [ + { + endpoint: '/_schema', + title: 'View Available Schemas', + description: + 'This command fetches a list of schemas from your connected database', + }, + { + endpoint: '/_table', + title: 'View Tables in Your Database', + description: 'This command lists all tables in your database', + }, + ], + File: [ + { + endpoint: '/', + title: 'View Available Folders', + description: + 'This command fetches a list of folders from your connected file storage', + }, + ], }; constructor( @@ -213,8 +224,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }, showMutatedRequest: true, onComplete: () => { - this.prepareCurlCommands(); - if ( this.apiDocElement && this.apiDocElement.nativeElement && @@ -245,11 +254,11 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { } private checkApiHealth(): void { - let endpointsToValidate = - this.healthCheckEndpointsMap[this.apiDocJson.info.group]; - if (this.serviceName && endpointsToValidate) { + let endpointsInfoToValidate = + this.healthCheckEndpointsInfo[this.apiDocJson.info.group]; + if (this.serviceName && endpointsInfoToValidate) { // Perform health check - this.performHealthCheck(endpointsToValidate[0]); + this.performHealthCheck(endpointsInfoToValidate[0].endpoint); } else { this.setHealthState('warning'); } @@ -309,32 +318,10 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }); } - copyCurlCommand(commandText: string) { - this.clipboard.copy(commandText); - } - toggleUnhealthyErrorDetails(): void { this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; } - private prepareCurlCommands(): void { - this.curlCommands = []; - if (!this.serviceName || !this.apiDocJson?.info?.group) { - return; - } - - const endpoints = this.healthCheckEndpointsMap[this.apiDocJson.info.group]; - if (endpoints && endpoints.length > 0) { - endpoints.forEach(endpoint => { - const sessionToken = this.userDataService.token - ? this.userDataService.token - : 'YOUR_SESSION_TOKEN'; - const command = `curl -X 'GET' '${window.location.origin}${BASE_URL}/${this.serviceName}${endpoint}' -H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; - this.curlCommands.push({ text: command }); - }); - } - } - private injectCustomContent( swaggerContainer: HTMLElement, infoContainer: HTMLElement | null, diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html new file mode 100644 index 00000000..88b5969c --- /dev/null +++ b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html @@ -0,0 +1,52 @@ + + + + + {{ 'apiBasicCurlCommands.title' | transloco }} + + + + +

+ {{ 'apiBasicCurlCommands.quickStartDetails' | transloco }} +

+
+

+ {{ i + 1 }}. {{ command.title }} +

+

+ {{ command.description }} +

+ + +
{{ command.text }}
+
+ + + +
+

{{ command.note }}

+
+ +

+ {{ + 'apiBasicCurlCommands.nextStepFooter.header' | transloco + }} + {{ 'apiBasicCurlCommands.nextStepFooter.body' | transloco }} +

+
+
+
\ No newline at end of file diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss new file mode 100644 index 00000000..b43548a0 --- /dev/null +++ b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss @@ -0,0 +1,26 @@ +.curl-command-text { + white-space: pre; + font-family: monospace; + font-size: 0.9em; + margin: 0; + color: var(--df-script-editor-text-color); + overflow-x: auto; +} + +.curl-commands-container { + display: flex; + flex-direction: column; + gap: 8px; + .actions-container { + padding: 0px 8px; + } +} + +.curl-command-title { + margin: 0; + font-weight: bold; +} + +.curl-command-note { + color: gray !important; +} diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts new file mode 100644 index 00000000..cd507b53 --- /dev/null +++ b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts @@ -0,0 +1,126 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslocoModule } from '@ngneat/transloco'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faCopy } from '@fortawesome/free-solid-svg-icons'; +import { Clipboard } from '@angular/cdk/clipboard'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; +import { BASE_URL } from 'src/app/shared/constants/urls'; +import { SESSION_TOKEN_HEADER } from 'src/app/shared/constants/http-headers'; +import { ApiDocJson } from 'src/app/shared/types/files'; +import { MatDividerModule } from '@angular/material/divider'; + +interface CurlCommand { + title: string; + description: string; + text: string; + note: string; +} + +const healthCheckEndpointsInfo: { + [key: string]: { endpoint: string; title: string; description: string }[]; +} = { + Database: [ + { + endpoint: '/_schema', + title: 'View Available Schemas', + description: + 'This command fetches a list of schemas from your connected database', + }, + { + endpoint: '/_table', + title: 'View Tables in Your Database', + description: 'This command lists all tables in your database', + }, + ], + File: [ + { + endpoint: '/', + title: 'View Available Folders', + description: + 'This command fetches a list of folders from your connected file storage', + }, + ], +}; + +@Component({ + selector: 'df-curl-command', + templateUrl: './df-curl-command.component.html', + styleUrls: ['./df-curl-command.component.scss'], + standalone: true, + imports: [ + CommonModule, + TranslocoModule, + MatExpansionModule, + MatCardModule, + MatIconModule, + MatTooltipModule, + FontAwesomeModule, + MatDividerModule, + MatButtonModule + ], +}) +export class DfCurlCommandComponent implements OnChanges { + @Input() apiDocJson: ApiDocJson; + @Input() serviceName: string; + + curlCommands: CurlCommand[] = []; + faCopy = faCopy; + + constructor( + private clipboard: Clipboard, + private snackBar: MatSnackBar, + private userDataService: DfUserDataService + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if ( + (changes['apiDocJson'] || changes['serviceName']) && + this.apiDocJson && + this.serviceName + ) { + this.prepareCurlCommands(); + } + } + + copyCurlCommand(commandText: string) { + this.clipboard.copy(commandText); + this.snackBar.open('CURL command copied to clipboard.', 'Close', { + duration: 2000, + }); + } + + private prepareCurlCommands(): void { + this.curlCommands = []; + if (!this.serviceName || !this.apiDocJson?.info?.group) { + return; + } + + const endpointsInfo = + healthCheckEndpointsInfo[this.apiDocJson.info.group]; + if (endpointsInfo?.length > 0) { + endpointsInfo.forEach(endpointInfo => { + const sessionToken = + this.userDataService.token || 'YOUR_SESSION_TOKEN'; + const command = `curl -X 'GET' '${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}' -H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; + + this.curlCommands.push({ + title: endpointInfo.title, + description: endpointInfo.description, + text: command, + note: this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary, + }); + }); + } + } + + trackByCommand(index: number, item: CurlCommand): string { + return item.text; + } +} \ No newline at end of file diff --git a/src/app/shared/types/files.ts b/src/app/shared/types/files.ts index a6e25dba..ba4c0541 100644 --- a/src/app/shared/types/files.ts +++ b/src/app/shared/types/files.ts @@ -36,3 +36,24 @@ export interface FileType { } type EntityType = 'file' | 'folder'; + +export interface ApiDocJson { + info: { + description?: string; + title: string; + version?: string; + group: string; + }; + paths: { + [endpoint: string]: { + [method: string]: { + operationId: string; + description: string; + summary: string; + tags: string[]; + [key: string]: any; + }; + }; + }; + [key: string]: any; +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 524ca228..2d837f75 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -126,8 +126,10 @@ "warningDefault": "Warning: This type of API currently does not support automatic health checks." }, "apiBasicCurlCommands": { - "title": "Test api yourself with this simple curl commads here:", - "copyTooltip": "Copy" + "title": "Quickstart: Test Your API Connection:", + "quickStartDetails": "Start by testing your API with these sample curl commands. They return real data and confirm your connection is active.", + "copyTooltip": "Copy", + "nextStepFooter": {"header": "Next Step:", "body": "Scroll below to explore more endpoints that allow you to read, write, and filter your data via generated REST"} }, "nav": { "error": { diff --git a/src/dark-style.scss b/src/dark-style.scss index f361b0a5..aa651f21 100644 --- a/src/dark-style.scss +++ b/src/dark-style.scss @@ -95,7 +95,8 @@ $df-purple-palette: mat.define-palette(theme.$df-purple-palette); input, textarea, button, - span { + span, + .themed-text { color: white !important; } .mat-mdc-form-field-required-marker { From 99f96f0fce41e147c1df6a40b09c4758c3b4fbc8 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Mon, 9 Jun 2025 15:20:04 +0300 Subject: [PATCH 12/13] Used a prittier --- .../df-api-docs/df-api-docs.component.scss | 2 +- .../df-curl-command/df-curl-command.component.html | 6 ++---- .../df-curl-command/df-curl-command.component.ts | 10 ++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss index 4351fb83..f2cbf028 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss @@ -143,4 +143,4 @@ .custom-swagger-content-wrapper { width: 100%; -} \ No newline at end of file +} diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html index 88b5969c..6d8682b0 100644 --- a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html +++ b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html @@ -31,9 +31,7 @@

@@ -49,4 +47,4 @@

- \ No newline at end of file + diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts index cd507b53..ba3bf4ce 100644 --- a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts +++ b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts @@ -63,7 +63,7 @@ const healthCheckEndpointsInfo: { MatTooltipModule, FontAwesomeModule, MatDividerModule, - MatButtonModule + MatButtonModule, ], }) export class DfCurlCommandComponent implements OnChanges { @@ -102,12 +102,10 @@ export class DfCurlCommandComponent implements OnChanges { return; } - const endpointsInfo = - healthCheckEndpointsInfo[this.apiDocJson.info.group]; + const endpointsInfo = healthCheckEndpointsInfo[this.apiDocJson.info.group]; if (endpointsInfo?.length > 0) { endpointsInfo.forEach(endpointInfo => { - const sessionToken = - this.userDataService.token || 'YOUR_SESSION_TOKEN'; + const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; const command = `curl -X 'GET' '${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}' -H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; this.curlCommands.push({ @@ -123,4 +121,4 @@ export class DfCurlCommandComponent implements OnChanges { trackByCommand(index: number, item: CurlCommand): string { return item.text; } -} \ No newline at end of file +} From cdddb7d7430f2bc83d2ba145ca31a06a6952769d Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Mon, 9 Jun 2025 16:11:41 +0300 Subject: [PATCH 13/13] Renamed a component --- .../df-api-docs/df-api-docs.component.html | 4 +- .../df-api-docs/df-api-docs.component.ts | 6 +- .../df-api-quickstart.component.html | 55 ++++++++++++++++ .../df-api-quickstart.component.scss | 62 +++++++++++++++++++ .../df-api-quickstart.component.ts} | 31 +++++----- .../df-curl-command.component.html | 50 --------------- .../df-curl-command.component.scss | 26 -------- src/assets/i18n/en.json | 2 +- 8 files changed, 140 insertions(+), 96 deletions(-) create mode 100644 src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html create mode 100644 src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss rename src/app/adf-api-docs/{df-curl-command/df-curl-command.component.ts => df-api-quickstart/df-api-quickstart.component.ts} (79%) delete mode 100644 src/app/adf-api-docs/df-curl-command/df-curl-command.component.html delete mode 100644 src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index 396bd261..9003c18f 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -76,10 +76,10 @@ - + [serviceName]="serviceName">
diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index aa447b87..26a293fa 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -48,8 +48,8 @@ import { import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; import { Subscription, of, forkJoin } from 'rxjs'; -import { DfCurlCommandComponent } from '../df-curl-command/df-curl-command.component'; -import { ApiDocJson } from '../../shared/types/files'; +import { DfApiQuickstartComponent } from '../df-api-quickstart/df-api-quickstart.component'; +import { ApiDocJson } from 'src/app/shared/types/files'; interface ServiceResponse { resource: Array<{ @@ -87,7 +87,7 @@ interface HealthCheckResult { MatTooltipModule, MatExpansionModule, MatCardModule, - DfCurlCommandComponent, + DfApiQuickstartComponent, ], }) export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html new file mode 100644 index 00000000..0d73cff7 --- /dev/null +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html @@ -0,0 +1,55 @@ + + + + + {{ 'apiBasicCurlCommands.title' | transloco }} + + + +
+

+ {{ 'apiBasicCurlCommands.quickStartDetails' | transloco }} +

+
+

+ {{ i + 1 }}. {{ command.title }} +

+

+ {{ command.description }} +

+ + +
{{ command.textForDisplay }}
+
+ + + +
+

{{ command.note }}

+
+ +
+

+ {{ + 'apiBasicCurlCommands.nextStepFooter.header' | transloco + }} + {{ 'apiBasicCurlCommands.nextStepFooter.body' | transloco }} +

+
+
+
diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss new file mode 100644 index 00000000..d6e9a152 --- /dev/null +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss @@ -0,0 +1,62 @@ +mat-expansion-panel-header { + padding: 0 12px; +} +.curl-command-text { + white-space: pre; + font-family: monospace; + font-size: 0.9em; + margin: 0; + color: var(--df-script-editor-text-color); + overflow-x: auto; +} + +.curl-commands-container { + display: flex; + flex-direction: column; + gap: 8px; + .actions-container { + padding: 0px 8px; + } +} + +.curl-command-title { + margin: 0; + font-weight: bold; +} + +.curl-command-note { + color: gray !important; +} + +.no-commands-container { + ul { + padding-left: 20px; + li { + margin-bottom: 10px; + } + } + + span[class^='method-'] { + font-weight: bold; + font-family: monospace; + padding: 2px 6px; + border-radius: 4px; + color: white; + } + + .method-get { + background-color: #61affe; // blue + } + .method-post { + background-color: #49cc90; // green + } + .method-put { + background-color: #fca130; // orange + } + .method-patch { + background-color: #fca130; // orange + } + .method-delete { + background-color: #f93e3e; // red + } +} diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts similarity index 79% rename from src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts rename to src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts index ba3bf4ce..6a97d20e 100644 --- a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.ts +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -9,17 +9,18 @@ import { MatButtonModule } from '@angular/material/button'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { Clipboard } from '@angular/cdk/clipboard'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; import { BASE_URL } from 'src/app/shared/constants/urls'; import { SESSION_TOKEN_HEADER } from 'src/app/shared/constants/http-headers'; import { ApiDocJson } from 'src/app/shared/types/files'; import { MatDividerModule } from '@angular/material/divider'; +import { MatSnackBar } from '@angular/material/snack-bar'; interface CurlCommand { title: string; description: string; - text: string; + textForDisplay: string; + textForCopy: string; note: string; } @@ -50,9 +51,9 @@ const healthCheckEndpointsInfo: { }; @Component({ - selector: 'df-curl-command', - templateUrl: './df-curl-command.component.html', - styleUrls: ['./df-curl-command.component.scss'], + selector: 'df-api-quickstart', + templateUrl: './df-api-quickstart.component.html', + styleUrls: ['./df-api-quickstart.component.scss'], standalone: true, imports: [ CommonModule, @@ -66,7 +67,7 @@ const healthCheckEndpointsInfo: { MatButtonModule, ], }) -export class DfCurlCommandComponent implements OnChanges { +export class DfApiQuickstartComponent implements OnChanges { @Input() apiDocJson: ApiDocJson; @Input() serviceName: string; @@ -75,8 +76,8 @@ export class DfCurlCommandComponent implements OnChanges { constructor( private clipboard: Clipboard, - private snackBar: MatSnackBar, - private userDataService: DfUserDataService + private userDataService: DfUserDataService, + private snackBar: MatSnackBar ) {} ngOnChanges(changes: SimpleChanges): void { @@ -91,9 +92,6 @@ export class DfCurlCommandComponent implements OnChanges { copyCurlCommand(commandText: string) { this.clipboard.copy(commandText); - this.snackBar.open('CURL command copied to clipboard.', 'Close', { - duration: 2000, - }); } private prepareCurlCommands(): void { @@ -106,12 +104,17 @@ export class DfCurlCommandComponent implements OnChanges { if (endpointsInfo?.length > 0) { endpointsInfo.forEach(endpointInfo => { const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; - const command = `curl -X 'GET' '${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}' -H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; + const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}`; + const headers = `-H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; + + const commandForDisplay = `curl -X 'GET' '${baseUrl}' \\\n ${headers}`; + const commandForCopy = `curl -X 'GET' '${baseUrl}' ${headers}`; this.curlCommands.push({ title: endpointInfo.title, description: endpointInfo.description, - text: command, + textForDisplay: commandForDisplay, + textForCopy: commandForCopy, note: this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary, }); }); @@ -119,6 +122,6 @@ export class DfCurlCommandComponent implements OnChanges { } trackByCommand(index: number, item: CurlCommand): string { - return item.text; + return item.textForCopy; } } diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html deleted file mode 100644 index 6d8682b0..00000000 --- a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - {{ 'apiBasicCurlCommands.title' | transloco }} - - - - -

- {{ 'apiBasicCurlCommands.quickStartDetails' | transloco }} -

-
-

- {{ i + 1 }}. {{ command.title }} -

-

- {{ command.description }} -

- - -
{{ command.text }}
-
- - - -
-

{{ command.note }}

-
- -

- {{ - 'apiBasicCurlCommands.nextStepFooter.header' | transloco - }} - {{ 'apiBasicCurlCommands.nextStepFooter.body' | transloco }} -

-
-
-
diff --git a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss b/src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss deleted file mode 100644 index b43548a0..00000000 --- a/src/app/adf-api-docs/df-curl-command/df-curl-command.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -.curl-command-text { - white-space: pre; - font-family: monospace; - font-size: 0.9em; - margin: 0; - color: var(--df-script-editor-text-color); - overflow-x: auto; -} - -.curl-commands-container { - display: flex; - flex-direction: column; - gap: 8px; - .actions-container { - padding: 0px 8px; - } -} - -.curl-command-title { - margin: 0; - font-weight: bold; -} - -.curl-command-note { - color: gray !important; -} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2d837f75..8b00adbf 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -129,7 +129,7 @@ "title": "Quickstart: Test Your API Connection:", "quickStartDetails": "Start by testing your API with these sample curl commands. They return real data and confirm your connection is active.", "copyTooltip": "Copy", - "nextStepFooter": {"header": "Next Step:", "body": "Scroll below to explore more endpoints that allow you to read, write, and filter your data via generated REST"} + "nextStepFooter": {"header": "Next Step:", "body": "Scroll below to explore generated endpoints that allow you to read, write, and filter your data via REST"} }, "nav": { "error": {